├── .clabot ├── .env-sample ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc.js ├── README.md ├── assets └── logo.svg ├── package.json ├── packages ├── address-table │ ├── .env-sample │ ├── README.md │ ├── contracts │ │ └── ArbitrumVIP.sol │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ └── exec.js ├── arb-shared-dependencies │ ├── hardhat.config.js │ ├── index.js │ └── package.json ├── custom-gateway-bridging │ ├── .env-sample │ ├── README.md │ ├── contracts │ │ ├── CrosschainMessenger.sol │ │ ├── L1CustomGateway.sol │ │ ├── L1Token.sol │ │ ├── L2CustomGateway.sol │ │ ├── L2Token.sol │ │ └── interfaces │ │ │ ├── IArbToken.sol │ │ │ ├── ICustomGateway.sol │ │ │ └── ICustomToken.sol │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ └── exec.js ├── custom-token-bridging │ ├── .env-sample │ ├── README.md │ ├── contracts │ │ ├── L1Token.sol │ │ ├── L2Token.sol │ │ └── interfaces │ │ │ ├── IArbToken.sol │ │ │ └── ICustomToken.sol │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ └── exec.js ├── delayedInbox-l2msg │ ├── .env-sample │ ├── README.md │ ├── contracts │ │ └── greeter.sol │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ ├── normalTx.js │ │ └── withdrawFunds.js ├── demo-dapp-election │ ├── .env-sample │ ├── .gitignore │ ├── README.md │ ├── contracts │ │ └── Election.sol │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ └── exec.js ├── demo-dapp-pet-shop │ ├── .env-sample │ ├── README.md │ ├── contracts │ │ └── Adoption.sol │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ └── exec.js ├── eth-deposit-to-different-address │ ├── .env-sample │ ├── .gitignore │ ├── README.md │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ └── exec.js ├── eth-deposit │ ├── .env-sample │ ├── .gitignore │ ├── README.md │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ └── exec.js ├── eth-withdraw │ ├── .env-sample │ ├── .gitignore │ ├── README.md │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ └── exec.js ├── gas-estimation │ ├── .env-sample │ ├── README.md │ ├── package.json │ └── scripts │ │ └── exec.ts ├── greeter │ ├── .env-sample │ ├── .gitignore │ ├── README.md │ ├── contracts │ │ ├── Greeter.sol │ │ ├── arbitrum │ │ │ └── GreeterL2.sol │ │ └── ethereum │ │ │ └── GreeterL1.sol │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ └── exec.js ├── l1-confirmation-checker │ ├── .env-sample │ ├── README.md │ ├── package.json │ └── scripts │ │ ├── exec.ts │ │ ├── getClargs.ts │ │ └── utils.ts ├── outbox-execute │ ├── .env-sample │ ├── README.md │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ └── exec.js ├── redeem-failed-retryable │ ├── .env-sample │ ├── .gitignore │ ├── README.md │ ├── contracts │ │ ├── Greeter.sol │ │ ├── arbitrum │ │ │ └── GreeterL2.sol │ │ └── ethereum │ │ │ └── GreeterL1.sol │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ ├── exec-createFailedRetryable.js │ │ └── exec-redeem.js ├── token-deposit │ ├── .env-sample │ ├── README.md │ ├── contracts │ │ └── DappToken.sol │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ │ └── exec.js └── token-withdraw │ ├── .env-sample │ ├── README.md │ ├── contracts │ └── DappToken.sol │ ├── hardhat.config.js │ ├── package.json │ └── scripts │ └── exec.js └── yarn.lock /.clabot: -------------------------------------------------------------------------------- 1 | { 2 | "contributors": "https://api.github.com/repos/OffchainLabs/clabot-config/contents/apache-contributors.json", 3 | "message": "We require contributors to sign our Contributor License Agreement. In order for us to review and merge your code, please sign one of the linked documents below to get yourself added. If you're an independent Individual please sign this form: https://na3.docusign.net/Member/PowerFormSigning.aspx?PowerFormId=1353a816-a9c1-47ba-847e-ec79f0f23d31&env=na3&acct=6e152afc-6284-44af-a4c1-d8ef291db402&v=2. If you're with a company (corporate) please sign this form: https://na3.docusign.net/Member/PowerFormSigning.aspx?PowerFormId=2b5fe8ba-51d4-4980-b4ee-605d66e675d4&env=na3&acct=6e152afc-6284-44af-a4c1-d8ef291db402&v=2. To agree to the CLA license, please fill out one of the attached forms." 4 | } 5 | -------------------------------------------------------------------------------- /.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/** 2 | node_modules/** 3 | packages/**/artifacts/** 4 | packages/**/build/** 5 | packages/**/cache/** 6 | packages/**/dist/** 7 | packages/**/node_modules/** -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | plugins: ['prettier'], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 14 | sourceType: 'module', // Allows for the use of imports 15 | }, 16 | rules: { 17 | 'prettier/prettier': 'error', 18 | 'no-unused-vars': 'off', 19 | 'prefer-const': [2, { destructuring: 'all' }], 20 | 'object-curly-spacing': ['error', 'always'], 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | node_modules/ 4 | packages/.DS_Store 5 | packages/*/.DS_Store 6 | packages/*/artifacts/ 7 | packages/*/cache/ 8 | packages/*/build/ 9 | packages/*/node_modules/ 10 | packages/*/.env 11 | yarn-error.log -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: | 2 | (?x)( 3 | ^packages/arb-avm-cpp/external/keccak/| 4 | ^packages/arb-bridge-eth/installed_contracts/ 5 | ) 6 | repos: 7 | - repo: https://github.com/psf/black 8 | rev: stable 9 | hooks: 10 | - id: black 11 | language_version: python3 12 | - repo: https://gitlab.com/pycqa/flake8 13 | rev: 3.9.0 14 | hooks: 15 | - id: flake8 16 | - repo: git://github.com/doublify/pre-commit-clang-format 17 | rev: master 18 | hooks: 19 | - id: clang-format 20 | - repo: git://github.com/dnephin/pre-commit-golang 21 | rev: master 22 | hooks: 23 | - id: go-fmt 24 | - repo: https://github.com/pre-commit/mirrors-prettier 25 | rev: "v2.2.1" # Use the sha or tag you want to point at 26 | hooks: 27 | - id: prettier 28 | - repo: https://github.com/pre-commit/mirrors-eslint 29 | rev: "v7.24.0" # Use the sha / tag you want to point at 30 | hooks: 31 | - id: eslint 32 | args: [--fix] 33 | types: [text] 34 | # ignore prettier config 35 | files: \.[jt]sx?$ 36 | additional_dependencies: 37 | - "eslint@7.3.1" 38 | - "typescript@3.8.3" 39 | - "@typescript-eslint/parser@3.4.0" 40 | - "@typescript-eslint/eslint-plugin@3.4.0" 41 | - eslint-config-prettier@6.11.0 42 | - repo: https://github.com/syntaqx/git-hooks 43 | rev: v0.0.16 44 | hooks: 45 | - id: circleci-config-validate 46 | - repo: local 47 | hooks: 48 | - id: prettier-soldity 49 | name: prettier-soldity 50 | language: node 51 | entry: ./node_modules/.bin/prettier --write --list-different 52 | files: \.sol$ 53 | additional_dependencies: 54 | - "prettier@2.0.5" 55 | - "prettier-plugin-solidity@^1.0.0-alpha.54" 56 | - id: go-vet 57 | name: Go Vet 58 | entry: ./scripts/run-go-packages "go list ./... | grep -v 'arb-node-core/ethbridge[^/]*contracts' | xargs go vet" 59 | language: script 60 | files: \.go$ 61 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/** 2 | node_modules/** 3 | packages/**/artifacts/** 4 | packages/**/build/** 5 | packages/**/cache/** 6 | packages/**/dist/** 7 | packages/**/node_modules/** -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'es5', 4 | singleQuote: true, 5 | printWidth: 80, 6 | tabWidth: 2, 7 | arrowParens: 'avoid', 8 | bracketSpacing: true, 9 | overrides: [ 10 | { 11 | files: '*.sol', 12 | options: { 13 | printWidth: 100, 14 | tabWidth: 4, 15 | useTabs: false, 16 | singleQuote: false, 17 | bracketSpacing: true, 18 | explicitTypes: 'always', 19 | }, 20 | }, 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arbitrum Tutorials 2 | 3 | This monorepo will help you get started with building on Arbitrum. It provides various simple demos showing and explaining how to interact with Arbitrum — deploying and using contracts directly on L2, moving Ether and tokens betweens L1 and L2, and more. 4 | 5 | We show how you can use broadly supported Ethereum ecosystem tooling (Hardhat, Ethers-js, etc.) as well as our special [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) for convenience. 6 | 7 | ## Installation 8 | 9 | From root directory: 10 | 11 | ```bash 12 | yarn install 13 | ``` 14 | 15 | ## What's included? 16 | 17 | #### :white_check_mark: Basics 18 | 19 | - 🐹 [Pet Shop DApp](./packages/demo-dapp-pet-shop/) (L2 only) 20 | - 🗳 [Election DApp](./packages/demo-dapp-election/) (L2 only) 21 | 22 | #### :white_check_mark: Moving Stuff around 23 | 24 | - ⤴️ 🔹 [Deposit Ether](./packages/eth-deposit/) 25 | - ⤵️ 🔹 [Withdraw Ether](./packages/eth-withdraw/) 26 | - ⤴️ 💸 [Deposit Token](./packages/token-deposit/) 27 | - ⤵️ 💸 [Withdraw token](./packages/token-withdraw/) 28 | 29 | #### :white_check_mark: General Interop 30 | 31 | - 🤝 [Greeter](./packages/greeter/) (L1 to L2) 32 | - 📤 [Outbox](./packages/outbox-execute/) (L2 to L1) 33 | - ⏰ [L1 Confirmation Checker](./packages/l1-confirmation-checker/) 34 | 35 | #### :white_check_mark: Advanced Features 36 | 37 | - ®️ [Arb Address Table](./packages/address-table/) 38 | - 🌉 [Bridging Custom Token](./packages/custom-token-bridging/) 39 | - ✈️ [Delayed inbox message(l2MSG)](./packages/delayedInbox-l2msg/) 40 | - 🎁 [Redeem Retryable Ticket](./packages/redeem-failed-retryable/) 41 | 42 |

43 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 32 | 33 | 34 | 36 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arbitrum-tutorials", 3 | "version": "1.0.0", 4 | "description": "The Arbitrum Tutorials Monorepo", 5 | "author": "Offchain Labs, Inc.", 6 | "license": "Apache-2.0", 7 | "private": "true", 8 | "engines": { 9 | "node": ">= 8.0.0 < 17.0.0", 10 | "npm": "^6.0.0", 11 | "yarn": "^1.0.0" 12 | }, 13 | "scripts": { 14 | "lint": "eslint .", 15 | "format": "prettier './**/*.{js,json,md,yml,sol}' --write && yarn run lint --fix" 16 | }, 17 | "devDependencies": { 18 | "eslint": "^8.15.0", 19 | "eslint-config-prettier": "^8.3.0", 20 | "eslint-plugin-mocha": "^9.0.0", 21 | "eslint-plugin-prettier": "^4.0.0", 22 | "prettier": "^2.3.2", 23 | "prettier-plugin-solidity": "^1.0.0-beta.17" 24 | }, 25 | "workspaces": { 26 | "packages": [ 27 | "packages/*" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/address-table/.env-sample: -------------------------------------------------------------------------------- 1 | DEVNET_PRIVKEY="0x your key here" 2 | 3 | # This is Arbitrum Goerli Testnet, can use any Arbitrum chain 4 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 5 | 6 | -------------------------------------------------------------------------------- /packages/address-table/README.md: -------------------------------------------------------------------------------- 1 | # Address Table Demo 2 | 3 | The Address table is a precompiled contract on Arbitrum for registering addresses which are then retrievable by an integer index; this saves gas by minimizing precious calldata required to input an address as a parameter. 4 | 5 | This demo shows a simple contract with affordances to retrieve an address from a contract by its index in the address table, and a client-side script to pre-register the given address (if necessary). 6 | 7 | See `exec.js` for inline comments / explanation. 8 | 9 | ### Run demo 10 | 11 | ``` 12 | yarn run exec 13 | ``` 14 | 15 | ## Config Environment Variables 16 | 17 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 18 | 19 | ```bash 20 | cp .env-sample .env 21 | ``` 22 | 23 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 24 | 25 | ### More info 26 | 27 | See our [developer documentation for more info](https://developer.offchainlabs.com/docs/special_features). 28 | 29 |

30 | 31 |

32 | -------------------------------------------------------------------------------- /packages/address-table/contracts/ArbitrumVIP.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.7.2; 3 | 4 | import "@arbitrum/nitro-contracts/src/precompiles/ArbAddressTable.sol"; 5 | import "hardhat/console.sol"; 6 | 7 | contract ArbitrumVIP { 8 | string greeting; 9 | mapping(address => uint256) arbitrumVIPPoints; // Maps address to vip points. More points you have, cooler you are. 10 | 11 | ArbAddressTable arbAddressTable; 12 | 13 | constructor() public { 14 | // connect to precomiled address table contract 15 | arbAddressTable = ArbAddressTable(102); 16 | } 17 | 18 | function addVIPPoints(uint256 addressIndex) external { 19 | // retreive address from address table 20 | address addressFromTable = arbAddressTable.lookupIndex(addressIndex); 21 | 22 | arbitrumVIPPoints[addressFromTable]++; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/address-table/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | const { hardhatConfig } = require('arb-shared-dependencies') 3 | 4 | module.exports = hardhatConfig 5 | -------------------------------------------------------------------------------- /packages/address-table/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "address-table", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "exec": "hardhat run scripts/exec.js --network l2" 7 | }, 8 | "devDependencies": { 9 | "@nomiclabs/hardhat-ethers": "^2.0.2", 10 | "@nomiclabs/hardhat-waffle": "^2.0.1", 11 | "chai": "^4.3.4", 12 | "ethereum-waffle": "^3.4.0", 13 | "ethers": "^5.4.2", 14 | "hardhat": "^2.5.0" 15 | }, 16 | "dependencies": { 17 | "@arbitrum/sdk": "^v3.1.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/address-table/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const hre = require('hardhat') 2 | const { 3 | ArbAddressTable__factory, 4 | } = require('@arbitrum/sdk/dist/lib/abi/factories/ArbAddressTable__factory') 5 | const { addDefaultLocalNetwork } = require('@arbitrum/sdk') 6 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 7 | requireEnvVariables(['DEVNET_PRIVKEY', 'L2RPC']) 8 | require('dotenv').config() 9 | 10 | async function main() { 11 | await arbLog('Using the Address Table') 12 | 13 | /** 14 | * Add the default local network configuration to the SDK 15 | * to allow this script to run on a local node 16 | */ 17 | addDefaultLocalNetwork() 18 | 19 | /** 20 | * Deploy ArbitrumVIP contract to L2 21 | */ 22 | const ArbitrumVIP = await hre.ethers.getContractFactory('ArbitrumVIP') 23 | const arbitrumVIP = await ArbitrumVIP.deploy() 24 | 25 | await arbitrumVIP.deployed() 26 | 27 | console.log('ArbitrumVIP deployed to:', arbitrumVIP.address) 28 | 29 | const signers = await hre.ethers.getSigners() 30 | const myAddress = signers[0].address 31 | 32 | /** 33 | * Connect to the Arbitrum Address table pre-compile contract 34 | */ 35 | const arbAddressTable = ArbAddressTable__factory.connect( 36 | '0x0000000000000000000000000000000000000066', 37 | signers[0] 38 | ) 39 | 40 | //** 41 | /* Let's find out if our address is registered in the table: 42 | */ 43 | const addressIsRegistered = await arbAddressTable.addressExists(myAddress) 44 | 45 | if (!addressIsRegistered) { 46 | //** 47 | /* If it isn't registered yet, let's register it! 48 | */ 49 | 50 | const txnRes = await arbAddressTable.register(myAddress) 51 | const txnRec = await txnRes.wait() 52 | console.log(`Successfully registered address ${myAddress} to address table`) 53 | } else { 54 | console.log(`Address ${myAddress} already (previously) registered to table`) 55 | } 56 | /** 57 | * Now that we know it's registered, let's go ahead and retrieve its index 58 | */ 59 | const addressIndex = await arbAddressTable.lookup(myAddress) 60 | 61 | /** 62 | * From here on out we can use this index instead of our address as a paramter into any contract with affordances to look up out address in the address data. 63 | */ 64 | 65 | const txnRes = await arbitrumVIP.addVIPPoints(addressIndex) 66 | await txnRes.wait() 67 | /** 68 | * We got VIP points, and we minimized the calldata required, saving us precious gas. Yay rollups! 69 | */ 70 | console.log( 71 | `Successfully added VIP points using address w/ index ${addressIndex.toNumber()}` 72 | ) 73 | } 74 | main() 75 | .then(() => process.exit(0)) 76 | .catch(error => { 77 | console.error(error) 78 | process.exit(1) 79 | }) 80 | -------------------------------------------------------------------------------- /packages/arb-shared-dependencies/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | module.exports = { 3 | solidity: { 4 | compilers: [ 5 | { 6 | version: '0.8.9', 7 | settings: {}, 8 | }, 9 | { 10 | version: '0.7.2', 11 | settings: {}, 12 | }, 13 | { 14 | version: '0.6.12', 15 | settings: {}, 16 | }, 17 | { 18 | version: '0.6.11', 19 | settings: {}, 20 | }, 21 | ], 22 | }, 23 | networks: { 24 | // When running the tutorials, we generally don't specify a network to use, but we configure the desired network 25 | // in the .env file. Thus, we generally end up using the default network config within hardhat, the "hardhat" network. 26 | // However, hardhat network config has some defaults that we want to override because they don't make sense 27 | // in other networks. 28 | hardhat: { 29 | gas: 'auto', // Default is 30000000 30 | }, 31 | l1: { 32 | gas: 2100000, 33 | gasLimit: 0, 34 | url: process.env['L1RPC'] || '', 35 | accounts: process.env['DEVNET_PRIVKEY'] 36 | ? [process.env['DEVNET_PRIVKEY']] 37 | : [], 38 | }, 39 | l2: { 40 | url: process.env['L2RPC'] || '', 41 | accounts: process.env['DEVNET_PRIVKEY'] 42 | ? [process.env['DEVNET_PRIVKEY']] 43 | : [], 44 | }, 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /packages/arb-shared-dependencies/index.js: -------------------------------------------------------------------------------- 1 | const hardhatConfig = require('./hardhat.config.js') 2 | const path = require('path') 3 | require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env') }) 4 | 5 | const wait = (ms = 0) => { 6 | return new Promise(res => setTimeout(res, ms || 0)) 7 | } 8 | 9 | const arbLog = async text => { 10 | let str = '🔵' 11 | for (let i = 0; i < 25; i++) { 12 | await wait(40) 13 | if (i == 12) { 14 | str = `🔵${'🔵'.repeat(i)}🔵` 15 | } else { 16 | str = `🔵${' '.repeat(i * 2)}🔵` 17 | } 18 | while (str.length < 60) { 19 | str = ` ${str} ` 20 | } 21 | 22 | console.log(str) 23 | } 24 | 25 | console.log('Arbitrum Demo:', text) 26 | await wait(2000) 27 | 28 | console.log('Lets') 29 | await wait(1000) 30 | 31 | console.log('Go ➡️') 32 | await wait(1000) 33 | console.log('...🚀') 34 | await wait(1000) 35 | console.log('') 36 | } 37 | 38 | const arbLogTitle = text => { 39 | console.log('\n###################') 40 | console.log(text) 41 | console.log('###################') 42 | } 43 | 44 | const requireEnvVariables = envVars => { 45 | for (const envVar of envVars) { 46 | if (!process.env[envVar]) { 47 | throw new Error(`Error: set your '${envVar}' environmental variable `) 48 | } 49 | } 50 | console.log('Environmental variables properly set 👍') 51 | } 52 | module.exports = { 53 | arbLog, 54 | arbLogTitle, 55 | hardhatConfig, 56 | requireEnvVariables, 57 | } 58 | -------------------------------------------------------------------------------- /packages/arb-shared-dependencies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arb-shared-dependencies", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile" 7 | }, 8 | "main": "index.js", 9 | "devDependencies": { 10 | "@nomiclabs/hardhat-ethers": "^2.0.2", 11 | "eslint": "^7.30.0", 12 | "ethers": "^5.1.2", 13 | "hardhat": "^2.2.0", 14 | "prettier": "^2.3.2" 15 | }, 16 | "dependencies": { 17 | "dotenv": "^8.2.0", 18 | "eslint-plugin-prettier": "^3.4.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC="" -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/README.md: -------------------------------------------------------------------------------- 1 | # Custom gateway bridging tutorial 2 | 3 | When neither the StandardERC20gateway nor the generic-custom-gateway are enough to fulfill the bridging requirements of a token, there is the possibility of creating and registering a custom gateway. `custom-gateway-bridging` demonstrates how to create and register a custom gateway in Arbitrum's Token Bridge protocol. 4 | 5 | For more info on bridging assets on Arbitrum, see our [token bridging docs](https://developer.arbitrum.io/asset-bridging). 6 | 7 | ## Token bridging using a custom gateway 8 | 9 | Bridging custom tokens through a custom gateway follow a similar process than that of Arbitrum's generic-custom gateway. The difference, however, is that during the gateway registration process, a custom gateway is registered instead of the generic-custom gateway. 10 | 11 | Here, we deploy a [demo custom token](./contracts/L1Token.sol) on L1 and a [demo custom token](./contracts/L2Token.sol) on L2. We also deploy a demo custom gateway on both [L1](./contracts/L1CustomGateway.sol) and [L2](./contracts/L2CustomGateway.sol). We then use the Arbitrum router contract to register our L1 and L2 gateways. 12 | 13 | We use the [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) library to initiate and verify the bridging. 14 | 15 | See [./exec.js](./scripts/exec.js) for inline explanation. 16 | 17 | ### Config Environment Variables 18 | 19 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 20 | 21 | ```bash 22 | cp .env-sample .env 23 | ``` 24 | 25 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 26 | 27 | ### Run: 28 | 29 | ``` 30 | yarn run exec 31 | ``` 32 | 33 | ## Disclaimer 34 | 35 | The code contained within this package is meant for testing purposes only and does not guarantee any level of security. It has not undergone any formal audit or security analysis. Use it at your own risk. Any potential damages or security breaches occurring from the use of this code are not the responsibility of the author(s) or contributor(s) of this repository. Please exercise caution and due diligence while using this code in any environment. 36 | 37 |

38 | 39 |

-------------------------------------------------------------------------------- /packages/custom-gateway-bridging/contracts/CrosschainMessenger.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; 5 | import "@arbitrum/nitro-contracts/src/libraries/AddressAliasHelper.sol"; 6 | 7 | /** 8 | * @title Interface needed to call function activeOutbox of the Bridge 9 | */ 10 | interface IBridge { 11 | function activeOutbox() external view returns (address); 12 | } 13 | 14 | /** 15 | * @title Interface needed to call functions createRetryableTicket and bridge of the Inbox 16 | */ 17 | interface IInbox { 18 | function createRetryableTicket( 19 | address to, 20 | uint256 arbTxCallValue, 21 | uint256 maxSubmissionCost, 22 | address submissionRefundAddress, 23 | address valueRefundAddress, 24 | uint256 gasLimit, 25 | uint256 maxFeePerGas, 26 | bytes calldata data 27 | ) external payable returns (uint256); 28 | 29 | function bridge() external view returns (IBridge); 30 | } 31 | 32 | /** 33 | * @title Interface needed to call function l2ToL1Sender of the Outbox 34 | */ 35 | interface IOutbox { 36 | function l2ToL1Sender() external view returns (address); 37 | } 38 | 39 | /** 40 | * @title Minimum expected implementation of a crosschain messenger contract to be deployed on L1 41 | */ 42 | abstract contract L1CrosschainMessenger { 43 | IInbox public immutable inbox; 44 | 45 | /** 46 | * Emitted when calling sendTxToL2CustomRefund 47 | * @param from account that submitted the retryable ticket 48 | * @param to account recipient of the retryable ticket 49 | * @param seqNum id for the retryable ticket 50 | * @param data data of the retryable ticket 51 | */ 52 | event TxToL2( 53 | address indexed from, 54 | address indexed to, 55 | uint256 indexed seqNum, 56 | bytes data 57 | ); 58 | 59 | constructor(address inbox_) { 60 | inbox = IInbox(inbox_); 61 | } 62 | 63 | modifier onlyCounterpartGateway(address l2Counterpart) { 64 | // A message coming from the counterpart gateway was executed by the bridge 65 | IBridge bridge = inbox.bridge(); 66 | require(msg.sender == address(bridge), "NOT_FROM_BRIDGE"); 67 | 68 | // And the outbox reports that the L2 address of the sender is the counterpart gateway 69 | address l2ToL1Sender = IOutbox(bridge.activeOutbox()).l2ToL1Sender(); 70 | require(l2ToL1Sender == l2Counterpart, "ONLY_COUNTERPART_GATEWAY"); 71 | 72 | _; 73 | } 74 | 75 | /** 76 | * Creates the retryable ticket to send over to L2 through the Inbox 77 | * @param to account to be credited with the tokens in the destination layer 78 | * @param refundTo account, or its L2 alias if it have code in L1, to be credited with excess gas refund in L2 79 | * @param user account with rights to cancel the retryable and receive call value refund 80 | * @param l1CallValue callvalue sent in the L1 submission transaction 81 | * @param l2CallValue callvalue for the L2 message 82 | * @param maxSubmissionCost max gas deducted from user's L2 balance to cover base submission fee 83 | * @param maxGas max gas deducted from user's L2 balance to cover L2 execution 84 | * @param gasPriceBid gas price for L2 execution 85 | * @param data encoded data for the retryable 86 | * @return seqnum id for the retryable ticket 87 | */ 88 | function _sendTxToL2CustomRefund( 89 | address to, 90 | address refundTo, 91 | address user, 92 | uint256 l1CallValue, 93 | uint256 l2CallValue, 94 | uint256 maxSubmissionCost, 95 | uint256 maxGas, 96 | uint256 gasPriceBid, 97 | bytes memory data 98 | ) internal returns (uint256) { 99 | uint256 seqNum = inbox.createRetryableTicket{ value: l1CallValue }( 100 | to, 101 | l2CallValue, 102 | maxSubmissionCost, 103 | refundTo, 104 | user, 105 | maxGas, 106 | gasPriceBid, 107 | data 108 | ); 109 | 110 | emit TxToL2(user, to, seqNum, data); 111 | return seqNum; 112 | } 113 | } 114 | 115 | /** 116 | * @title Minimum expected implementation of a crosschain messenger contract to be deployed on L2 117 | */ 118 | abstract contract L2CrosschainMessenger { 119 | address internal constant ARB_SYS_ADDRESS = address(100); 120 | 121 | /** 122 | * Emitted when calling sendTxToL1 123 | * @param from account that submits the L2-to-L1 message 124 | * @param to account recipient of the L2-to-L1 message 125 | * @param id id for the L2-to-L1 message 126 | * @param data data of the L2-to-L1 message 127 | */ 128 | event TxToL1( 129 | address indexed from, 130 | address indexed to, 131 | uint256 indexed id, 132 | bytes data 133 | ); 134 | 135 | modifier onlyCounterpartGateway(address l1Counterpart) { 136 | require( 137 | msg.sender == AddressAliasHelper.applyL1ToL2Alias(l1Counterpart), 138 | "ONLY_COUNTERPART_GATEWAY" 139 | ); 140 | 141 | _; 142 | } 143 | 144 | /** 145 | * Creates an L2-to-L1 message to send over to L1 through ArbSys 146 | * @param from account that is sending funds from L2 147 | * @param to account to be credited with the tokens in the destination layer 148 | * @param data encoded data for the L2-to-L1 message 149 | * @return id id for the L2-to-L1 message 150 | */ 151 | function _sendTxToL1( 152 | address from, 153 | address to, 154 | bytes memory data 155 | ) internal returns (uint256) { 156 | uint256 id = ArbSys(ARB_SYS_ADDRESS).sendTxToL1(to, data); 157 | 158 | emit TxToL1(from, to, id, data); 159 | return id; 160 | } 161 | } -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/contracts/L1CustomGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "./interfaces/ICustomGateway.sol"; 5 | import "./CrosschainMessenger.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/access/Ownable.sol"; 8 | 9 | /** 10 | * @title Example implementation of a custom gateway to be deployed on L1 11 | * @dev Inheritance of Ownable is optional. In this case we use it to call the function setTokenBridgeInformation 12 | * and simplify the test 13 | */ 14 | contract L1CustomGateway is IL1CustomGateway, L1CrosschainMessenger, Ownable { 15 | 16 | // Token bridge state variables 17 | address public l1CustomToken; 18 | address public l2CustomToken; 19 | address public l2Gateway; 20 | address public router; 21 | 22 | // Custom functionality 23 | bool public allowsDeposits; 24 | 25 | /** 26 | * Contract constructor, sets the L1 router to be used in the contract's functions and calls L1CrosschainMessenger's constructor 27 | * @param router_ L1GatewayRouter address 28 | * @param inbox_ Inbox address 29 | */ 30 | constructor( 31 | address router_, 32 | address inbox_ 33 | ) L1CrosschainMessenger(inbox_) { 34 | router = router_; 35 | allowsDeposits = false; 36 | } 37 | 38 | /** 39 | * Sets the information needed to use the gateway. To simplify the process of testing, this function can be called once 40 | * by the owner of the contract to set these addresses. 41 | * @param l1CustomToken_ address of the custom token on L1 42 | * @param l2CustomToken_ address of the custom token on L2 43 | * @param l2Gateway_ address of the counterpart gateway (on L2) 44 | */ 45 | function setTokenBridgeInformation( 46 | address l1CustomToken_, 47 | address l2CustomToken_, 48 | address l2Gateway_ 49 | ) public onlyOwner { 50 | require(l1CustomToken == address(0), "Token bridge information already set"); 51 | l1CustomToken = l1CustomToken_; 52 | l2CustomToken = l2CustomToken_; 53 | l2Gateway = l2Gateway_; 54 | 55 | // Allows deposits after the information has been set 56 | allowsDeposits = true; 57 | } 58 | 59 | /// @dev See {ICustomGateway-outboundTransfer} 60 | function outboundTransfer( 61 | address l1Token, 62 | address to, 63 | uint256 amount, 64 | uint256 maxGas, 65 | uint256 gasPriceBid, 66 | bytes calldata data 67 | ) public payable override returns (bytes memory) { 68 | return outboundTransferCustomRefund(l1Token, to, to, amount, maxGas, gasPriceBid, data); 69 | } 70 | 71 | /// @dev See {IL1CustomGateway-outboundTransferCustomRefund} 72 | function outboundTransferCustomRefund( 73 | address l1Token, 74 | address refundTo, 75 | address to, 76 | uint256 amount, 77 | uint256 maxGas, 78 | uint256 gasPriceBid, 79 | bytes calldata data 80 | ) public payable override returns (bytes memory res) { 81 | // Only execute if deposits are allowed 82 | require(allowsDeposits == true, "Deposits are currently disabled"); 83 | 84 | // Only allow calls from the router 85 | require(msg.sender == router, "Call not received from router"); 86 | 87 | // Only allow the custom token to be bridged through this gateway 88 | require(l1Token == l1CustomToken, "Token is not allowed through this gateway"); 89 | 90 | address from; 91 | uint256 seqNum; 92 | { 93 | bytes memory extraData; 94 | uint256 maxSubmissionCost; 95 | (from, maxSubmissionCost, extraData) = _parseOutboundData(data); 96 | 97 | // The inboundEscrowAndCall functionality has been disabled, so no data is allowed 98 | require(extraData.length == 0, "EXTRA_DATA_DISABLED"); 99 | 100 | // Escrowing the tokens in the gateway 101 | IERC20(l1Token).transferFrom(from, address(this), amount); 102 | 103 | // We override the res field to save on the stack 104 | res = getOutboundCalldata(l1Token, from, to, amount, extraData); 105 | 106 | // Trigger the crosschain message 107 | seqNum = _sendTxToL2CustomRefund( 108 | l2Gateway, 109 | refundTo, 110 | from, 111 | msg.value, 112 | 0, 113 | maxSubmissionCost, 114 | maxGas, 115 | gasPriceBid, 116 | res 117 | ); 118 | } 119 | 120 | emit DepositInitiated(l1Token, from, to, seqNum, amount); 121 | res = abi.encode(seqNum); 122 | } 123 | 124 | /// @dev See {ICustomGateway-finalizeInboundTransfer} 125 | function finalizeInboundTransfer( 126 | address l1Token, 127 | address from, 128 | address to, 129 | uint256 amount, 130 | bytes calldata data 131 | ) public payable override onlyCounterpartGateway(l2Gateway) { 132 | // Only allow the custom token to be bridged through this gateway 133 | require(l1Token == l1CustomToken, "Token is not allowed through this gateway"); 134 | 135 | // Decoding exitNum 136 | (uint256 exitNum, ) = abi.decode(data, (uint256, bytes)); 137 | 138 | // Releasing the tokens in the gateway 139 | IERC20(l1Token).transfer(to, amount); 140 | 141 | emit WithdrawalFinalized(l1Token, from, to, exitNum, amount); 142 | } 143 | 144 | /// @dev See {ICustomGateway-getOutboundCalldata} 145 | function getOutboundCalldata( 146 | address l1Token, 147 | address from, 148 | address to, 149 | uint256 amount, 150 | bytes memory data 151 | ) public pure override returns (bytes memory outboundCalldata) { 152 | bytes memory emptyBytes = ""; 153 | 154 | outboundCalldata = abi.encodeWithSelector( 155 | ICustomGateway.finalizeInboundTransfer.selector, 156 | l1Token, 157 | from, 158 | to, 159 | amount, 160 | abi.encode(emptyBytes, data) 161 | ); 162 | 163 | return outboundCalldata; 164 | } 165 | 166 | /// @dev See {ICustomGateway-calculateL2TokenAddress} 167 | function calculateL2TokenAddress(address l1Token) public view override returns (address) { 168 | if (l1Token == l1CustomToken) { 169 | return l2CustomToken; 170 | } 171 | 172 | return address(0); 173 | } 174 | 175 | /// @dev See {ICustomGateway-counterpartGateway} 176 | function counterpartGateway() public view override returns (address) { 177 | return l2Gateway; 178 | } 179 | 180 | /** 181 | * Parse data received in outboundTransfer 182 | * @param data encoded data received 183 | * @return from account that initiated the deposit, 184 | * maxSubmissionCost max gas deducted from user's L2 balance to cover base submission fee, 185 | * extraData decoded data 186 | */ 187 | function _parseOutboundData(bytes memory data) 188 | internal 189 | pure 190 | returns ( 191 | address from, 192 | uint256 maxSubmissionCost, 193 | bytes memory extraData 194 | ) 195 | { 196 | // Router encoded 197 | (from, extraData) = abi.decode(data, (address, bytes)); 198 | 199 | // User encoded 200 | (maxSubmissionCost, extraData) = abi.decode(extraData, (uint256, bytes)); 201 | } 202 | 203 | // -------------------- 204 | // Custom methods 205 | // -------------------- 206 | /** 207 | * Disables the ability to deposit funds 208 | */ 209 | function disableDeposits() external onlyOwner { 210 | allowsDeposits = false; 211 | } 212 | 213 | /** 214 | * Enables the ability to deposit funds 215 | */ 216 | function enableDeposits() external onlyOwner { 217 | require(l1CustomToken != address(0), "Token bridge information has not been set yet"); 218 | allowsDeposits = true; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/contracts/L1Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "./interfaces/ICustomToken.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | /** 9 | * @title Interface needed to call function registerTokenToL2 of the L1CustomGateway 10 | * (We don't need this interface for this example, but we're keeping it for completion) 11 | */ 12 | interface IL1CustomGenericGateway { 13 | function registerTokenToL2( 14 | address l2Address, 15 | uint256 maxGas, 16 | uint256 gasPriceBid, 17 | uint256 maxSubmissionCost, 18 | address creditBackAddress 19 | ) external payable returns (uint256); 20 | } 21 | 22 | /** 23 | * @title Interface needed to call function setGateway of the L2GatewayRouter 24 | */ 25 | interface IL1GatewayRouter { 26 | function setGateway( 27 | address gateway, 28 | uint256 maxGas, 29 | uint256 gasPriceBid, 30 | uint256 maxSubmissionCost, 31 | address creditBackAddress 32 | ) external payable returns (uint256); 33 | } 34 | 35 | /** 36 | * @title Example implementation of a custom ERC20 token to be deployed on L1 37 | */ 38 | contract L1Token is Ownable, ICustomToken, ERC20 { 39 | address public l1GatewayAddress; 40 | address public routerAddress; 41 | bool private shouldRegisterGateway; 42 | 43 | /** 44 | * @dev See {ERC20-constructor} and {Ownable-constructor} 45 | * An initial supply amount is passed, which is preminted to the deployer. 46 | * @param l1GatewayAddress_ address of the L1 custom gateway 47 | * @param routerAddress_ address of the L1GatewayRouter 48 | * @param initialSupply initial supply amount to be minted to the deployer 49 | */ 50 | constructor(address l1GatewayAddress_, address routerAddress_, uint256 initialSupply) ERC20("L1CustomToken", "LCT") { 51 | l1GatewayAddress = l1GatewayAddress_; 52 | routerAddress = routerAddress_; 53 | _mint(msg.sender, initialSupply * 10 ** decimals()); 54 | } 55 | 56 | /// @dev we only set shouldRegisterGateway to true when in `registerTokenOnL2` 57 | function isArbitrumEnabled() external view override returns (uint8) { 58 | require(shouldRegisterGateway, "NOT_EXPECTED_CALL"); 59 | return uint8(0xb1); 60 | } 61 | 62 | /** 63 | * @dev See {ICustomToken-registerTokenOnL2} 64 | * In this case, we don't need to call IL1CustomGateway.registerTokenToL2, because our 65 | * custom gateway works for a single token it already knows. 66 | */ 67 | function registerTokenOnL2( 68 | address, /* l2CustomTokenAddress */ 69 | uint256, /* maxSubmissionCostForCustomGateway */ 70 | uint256 maxSubmissionCostForRouter, 71 | uint256, /* maxGasForCustomGateway */ 72 | uint256 maxGasForRouter, 73 | uint256 gasPriceBid, 74 | uint256, /* valueForGateway */ 75 | uint256 valueForRouter, 76 | address creditBackAddress 77 | ) public override payable onlyOwner { 78 | // we temporarily set `shouldRegisterGateway` to true for the callback in registerTokenToL2 to succeed 79 | bool prev = shouldRegisterGateway; 80 | shouldRegisterGateway = true; 81 | 82 | IL1GatewayRouter(routerAddress).setGateway{ value: valueForRouter }( 83 | l1GatewayAddress, 84 | maxGasForRouter, 85 | gasPriceBid, 86 | maxSubmissionCostForRouter, 87 | creditBackAddress 88 | ); 89 | 90 | shouldRegisterGateway = prev; 91 | } 92 | 93 | /// @dev See {ERC20-transferFrom} 94 | function transferFrom( 95 | address sender, 96 | address recipient, 97 | uint256 amount 98 | ) public override(ICustomToken, ERC20) returns (bool) { 99 | return super.transferFrom(sender, recipient, amount); 100 | } 101 | 102 | /// @dev See {ERC20-balanceOf} 103 | function balanceOf(address account) public view override(ICustomToken, ERC20) returns (uint256) { 104 | return super.balanceOf(account); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/contracts/L2CustomGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "./interfaces/ICustomGateway.sol"; 5 | import "./CrosschainMessenger.sol"; 6 | import "./interfaces/IArbToken.sol"; 7 | import "@openzeppelin/contracts/access/Ownable.sol"; 8 | 9 | /** 10 | * @title Example implementation of a custom gateway to be deployed on L2 11 | * @dev Inheritance of Ownable is optional. In this case we use it to call the function setTokenBridgeInformation 12 | * and simplify the test 13 | */ 14 | contract L2CustomGateway is IL2CustomGateway, L2CrosschainMessenger, Ownable { 15 | // Exit number (used for tradeable exits) 16 | uint256 public exitNum; 17 | 18 | // Token bridge state variables 19 | address public l1CustomToken; 20 | address public l2CustomToken; 21 | address public l1Gateway; 22 | address public router; 23 | 24 | // Custom functionality 25 | bool public allowsWithdrawals; 26 | 27 | /** 28 | * Contract constructor, sets the L2 router to be used in the contract's functions 29 | * @param router_ L2GatewayRouter address 30 | */ 31 | constructor(address router_) { 32 | router = router_; 33 | allowsWithdrawals = false; 34 | } 35 | 36 | /** 37 | * Sets the information needed to use the gateway. To simplify the process of testing, this function can be called once 38 | * by the owner of the contract to set these addresses. 39 | * @param l1CustomToken_ address of the custom token on L1 40 | * @param l2CustomToken_ address of the custom token on L2 41 | * @param l1Gateway_ address of the counterpart gateway (on L1) 42 | */ 43 | function setTokenBridgeInformation( 44 | address l1CustomToken_, 45 | address l2CustomToken_, 46 | address l1Gateway_ 47 | ) public onlyOwner { 48 | require(l1CustomToken == address(0), "Token bridge information already set"); 49 | l1CustomToken = l1CustomToken_; 50 | l2CustomToken = l2CustomToken_; 51 | l1Gateway = l1Gateway_; 52 | 53 | // Allows withdrawals after the information has been set 54 | allowsWithdrawals = true; 55 | } 56 | 57 | /// @dev See {ICustomGateway-outboundTransfer} 58 | function outboundTransfer( 59 | address l1Token, 60 | address to, 61 | uint256 amount, 62 | bytes calldata data 63 | ) public payable returns (bytes memory) { 64 | return outboundTransfer(l1Token, to, amount, 0, 0, data); 65 | } 66 | 67 | /// @dev See {ICustomGateway-outboundTransfer} 68 | function outboundTransfer( 69 | address l1Token, 70 | address to, 71 | uint256 amount, 72 | uint256, /* _maxGas */ 73 | uint256, /* _gasPriceBid */ 74 | bytes calldata data 75 | ) public payable override returns (bytes memory res) { 76 | // Only execute if deposits are allowed 77 | require(allowsWithdrawals == true, "Withdrawals are currently disabled"); 78 | 79 | // The function is marked as payable to conform to the inheritance setup 80 | // This particular code path shouldn't have a msg.value > 0 81 | require(msg.value == 0, "NO_VALUE"); 82 | 83 | // Only allow the custom token to be bridged through this gateway 84 | require(l1Token == l1CustomToken, "Token is not allowed through this gateway"); 85 | 86 | (address from, bytes memory extraData) = _parseOutboundData(data); 87 | 88 | // The inboundEscrowAndCall functionality has been disabled, so no data is allowed 89 | require(extraData.length == 0, "EXTRA_DATA_DISABLED"); 90 | 91 | // Burns L2 tokens in order to release escrowed L1 tokens 92 | IArbToken(l2CustomToken).bridgeBurn(from, amount); 93 | 94 | // Current exit number for this operation 95 | uint256 currExitNum = exitNum++; 96 | 97 | // We override the res field to save on the stack 98 | res = getOutboundCalldata(l1Token, from, to, amount, extraData); 99 | 100 | // Trigger the crosschain message 101 | uint256 id = _sendTxToL1( 102 | from, 103 | l1Gateway, 104 | res 105 | ); 106 | 107 | emit WithdrawalInitiated(l1Token, from, to, id, currExitNum, amount); 108 | return abi.encode(id); 109 | } 110 | 111 | /// @dev See {ICustomGateway-finalizeInboundTransfer} 112 | function finalizeInboundTransfer( 113 | address l1Token, 114 | address from, 115 | address to, 116 | uint256 amount, 117 | bytes calldata data 118 | ) public payable override onlyCounterpartGateway(l1Gateway) { 119 | // Only allow the custom token to be bridged through this gateway 120 | require(l1Token == l1CustomToken, "Token is not allowed through this gateway"); 121 | 122 | // Abi decode may revert, but the encoding is done by L1 gateway, so we trust it 123 | (, bytes memory callHookData) = abi.decode(data, (bytes, bytes)); 124 | if (callHookData.length != 0) { 125 | // callHookData should always be 0 since inboundEscrowAndCall is disabled 126 | callHookData = bytes(""); 127 | } 128 | 129 | // Mints L2 tokens 130 | IArbToken(l2CustomToken).bridgeMint(to, amount); 131 | 132 | emit DepositFinalized(l1Token, from, to, amount); 133 | } 134 | 135 | /// @dev See {ICustomGateway-getOutboundCalldata} 136 | function getOutboundCalldata( 137 | address l1Token, 138 | address from, 139 | address to, 140 | uint256 amount, 141 | bytes memory data 142 | ) public view override returns (bytes memory outboundCalldata) { 143 | outboundCalldata = abi.encodeWithSelector( 144 | ICustomGateway.finalizeInboundTransfer.selector, 145 | l1Token, 146 | from, 147 | to, 148 | amount, 149 | abi.encode(exitNum, data) 150 | ); 151 | 152 | return outboundCalldata; 153 | } 154 | 155 | /// @dev See {ICustomGateway-calculateL2TokenAddress} 156 | function calculateL2TokenAddress(address l1Token) public view override returns (address) { 157 | if (l1Token == l1CustomToken) { 158 | return l2CustomToken; 159 | } 160 | 161 | return address(0); 162 | } 163 | 164 | /// @dev See {ICustomGateway-counterpartGateway} 165 | function counterpartGateway() public view override returns (address) { 166 | return l1Gateway; 167 | } 168 | 169 | /** 170 | * Parse data received in outboundTransfer 171 | * @param data encoded data received 172 | * @return from account that initiated the deposit, 173 | * extraData decoded data 174 | */ 175 | function _parseOutboundData(bytes memory data) 176 | internal 177 | view 178 | returns ( 179 | address from, 180 | bytes memory extraData 181 | ) 182 | { 183 | if (msg.sender == router) { 184 | // Router encoded 185 | (from, extraData) = abi.decode(data, (address, bytes)); 186 | } else { 187 | from = msg.sender; 188 | extraData = data; 189 | } 190 | } 191 | 192 | // -------------------- 193 | // Custom methods 194 | // -------------------- 195 | /** 196 | * Disables the ability to deposit funds 197 | */ 198 | function disableWithdrawals() external onlyOwner { 199 | allowsWithdrawals = false; 200 | } 201 | 202 | /** 203 | * Enables the ability to deposit funds 204 | */ 205 | function enableWithdrawals() external onlyOwner { 206 | require(l1CustomToken != address(0), "Token bridge information has not been set yet"); 207 | allowsWithdrawals = true; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/contracts/L2Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "./interfaces/IArbToken.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | /** 8 | * @title Example implementation of a custom ERC20 token to be deployed on L2 9 | */ 10 | contract L2Token is ERC20, IArbToken { 11 | address public l2GatewayAddress; 12 | address public override l1Address; 13 | 14 | modifier onlyL2Gateway() { 15 | require(msg.sender == l2GatewayAddress, "NOT_GATEWAY"); 16 | _; 17 | } 18 | 19 | /** 20 | * @dev See {ERC20-constructor} 21 | * @param l2GatewayAddress_ address of the L2 custom gateway 22 | * @param l1TokenAddress_ address of the custom token deployed on L1 23 | */ 24 | constructor(address l2GatewayAddress_, address l1TokenAddress_) ERC20("L2CustomToken", "LCT") { 25 | l2GatewayAddress = l2GatewayAddress_; 26 | l1Address = l1TokenAddress_; 27 | } 28 | 29 | /** 30 | * Should increase token supply by amount, and should only be callable by the L2Gateway. 31 | */ 32 | function bridgeMint(address account, uint256 amount) external override onlyL2Gateway { 33 | _mint(account, amount); 34 | } 35 | 36 | /** 37 | * Should decrease token supply by amount, and should only be callable by the L2Gateway. 38 | */ 39 | function bridgeBurn(address account, uint256 amount) external override onlyL2Gateway { 40 | _burn(account, amount); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/contracts/interfaces/IArbToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @title Minimum expected interface for L2 token that interacts with the L2 token bridge (this is the interface necessary 6 | * for a custom token that interacts with the bridge). 7 | */ 8 | interface IArbToken { 9 | /** 10 | * Should increase token supply by amount, and should only be callable by the L1 gateway. 11 | * @param account Account to be credited with the tokens in the L2 12 | * @param amount Token amount 13 | */ 14 | function bridgeMint(address account, uint256 amount) external; 15 | 16 | /** 17 | * Should decrease token supply by amount. 18 | * @param account Account whose tokens will be burned in the L2, to be released on L1 19 | * @param amount Token amount 20 | */ 21 | function bridgeBurn(address account, uint256 amount) external; 22 | 23 | /** 24 | * @return address of layer 1 token 25 | */ 26 | function l1Address() external view returns (address); 27 | } -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/contracts/interfaces/ICustomGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @title Minimum expected interface for a custom gateway 6 | */ 7 | interface ICustomGateway { 8 | /** 9 | * Initiates an ERC20 token transfer between Ethereum and Arbitrum using the registered or otherwise default gateway 10 | * @dev Some legacy gateway might not have the outboundTransferCustomRefund method and will revert, in such case use outboundTransfer instead 11 | * @dev L2 address alias will not be applied to the following types of addresses on L1: 12 | * - an externally-owned account 13 | * - a contract in construction 14 | * - an address where a contract will be created 15 | * - an address where a contract lived, but was destroyed 16 | * @param l1Token L1 address of ERC20 17 | * @param to Account to be credited with the tokens in the counterpart layer (can be the user's account or a contract in the destination layer), not subject to L2 aliasing 18 | In case of deposits (L1->L2), this account, or its L2 alias if it has code in L1, will also be able to cancel the retryable ticket and receive callvalue refund 19 | * @param amount Token Amount 20 | * @param maxGas Max gas deducted from user's L2 balance to cover L2 execution (only needed for deposits, L1->L2) 21 | * @param gasPriceBid Gas price for L2 execution (only needed for deposits, L1->L2) 22 | * @param data encoded data from router and user 23 | * @return res abi encoded inbox sequence number in case of deposits (L1->L2) or id from ArbSys call in case of withdrawals (L2->L1) 24 | */ 25 | function outboundTransfer( 26 | address l1Token, 27 | address to, 28 | uint256 amount, 29 | uint256 maxGas, 30 | uint256 gasPriceBid, 31 | bytes calldata data 32 | ) external payable returns (bytes memory); 33 | 34 | /** 35 | * In case of deposits (L1->L2), mints tokens on L2 upon L1 deposit. In case of withdrawals (L2->L1), release tokens on L1. 36 | * @dev Callable only by the L1Gateway.outboundTransfer method, or L2Gateway.outboundTransfer method 37 | * @param l1Token L1 address of ERC20 38 | * @param from account that initiated the deposit in the L1, or the withdrawal on L2 39 | * @param to account to be credited with the tokens in the destination layer 40 | * @param amount token amount to be credited to the user 41 | * @param data encoded additional callhook data, and exitNum (sequentially increasing exit counter determined by the L2Gateway) for withdrawals (L2->L1) 42 | */ 43 | function finalizeInboundTransfer( 44 | address l1Token, 45 | address from, 46 | address to, 47 | uint256 amount, 48 | bytes calldata data 49 | ) external payable; 50 | 51 | /** 52 | * Returns the data to be added to the outboundTransfer calls 53 | * @param l1Token L1 address of ERC20 54 | * @param from account that will initiate the deposit in the L1, or the withdrawal on L2 55 | * @param to account to be credited with the tokens in the destination layer 56 | * @param amount token amount to be credited to the user 57 | * @param data encoded additional callhook data, and exitNum (sequentially increasing exit counter determined by the L2Gateway) for withdrawals (L2->L1) 58 | */ 59 | function getOutboundCalldata( 60 | address l1Token, 61 | address from, 62 | address to, 63 | uint256 amount, 64 | bytes memory data 65 | ) external view returns (bytes memory); 66 | 67 | /** 68 | * Calculate the address used when bridging an ERC20 token 69 | * @dev the L1 and L2 address oracles may not always be in sync. 70 | * For example, a custom token may have been registered but not deploy or the contract self destructed. 71 | * @param l1Token address of L1 token 72 | * @return L2 address of a bridged ERC20 token 73 | */ 74 | function calculateL2TokenAddress(address l1Token) external view returns (address); 75 | 76 | /** 77 | * Returns the address of the counterpart gateway 78 | */ 79 | function counterpartGateway() external view returns (address); 80 | } 81 | 82 | /** 83 | * @title Minimum expected interface for a custom gateway to be deployed on L1 84 | */ 85 | interface IL1CustomGateway is ICustomGateway { 86 | /** 87 | * Emitted when calling outboundTransfer 88 | * @param l1Token L1 address of ERC20 89 | * @param from account that initiated the deposit in the L1 90 | * @param to account to be credited with the tokens in the L2 91 | * @param sequenceNumber id for retryable ticket 92 | * @param amount token amount to be credited to the user 93 | */ 94 | event DepositInitiated( 95 | address l1Token, 96 | address indexed from, 97 | address indexed to, 98 | uint256 indexed sequenceNumber, 99 | uint256 amount 100 | ); 101 | 102 | /** 103 | * Emitted when receiving the call in finalizeInboundTransfer 104 | * @param l1Token L1 address of ERC20 105 | * @param from account that initiated the deposit in the L1 106 | * @param to account that was credited with the tokens in the L2 107 | * @param exitNum sequentially increasing exit counter determined by the L2Gateway 108 | * @param amount token amount to be credited to the user 109 | */ 110 | event WithdrawalFinalized( 111 | address l1Token, 112 | address indexed from, 113 | address indexed to, 114 | uint256 indexed exitNum, 115 | uint256 amount 116 | ); 117 | 118 | /** 119 | * Initiates an ERC20 token transfer between Ethereum and Arbitrum using the registered or otherwise default gateway 120 | * @dev Some legacy gateway might not have the outboundTransferCustomRefund method and will revert, in such case use outboundTransfer instead 121 | * @dev L2 address alias will not be applied to the following types of addresses on L1: 122 | * - an externally-owned account 123 | * - a contract in construction 124 | * - an address where a contract will be created 125 | * - an address where a contract lived, but was destroyed 126 | * @param l1Token L1 address of ERC20 127 | * @param refundTo Account, or its L2 alias if it have code in L1, to be credited with excess gas refund in L2 128 | * @param to Account to be credited with the tokens in the counterpart layer (can be the user's account or a contract in the destination layer), not subject to L2 aliasing 129 | In case of deposits (L1->L2), this account, or its L2 alias if it has code in L1, will also be able to cancel the retryable ticket and receive callvalue refund 130 | * @param amount Token Amount 131 | * @param maxGas Max gas deducted from user's L2 balance to cover L2 execution (only needed for deposits, L1->L2) 132 | * @param gasPriceBid Gas price for L2 execution (only needed for deposits, L1->L2) 133 | * @param data encoded data from router and user 134 | * @return res abi encoded inbox sequence number 135 | */ 136 | function outboundTransferCustomRefund( 137 | address l1Token, 138 | address refundTo, 139 | address to, 140 | uint256 amount, 141 | uint256 maxGas, 142 | uint256 gasPriceBid, 143 | bytes calldata data 144 | ) external payable returns (bytes memory); 145 | } 146 | 147 | /** 148 | * @title Minimum expected interface for a custom gateway to be deployed on L2 149 | */ 150 | interface IL2CustomGateway is ICustomGateway { 151 | /** 152 | * Emitted when calling outboundTransfer 153 | * @param l1Token L1 address of ERC20 154 | * @param from account that initiated the deposit in the L1 155 | * @param to account that was credited with the tokens in the L2 156 | * @param l2ToL1Id unique identifier for the L2-to-L1 transaction 157 | * @param exitNum sequentially increasing exit counter determined by the L2Gateway 158 | * @param amount token amount to be credited to the user 159 | */ 160 | event WithdrawalInitiated( 161 | address l1Token, 162 | address indexed from, 163 | address indexed to, 164 | uint256 indexed l2ToL1Id, 165 | uint256 exitNum, 166 | uint256 amount 167 | ); 168 | 169 | /** 170 | * Emitted when receiving the call in finalizeInboundTransfer 171 | * @param l1Token L1 address of ERC20 172 | * @param from account that initiated the deposit in the L1 173 | * @param to account to be credited with the tokens in the L2 174 | * @param amount token amount to be credited to the user 175 | */ 176 | event DepositFinalized( 177 | address indexed l1Token, 178 | address indexed from, 179 | address indexed to, 180 | uint256 amount 181 | ); 182 | } -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/contracts/interfaces/ICustomToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | interface ArbitrumEnabledToken { 5 | /// Should return `0xb1` if token is enabled for arbitrum gateways 6 | function isArbitrumEnabled() external view returns (uint8); 7 | } 8 | 9 | /** 10 | * @title Minimum expected interface for an L1 custom token 11 | */ 12 | interface ICustomToken is ArbitrumEnabledToken { 13 | /** 14 | * Should make an external call to L2GatewayRouter.setGateway and probably L1CustomGateway.registerTokenToL2 15 | * @param l2CustomTokenAddress address of the custom token on L2 16 | * @param maxSubmissionCostForCustomBridge max gas deducted from user's L2 balance to cover submission fee for registerTokenToL2 17 | * @param maxSubmissionCostForRouter max gas deducted from user's L2 balance to cover submission fee for setGateway 18 | * @param maxGasForCustomBridge max gas deducted from user's L2 balance to cover L2 execution of registerTokenToL2 19 | * @param maxGasForRouter max gas deducted from user's L2 balance to cover L2 execution of setGateway 20 | * @param gasPriceBid gas price for L2 execution 21 | * @param valueForGateway callvalue sent on call to registerTokenToL2 22 | * @param valueForRouter callvalue sent on call to setGateway 23 | * @param creditBackAddress address for crediting back overpayment of maxSubmissionCosts 24 | */ 25 | function registerTokenOnL2( 26 | address l2CustomTokenAddress, 27 | uint256 maxSubmissionCostForCustomBridge, 28 | uint256 maxSubmissionCostForRouter, 29 | uint256 maxGasForCustomBridge, 30 | uint256 maxGasForRouter, 31 | uint256 gasPriceBid, 32 | uint256 valueForGateway, 33 | uint256 valueForRouter, 34 | address creditBackAddress 35 | ) external payable; 36 | 37 | /// @dev See {IERC20-transferFrom} 38 | function transferFrom( 39 | address sender, 40 | address recipient, 41 | uint256 amount 42 | ) external returns (bool); 43 | 44 | /// @dev See {IERC20-balanceOf} 45 | function balanceOf(address account) external view returns (uint256); 46 | } 47 | -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | const { hardhatConfig } = require('arb-shared-dependencies') 3 | 4 | module.exports = hardhatConfig 5 | -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-gateway-bridging", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "hardhat compile", 6 | "exec": "hardhat run scripts/exec.js" 7 | }, 8 | "devDependencies": { 9 | "@nomiclabs/hardhat-ethers": "^2.0.2", 10 | "@openzeppelin/contracts": "^4.8.3", 11 | "chai": "^4.3.4", 12 | "ethers": "^5.1.2", 13 | "hardhat": "^2.6.6" 14 | }, 15 | "dependencies": { 16 | "@arbitrum/sdk": "^v3.1.2", 17 | "dotenv": "^8.2.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/custom-gateway-bridging/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require('hardhat') 2 | const { providers, Wallet, BigNumber } = require('ethers') 3 | const { 4 | getL2Network, 5 | addDefaultLocalNetwork, 6 | L1ToL2MessageStatus, 7 | } = require('@arbitrum/sdk') 8 | const { 9 | arbLog, 10 | arbLogTitle, 11 | requireEnvVariables, 12 | } = require('arb-shared-dependencies') 13 | const { 14 | AdminErc20Bridger, 15 | Erc20Bridger, 16 | } = require('@arbitrum/sdk/dist/lib/assetBridger/erc20Bridger') 17 | const { expect } = require('chai') 18 | require('dotenv').config() 19 | requireEnvVariables(['DEVNET_PRIVKEY', 'L1RPC', 'L2RPC']) 20 | 21 | /** 22 | * Set up: instantiate L1 / L2 wallets connected to providers 23 | */ 24 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 25 | 26 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 27 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 28 | 29 | const l1Wallet = new Wallet(walletPrivateKey, l1Provider) 30 | const l2Wallet = new Wallet(walletPrivateKey, l2Provider) 31 | 32 | /** 33 | * Set the initial supply of L1 token that we want to bridge 34 | * Note that you can change the value. 35 | * We also set the amount we want to send in the test deposit and withdraw 36 | */ 37 | const premint = ethers.utils.parseEther('1000') 38 | const tokenAmountToDeposit = BigNumber.from(50) 39 | const tokenAmountToWithdraw = BigNumber.from(30) 40 | 41 | const main = async () => { 42 | await arbLog( 43 | 'Setting up your token with a custom gateway using Arbitrum SDK library' 44 | ) 45 | 46 | /** 47 | * Add the default local network configuration to the SDK 48 | * to allow this script to run on a local node 49 | */ 50 | addDefaultLocalNetwork() 51 | 52 | /** 53 | * Use l2Network to create an Arbitrum SDK AdminErc20Bridger instance 54 | * We'll use AdminErc20Bridger for its convenience methods around registering tokens to a custom gateway 55 | */ 56 | const l2Network = await getL2Network(l2Provider) 57 | const erc20Bridger = new Erc20Bridger(l2Network) 58 | const adminTokenBridger = new AdminErc20Bridger(l2Network) 59 | const l1Router = l2Network.tokenBridge.l1GatewayRouter 60 | const l2Router = l2Network.tokenBridge.l2GatewayRouter 61 | const inbox = l2Network.ethBridge.inbox 62 | 63 | arbLogTitle('Deployment of custom gateways and tokens') 64 | 65 | /** 66 | * Deploy our custom gateway to L1 67 | */ 68 | const L1CustomGateway = await await ethers.getContractFactory( 69 | 'L1CustomGateway', 70 | l1Wallet 71 | ) 72 | console.log('Deploying custom gateway to L1') 73 | const l1CustomGateway = await L1CustomGateway.deploy(l1Router, inbox) 74 | await l1CustomGateway.deployed() 75 | console.log(`Custom gateway is deployed to L1 at ${l1CustomGateway.address}`) 76 | const l1CustomGatewayAddress = l1CustomGateway.address 77 | 78 | /** 79 | * Deploy our custom gateway to L2 80 | */ 81 | const L2CustomGateway = await await ethers.getContractFactory( 82 | 'L2CustomGateway', 83 | l2Wallet 84 | ) 85 | console.log('Deploying custom gateway to L2') 86 | const l2CustomGateway = await L2CustomGateway.deploy(l2Router) 87 | await l2CustomGateway.deployed() 88 | console.log(`Custom gateway is deployed to L2 at ${l2CustomGateway.address}`) 89 | const l2CustomGatewayAddress = l2CustomGateway.address 90 | 91 | /** 92 | * Deploy our custom token smart contract to L1 93 | * We give the custom token contract the address of l1CustomGateway and l1GatewayRouter as well as the initial supply (premint) 94 | */ 95 | const L1CustomToken = await await ethers.getContractFactory( 96 | 'L1Token', 97 | l1Wallet 98 | ) 99 | console.log('Deploying custom token to L1') 100 | const l1CustomToken = await L1CustomToken.deploy( 101 | l1CustomGatewayAddress, 102 | l1Router, 103 | premint 104 | ) 105 | await l1CustomToken.deployed() 106 | console.log(`custom token is deployed to L1 at ${l1CustomToken.address}`) 107 | 108 | /** 109 | * Deploy our custom token smart contract to L2 110 | * We give the custom token contract the address of l2CustomGateway and our l1CustomToken 111 | */ 112 | const L2CustomToken = await await ethers.getContractFactory( 113 | 'L2Token', 114 | l2Wallet 115 | ) 116 | console.log('Deploying custom token to L2') 117 | const l2CustomToken = await L2CustomToken.deploy( 118 | l2CustomGatewayAddress, 119 | l1CustomToken.address 120 | ) 121 | await l2CustomToken.deployed() 122 | console.log(`custom token is deployed to L2 at ${l2CustomToken.address}`) 123 | 124 | /** 125 | * Set the token bridge information on the custom gateways 126 | * (This is an optional step that depends on your configuration. In this example, we've added one-shot 127 | * functions on the custom gateways to set the token bridge addresses in a second step. This could be 128 | * avoided if you are using proxies or the opcode CREATE2 for example) 129 | */ 130 | console.log('Setting token bridge information on L1CustomGateway:') 131 | const setTokenBridgeInfoOnL1 = 132 | await l1CustomGateway.setTokenBridgeInformation( 133 | l1CustomToken.address, 134 | l2CustomToken.address, 135 | l2CustomGatewayAddress 136 | ) 137 | 138 | const setTokenBridgeInfoOnL1Rec = await setTokenBridgeInfoOnL1.wait() 139 | console.log( 140 | `Token bridge information set on L1CustomGateway! L1 receipt is: ${setTokenBridgeInfoOnL1Rec.transactionHash}` 141 | ) 142 | 143 | console.log('Setting token bridge information on L2CustomGateway:') 144 | const setTokenBridgeInfoOnL2 = 145 | await l2CustomGateway.setTokenBridgeInformation( 146 | l1CustomToken.address, 147 | l2CustomToken.address, 148 | l1CustomGatewayAddress 149 | ) 150 | 151 | const setTokenBridgeInfoOnL2Rec = await setTokenBridgeInfoOnL2.wait() 152 | console.log( 153 | `Token bridge information set on L2CustomGateway! L2 receipt is: ${setTokenBridgeInfoOnL2Rec.transactionHash}` 154 | ) 155 | 156 | /** 157 | * Register the custom gateway as the gateway of our custom token 158 | */ 159 | console.log('Registering custom token on L2:') 160 | const registerTokenTx = await adminTokenBridger.registerCustomToken( 161 | l1CustomToken.address, 162 | l2CustomToken.address, 163 | l1Wallet, 164 | l2Provider 165 | ) 166 | 167 | const registerTokenRec = await registerTokenTx.wait() 168 | console.log( 169 | `Registering token txn confirmed on L1! 🙌 L1 receipt is: ${registerTokenRec.transactionHash}.` 170 | ) 171 | console.log( 172 | `Waiting for L2 retryable (takes 10-15 minutes); current time: ${new Date().toTimeString()})` 173 | ) 174 | 175 | /** 176 | * The L1 side is confirmed; now we listen and wait for the L2 side to be executed; we can do this by computing the expected txn hash of the L2 transaction. 177 | * To compute this txn hash, we need our message's "sequence numbers", unique identifiers of each L1 to L2 message. 178 | * We'll fetch them from the event logs with a helper method. 179 | */ 180 | const l1ToL2Msgs = await registerTokenRec.getL1ToL2Messages(l2Provider) 181 | 182 | /** 183 | * In this case, the registerTokenOnL2 method creates 1 L1-to-L2 messages to set the L1 token to the Custom Gateway via the Router 184 | * Here, We check if that message is redeemed on L2 185 | */ 186 | expect(l1ToL2Msgs.length, 'Should be 1 message.').to.eq(1) 187 | 188 | const setGateways = await l1ToL2Msgs[0].waitForStatus() 189 | expect(setGateways.status, 'Set gateways not redeemed.').to.eq( 190 | L1ToL2MessageStatus.REDEEMED 191 | ) 192 | 193 | console.log( 194 | 'Your custom token and gateways are now registered on the token bridge 🥳!' 195 | ) 196 | 197 | /** 198 | * We now test a deposit to verify the gateway is working as intended 199 | */ 200 | arbLogTitle('Test deposit') 201 | 202 | const expectedL1GatewayAddress = await erc20Bridger.getL1GatewayAddress( 203 | l1CustomToken.address, 204 | l1Provider 205 | ) 206 | expect( 207 | expectedL1GatewayAddress, 208 | `Expected L1 gateway address is not right: ${expectedL1GatewayAddress} but expected ${l1CustomGatewayAddress}` 209 | ).to.eq(l1CustomGatewayAddress) 210 | 211 | const initialBridgeTokenBalance = await l1CustomToken.balanceOf( 212 | expectedL1GatewayAddress 213 | ) 214 | 215 | /** 216 | * Because the token might have decimals, we update the amount to deposit taking into account those decimals 217 | */ 218 | const tokenDecimals = await l1CustomToken.decimals() 219 | const tokenDepositAmount = tokenAmountToDeposit.mul( 220 | BigNumber.from(10).pow(tokenDecimals) 221 | ) 222 | 223 | /** 224 | * Approving the l1CustomGateway to transfer the tokens being deposited 225 | */ 226 | console.log('Approving L1CustomGateway:') 227 | const approveTx = await erc20Bridger.approveToken({ 228 | l1Signer: l1Wallet, 229 | erc20L1Address: l1CustomToken.address, 230 | }) 231 | 232 | const approveRec = await approveTx.wait() 233 | console.log( 234 | `You successfully allowed the Arbitrum Bridge to spend L1Token. Tx hash: ${approveRec.transactionHash}` 235 | ) 236 | 237 | /** 238 | * Deposit L1Token to L2 using erc20Bridger. This will escrow funds in the custom gateway contract on L1, and send a message to mint tokens on L2 239 | */ 240 | console.log('Transferring L1Token to L2:') 241 | const depositTx = await erc20Bridger.deposit({ 242 | amount: tokenDepositAmount, 243 | erc20L1Address: l1CustomToken.address, 244 | l1Signer: l1Wallet, 245 | l2Provider: l2Provider, 246 | }) 247 | 248 | /** 249 | * Now we wait for L1 and L2 side of transactions to be confirmed 250 | */ 251 | console.log( 252 | `Deposit initiated: waiting for L2 retryable (takes 10-15 minutes; current time: ${new Date().toTimeString()}) ` 253 | ) 254 | const depositRec = await depositTx.wait() 255 | const l2Result = await depositRec.waitForL2(l2Provider) 256 | 257 | /** 258 | * The `complete` boolean tells us if the l1 to l2 message was successful 259 | */ 260 | l2Result.complete 261 | ? console.log( 262 | `L2 message successful: status: ${L1ToL2MessageStatus[l2Result.status]}` 263 | ) 264 | : console.log( 265 | `L2 message failed: status ${L1ToL2MessageStatus[l2Result.status]}` 266 | ) 267 | 268 | /** 269 | * Get the Bridge token balance 270 | */ 271 | const finalBridgeTokenBalance = await l1CustomToken.balanceOf( 272 | expectedL1GatewayAddress 273 | ) 274 | 275 | /** 276 | * Check if Bridge balance has been updated correctly 277 | */ 278 | expect( 279 | initialBridgeTokenBalance 280 | .add(tokenDepositAmount) 281 | .eq(finalBridgeTokenBalance), 282 | 'bridge balance not updated after L1 token deposit txn' 283 | ).to.be.true 284 | 285 | /** 286 | * Check if our l2Wallet DappToken balance has been updated correctly 287 | * To do so, we use erc20Bridger to get the l2Token address and contract 288 | */ 289 | const l2TokenAddress = await erc20Bridger.getL2ERC20Address( 290 | l1CustomToken.address, 291 | l1Provider 292 | ) 293 | expect( 294 | l2TokenAddress, 295 | `Expected L2 token address is not right: ${l2TokenAddress} but expected ${l2CustomToken.address}` 296 | ).to.eq(l2CustomToken.address) 297 | 298 | const testWalletL2Balance = await l2CustomToken.balanceOf(l2Wallet.address) 299 | expect( 300 | testWalletL2Balance.eq(tokenDepositAmount), 301 | 'l2 wallet not updated after deposit' 302 | ).to.be.true 303 | 304 | /** 305 | * We finally test a withdrawal to verify the L2 gateway is also working as intended 306 | */ 307 | arbLogTitle('Test withdrawal') 308 | 309 | /** 310 | * Because the token might have decimals, we update the amount to withdraw taking into account those decimals 311 | */ 312 | const tokenWithdrawAmount = tokenAmountToWithdraw.mul( 313 | BigNumber.from(10).pow(tokenDecimals) 314 | ) 315 | 316 | /** 317 | * Withdraw L2Token to L1 using erc20Bridger. This will burn tokens on L2 and release funds in the custom gateway contract on L1 318 | */ 319 | console.log('Withdrawing L2Token to L1:') 320 | const withdrawTx = await erc20Bridger.withdraw({ 321 | amount: tokenWithdrawAmount, 322 | destinationAddress: l1Wallet.address, 323 | erc20l1Address: l1CustomToken.address, 324 | l2Signer: l2Wallet, 325 | }) 326 | const withdrawRec = await withdrawTx.wait() 327 | console.log(`Token withdrawal initiated! 🥳 ${withdrawRec.transactionHash}`) 328 | 329 | /** 330 | * And with that, our withdrawal is initiated. 331 | * Any time after the transaction's assertion is confirmed (around 7 days), funds can be transferred out of the bridge via the outbox contract 332 | * We'll check our l2Wallet L2CustomToken balance here: 333 | */ 334 | const l2WalletBalance = await l2CustomToken.balanceOf(l2Wallet.address) 335 | 336 | expect( 337 | l2WalletBalance.add(tokenWithdrawAmount).eq(tokenDepositAmount), 338 | 'token withdraw balance not deducted' 339 | ).to.be.true 340 | 341 | console.log( 342 | `To to claim funds (after dispute period), see outbox-execute repo 🤞🏻` 343 | ) 344 | 345 | /** 346 | * As a final test, we remove the ability to do deposits and test the reverting call 347 | */ 348 | arbLogTitle('Test custom functionality (Disable deposits)') 349 | const disableTx = await l1CustomGateway.disableDeposits() 350 | await disableTx.wait() 351 | 352 | console.log('Trying to deposit tokens after disabling deposits') 353 | try { 354 | await erc20Bridger.deposit({ 355 | amount: tokenDepositAmount, 356 | erc20L1Address: l1CustomToken.address, 357 | l1Signer: l1Wallet, 358 | l2Provider: l2Provider, 359 | }) 360 | return 361 | } catch (error) { 362 | console.log('Transaction failed as expected') 363 | } 364 | 365 | const enableTx = await l1CustomGateway.enableDeposits() 366 | await enableTx.wait() 367 | 368 | console.log('Trying to deposit after enabling deposits back:') 369 | const depositEnabledTx = await erc20Bridger.deposit({ 370 | amount: tokenDepositAmount, 371 | erc20L1Address: l1CustomToken.address, 372 | l1Signer: l1Wallet, 373 | l2Provider: l2Provider, 374 | }) 375 | const depositEnabledRec = await depositEnabledTx.wait() 376 | console.log( 377 | `Deposit initiated: waiting for L2 retryable (takes 10-15 minutes; current time: ${new Date().toTimeString()}) ` 378 | ) 379 | const l2FinalResult = await depositEnabledRec.waitForL2(l2Provider) 380 | 381 | /** 382 | * The `complete` boolean tells us if the l1 to l2 message was successful 383 | */ 384 | l2FinalResult.complete 385 | ? console.log( 386 | `L2 message successful: status: ${ 387 | L1ToL2MessageStatus[l2FinalResult.status] 388 | }` 389 | ) 390 | : console.log( 391 | `L2 message failed: status ${L1ToL2MessageStatus[l2FinalResult.status]}` 392 | ) 393 | } 394 | 395 | main() 396 | .then(() => process.exit(0)) 397 | .catch(error => { 398 | console.error(error) 399 | process.exit(1) 400 | }) 401 | -------------------------------------------------------------------------------- /packages/custom-token-bridging/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC="" -------------------------------------------------------------------------------- /packages/custom-token-bridging/README.md: -------------------------------------------------------------------------------- 1 | # custom-token-bridging Tutorial 2 | 3 | There are some tokens with requirements beyond what are offered via our StandardERC20 gateway. `custom-token-bridging` demonstrates how to get these custom tokens set up to use our Generic-Custom gateway. 4 | 5 | For more info on bridging assets on Arbitrum, see our [token bridging docs](https://developer.arbitrum.io/asset-bridging). 6 | 7 | #### **Custom Token Bridging Using the Generic-Custom Gateway** 8 | 9 | Bridging a custom token to the Arbitrum chain is done via the Arbitrum Generic-Custom gateway. Our Generic-Custom Gateway is designed to be flexible enough to be suitable for most (but not necessarily all) custom fungible token needs. 10 | 11 | Here, we deploy a [demo custom token](./contracts/L1Token.sol) on L1 and a [demo custom token](./contracts/L2Token.sol) on L2. We then use the Arbitrum Custom Gateway contract to register our L1 custom token to our L2 custom token. Once done with token's registration to the Custom Gateway, we register our L1 token to the Arbitrum Gateway Router on L1. 12 | 13 | We use our [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) library to initiate and verify the bridging. 14 | 15 | See [./exec.js](./scripts/exec.js) for inline explanation. 16 | 17 | ### Config Environment Variables 18 | 19 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 20 | 21 | ```bash 22 | cp .env-sample .env 23 | ``` 24 | 25 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 26 | 27 | ### Run: 28 | 29 | ``` 30 | yarn run custom-token-bridging 31 | ``` 32 | 33 |

34 | 35 |

36 | 37 | -------------------------------------------------------------------------------- /packages/custom-token-bridging/contracts/L1Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./interfaces/ICustomToken.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | /** 9 | * @title Interface needed to call function registerTokenToL2 of the L1CustomGateway 10 | */ 11 | interface IL1CustomGateway { 12 | function registerTokenToL2( 13 | address _l2Address, 14 | uint256 _maxGas, 15 | uint256 _gasPriceBid, 16 | uint256 _maxSubmissionCost, 17 | address _creditBackAddress 18 | ) external payable returns (uint256); 19 | } 20 | 21 | /** 22 | * @title Interface needed to call function setGateway of the L2GatewayRouter 23 | */ 24 | interface IL1GatewayRouter { 25 | function setGateway( 26 | address _gateway, 27 | uint256 _maxGas, 28 | uint256 _gasPriceBid, 29 | uint256 _maxSubmissionCost, 30 | address _creditBackAddress 31 | ) external payable returns (uint256); 32 | } 33 | 34 | contract L1Token is Ownable, ICustomToken, ERC20 { 35 | address private customGatewayAddress; 36 | address private routerAddress; 37 | bool private shouldRegisterGateway; 38 | 39 | /** 40 | * @dev See {ERC20-constructor} and {Ownable-constructor} 41 | * 42 | * An initial supply amount is passed, which is preminted to the deployer. 43 | */ 44 | constructor(address _customGatewayAddress, address _routerAddress, uint256 _initialSupply) ERC20("L1CustomToken", "LCT") { 45 | customGatewayAddress = _customGatewayAddress; 46 | routerAddress = _routerAddress; 47 | _mint(msg.sender, _initialSupply * 10 ** decimals()); 48 | } 49 | 50 | /// @dev we only set shouldRegisterGateway to true when in `registerTokenOnL2` 51 | function isArbitrumEnabled() external view override returns (uint8) { 52 | require(shouldRegisterGateway, "NOT_EXPECTED_CALL"); 53 | return uint8(0xb1); 54 | } 55 | 56 | /// @dev See {ICustomToken-registerTokenOnL2} 57 | function registerTokenOnL2( 58 | address l2CustomTokenAddress, 59 | uint256 maxSubmissionCostForCustomGateway, 60 | uint256 maxSubmissionCostForRouter, 61 | uint256 maxGasForCustomGateway, 62 | uint256 maxGasForRouter, 63 | uint256 gasPriceBid, 64 | uint256 valueForGateway, 65 | uint256 valueForRouter, 66 | address creditBackAddress 67 | ) public override payable onlyOwner { 68 | // we temporarily set `shouldRegisterGateway` to true for the callback in registerTokenToL2 to succeed 69 | bool prev = shouldRegisterGateway; 70 | shouldRegisterGateway = true; 71 | 72 | IL1CustomGateway(customGatewayAddress).registerTokenToL2{ value: valueForGateway }( 73 | l2CustomTokenAddress, 74 | maxGasForCustomGateway, 75 | gasPriceBid, 76 | maxSubmissionCostForCustomGateway, 77 | creditBackAddress 78 | ); 79 | 80 | IL1GatewayRouter(routerAddress).setGateway{ value: valueForRouter }( 81 | customGatewayAddress, 82 | maxGasForRouter, 83 | gasPriceBid, 84 | maxSubmissionCostForRouter, 85 | creditBackAddress 86 | ); 87 | 88 | shouldRegisterGateway = prev; 89 | } 90 | 91 | /// @dev See {ERC20-transferFrom} 92 | function transferFrom( 93 | address sender, 94 | address recipient, 95 | uint256 amount 96 | ) public override(ICustomToken, ERC20) returns (bool) { 97 | return super.transferFrom(sender, recipient, amount); 98 | } 99 | 100 | /// @dev See {ERC20-balanceOf} 101 | function balanceOf(address account) public view override(ICustomToken, ERC20) returns (uint256) { 102 | return super.balanceOf(account); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/custom-token-bridging/contracts/L2Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "./interfaces/IArbToken.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract L2Token is ERC20, IArbToken { 8 | address public l2Gateway; 9 | address public override l1Address; 10 | 11 | modifier onlyL2Gateway() { 12 | require(msg.sender == l2Gateway, "NOT_GATEWAY"); 13 | _; 14 | } 15 | 16 | constructor(address _l2Gateway, address _l1TokenAddress) ERC20("L2CustomToken", "LCT") { 17 | l2Gateway = _l2Gateway; 18 | l1Address = _l1TokenAddress; 19 | } 20 | 21 | /** 22 | * @notice should increase token supply by amount, and should only be callable by the L2Gateway. 23 | */ 24 | function bridgeMint(address account, uint256 amount) external override onlyL2Gateway { 25 | _mint(account, amount); 26 | } 27 | 28 | /** 29 | * @notice should decrease token supply by amount, and should only be callable by the L2Gateway. 30 | */ 31 | function bridgeBurn(address account, uint256 amount) external override onlyL2Gateway { 32 | _burn(account, amount); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/custom-token-bridging/contracts/interfaces/IArbToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /* 4 | * Copyright 2020, Offchain Labs, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | /** 20 | * @title Minimum expected interface for L2 token that interacts with the L2 token bridge (this is the interface necessary 21 | * for a custom token that interacts with the bridge, see TestArbCustomToken.sol for an example implementation). 22 | */ 23 | 24 | // solhint-disable-next-line compiler-version 25 | pragma solidity >=0.6.9 <0.9.0; 26 | 27 | interface IArbToken { 28 | /** 29 | * @notice should increase token supply by amount, and should (probably) only be callable by the L1 bridge. 30 | */ 31 | function bridgeMint(address account, uint256 amount) external; 32 | 33 | /** 34 | * @notice should decrease token supply by amount, and should (probably) only be callable by the L1 bridge. 35 | */ 36 | function bridgeBurn(address account, uint256 amount) external; 37 | 38 | /** 39 | * @return address of layer 1 token 40 | */ 41 | function l1Address() external view returns (address); 42 | } -------------------------------------------------------------------------------- /packages/custom-token-bridging/contracts/interfaces/ICustomToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2020, Offchain Labs, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | // solhint-disable-next-line compiler-version 20 | pragma solidity >=0.6.9 <0.9.0; 21 | 22 | interface ArbitrumEnabledToken { 23 | /// @notice should return `0xb1` if token is enabled for arbitrum gateways 24 | function isArbitrumEnabled() external view returns (uint8); 25 | } 26 | 27 | /** 28 | * @title Minimum expected interface for L1 custom token 29 | */ 30 | interface ICustomToken is ArbitrumEnabledToken { 31 | /** 32 | * @notice Should make an external call to L1CustomGateway.registerTokenToL2 and L2GatewayRouter.setGateway 33 | */ 34 | function registerTokenOnL2( 35 | address l2CustomTokenAddress, 36 | uint256 maxSubmissionCostForCustomBridge, 37 | uint256 maxSubmissionCostForRouter, 38 | uint256 maxGasForCustomBridge, 39 | uint256 maxGasForRouter, 40 | uint256 gasPriceBid, 41 | uint256 valueForGateway, 42 | uint256 valueForRouter, 43 | address creditBackAddress 44 | ) external payable; 45 | 46 | function transferFrom( 47 | address sender, 48 | address recipient, 49 | uint256 amount 50 | ) external returns (bool); 51 | 52 | function balanceOf(address account) external view returns (uint256); 53 | } 54 | 55 | interface L1MintableToken is ICustomToken { 56 | function bridgeMint(address account, uint256 amount) external; 57 | } 58 | 59 | interface L1ReverseToken is L1MintableToken { 60 | function bridgeBurn(address account, uint256 amount) external; 61 | } -------------------------------------------------------------------------------- /packages/custom-token-bridging/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | const { hardhatConfig } = require('arb-shared-dependencies') 3 | 4 | module.exports = hardhatConfig 5 | -------------------------------------------------------------------------------- /packages/custom-token-bridging/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-token-bridging", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "hardhat compile", 6 | "custom-token-bridging": "hardhat run scripts/exec.js" 7 | }, 8 | "devDependencies": { 9 | "@nomiclabs/hardhat-ethers": "^2.0.2", 10 | "@openzeppelin/contracts": "^4.8.3", 11 | "chai": "^4.3.4", 12 | "ethers": "^5.1.2", 13 | "hardhat": "^2.6.6" 14 | }, 15 | "dependencies": { 16 | "@arbitrum/sdk": "^v3.1.2", 17 | "dotenv": "^8.2.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/custom-token-bridging/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require('hardhat') 2 | const { providers, Wallet } = require('ethers') 3 | const { 4 | getL2Network, 5 | addDefaultLocalNetwork, 6 | L1ToL2MessageStatus, 7 | } = require('@arbitrum/sdk') 8 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 9 | const { 10 | AdminErc20Bridger, 11 | } = require('@arbitrum/sdk/dist/lib/assetBridger/erc20Bridger') 12 | const { expect } = require('chai') 13 | require('dotenv').config() 14 | requireEnvVariables(['DEVNET_PRIVKEY', 'L1RPC', 'L2RPC']) 15 | 16 | /** 17 | * Set up: instantiate L1 / L2 wallets connected to providers 18 | */ 19 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 20 | 21 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 22 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 23 | 24 | const l1Wallet = new Wallet(walletPrivateKey, l1Provider) 25 | const l2Wallet = new Wallet(walletPrivateKey, l2Provider) 26 | 27 | /** 28 | * Set the initial supply of L1 token that we want to bridge 29 | * Note that you can change the value 30 | */ 31 | const premine = ethers.utils.parseEther('3') 32 | 33 | const main = async () => { 34 | await arbLog( 35 | 'Setting Up Your Token With The Generic Custom Gateway Using Arbitrum SDK Library' 36 | ) 37 | 38 | /** 39 | * Add the default local network configuration to the SDK 40 | * to allow this script to run on a local node 41 | */ 42 | addDefaultLocalNetwork() 43 | 44 | /** 45 | * Use l2Network to create an Arbitrum SDK AdminErc20Bridger instance 46 | * We'll use AdminErc20Bridger for its convenience methods around registering tokens to the custom gateway 47 | */ 48 | const l2Network = await getL2Network(l2Provider) 49 | const adminTokenBridger = new AdminErc20Bridger(l2Network) 50 | 51 | const l1Gateway = l2Network.tokenBridge.l1CustomGateway 52 | const l1Router = l2Network.tokenBridge.l1GatewayRouter 53 | const l2Gateway = l2Network.tokenBridge.l2CustomGateway 54 | 55 | /** 56 | * Deploy our custom token smart contract to L1 57 | * We give the custom token contract the address of l1CustomGateway and l1GatewayRouter as well as the initial supply (premine) 58 | */ 59 | const L1CustomToken = await ( 60 | await ethers.getContractFactory('L1Token') 61 | ).connect(l1Wallet) 62 | console.log('Deploying custom token to L1') 63 | const l1CustomToken = await L1CustomToken.deploy(l1Gateway, l1Router, premine) 64 | await l1CustomToken.deployed() 65 | console.log(`custom token is deployed to L1 at ${l1CustomToken.address}`) 66 | 67 | /** 68 | * Deploy our custom token smart contract to L2 69 | * We give the custom token contract the address of l2CustomGateway and our l1CustomToken 70 | */ 71 | const L2CustomToken = await ( 72 | await ethers.getContractFactory('L2Token') 73 | ).connect(l2Wallet) 74 | console.log('Deploying custom token to L2') 75 | const l2CustomToken = await L2CustomToken.deploy( 76 | l2Gateway, 77 | l1CustomToken.address 78 | ) 79 | await l2CustomToken.deployed() 80 | console.log(`custom token is deployed to L2 at ${l2CustomToken.address}`) 81 | 82 | console.log('Registering custom token on L2:') 83 | 84 | /** 85 | * Register custom token on our custom gateway 86 | */ 87 | const registerTokenTx = await adminTokenBridger.registerCustomToken( 88 | l1CustomToken.address, 89 | l2CustomToken.address, 90 | l1Wallet, 91 | l2Provider 92 | ) 93 | 94 | const registerTokenRec = await registerTokenTx.wait() 95 | console.log( 96 | `Registering token txn confirmed on L1! 🙌 L1 receipt is: ${registerTokenRec.transactionHash}` 97 | ) 98 | 99 | /** 100 | * The L1 side is confirmed; now we listen and wait for the L2 side to be executed; we can do this by computing the expected txn hash of the L2 transaction. 101 | * To compute this txn hash, we need our message's "sequence numbers", unique identifiers of each L1 to L2 message. 102 | * We'll fetch them from the event logs with a helper method. 103 | */ 104 | const l1ToL2Msgs = await registerTokenRec.getL1ToL2Messages(l2Provider) 105 | 106 | /** 107 | * In principle, a single L1 txn can trigger any number of L1-to-L2 messages (each with its own sequencer number). 108 | * In this case, the registerTokenOnL2 method created 2 L1-to-L2 messages; 109 | * - (1) one to set the L1 token to the Custom Gateway via the Router, and 110 | * - (2) another to set the L1 token to its L2 token address via the Generic-Custom Gateway 111 | * Here, We check if both messages are redeemed on L2 112 | */ 113 | expect(l1ToL2Msgs.length, 'Should be 2 messages.').to.eq(2) 114 | 115 | const setTokenTx = await l1ToL2Msgs[0].waitForStatus() 116 | expect(setTokenTx.status, 'Set token not redeemed.').to.eq( 117 | L1ToL2MessageStatus.REDEEMED 118 | ) 119 | 120 | const setGateways = await l1ToL2Msgs[1].waitForStatus() 121 | expect(setGateways.status, 'Set gateways not redeemed.').to.eq( 122 | L1ToL2MessageStatus.REDEEMED 123 | ) 124 | 125 | console.log( 126 | 'Your custom token is now registered on our custom gateway 🥳 Go ahead and make the deposit!' 127 | ) 128 | } 129 | 130 | main() 131 | .then(() => process.exit(0)) 132 | .catch(error => { 133 | console.error(error) 134 | process.exit(1) 135 | }) 136 | -------------------------------------------------------------------------------- /packages/delayedInbox-l2msg/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY ='0x your key here' 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint) 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC= -------------------------------------------------------------------------------- /packages/delayedInbox-l2msg/README.md: -------------------------------------------------------------------------------- 1 | # delayedInbox-l2msg Tutorial 2 | 3 | (Note this can only be done in nitro stack, not in Arbitrum classic.) 4 | 5 | delayedInbox-l2msg is a simple sample example that allows you to send a l2 msg without using sequencer way, this can be used when a sequencer censors your tx or when a sequencer is down. 6 | 7 | The demo allows you to send an L2 message without having to use the L2 RPC (only using the L1 RPC). This demo has 2 parts; (1) one part will show how to send a normal L2 transaction using the delayed inbox, (2) another will show how to withdraw your funds back without the sequencer. 8 | 9 | If the sequencer goes down when running the `Withdraw Funds`, you need to use our [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk/blob/master/src/lib/inbox/inbox.ts#L256) to force include your tx to continue. (example [here](https://github.com/OffchainLabs/arbitrum-sdk/blob/401fa424bb4c21b54b77d95fbc95faec15787fe2/fork_test/inbox.test.ts#L131)) 10 | 11 | ## Config Environment Variables 12 | 13 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 14 | 15 | ```bash 16 | cp .env-sample .env 17 | ``` 18 | 19 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 20 | 21 | ### Run Demo 22 | 23 | Normal Transaction: 24 | 25 | ```bash 26 | yarn normalTx 27 | ``` 28 | 29 | Withdraw Funds: 30 | 31 | ```bash 32 | yarn withdrawFunds 33 | ``` 34 | 35 | ## Curious to see the output on the Arbitrum chain? 36 | 37 | Once the script is successfully executed, you can go to the [Arbitrum nitro block explorer](https://goerli-rollup-explorer.arbitrum.io/), enter your L2 address, and see the corresponding transactions on the Arbitrum chain! 38 | 39 |

40 | 41 |

42 | 43 | -------------------------------------------------------------------------------- /packages/delayedInbox-l2msg/contracts/greeter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity >=0.6.11; 3 | 4 | contract Greeter { 5 | string greeting; 6 | address public deployer; 7 | 8 | //use this to check if the tx sent from delayed inbox doesn't alias the sender address. 9 | modifier onlyDeployer() { 10 | require(msg.sender == deployer, "Only deployer can do this"); 11 | _; 12 | } 13 | 14 | constructor(string memory _greeting) public { 15 | greeting = _greeting; 16 | deployer = msg.sender; 17 | } 18 | 19 | function greet() public view returns (string memory) { 20 | return greeting; 21 | } 22 | 23 | function setGreeting(string memory _greeting) public onlyDeployer { 24 | greeting = _greeting; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/delayedInbox-l2msg/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | 3 | const { hardhatConfig } = require('arb-shared-dependencies') 4 | 5 | module.exports = hardhatConfig 6 | -------------------------------------------------------------------------------- /packages/delayedInbox-l2msg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delayedinbox-l2msg", 3 | "version": "1.0.0", 4 | "description": "(Noted this can only be done in nitro stack, not in arbitrum classic, so l2 rpc please use nitro test rpc) delayedInbox-l2msg is a simple sample example that allows you to send a l2 msg without using sequencer way, this can be used when sequencer censor your tx or when sequencer downs.", 5 | "main": "hardhat.config.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "build": "hardhat compile", 11 | "normalTx": "hardhat run scripts/normalTx.js", 12 | "withdrawFunds": "hardhat run scripts/withdrawFunds.js" 13 | }, 14 | "author": "Offchain Labs, Inc.", 15 | "license": "Apache-2.0", 16 | "devDependencies": { 17 | "hardhat": "^2.10.1" 18 | }, 19 | "dependencies": { 20 | "hardhat": "^2.10.1", 21 | "@arbitrum/sdk": "^v3.1.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/delayedInbox-l2msg/scripts/normalTx.js: -------------------------------------------------------------------------------- 1 | const { providers, Wallet, ethers } = require('ethers') 2 | const hre = require('hardhat') 3 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 4 | const { 5 | getL2Network, 6 | addDefaultLocalNetwork, 7 | } = require('@arbitrum/sdk/dist/lib/dataEntities/networks') 8 | const { InboxTools } = require('@arbitrum/sdk') 9 | requireEnvVariables(['DEVNET_PRIVKEY', 'L2RPC', 'L1RPC']) 10 | 11 | /** 12 | * Set up: instantiate L1 / L2 wallets connected to providers 13 | */ 14 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 15 | 16 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 17 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 18 | 19 | const l1Wallet = new Wallet(walletPrivateKey, l1Provider) 20 | const l2Wallet = new Wallet(walletPrivateKey, l2Provider) 21 | 22 | const main = async () => { 23 | await arbLog('DelayedInbox normal contract call (L2MSG_signedTx)') 24 | 25 | /** 26 | * Add the default local network configuration to the SDK 27 | * to allow this script to run on a local node 28 | */ 29 | addDefaultLocalNetwork() 30 | 31 | const l2Network = await getL2Network(await l2Wallet.getChainId()) 32 | 33 | const inboxSdk = new InboxTools(l1Wallet, l2Network) 34 | 35 | /** 36 | * We deploy greeter to L2, to see if delayed inbox tx can be executed as we thought 37 | */ 38 | const L2Greeter = await ( 39 | await hre.ethers.getContractFactory('Greeter') 40 | ).connect(l2Wallet) 41 | 42 | console.log('Deploying Greeter on L2 👋👋') 43 | 44 | const l2Greeter = await L2Greeter.deploy('Hello world') 45 | await l2Greeter.deployed() 46 | console.log(`deployed to ${l2Greeter.address}`) 47 | 48 | /** 49 | * Let's log the L2 greeting string 50 | */ 51 | const currentL2Greeting = await l2Greeter.greet() 52 | console.log(`Current L2 greeting: "${currentL2Greeting}"`) 53 | 54 | console.log( 55 | `Now we send a l2 tx through l1 delayed inbox (Please don't send any tx on l2 using ${l2Wallet.address} during this time):` 56 | ) 57 | 58 | /** 59 | * Here we have a new greeting message that we want to set as the L2 greeting; we'll be setting it by sending it as a message from delayed inbox!!! 60 | */ 61 | const newGreeting = 'Greeting from delayedInbox' 62 | 63 | const GreeterIface = l2Greeter.interface 64 | 65 | const calldatal2 = GreeterIface.encodeFunctionData('setGreeting', [ 66 | newGreeting, 67 | ]) 68 | 69 | const transactionl2Request = { 70 | data: calldatal2, 71 | to: l2Greeter.address, 72 | value: 0, 73 | } 74 | 75 | /** 76 | * We need extract l2's tx hash first so we can check if this tx executed on l2 later. 77 | */ 78 | const l2SignedTx = await inboxSdk.signL2Tx(transactionl2Request, l2Wallet) 79 | 80 | const l2Txhash = ethers.utils.parseTransaction(l2SignedTx).hash 81 | 82 | const l1Tx = await inboxSdk.sendL2SignedTx(l2SignedTx) 83 | 84 | const inboxRec = await l1Tx.wait() 85 | 86 | console.log(`Greeting txn confirmed on L1! 🙌 ${inboxRec.transactionHash}`) 87 | 88 | /** 89 | * Now we successfully send the tx to l1 delayed inbox, then we need to wait the tx executed on l2 90 | */ 91 | console.log( 92 | `Now we need to wait tx: ${l2Txhash} to be included on l2 (may take 15 minutes) ....... ` 93 | ) 94 | 95 | const l2TxReceipt = await l2Provider.waitForTransaction(l2Txhash) 96 | 97 | const status = l2TxReceipt.status 98 | if (status == true) { 99 | console.log(`L2 txn executed!!! 🥳 `) 100 | } else { 101 | console.log(`L2 txn failed, see if your gas is enough?`) 102 | return 103 | } 104 | 105 | /** 106 | * Now when we call greet again, we should see our new string on L2! 107 | */ 108 | const newGreetingL2 = await l2Greeter.greet() 109 | console.log(`Updated L2 greeting: "${newGreetingL2}"`) 110 | console.log('✌️') 111 | } 112 | 113 | main() 114 | .then(() => process.exit(0)) 115 | .catch(error => { 116 | console.error(error) 117 | process.exit(1) 118 | }) 119 | -------------------------------------------------------------------------------- /packages/delayedInbox-l2msg/scripts/withdrawFunds.js: -------------------------------------------------------------------------------- 1 | const { providers, Wallet, ethers } = require('ethers') 2 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 3 | const { 4 | getL2Network, 5 | addDefaultLocalNetwork, 6 | } = require('@arbitrum/sdk/dist/lib/dataEntities/networks') 7 | const { InboxTools } = require('@arbitrum/sdk') 8 | const { 9 | ArbSys__factory, 10 | } = require('@arbitrum/sdk/dist/lib/abi/factories/ArbSys__factory') 11 | 12 | const { 13 | ARB_SYS_ADDRESS, 14 | } = require('@arbitrum/sdk/dist/lib/dataEntities/constants') 15 | requireEnvVariables(['DEVNET_PRIVKEY', 'L2RPC', 'L1RPC']) 16 | 17 | /** 18 | * Set up: instantiate L1 / L2 wallets connected to providers 19 | */ 20 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 21 | 22 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 23 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 24 | 25 | const l1Wallet = new Wallet(walletPrivateKey, l1Provider) 26 | const l2Wallet = new Wallet(walletPrivateKey, l2Provider) 27 | 28 | const main = async () => { 29 | await arbLog('DelayedInbox withdraw funds from l2 (L2MSG_signedTx)') 30 | 31 | /** 32 | * Add the default local network configuration to the SDK 33 | * to allow this script to run on a local node 34 | */ 35 | addDefaultLocalNetwork() 36 | 37 | const l2Network = await getL2Network(await l2Wallet.getChainId()) 38 | 39 | const inboxSdk = new InboxTools(l1Wallet, l2Network) 40 | 41 | /** 42 | * Here we have a arbsys abi to withdraw our funds; we'll be setting it by sending it as a message from delayed inbox!!! 43 | */ 44 | 45 | const arbSys = ArbSys__factory.connect(ARB_SYS_ADDRESS, l2Provider) 46 | 47 | const arbsysIface = arbSys.interface 48 | const calldatal2 = arbsysIface.encodeFunctionData('withdrawEth', [ 49 | l1Wallet.address, 50 | ]) 51 | 52 | const transactionl2Request = { 53 | data: calldatal2, 54 | to: ARB_SYS_ADDRESS, 55 | value: 1, // Only set 1 wei since it just a test tutorial, you can set whatever you want in real runtime. 56 | } 57 | 58 | /** 59 | * We need extract l2's tx hash first so we can check if this tx executed on l2 later. 60 | */ 61 | const l2SignedTx = await inboxSdk.signL2Tx(transactionl2Request, l2Wallet) 62 | 63 | const l2Txhash = ethers.utils.parseTransaction(l2SignedTx).hash 64 | 65 | const resultsL1 = await inboxSdk.sendL2SignedTx(l2SignedTx) 66 | 67 | const inboxRec = await resultsL1.wait() 68 | 69 | console.log(`Withdraw txn initiated on L1! 🙌 ${inboxRec.transactionHash}`) 70 | 71 | /** 72 | * Now we successfully send the tx to l1 delayed inbox, then we need to wait the tx to be executed on l2 73 | */ 74 | console.log( 75 | `Now we need to wait tx: ${l2Txhash} to be included on l2 (may take 15 minutes, if longer than 1 day, you can use sdk to force include) ....... ` 76 | ) 77 | 78 | const l2TxReceipt = await l2Provider.waitForTransaction(l2Txhash) 79 | 80 | const status = l2TxReceipt.status 81 | if (status == true) { 82 | console.log( 83 | `L2 txn executed!!! 🥳 , you can go to https://bridge.arbitrum.io/ to execute your withdrawal and recieve your funds after challenge period!` 84 | ) 85 | } else { 86 | console.log(`L2 txn failed, see if your gas is enough?`) 87 | return 88 | } 89 | } 90 | 91 | main() 92 | .then(() => process.exit(0)) 93 | .catch(error => { 94 | console.error(error) 95 | process.exit(1) 96 | }) 97 | -------------------------------------------------------------------------------- /packages/demo-dapp-election/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" -------------------------------------------------------------------------------- /packages/demo-dapp-election/.gitignore: -------------------------------------------------------------------------------- 1 | *.ao 2 | .DS_Store 3 | build 4 | compiled.json 5 | compose 6 | docker-compose.arb-deploy.yml 7 | node_modules 8 | validator-states 9 | -------------------------------------------------------------------------------- /packages/demo-dapp-election/README.md: -------------------------------------------------------------------------------- 1 | # demo-dapp-election Tutorial 2 | 3 | demo-dapp-election is a simple sample example that allows you to deploy the Election contract to Arbitrum and run its functions. 4 | 5 | The contract lives entirely on L2 / involves no direct L1 interacts; writing, deploying, and interacting with it works just like using an L1 contract. 6 | 7 | ## Config Environment Variables 8 | 9 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 10 | 11 | ```bash 12 | cp .env-sample .env 13 | ``` 14 | 15 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 16 | 17 | ### Run Demo 18 | 19 | ```bash 20 | yarn run exec 21 | ``` 22 | 23 | ## Curious to see the output on the Arbitrum chain? 24 | 25 | Once the script is successfully executed, you can go to the [Arbitrum block explorer](https://goerli-rollup-explorer.arbitrum.io/), enter your L2 address, and see the corresponding transactions on the Arbitrum chain! 26 | 27 |

28 | 29 |

30 | -------------------------------------------------------------------------------- /packages/demo-dapp-election/contracts/Election.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.7.0; 2 | 3 | contract Election { 4 | // Model a Candidate 5 | struct Candidate { 6 | uint256 id; 7 | string name; 8 | uint256 voteCount; 9 | } 10 | 11 | // Store accounts that have voted 12 | mapping(address => bool) public voters; 13 | // Store Candidates 14 | // Fetch Candidate 15 | mapping(uint256 => Candidate) public candidates; 16 | // Store Candidates Count 17 | uint256 public candidatesCount; 18 | 19 | // voted event 20 | event votedEvent(uint256 indexed _candidateId); 21 | 22 | constructor() public { 23 | addCandidate("Candidate 1"); 24 | addCandidate("Candidate 2"); 25 | } 26 | 27 | function addCandidate(string memory _name) private { 28 | candidatesCount++; 29 | candidates[candidatesCount] = Candidate(candidatesCount, _name, 0); 30 | } 31 | 32 | function vote(uint256 _candidateId) public { 33 | // require that they haven't voted before 34 | require(!voters[msg.sender]); 35 | 36 | // require a valid candidate 37 | require(_candidateId > 0 && _candidateId <= candidatesCount); 38 | 39 | // record that voter has voted 40 | voters[msg.sender] = true; 41 | 42 | // update candidate vote Count 43 | candidates[_candidateId].voteCount++; 44 | 45 | // trigger voted event 46 | emit votedEvent(_candidateId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/demo-dapp-election/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | 3 | const { hardhatConfig } = require('arb-shared-dependencies') 4 | 5 | module.exports = hardhatConfig 6 | -------------------------------------------------------------------------------- /packages/demo-dapp-election/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-dapp-election", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "exec": "hardhat run scripts/exec.js --network l2" 8 | }, 9 | "devDependencies": { 10 | "@nomiclabs/hardhat-ethers": "^2.0.2", 11 | "chai": "^4.3.4", 12 | "ethers": "^5.1.2", 13 | "hardhat": "^2.2.0" 14 | }, 15 | "dependencies": { 16 | "dotenv": "^8.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/demo-dapp-election/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const hre = require('hardhat') 2 | const { ethers } = require('hardhat') 3 | const { expect } = require('chai') 4 | 5 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 6 | require('dotenv').config() 7 | 8 | requireEnvVariables(['DEVNET_PRIVKEY', 'L2RPC']) 9 | 10 | const main = async () => { 11 | await arbLog('Simple Election DApp') 12 | 13 | const l2Wallet = (await hre.ethers.getSigners())[0] 14 | console.log('Your wallet address:', l2Wallet.address) 15 | 16 | const L2Election = await ( 17 | await ethers.getContractFactory('Election') 18 | ).connect(l2Wallet) 19 | console.log('Deploying Election contract to L2') 20 | const l2election = await L2Election.deploy() 21 | await l2election.deployed() 22 | console.log( 23 | `Election contract is initialized with 2 candidates and deployed to ${l2election.address}` 24 | ) 25 | 26 | //Fetch the candidate count 27 | const count = await l2election.candidatesCount() 28 | expect(count.toNumber()).to.equal(2) 29 | console.log('The election is indeed initialized with two candidates!') 30 | 31 | //Fetch the candidates values (id, name, voteCount) and make sure they are set correctly 32 | var candidate1 = await l2election.candidates(1) 33 | expect(candidate1[0].toNumber()).to.equal(1) 34 | expect(candidate1[1]).to.equal('Candidate 1') 35 | expect(candidate1[2].toNumber()).to.equal(0) 36 | 37 | var candidate2 = await l2election.candidates(2) 38 | expect(candidate2[0].toNumber()).to.equal(2) 39 | expect(candidate2[1]).to.equal('Candidate 2') 40 | expect(candidate2[2].toNumber()).to.equal(0) 41 | console.log('candidates are initialized with the correct values!') 42 | 43 | //Cast a vote for candidate1 44 | var candidateId 45 | var candidate 46 | var voteCount 47 | candidateId = 1 48 | 49 | const voteTx1 = await l2election.vote(candidateId) 50 | const vote1Rec = await voteTx1.wait() 51 | expect(vote1Rec.status).to.equal(1) 52 | console.log('Vote tx is executed!') 53 | 54 | const voted = await l2election.voters(l2Wallet.address) 55 | expect(voted).to.be.true 56 | console.log('You have voted for candidate1!') 57 | 58 | //Fetch the candidate1 voteCount and make sure it's equal to 1 59 | candidate = await l2election.candidates(candidateId) 60 | voteCount = candidate[2] 61 | expect(voteCount.toNumber()).to.equal(1) 62 | console.log('Candidate1 has one vote!') 63 | 64 | //Fetch Candidate2 and make sure it did not receive any votes yet 65 | candidate = await l2election.candidates(2) 66 | voteCount = candidate[2] 67 | expect(voteCount.toNumber()).to.equal(0) 68 | console.log('Candidate2 has zero vote!') 69 | } 70 | 71 | main() 72 | .then(() => process.exit(0)) 73 | .catch(error => { 74 | console.error(error) 75 | process.exit(1) 76 | }) 77 | -------------------------------------------------------------------------------- /packages/demo-dapp-pet-shop/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" -------------------------------------------------------------------------------- /packages/demo-dapp-pet-shop/README.md: -------------------------------------------------------------------------------- 1 | # demo-dapp-pet-shop Tutorial 2 | 3 | demo-dapp-pet-shop is a simple sample example that allows you to deploy the adoption contract to Arbitrum and run its functions. 4 | 5 | The contract lives entirely on L2 / involves no direct L1 interacts; writing, deploying, and interacting with it works just like using an L1 contract. 6 | 7 | ## Config Environment Variables 8 | 9 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 10 | 11 | ```bash 12 | cp .env-sample .env 13 | ``` 14 | 15 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 16 | 17 | ### Run Demo 18 | 19 | ```bash 20 | yarn run exec 21 | ``` 22 | 23 | ## Curious to see the output on the Arbitrum chain? 24 | 25 | Once the script is successfully executed, you can go to the [Arbitrum block explorer](https://goerli-rollup-explorer.arbitrum.io/), enter your L2 address, and see the corresponding transactions on the Arbitrum chain! 26 | 27 |

28 | 29 |

-------------------------------------------------------------------------------- /packages/demo-dapp-pet-shop/contracts/Adoption.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.7.0; 2 | 3 | contract Adoption { 4 | event PetAdopted(uint256 returnValue); 5 | 6 | address[16] public adopters = [ 7 | address(0), 8 | address(0), 9 | address(0), 10 | address(0), 11 | address(0), 12 | address(0), 13 | address(0), 14 | address(0), 15 | address(0), 16 | address(0), 17 | address(0), 18 | address(0), 19 | address(0), 20 | address(0), 21 | address(0), 22 | address(0) 23 | ]; 24 | 25 | // Adopting a pet 26 | function adopt(uint256 petId) public returns (uint256) { 27 | require(petId >= 0 && petId <= 15); 28 | 29 | adopters[petId] = msg.sender; 30 | emit PetAdopted(petId); 31 | return petId; 32 | } 33 | 34 | // Retrieving the adopters 35 | function getAdopters() public view returns (address[16] memory) { 36 | return adopters; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/demo-dapp-pet-shop/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | 3 | const { hardhatConfig } = require('arb-shared-dependencies') 4 | 5 | module.exports = hardhatConfig 6 | -------------------------------------------------------------------------------- /packages/demo-dapp-pet-shop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-dapp-pet-shop", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "exec": "hardhat run scripts/exec.js --network l2" 8 | }, 9 | "devDependencies": { 10 | "@nomiclabs/hardhat-ethers": "^2.0.2", 11 | "ethers": "^5.1.2", 12 | "hardhat": "^2.2.0" 13 | }, 14 | "dependencies": { 15 | "dotenv": "^8.2.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/demo-dapp-pet-shop/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const hre = require('hardhat') 2 | const { ethers } = require('hardhat') 3 | const { expect } = require('chai') 4 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 5 | require('dotenv').config() 6 | 7 | requireEnvVariables(['DEVNET_PRIVKEY', 'L2RPC']) 8 | 9 | const main = async () => { 10 | await arbLog('Simple Pet Shop DApp') 11 | 12 | const l2Wallet = (await hre.ethers.getSigners())[0] 13 | console.log('Your wallet address:', l2Wallet.address) 14 | 15 | const L2Adoption = await ( 16 | await ethers.getContractFactory('Adoption') 17 | ).connect(l2Wallet) 18 | console.log('Deploying Adoption contract to L2') 19 | const l2adoption = await L2Adoption.deploy() 20 | await l2adoption.deployed() 21 | console.log(`Adoption contract is deployed to ${l2adoption.address}`) 22 | 23 | // The id of the pet that will be used for testing 24 | const expectedPetId = 8 25 | 26 | // The expected owner of adopted pet is your l2wallet 27 | const expectedAdopter = l2Wallet.address 28 | 29 | // Testing the adopt() function 30 | console.log('Adopting pet:') 31 | 32 | const adoptionEventData = await l2adoption.adopt(expectedPetId) 33 | expect(adoptionEventData).to.exist 34 | 35 | // Testing retrieval of a single pet's owner 36 | const adopter = await l2adoption.adopters(expectedPetId) 37 | expect(expectedAdopter).to.equal(adopter) // The owner of the expected pet should be your l2wallet 38 | console.log(`Pet adopted; owner: ${adopter}`) 39 | 40 | // Testing retrieval of all pet owners 41 | let adopters = [16] 42 | adopters = await l2adoption.getAdopters() 43 | expect(adopters[expectedPetId]).to.equal(expectedAdopter) // The owner of the expected pet should be your l2wallet 44 | console.log('All pet owners:', adopters) 45 | } 46 | 47 | main() 48 | .then(() => process.exit(0)) 49 | .catch(error => { 50 | console.error(error) 51 | process.exit(1) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/eth-deposit-to-different-address/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC="" -------------------------------------------------------------------------------- /packages/eth-deposit-to-different-address/.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | cache/ 3 | build/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /packages/eth-deposit-to-different-address/README.md: -------------------------------------------------------------------------------- 1 | # eth-deposit-to-different-address Tutorial 2 | 3 | `eth-deposit-to-different-address` shows how to move Ether from Ethereum (Layer 1) into the Arbitrum (Layer 2) chain, to an address different than the depositor. 4 | 5 | ## How it works (Under the hood) 6 | 7 | For the common case of depositing ETH to the same account on L2, use the tutorial [eth-deposit](../eth-deposit/README.md). 8 | In this specific case, we will use the retryable tickets (Arbitrum's canonical method for creating L1 to L2 messages) to deposit ETH into a different address. We will use the parameter `l2CallValue` of the retryable ticket to specify the amount of ETH to deposit, and `callValueRefundAddress` to specify the destination address. For more info on retryable tickets, see [retryable tickets documentation](https://developer.offchainlabs.com/docs/l1_l2_messages#depositing-eth-via-retryables). 9 | 10 | ### **Using Arbitrum SDK tooling** 11 | 12 | Our [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) provides a simple convenient method for creating a retryable ticket, abstracting away the need for the client to connect to any contracts manually. 13 | 14 | See [./exec.js](./scripts/exec.js) for inline explanation. 15 | 16 | To run: 17 | 18 | ``` 19 | yarn run exec 20 | ``` 21 | 22 | ## Config Environment Variables 23 | 24 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 25 | 26 | ```bash 27 | cp .env-sample .env 28 | ``` 29 | 30 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 31 | 32 | --- 33 | 34 | Once the script is successfully executed, you can go to the [Arbitrum block explorer](https://goerli-rollup-explorer.arbitrum.io/), enter your address, and see the amount of ETH that has been assigned to the specified address on the Arbitrum chain! 35 | 36 |

37 | -------------------------------------------------------------------------------- /packages/eth-deposit-to-different-address/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | const { hardhatConfig } = require('arb-shared-dependencies') 3 | 4 | module.exports = hardhatConfig 5 | -------------------------------------------------------------------------------- /packages/eth-deposit-to-different-address/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eth-deposit-to-different-address", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "exec": "hardhat run scripts/exec.js" 8 | }, 9 | "devDependencies": { 10 | "@nomiclabs/hardhat-ethers": "^2.0.2", 11 | "ethers": "^5.4.1", 12 | "hardhat": "^2.2.0" 13 | }, 14 | "dependencies": { 15 | "@arbitrum/sdk": "^v3.1.2", 16 | "dotenv": "^8.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/eth-deposit-to-different-address/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const { utils, providers, Wallet } = require('ethers') 2 | const { 3 | EthBridger, 4 | getL2Network, 5 | EthDepositStatus, 6 | addDefaultLocalNetwork, 7 | } = require('@arbitrum/sdk') 8 | const { parseEther } = utils 9 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 10 | require('dotenv').config() 11 | requireEnvVariables(['DEVNET_PRIVKEY', 'L1RPC', 'L2RPC']) 12 | 13 | /** 14 | * Set up: Connect to L1/L2 providers and instantiate an L1 wallet 15 | */ 16 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 17 | 18 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 19 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 20 | 21 | const l1Wallet = new Wallet(walletPrivateKey, l1Provider) 22 | 23 | /** 24 | * Set the destination address and amount to be deposited in L2 (in wei) 25 | */ 26 | const destAddress = '0x2D98cBc6f944c4bD36EdfE9f98cd7CB57faEC8d6' 27 | const ethToL2DepositAmount = parseEther('0.0001') 28 | 29 | const main = async () => { 30 | await arbLog('Deposit Eth via Arbitrum SDK on a different address') 31 | 32 | /** 33 | * Add the default local network configuration to the SDK 34 | * to allow this script to run on a local node 35 | */ 36 | addDefaultLocalNetwork() 37 | 38 | /** 39 | * Use l2Network to create an Arbitrum SDK EthBridger instance 40 | * We'll use EthBridger for its convenience methods around transferring ETH to L2 41 | */ 42 | const l2Network = await getL2Network(l2Provider) 43 | const ethBridger = new EthBridger(l2Network) 44 | 45 | /** 46 | * First, let's check the ETH balance of the destination address 47 | */ 48 | const destinationAddressInitialEthBalance = await l2Provider.getBalance( 49 | destAddress 50 | ) 51 | 52 | /** 53 | * Transfer ether from L1 to L2 54 | * This convenience method automatically queries for the retryable's max submission cost and forwards the appropriate amount to the specified address on L2 55 | * Arguments required are: 56 | * (1) amount: The amount of ETH to be transferred to L2 57 | * (2) l1Signer: The L1 address transferring ETH to L2 58 | * (3) l2Provider: An l2 provider 59 | * (4) destinationAddress: The address where the ETH will be sent to 60 | */ 61 | const depositTx = await ethBridger.depositTo({ 62 | amount: ethToL2DepositAmount, 63 | l1Signer: l1Wallet, 64 | l2Provider: l2Provider, 65 | destinationAddress: destAddress, 66 | }) 67 | const depositRec = await depositTx.wait() 68 | console.warn('Deposit L1 receipt is:', depositRec.transactionHash) 69 | 70 | /** 71 | * With the transaction confirmed on L1, we now wait for the L2 side (i.e., balance credited to L2) to be confirmed as well. 72 | * Here we're waiting for the Sequencer to include the L2 message in its off-chain queue. The Sequencer should include it in under 15 minutes. 73 | */ 74 | console.warn('Now we wait for L2 side of the transaction to be executed ⏳') 75 | const l2Result = await depositRec.waitForL2(l2Provider) 76 | 77 | /** 78 | * The `complete` boolean tells us if the l1 to l2 message was successful 79 | */ 80 | l2Result.complete 81 | ? console.log( 82 | `L2 message successful: status: ${ 83 | EthDepositStatus[await l2Result.message.status()] 84 | }` 85 | ) 86 | : console.log( 87 | `L2 message failed: status ${ 88 | EthDepositStatus[await l2Result.message.status()] 89 | }` 90 | ) 91 | 92 | /** 93 | * Our destination address ETH balance should be updated now 94 | */ 95 | const destinationAddressUpdatedEthBalance = await l2Provider.getBalance( 96 | destAddress 97 | ) 98 | console.log( 99 | `L2 ETH balance of the destination address has been updated from ${destinationAddressInitialEthBalance.toString()} to ${destinationAddressUpdatedEthBalance.toString()}` 100 | ) 101 | } 102 | main() 103 | .then(() => process.exit(0)) 104 | .catch(error => { 105 | console.error(error) 106 | process.exit(1) 107 | }) 108 | -------------------------------------------------------------------------------- /packages/eth-deposit/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC="" -------------------------------------------------------------------------------- /packages/eth-deposit/.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | cache/ 3 | build/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /packages/eth-deposit/README.md: -------------------------------------------------------------------------------- 1 | # eth-deposit Tutorial 2 | 3 | `eth-deposit` shows how to move Ether from Ethereum (Layer 1) into the Arbitrum (Layer 2) chain. 4 | 5 | ## How it works (Under the hood) 6 | 7 | A user deposits Ether onto Arbitrum using Arbitrum's general L1-to-L2 message passing system, and simply passing the desired Ether as callvalue and no additional data. For more info, see [Retryable Tickets documentation](https://developer.offchainlabs.com/docs/l1_l2_messages#depositing-eth-via-retryables). 8 | 9 | ### **Using Arbitrum SDK tooling** 10 | 11 | Our [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) provides a simply convenience method for depositing Ether, abstracting away the need for the client to connect to any contracts manually. 12 | 13 | See [./exec.js](./scripts/exec.js) for inline explanation. 14 | 15 | To run: 16 | 17 | ``` 18 | yarn run depositETH 19 | ``` 20 | 21 | ## Config Environment Variables 22 | 23 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 24 | 25 | ```bash 26 | cp .env-sample .env 27 | ``` 28 | 29 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 30 | 31 | --- 32 | 33 | Once the script is successfully executed, you can go to the [Arbitrum block explorer](https://goerli-rollup-explorer.arbitrum.io/), enter your address, and see the amount of ETH that has been assigned to your address on the Arbitrum chain! 34 | 35 |

36 | 37 |

38 | -------------------------------------------------------------------------------- /packages/eth-deposit/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | const { hardhatConfig } = require('arb-shared-dependencies') 3 | 4 | module.exports = hardhatConfig 5 | -------------------------------------------------------------------------------- /packages/eth-deposit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eth-deposit", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "depositETH": "hardhat run scripts/exec.js" 8 | }, 9 | "devDependencies": { 10 | "@nomiclabs/hardhat-ethers": "^2.0.2", 11 | "ethers": "^5.4.1", 12 | "hardhat": "^2.2.0" 13 | }, 14 | "dependencies": { 15 | "@arbitrum/sdk": "^v3.1.2", 16 | "dotenv": "^8.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/eth-deposit/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const { utils, providers, Wallet } = require('ethers') 2 | const { 3 | EthBridger, 4 | getL2Network, 5 | EthDepositStatus, 6 | addDefaultLocalNetwork, 7 | } = require('@arbitrum/sdk') 8 | const { parseEther } = utils 9 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 10 | require('dotenv').config() 11 | requireEnvVariables(['DEVNET_PRIVKEY', 'L1RPC', 'L2RPC']) 12 | 13 | /** 14 | * Set up: instantiate L1 / L2 wallets connected to providers 15 | */ 16 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 17 | 18 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 19 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 20 | 21 | const l1Wallet = new Wallet(walletPrivateKey, l1Provider) 22 | const l2Wallet = new Wallet(walletPrivateKey, l2Provider) 23 | 24 | /** 25 | * Set the amount to be deposited in L2 (in wei) 26 | */ 27 | const ethToL2DepositAmount = parseEther('0.0001') 28 | 29 | const main = async () => { 30 | await arbLog('Deposit Eth via Arbitrum SDK') 31 | 32 | /** 33 | * Add the default local network configuration to the SDK 34 | * to allow this script to run on a local node 35 | */ 36 | addDefaultLocalNetwork() 37 | 38 | /** 39 | * Use l2Network to create an Arbitrum SDK EthBridger instance 40 | * We'll use EthBridger for its convenience methods around transferring ETH to L2 41 | */ 42 | 43 | const l2Network = await getL2Network(l2Provider) 44 | const ethBridger = new EthBridger(l2Network) 45 | 46 | /** 47 | * First, let's check the l2Wallet initial ETH balance 48 | */ 49 | const l2WalletInitialEthBalance = await l2Wallet.getBalance() 50 | 51 | /** 52 | * transfer ether from L1 to L2 53 | * This convenience method automatically queries for the retryable's max submission cost and forwards the appropriate amount to L2 54 | * Arguments required are: 55 | * (1) amount: The amount of ETH to be transferred to L2 56 | * (2) l1Signer: The L1 address transferring ETH to L2 57 | * (3) l2Provider: An l2 provider 58 | */ 59 | const depositTx = await ethBridger.deposit({ 60 | amount: ethToL2DepositAmount, 61 | l1Signer: l1Wallet, 62 | l2Provider: l2Provider, 63 | }) 64 | 65 | const depositRec = await depositTx.wait() 66 | console.warn('deposit L1 receipt is:', depositRec.transactionHash) 67 | 68 | /** 69 | * With the transaction confirmed on L1, we now wait for the L2 side (i.e., balance credited to L2) to be confirmed as well. 70 | * Here we're waiting for the Sequencer to include the L2 message in its off-chain queue. The Sequencer should include it in under 10 minutes. 71 | */ 72 | console.warn('Now we wait for L2 side of the transaction to be executed ⏳') 73 | const l2Result = await depositRec.waitForL2(l2Provider) 74 | /** 75 | * The `complete` boolean tells us if the l1 to l2 message was successful 76 | */ 77 | l2Result.complete 78 | ? console.log( 79 | `L2 message successful: status: ${ 80 | EthDepositStatus[await l2Result.message.status()] 81 | }` 82 | ) 83 | : console.log( 84 | `L2 message failed: status ${ 85 | EthDepositStatus[await l2Result.message.status()] 86 | }` 87 | ) 88 | 89 | /** 90 | * Our l2Wallet ETH balance should be updated now 91 | */ 92 | const l2WalletUpdatedEthBalance = await l2Wallet.getBalance() 93 | console.log( 94 | `your L2 ETH balance is updated from ${l2WalletInitialEthBalance.toString()} to ${l2WalletUpdatedEthBalance.toString()}` 95 | ) 96 | } 97 | main() 98 | .then(() => process.exit(0)) 99 | .catch(error => { 100 | console.error(error) 101 | process.exit(1) 102 | }) -------------------------------------------------------------------------------- /packages/eth-withdraw/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC="" -------------------------------------------------------------------------------- /packages/eth-withdraw/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | artifacts/ 3 | cache/ 4 | build/ 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /packages/eth-withdraw/README.md: -------------------------------------------------------------------------------- 1 | # eth-withdraw Tutorial 2 | 3 | `eth-withdraw` shows how to move Ether from Arbitrum (Layer 2) into the Ethereum (Layer 1) chain. 4 | 5 | Note that this repo covers initiating and Ether withdrawal; for a demo on (later) releasing the funds from the Outbox, see [outbox-execute](../outbox-execute/README.md) 6 | 7 | ## How it works (Under the hood) 8 | 9 | To withdraw Ether from Arbitrum, a client creates an outgoing / L2 to L1 message using the `ArbSys` interface that later lets them release Ether from its escrow in the L1 Bridge.sol contract. For more info, see [Outgoing messages documentation](https://developer.offchainlabs.com/docs/l1_l2_messages#l2-to-l1-messages-lifecycle). 10 | 11 | --- 12 | 13 | _Note: Executing scripts will require your L2 account be funded with .000001 Eth._ 14 | 15 | ### **Using Arbitrum SDK tooling** 16 | 17 | Our [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) provides a simply convenience method for withdrawing Ether, abstracting away the need for the client to connect to any contracts manually. 18 | 19 | See [./exec.js](./scripts/exec.js) for inline explanation. 20 | 21 | To run: 22 | 23 | ``` 24 | yarn run withdrawETH 25 | ``` 26 | 27 | ## Config Environment Variables 28 | 29 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 30 | 31 | ```bash 32 | cp .env-sample .env 33 | ``` 34 | 35 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 36 | 37 | --- 38 | 39 | ## Curious to see the output on the Arbitrum chain? 40 | 41 | Once the script is successfully executed, you can go to the [Arbitrum block explorer](https://goerli-rollup-explorer.arbitrum.io/), enter your address and see the amount of ETH has been deducted from your Layer 2 balance. Note that your Layer 1 balance will only be updated after rollup's confirmation period is over. 42 | 43 |

44 | 45 |

-------------------------------------------------------------------------------- /packages/eth-withdraw/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | const { hardhatConfig } = require('arb-shared-dependencies') 3 | 4 | module.exports = hardhatConfig 5 | -------------------------------------------------------------------------------- /packages/eth-withdraw/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eth-withdraw", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "withdrawETH": "hardhat run scripts/exec.js" 8 | }, 9 | "devDependencies": { 10 | "@nomiclabs/hardhat-ethers": "^2.0.2", 11 | "ethers": "^5.1.2", 12 | "hardhat": "^2.2.0" 13 | }, 14 | "dependencies": { 15 | "@arbitrum/sdk": "^v3.1.2", 16 | "dotenv": "^8.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/eth-withdraw/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const { utils, providers, Wallet } = require('ethers') 2 | const { 3 | EthBridger, 4 | getL2Network, 5 | addDefaultLocalNetwork, 6 | } = require('@arbitrum/sdk') 7 | const { parseEther } = utils 8 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 9 | require('dotenv').config() 10 | requireEnvVariables(['DEVNET_PRIVKEY', 'L2RPC', 'L1RPC']) 11 | 12 | /** 13 | * Set up: instantiate L2 wallet connected to provider 14 | */ 15 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 16 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 17 | const l2Wallet = new Wallet(walletPrivateKey, l2Provider) 18 | 19 | /** 20 | * Set the amount to be withdrawn from L2 (in wei) 21 | */ 22 | const ethFromL2WithdrawAmount = parseEther('0.000001') 23 | 24 | const main = async () => { 25 | await arbLog('Withdraw Eth via Arbitrum SDK') 26 | 27 | /** 28 | * Add the default local network configuration to the SDK 29 | * to allow this script to run on a local node 30 | */ 31 | addDefaultLocalNetwork() 32 | 33 | /** 34 | * Use l2Network to create an Arbitrum SDK EthBridger instance 35 | * We'll use EthBridger for its convenience methods around transferring ETH from L2 to L1 36 | */ 37 | 38 | const l2Network = await getL2Network(l2Provider) 39 | const ethBridger = new EthBridger(l2Network) 40 | 41 | /** 42 | * First, let's check our L2 wallet's initial ETH balance and ensure there's some ETH to withdraw 43 | */ 44 | const l2WalletInitialEthBalance = await l2Wallet.getBalance() 45 | 46 | if (l2WalletInitialEthBalance.lt(ethFromL2WithdrawAmount)) { 47 | console.log( 48 | `Oops - not enough ether; fund your account L2 wallet currently ${l2Wallet.address} with at least 0.000001 ether` 49 | ) 50 | process.exit(1) 51 | } 52 | console.log('Wallet properly funded: initiating withdrawal now') 53 | 54 | /** 55 | * We're ready to withdraw ETH using the ethBridger instance from Arbitrum SDK 56 | * It will use our current wallet's address as the default destination 57 | */ 58 | 59 | const withdrawTx = await ethBridger.withdraw({ 60 | amount: ethFromL2WithdrawAmount, 61 | l2Signer: l2Wallet, 62 | destinationAddress: l2Wallet.address, 63 | }) 64 | const withdrawRec = await withdrawTx.wait() 65 | 66 | /** 67 | * And with that, our withdrawal is initiated! No additional time-sensitive actions are required. 68 | * Any time after the transaction's assertion is confirmed, funds can be transferred out of the bridge via the outbox contract 69 | * We'll display the withdrawals event data here: 70 | */ 71 | console.log(`Ether withdrawal initiated! 🥳 ${withdrawRec.transactionHash}`) 72 | 73 | const withdrawEventsData = await withdrawRec.getL2ToL1Events() 74 | console.log('Withdrawal data:', withdrawEventsData) 75 | console.log( 76 | `To claim funds (after dispute period), see outbox-execute repo 🫡` 77 | ) 78 | } 79 | 80 | main() 81 | .then(() => process.exit(0)) 82 | .catch(error => { 83 | console.error(error) 84 | process.exit(1) 85 | }) 86 | -------------------------------------------------------------------------------- /packages/gas-estimation/.env-sample: -------------------------------------------------------------------------------- 1 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 2 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 3 | -------------------------------------------------------------------------------- /packages/gas-estimation/README.md: -------------------------------------------------------------------------------- 1 | # Gas estimation tutorial 2 | 3 | `gas-estimation` is a simple demo of how a developer can estimate transaction fees on Arbitrum. 4 | 5 | It uses the formula described in this Medium article to estimate the fees to be paid on a transaction, also estimating each component of the formula sepparately: [Understanding Arbitrum: 2-Dimensional Fees](https://medium.com/offchainlabs/understanding-arbitrum-2-dimensional-fees-fd1d582596c9). 6 | 7 | See [./exec.ts](./scripts/exec.ts) for inline explanations. 8 | 9 | Inside the script, you can edit `txData` constant to suit your needs. 10 | 11 | To run: 12 | 13 | ``` 14 | yarn run exec 15 | ``` 16 | 17 | ## Config Environment Variables 18 | 19 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 20 | 21 | ```bash 22 | cp .env-sample .env 23 | ``` 24 | 25 | (you'll still need to edit some variables, i.e., `L2RPC`) 26 | 27 |

28 | 29 |

-------------------------------------------------------------------------------- /packages/gas-estimation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gas-estimation", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "exec": "ts-node scripts/exec.ts" 7 | }, 8 | "dependencies": { 9 | "@arbitrum/sdk": "^v3.1.2", 10 | "ts-node": "^10.8.1", 11 | "typescript": "^4.7.3" 12 | }, 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /packages/gas-estimation/scripts/exec.ts: -------------------------------------------------------------------------------- 1 | import { utils, providers } from "ethers"; 2 | import { addDefaultLocalNetwork } from "@arbitrum/sdk"; 3 | import { NodeInterface__factory } from "@arbitrum/sdk/dist/lib/abi/factories/NodeInterface__factory"; 4 | import { NODE_INTERFACE_ADDRESS } from "@arbitrum/sdk/dist/lib/dataEntities/constants"; 5 | const { requireEnvVariables } = require('arb-shared-dependencies'); 6 | 7 | // Importing configuration // 8 | require('dotenv').config(); 9 | requireEnvVariables(['L2RPC']); 10 | 11 | // Initial setup // 12 | const baseL2Provider = new providers.StaticJsonRpcProvider(process.env.L2RPC); 13 | 14 | /////////////////////////////////////////// 15 | // Values of the transaction to estimate // 16 | /////////////////////////////////////////// 17 | 18 | // Address where the transaction being estimated will be sent 19 | // (add here the address you will send the transaction to) 20 | const destinationAddress = "0x1234563d5de0d7198451f87bcbf15aefd00d434d"; 21 | 22 | // The input data of the transaction, in hex. You can find examples of this information in Arbiscan, 23 | // in the "Input Data" field of a transaction. 24 | // (add here the calladata you will send in the transaction) 25 | const txData = "0x"; 26 | 27 | const gasEstimator = async () => { 28 | // *************************** 29 | // * Gas formula explanation * 30 | // *************************** 31 | // 32 | // Transaction fees (TXFEES) = L2 Gas Price (P) * Gas Limit (G) 33 | // ----> Gas Limit (G) = L2 Gas used (L2G) + Extra Buffer for L1 cost (B) 34 | // ----> L1 Estimated Cost (L1C) = L1 estimated calldata price per byte (L1P) * L1 Calldata size in bytes (L1S) 35 | // ----> Extra Buffer (B) = L1 Cost (L1C) / L2 Gas Price (P) 36 | // 37 | // TXFEES = P * (L2G + ((L1P * L1S) / P)) 38 | 39 | // ******************************************** 40 | // * How do we get all parts of that equation * 41 | // ******************************************** 42 | // P (L2 Gas Price) => 43 | // ArbGasInfo.getPricesInWei() and get the sixth element => result[5] 44 | // NodeInterface.GasEstimateL1Component() and get the second element => result[1] 45 | // NodeInterface.GasEstimateComponents() and get the third element => result[2] 46 | // L2G (L2 Gas used) => Will depend on the transaction itself 47 | // L1P (L1 estimated calldata price per byte) => 48 | // (this is the L2's estimated view of the current L1's price per byte of data, which the L2 dynamically adjusts over time) 49 | // ArbGasInfo.getL1BaseFeeEstimate() and multiply by 16 50 | // ArbGasInfo.getL1GasPriceEstimate() and multiply by 16 51 | // ArbGasInfo.getPricesInWei() and get the second element => result[1] 52 | // NodeInterface.GasEstimateL1Component() and get the third element and multiply by 16 => result[2]*16 53 | // NodeInterface.GasEstimateComponents() and get the fourth element and multiply by 16 => result[3]*16 54 | // L1S (Size in bytes of the calldata to post on L1) => 55 | // Will depend on the size (in bytes) of the calldata of the transaction 56 | // We add a fixed amount of 140 bytes to that amount for the transaction metadata (recipient, nonce, gas price, ...) 57 | // Final size will be less after compression, but this calculation gives a good estimation 58 | 59 | // **************************** 60 | // * Other values you can get * 61 | // **************************** 62 | // B => 63 | // NodeInterface.GasEstimateL1Component() and get the first element => result[0] 64 | // NodeInterface.GasEstimateComponents() and get the second element => result[1] 65 | // 66 | 67 | // Add the default local network configuration to the SDK 68 | // to allow this script to run on a local node 69 | addDefaultLocalNetwork() 70 | 71 | // Instantiation of the NodeInterface object 72 | const nodeInterface = NodeInterface__factory.connect( 73 | NODE_INTERFACE_ADDRESS, 74 | baseL2Provider 75 | ); 76 | 77 | // Getting the estimations from NodeInterface.GasEstimateComponents() 78 | // ------------------------------------------------------------------ 79 | const gasEstimateComponents = await nodeInterface.callStatic.gasEstimateComponents( 80 | destinationAddress, 81 | false, 82 | txData, 83 | { 84 | blockTag: "latest" 85 | } 86 | ); 87 | 88 | // Getting useful values for calculating the formula 89 | const l1GasEstimated = gasEstimateComponents.gasEstimateForL1; 90 | const l2GasUsed = gasEstimateComponents.gasEstimate.sub(gasEstimateComponents.gasEstimateForL1); 91 | const l2EstimatedPrice = gasEstimateComponents.baseFee; 92 | const l1EstimatedPrice = gasEstimateComponents.l1BaseFeeEstimate.mul(16); 93 | 94 | 95 | // Calculating some extra values to be able to apply all variables of the formula 96 | // ------------------------------------------------------------------------------- 97 | // NOTE: This one might be a bit confusing, but l1GasEstimated (B in the formula) is calculated based on l2 gas fees 98 | const l1Cost = l1GasEstimated.mul(l2EstimatedPrice); 99 | // NOTE: This is similar to 140 + utils.hexDataLength(txData); 100 | const l1Size = l1Cost.div(l1EstimatedPrice); 101 | 102 | // Getting the result of the formula 103 | // --------------------------------- 104 | // Setting the basic variables of the formula 105 | const P = l2EstimatedPrice; 106 | const L2G = l2GasUsed; 107 | const L1P = l1EstimatedPrice; 108 | const L1S = l1Size; 109 | 110 | // L1C (L1 Cost) = L1P * L1S 111 | const L1C = L1P.mul(L1S); 112 | 113 | // B (Extra Buffer) = L1C / P 114 | const B = L1C.div(P); 115 | 116 | // G (Gas Limit) = L2G + B 117 | const G = L2G.add(B); 118 | 119 | // TXFEES (Transaction fees) = P * G 120 | const TXFEES = P.mul(G); 121 | 122 | console.log("Gas estimation components"); 123 | console.log("-------------------"); 124 | console.log(`Full gas estimation = ${gasEstimateComponents.gasEstimate.toNumber()} units`); 125 | console.log(`L2 Gas (L2G) = ${L2G.toNumber()} units`); 126 | console.log(`L1 estimated Gas (L1G) = ${l1GasEstimated.toNumber()} units`); 127 | 128 | console.log(`P (L2 Gas Price) = ${utils.formatUnits(P, "gwei")} gwei`); 129 | console.log(`L1P (L1 estimated calldata price per byte) = ${utils.formatUnits(L1P, "gwei")} gwei`); 130 | console.log(`L1S (L1 Calldata size in bytes) = ${L1S} bytes`); 131 | 132 | console.log("-------------------"); 133 | console.log(`Transaction estimated fees to pay = ${utils.formatEther(TXFEES)} ETH`); 134 | } 135 | 136 | gasEstimator() 137 | .then(() => process.exit(0)) 138 | .catch(error => { 139 | console.error(error) 140 | process.exit(1) 141 | }); 142 | -------------------------------------------------------------------------------- /packages/greeter/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC="" -------------------------------------------------------------------------------- /packages/greeter/.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | cache/ 3 | build/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /packages/greeter/README.md: -------------------------------------------------------------------------------- 1 | # Greeter Tutorial 2 | 3 | `greeter` is a simple demo of Arbitrum's L1-to-L2 message passing system (aka "retryable tickets"). 4 | 5 | It deploys 2 contracts - one to L1, and another to L2, and has the L1 contract send a message to the L2 contract to be executed automatically. 6 | 7 | The script and contracts demonstrate how to interact with Arbitrum's core bridge contracts to create these retryable messages, how to calculate and forward appropriate fees from L1 to L2, and how to use Arbitrum's L1-to-L2 message [address aliasing](https://developer.offchainlabs.com/docs/l1_l2_messages#address-aliasing). 8 | 9 | See [./exec.js](./scripts/exec.js) for inline explanation. 10 | 11 | [Click here](https://developer.offchainlabs.com/docs/l1_l2_messages) for more info on retryable tickets. 12 | 13 | ### Run Demo: 14 | 15 | ``` 16 | yarn run greeter 17 | ``` 18 | 19 | ## Config Environment Variables 20 | 21 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 22 | 23 | ```bash 24 | cp .env-sample .env 25 | ``` 26 | 27 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 28 | 29 |

30 | 31 |

32 | -------------------------------------------------------------------------------- /packages/greeter/contracts/Greeter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity >=0.6.11; 3 | 4 | contract Greeter { 5 | string greeting; 6 | 7 | constructor(string memory _greeting) { 8 | greeting = _greeting; 9 | } 10 | 11 | function greet() public view returns (string memory) { 12 | return greeting; 13 | } 14 | 15 | function setGreeting(string memory _greeting) public virtual { 16 | greeting = _greeting; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/greeter/contracts/arbitrum/GreeterL2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity >=0.6.11; 3 | 4 | import "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; 5 | import "@arbitrum/nitro-contracts/src/libraries/AddressAliasHelper.sol"; 6 | import "../Greeter.sol"; 7 | 8 | contract GreeterL2 is Greeter { 9 | ArbSys constant arbsys = ArbSys(address(100)); 10 | address public l1Target; 11 | 12 | event L2ToL1TxCreated(uint256 indexed withdrawalId); 13 | 14 | constructor(string memory _greeting, address _l1Target) Greeter(_greeting) { 15 | l1Target = _l1Target; 16 | } 17 | 18 | function updateL1Target(address _l1Target) public { 19 | l1Target = _l1Target; 20 | } 21 | 22 | function setGreetingInL1(string memory _greeting) public returns (uint256) { 23 | bytes memory data = abi.encodeWithSelector(Greeter.setGreeting.selector, _greeting); 24 | 25 | uint256 withdrawalId = arbsys.sendTxToL1(l1Target, data); 26 | 27 | emit L2ToL1TxCreated(withdrawalId); 28 | return withdrawalId; 29 | } 30 | 31 | /// @notice only l1Target can update greeting 32 | function setGreeting(string memory _greeting) public override { 33 | // To check that message came from L1, we check that the sender is the L1 contract's L2 alias. 34 | require( 35 | msg.sender == AddressAliasHelper.applyL1ToL2Alias(l1Target), 36 | "Greeting only updateable by L1" 37 | ); 38 | Greeter.setGreeting(_greeting); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/greeter/contracts/ethereum/GreeterL1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity >=0.6.11; 3 | 4 | import "@arbitrum/nitro-contracts/src/bridge/Inbox.sol"; 5 | import "@arbitrum/nitro-contracts/src/bridge/Outbox.sol"; 6 | import "../Greeter.sol"; 7 | 8 | contract GreeterL1 is Greeter { 9 | address public l2Target; 10 | IInbox public inbox; 11 | 12 | event RetryableTicketCreated(uint256 indexed ticketId); 13 | 14 | constructor( 15 | string memory _greeting, 16 | address _l2Target, 17 | address _inbox 18 | ) Greeter(_greeting) { 19 | l2Target = _l2Target; 20 | inbox = IInbox(_inbox); 21 | } 22 | 23 | function updateL2Target(address _l2Target) public { 24 | l2Target = _l2Target; 25 | } 26 | 27 | function setGreetingInL2( 28 | string memory _greeting, 29 | uint256 maxSubmissionCost, 30 | uint256 maxGas, 31 | uint256 gasPriceBid 32 | ) public payable returns (uint256) { 33 | bytes memory data = abi.encodeWithSelector(Greeter.setGreeting.selector, _greeting); 34 | uint256 ticketID = inbox.createRetryableTicket{ value: msg.value }( 35 | l2Target, 36 | 0, 37 | maxSubmissionCost, 38 | msg.sender, 39 | msg.sender, 40 | maxGas, 41 | gasPriceBid, 42 | data 43 | ); 44 | 45 | emit RetryableTicketCreated(ticketID); 46 | return ticketID; 47 | } 48 | 49 | /// @notice only l2Target can update greeting 50 | function setGreeting(string memory _greeting) public override { 51 | IBridge bridge = inbox.bridge(); 52 | // this prevents reentrancies on L2 to L1 txs 53 | require(msg.sender == address(bridge), "NOT_BRIDGE"); 54 | IOutbox outbox = IOutbox(bridge.activeOutbox()); 55 | address l2Sender = outbox.l2ToL1Sender(); 56 | require(l2Sender == l2Target, "Greeting only updateable by L2"); 57 | 58 | Greeter.setGreeting(_greeting); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/greeter/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | const { hardhatConfig } = require('arb-shared-dependencies') 3 | 4 | module.exports = hardhatConfig 5 | -------------------------------------------------------------------------------- /packages/greeter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "greeter", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "greeter": "hardhat run scripts/exec.js" 8 | }, 9 | "devDependencies": { 10 | "@nomiclabs/hardhat-ethers": "^2.0.2", 11 | "ethers": "^5.1.2", 12 | "hardhat": "^2.2.0" 13 | }, 14 | "dependencies": { 15 | "@arbitrum/sdk": "^v3.1.2", 16 | "@arbitrum/nitro-contracts": "v1.0.2", 17 | "dotenv": "^8.2.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/greeter/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const { providers, Wallet } = require('ethers') 2 | const { BigNumber } = require('@ethersproject/bignumber') 3 | const hre = require('hardhat') 4 | const ethers = require('ethers') 5 | const { 6 | L1ToL2MessageGasEstimator, 7 | } = require('@arbitrum/sdk/dist/lib/message/L1ToL2MessageGasEstimator') 8 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 9 | const { 10 | L1TransactionReceipt, 11 | L1ToL2MessageStatus, 12 | EthBridger, 13 | getL2Network, 14 | addDefaultLocalNetwork, 15 | } = require('@arbitrum/sdk') 16 | const { getBaseFee } = require('@arbitrum/sdk/dist/lib/utils/lib') 17 | requireEnvVariables(['DEVNET_PRIVKEY', 'L2RPC', 'L1RPC']) 18 | 19 | /** 20 | * Set up: instantiate L1 / L2 wallets connected to providers 21 | */ 22 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 23 | 24 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 25 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 26 | 27 | const l1Wallet = new Wallet(walletPrivateKey, l1Provider) 28 | const l2Wallet = new Wallet(walletPrivateKey, l2Provider) 29 | 30 | const main = async () => { 31 | await arbLog('Cross-chain Greeter') 32 | 33 | /** 34 | * Add the default local network configuration to the SDK 35 | * to allow this script to run on a local node 36 | */ 37 | addDefaultLocalNetwork() 38 | 39 | /** 40 | * Use l2Network to create an Arbitrum SDK EthBridger instance 41 | * We'll use EthBridger to retrieve the Inbox address 42 | */ 43 | 44 | const l2Network = await getL2Network(l2Provider) 45 | const ethBridger = new EthBridger(l2Network) 46 | const inboxAddress = ethBridger.l2Network.ethBridge.inbox 47 | 48 | /** 49 | * We deploy L1 Greeter to L1, L2 greeter to L2, each with a different "greeting" message. 50 | * After deploying, save set each contract's counterparty's address to its state so that they can later talk to each other. 51 | */ 52 | const L1Greeter = await ( 53 | await hre.ethers.getContractFactory('GreeterL1') 54 | ).connect(l1Wallet) // 55 | console.log('Deploying L1 Greeter 👋') 56 | const l1Greeter = await L1Greeter.deploy( 57 | 'Hello world in L1', 58 | ethers.constants.AddressZero, // temp l2 addr 59 | inboxAddress 60 | ) 61 | await l1Greeter.deployed() 62 | console.log(`deployed to ${l1Greeter.address}`) 63 | const L2Greeter = await ( 64 | await hre.ethers.getContractFactory('GreeterL2') 65 | ).connect(l2Wallet) 66 | 67 | console.log('Deploying L2 Greeter 👋👋') 68 | 69 | const l2Greeter = await L2Greeter.deploy( 70 | 'Hello world in L2', 71 | ethers.constants.AddressZero // temp l1 addr 72 | ) 73 | await l2Greeter.deployed() 74 | console.log(`deployed to ${l2Greeter.address}`) 75 | 76 | const updateL1Tx = await l1Greeter.updateL2Target(l2Greeter.address) 77 | await updateL1Tx.wait() 78 | 79 | const updateL2Tx = await l2Greeter.updateL1Target(l1Greeter.address) 80 | await updateL2Tx.wait() 81 | console.log('Counterpart contract addresses set in both greeters 👍') 82 | 83 | /** 84 | * Let's log the L2 greeting string 85 | */ 86 | const currentL2Greeting = await l2Greeter.greet() 87 | console.log(`Current L2 greeting: "${currentL2Greeting}"`) 88 | 89 | console.log('Updating greeting from L1 to L2:') 90 | 91 | /** 92 | * Here we have a new greeting message that we want to set as the L2 greeting; we'll be setting it by sending it as a message from layer 1!!! 93 | */ 94 | const newGreeting = 'Greeting from far, far away' 95 | 96 | /** 97 | * Now we can query the required gas params using the estimateAll method in Arbitrum SDK 98 | */ 99 | const l1ToL2MessageGasEstimate = new L1ToL2MessageGasEstimator(l2Provider) 100 | 101 | /** 102 | * To be able to estimate the gas related params to our L1-L2 message, we need to know how many bytes of calldata out retryable ticket will require 103 | * i.e., we need to calculate the calldata for the function being called (setGreeting()) 104 | */ 105 | const ABI = ['function setGreeting(string _greeting)'] 106 | const iface = new ethers.utils.Interface(ABI) 107 | const calldata = iface.encodeFunctionData('setGreeting', [newGreeting]) 108 | 109 | /** 110 | * Users can override the estimated gas params when sending an L1-L2 message 111 | * Note that this is totally optional 112 | * Here we include and example for how to provide these overriding values 113 | */ 114 | 115 | const RetryablesGasOverrides = { 116 | gasLimit: { 117 | base: undefined, // when undefined, the value will be estimated from rpc 118 | min: BigNumber.from(10000), // set a minimum gas limit, using 10000 as an example 119 | percentIncrease: BigNumber.from(30), // how much to increase the base for buffer 120 | }, 121 | maxSubmissionFee: { 122 | base: undefined, 123 | percentIncrease: BigNumber.from(30), 124 | }, 125 | maxFeePerGas: { 126 | base: undefined, 127 | percentIncrease: BigNumber.from(30), 128 | }, 129 | } 130 | 131 | /** 132 | * The estimateAll method gives us the following values for sending an L1->L2 message 133 | * (1) maxSubmissionCost: The maximum cost to be paid for submitting the transaction 134 | * (2) gasLimit: The L2 gas limit 135 | * (3) deposit: The total amount to deposit on L1 to cover L2 gas and L2 call value 136 | */ 137 | const L1ToL2MessageGasParams = await l1ToL2MessageGasEstimate.estimateAll( 138 | { 139 | from: await l1Greeter.address, 140 | to: await l2Greeter.address, 141 | l2CallValue: 0, 142 | excessFeeRefundAddress: await l2Wallet.address, 143 | callValueRefundAddress: await l2Wallet.address, 144 | data: calldata, 145 | }, 146 | await getBaseFee(l1Provider), 147 | l1Provider, 148 | RetryablesGasOverrides //if provided, it will override the estimated values. Note that providing "RetryablesGasOverrides" is totally optional. 149 | ) 150 | console.log( 151 | `Current retryable base submission price is: ${L1ToL2MessageGasParams.maxSubmissionCost.toString()}` 152 | ) 153 | 154 | /** 155 | * For the L2 gas price, we simply query it from the L2 provider, as we would when using L1 156 | */ 157 | const gasPriceBid = await l2Provider.getGasPrice() 158 | console.log(`L2 gas price: ${gasPriceBid.toString()}`) 159 | 160 | console.log( 161 | `Sending greeting to L2 with ${L1ToL2MessageGasParams.deposit.toString()} callValue for L2 fees:` 162 | ) 163 | const setGreetingTx = await l1Greeter.setGreetingInL2( 164 | newGreeting, // string memory _greeting, 165 | L1ToL2MessageGasParams.maxSubmissionCost, 166 | L1ToL2MessageGasParams.gasLimit, 167 | gasPriceBid, 168 | { 169 | value: L1ToL2MessageGasParams.deposit, 170 | } 171 | ) 172 | const setGreetingRec = await setGreetingTx.wait() 173 | 174 | console.log( 175 | `Greeting txn confirmed on L1! 🙌 ${setGreetingRec.transactionHash}` 176 | ) 177 | 178 | const l1TxReceipt = new L1TransactionReceipt(setGreetingRec) 179 | 180 | /** 181 | * In principle, a single L1 txn can trigger any number of L1-to-L2 messages (each with its own sequencer number). 182 | * In this case, we know our txn triggered only one 183 | * Here, We check if our L1 to L2 message is redeemed on L2 184 | */ 185 | const messages = await l1TxReceipt.getL1ToL2Messages(l2Wallet) 186 | const message = messages[0] 187 | console.log('Waiting for the L2 execution of the transaction. This may take up to 10-15 minutes ⏰') 188 | const messageResult = await message.waitForStatus() 189 | const status = messageResult.status 190 | if (status === L1ToL2MessageStatus.REDEEMED) { 191 | console.log( 192 | `L2 retryable ticket is executed 🥳 ${messageResult.l2TxReceipt.transactionHash}` 193 | ) 194 | } else { 195 | console.log( 196 | `L2 retryable ticket is failed with status ${L1ToL2MessageStatus[status]}` 197 | ) 198 | } 199 | 200 | /** 201 | * Note that during L2 execution, a retryable's sender address is transformed to its L2 alias. 202 | * Thus, when GreeterL2 checks that the message came from the L1, we check that the sender is this L2 Alias. 203 | * See setGreeting in GreeterL2.sol for this check. 204 | */ 205 | 206 | /** 207 | * Now when we call greet again, we should see our new string on L2! 208 | */ 209 | const newGreetingL2 = await l2Greeter.greet() 210 | console.log(`Updated L2 greeting: "${newGreetingL2}" 🥳`) 211 | } 212 | 213 | main() 214 | .then(() => process.exit(0)) 215 | .catch(error => { 216 | console.error(error) 217 | process.exit(1) 218 | }) 219 | -------------------------------------------------------------------------------- /packages/l1-confirmation-checker/.env-sample: -------------------------------------------------------------------------------- 1 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 2 | L2RPC="https://arb1.arbitrum.io/rpc" 3 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 4 | L1RPC="" -------------------------------------------------------------------------------- /packages/l1-confirmation-checker/README.md: -------------------------------------------------------------------------------- 1 | # L1 Confirmation Checker Tutorial 2 | 3 | `l1 confirmation checker` is a simple demo of Arbitrum's transaction finality checker (used to check if transaction submitted to l1 or not). 4 | 5 | It calls precompile `NodeInterface` to find information about an L1 transaction that posted the L2 transaction in a batch. 6 | 7 | It has 2 functions; both functions will show you whether your L2 transaction has been posted in an L1 batch. 8 | The first function, `checkConfirmation`, will output the number of L1 block confirmations the L1 batch-posting transaction has. 9 | The second is `findSubmissionTx`, which will output the L1 batch-posting transaction hash. 10 | 11 | See [./exec.js](./scripts/exec.js) for inline explanations. 12 | 13 | 14 | ### Run Demo: 15 | 16 | Check if tx recorded in L1 or not: 17 | ``` 18 | yarn checkConfirmation --txHash {YOUR_TX_HASH} 19 | ``` 20 | Get submissiontx by a given L2 transaction status: 21 | ``` 22 | yarn findSubmissionTx --txHash {YOUR_TX_HASH} 23 | ``` 24 | 25 | ## Config Environment Variables 26 | 27 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 28 | 29 | ```bash 30 | cp .env-sample .env 31 | ``` 32 | 33 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 34 | 35 |

36 | 37 |

38 | -------------------------------------------------------------------------------- /packages/l1-confirmation-checker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "l1-confirmation-checker", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "checkConfirmation": "yarn ts-node scripts/exec.ts --action checkConfirmation", 9 | "findSubmissionTx": "yarn ts-node scripts/exec.ts --action findSubmissionTx" 10 | }, 11 | "dependencies": { 12 | "@arbitrum/sdk": "^v3.1.2", 13 | "@types/yargs": "^17.0.17", 14 | "ts-node": "^10.8.1", 15 | "typescript": "^4.7.3", 16 | "yargs": "^17.2.1" 17 | }, 18 | "author": "", 19 | "license": "ISC" 20 | } 21 | -------------------------------------------------------------------------------- /packages/l1-confirmation-checker/scripts/exec.ts: -------------------------------------------------------------------------------- 1 | import {providers} from "ethers" 2 | import args from './getClargs'; 3 | import { checkConfirmation, findSubmissionTx } from "./utils"; 4 | const { requireEnvVariables } = require('arb-shared-dependencies') 5 | 6 | requireEnvVariables(['L2RPC']) 7 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 8 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 9 | 10 | const main = async () => { 11 | // check action param 12 | switch(args.action) { 13 | case "checkConfirmation": 14 | const confirmations = await checkConfirmation(args.txHash, l2Provider) 15 | if(confirmations.eq(0)) { 16 | console.log("Block has not been submitted to l1 yet, please check it later...") 17 | } else { 18 | console.log(`Congrats! This block has been submitted to l1 for ${confirmations} blocks`) 19 | } 20 | break 21 | 22 | case "findSubmissionTx": 23 | if(process.env.L1RPC === ''){ 24 | throw new Error("L1RPC not defined in env!") 25 | } 26 | const submissionTx = await findSubmissionTx(args.txHash, l1Provider, l2Provider) 27 | if(submissionTx === "") { 28 | console.log("No submission transaction found. (If event too old some rpc will discard it)") 29 | } else { 30 | console.log(`Submission transaction found: ${submissionTx}`) 31 | } 32 | break 33 | 34 | default: 35 | console.log(`Unknown action: ${args.action}`) 36 | } 37 | } 38 | 39 | main() 40 | .then(() => process.exit(0)) 41 | .catch(error => { 42 | console.error(error) 43 | process.exit(1) 44 | }) 45 | -------------------------------------------------------------------------------- /packages/l1-confirmation-checker/scripts/getClargs.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | import yargs from 'yargs/yargs'; 19 | 20 | const argv = yargs(process.argv.slice(2)) 21 | .options({ 22 | l2NetworkID: { 23 | type: 'number', 24 | }, 25 | action: { 26 | type: 'string', 27 | }, 28 | txHash: { 29 | type: 'string' 30 | } 31 | }) 32 | .demandOption('action') 33 | .demandOption('txHash') 34 | .parseSync(); 35 | 36 | export default argv; 37 | -------------------------------------------------------------------------------- /packages/l1-confirmation-checker/scripts/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | import { NodeInterface__factory } from "@arbitrum/sdk/dist/lib/abi/factories/NodeInterface__factory" 3 | import { SequencerInbox__factory } from "@arbitrum/sdk/dist/lib/abi/factories/SequencerInbox__factory" 4 | import { NODE_INTERFACE_ADDRESS } from "@arbitrum/sdk/dist/lib/dataEntities/constants" 5 | import { getL2Network, addDefaultLocalNetwork } from "@arbitrum/sdk" 6 | import { providers, BigNumber } from "ethers"; 7 | 8 | /** 9 | * This function will output the number of L1 block confirmations the L1 batch-posting transaction has 10 | * by a given L2 transaction 11 | */ 12 | // by a given L2 transaction. 13 | export const checkConfirmation = async ( 14 | txHash: string, 15 | l2Provider:providers.JsonRpcProvider 16 | ): Promise => { 17 | // Add the default local network configuration to the SDK 18 | // to allow this script to run on a local node 19 | addDefaultLocalNetwork() 20 | 21 | // Call the related block hash 22 | let blockHash 23 | try{ 24 | blockHash = (await l2Provider.getTransactionReceipt(txHash)).blockHash 25 | } catch(e){ 26 | throw new Error("Check blockNumber fail, reason: " + e) 27 | } 28 | 29 | const nodeInterface = NodeInterface__factory.connect( NODE_INTERFACE_ADDRESS, l2Provider) 30 | let result 31 | 32 | // Call nodeInterface precompile to get the number of L1 confirmations the sequencer batch has. 33 | try { 34 | result = await nodeInterface.functions.getL1Confirmations(blockHash) 35 | } catch(e){ 36 | throw new Error("Check fail, reason: " + e) 37 | } 38 | 39 | return result.confirmations 40 | } 41 | 42 | // This function will output the L1 batch-posting transaction hash by a given L2 transaction hash. 43 | export const findSubmissionTx = async ( 44 | txHash: string, 45 | l1Provider: providers.JsonRpcProvider, 46 | l2Provider: providers.JsonRpcProvider 47 | ): Promise => { 48 | // Add the default local network configuration to the SDK 49 | // to allow this script to run on a local node 50 | addDefaultLocalNetwork() 51 | 52 | // Get the related block number 53 | let blockNumber 54 | try{ 55 | blockNumber = (await l2Provider.getTransactionReceipt(txHash)).blockNumber 56 | } catch(e){ 57 | throw new Error("Check blockNumber fail, reason: " + e) 58 | } 59 | 60 | const l2Network = await getL2Network(l2Provider) 61 | const nodeInterface = NodeInterface__factory.connect( NODE_INTERFACE_ADDRESS, l2Provider) 62 | const sequencer = SequencerInbox__factory.connect(l2Network.ethBridge.sequencerInbox, l1Provider) 63 | 64 | // Call the nodeInterface precompile to get the batch number first 65 | let result: BigNumber 66 | try { 67 | result = await (await nodeInterface.functions.findBatchContainingBlock(blockNumber)).batch 68 | } catch(e){ 69 | throw new Error("Check l2 block fail, reason: " + e) 70 | } 71 | 72 | /** 73 | * We use the batch number to query the L1 sequencerInbox's SequencerBatchDelivered event 74 | * then, we get its emitted transaction hash. 75 | */ 76 | const queryBatch = sequencer.filters.SequencerBatchDelivered(result) 77 | const emittedEvent = await sequencer.queryFilter(queryBatch) 78 | 79 | // If no event has been emitted, it just returns "" 80 | if(emittedEvent.length === 0) { 81 | return "" 82 | } else { 83 | return emittedEvent[0].transactionHash 84 | } 85 | } -------------------------------------------------------------------------------- /packages/outbox-execute/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC="" -------------------------------------------------------------------------------- /packages/outbox-execute/README.md: -------------------------------------------------------------------------------- 1 | # Outbox Demo 2 | 3 | The Outbox contract is responsible for receiving and executing all "outgoing" messages; i.e., messages passed from Arbitrum to Ethereum. 4 | 5 | The (expected) most-common use-case is withdrawals (of, i.e., Ether or tokens), but the Outbox handles any arbitrary contract call, as this demo illustrates. 6 | 7 | See [./exec.js](./scripts/exec.js) for inline comments / explanation. 8 | 9 | ## Config Environment Variables 10 | 11 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 12 | 13 | ```bash 14 | cp .env-sample .env 15 | ``` 16 | 17 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 18 | 19 | ### Run demo 20 | 21 | ``` 22 | yarn outbox-exec --txhash 0xmytxnhash 23 | ``` 24 | 25 | - _0xmytxnhash_ is expected to be the transaction hash of an L2 transaction that triggered an L2 to L1 message. 26 | 27 | ### More info 28 | 29 | See our [developer documentation on messaging between layers](https://developer.offchainlabs.com/docs/l1_l2_messages). 30 | 31 |

32 | 33 |

-------------------------------------------------------------------------------- /packages/outbox-execute/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | const main = require('./scripts/exec.js') 3 | const { hardhatConfig } = require('arb-shared-dependencies') 4 | 5 | const { task } = require('hardhat/config') 6 | 7 | const accounts = { 8 | mnemonic: 9 | 'rule nation tired logic palace city picnic bubble ridge grain problem pilot', 10 | path: "m/44'/60'/0'/0", 11 | initialIndex: 0, 12 | count: 10, 13 | } 14 | 15 | task('outbox-exec', "Prints an account's balance") 16 | .addParam('txhash', 'Hash of txn that triggered and L2 to L1 message') 17 | 18 | .setAction(async args => { 19 | await main(args.txhash) 20 | }) 21 | 22 | /** 23 | * @type import('hardhat/config').HardhatUserConfig 24 | */ 25 | module.exports = hardhatConfig 26 | -------------------------------------------------------------------------------- /packages/outbox-execute/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "outbox-execute", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "outbox-exec": "hardhat outbox-exec" 8 | }, 9 | "devDependencies": { 10 | "@nomiclabs/hardhat-ethers": "^2.0.2", 11 | "chai": "^4.3.4", 12 | "ethers": "^5.1.2", 13 | "hardhat": "^2.9.1" 14 | }, 15 | "dependencies": { 16 | "@arbitrum/sdk": "^v3.1.2", 17 | "dotenv": "^8.2.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/outbox-execute/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const { providers, Wallet } = require('ethers') 2 | const { 3 | addDefaultLocalNetwork, 4 | L2TransactionReceipt, 5 | L2ToL1MessageStatus, 6 | } = require('@arbitrum/sdk') 7 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 8 | require('dotenv').config() 9 | requireEnvVariables(['DEVNET_PRIVKEY', 'L2RPC', 'L1RPC']) 10 | 11 | /** 12 | * Set up: instantiate L1 wallet connected to provider 13 | */ 14 | 15 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 16 | 17 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 18 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 19 | const l1Wallet = new Wallet(walletPrivateKey, l1Provider) 20 | 21 | module.exports = async txnHash => { 22 | await arbLog('Outbox Execution') 23 | 24 | /** 25 | * Add the default local network configuration to the SDK 26 | * to allow this script to run on a local node 27 | */ 28 | addDefaultLocalNetwork() 29 | 30 | /** 31 | / * We start with a txn hash; we assume this is transaction that triggered an L2 to L1 Message on L2 (i.e., ArbSys.sendTxToL1) 32 | */ 33 | if (!txnHash) 34 | throw new Error( 35 | 'Provide a transaction hash of an L2 transaction that sends an L2 to L1 message' 36 | ) 37 | if (!txnHash.startsWith('0x') || txnHash.trim().length != 66) 38 | throw new Error(`Hmm, ${txnHash} doesn't look like a txn hash...`) 39 | 40 | /** 41 | * First, let's find the Arbitrum txn from the txn hash provided 42 | */ 43 | const receipt = await l2Provider.getTransactionReceipt(txnHash) 44 | const l2Receipt = new L2TransactionReceipt(receipt) 45 | 46 | /** 47 | * Note that in principle, a single transaction could trigger any number of outgoing messages; the common case will be there's only one. 48 | * For the sake of this script, we assume there's only one / just grad the first one. 49 | */ 50 | const messages = await l2Receipt.getL2ToL1Messages(l1Wallet) 51 | const l2ToL1Msg = messages[0] 52 | 53 | /** 54 | * Check if already executed 55 | */ 56 | if ((await l2ToL1Msg.status(l2Provider)) == L2ToL1MessageStatus.EXECUTED) { 57 | console.log(`Message already executed! Nothing else to do here`) 58 | process.exit(1) 59 | } 60 | 61 | /** 62 | * before we try to execute out message, we need to make sure the l2 block it's included in is confirmed! (It can only be confirmed after the dispute period; Arbitrum is an optimistic rollup after-all) 63 | * waitUntilReadyToExecute() waits until the item outbox entry exists 64 | */ 65 | const timeToWaitMs = 1000 * 60 66 | console.log( 67 | "Waiting for the outbox entry to be created. This only happens when the L2 block is confirmed on L1, ~1 week after it's creation." 68 | ) 69 | await l2ToL1Msg.waitUntilReadyToExecute(l2Provider, timeToWaitMs) 70 | console.log('Outbox entry exists! Trying to execute now') 71 | 72 | /** 73 | * Now that its confirmed and not executed, we can execute our message in its outbox entry. 74 | */ 75 | const res = await l2ToL1Msg.execute(l2Provider) 76 | const rec = await res.wait() 77 | console.log('Done! Your transaction is executed', rec) 78 | } 79 | -------------------------------------------------------------------------------- /packages/redeem-failed-retryable/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC="" -------------------------------------------------------------------------------- /packages/redeem-failed-retryable/.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | cache/ 3 | build/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /packages/redeem-failed-retryable/README.md: -------------------------------------------------------------------------------- 1 | # Redeem Failed Retryable Tickets Tutorial 2 | 3 | Retryable tickets are the Arbitrum protocol’s canonical method for passing generalized messages from Ethereum to Arbitrum. A retryable ticket is an L2 message encoded and delivered by L1; if gas is provided, it will be executed immediately. If no gas is provided or the execution reverts, it will be placed in the L2 retry buffer, where any user can re-execute for some fixed period (roughly one week). 4 | You can use `exec-createFailedRetryable` script to create a failed retryable ticket and then use `redeem-failed-retryable` which shows you how to redeem (re-execute) a ticket that is sitting in the L2 retry buffer. 5 | 6 | See [./exec-redeem.js](./scripts/exec-redeem.js) for inline explanation. 7 | 8 | ### Run Demo: 9 | 10 | To create a failed retryable ticket: 11 | 12 | ``` 13 | yarn run createFailedRetryable 14 | ``` 15 | 16 | To redeem a failed retryable ticket: 17 | 18 | ``` 19 | yarn redeemFailedRetryable --txhash 0xmytxnhash 20 | ``` 21 | 22 | - _0xmytxnhash_ is expected to be the transaction hash of an L1 transaction that triggered an L1 to L2 message. 23 | 24 | ## Config Environment Variables 25 | 26 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 27 | 28 | ```bash 29 | cp .env-sample .env 30 | ``` 31 | 32 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 33 | 34 | ### More info 35 | 36 | For more information on the retryable tickets, see our [developer documentation on messaging between layers](https://developer.offchainlabs.com/docs/l1_l2_messages). 37 | 38 |

39 | 40 |

-------------------------------------------------------------------------------- /packages/redeem-failed-retryable/contracts/Greeter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity >=0.6.11; 3 | 4 | contract Greeter { 5 | string greeting; 6 | 7 | constructor(string memory _greeting) { 8 | greeting = _greeting; 9 | } 10 | 11 | function greet() public view returns (string memory) { 12 | return greeting; 13 | } 14 | 15 | function setGreeting(string memory _greeting) public virtual { 16 | greeting = _greeting; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/redeem-failed-retryable/contracts/arbitrum/GreeterL2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity >=0.6.11; 3 | 4 | import "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; 5 | import "@arbitrum/nitro-contracts/src/libraries/AddressAliasHelper.sol"; 6 | import "../Greeter.sol"; 7 | 8 | contract GreeterL2 is Greeter { 9 | ArbSys constant arbsys = ArbSys(address(100)); 10 | address public l1Target; 11 | 12 | event L2ToL1TxCreated(uint256 indexed withdrawalId); 13 | 14 | constructor(string memory _greeting, address _l1Target) Greeter(_greeting) { 15 | l1Target = _l1Target; 16 | } 17 | 18 | function updateL1Target(address _l1Target) public { 19 | l1Target = _l1Target; 20 | } 21 | 22 | function setGreetingInL1(string memory _greeting) public returns (uint256) { 23 | bytes memory data = abi.encodeWithSelector(Greeter.setGreeting.selector, _greeting); 24 | 25 | uint256 withdrawalId = arbsys.sendTxToL1(l1Target, data); 26 | 27 | emit L2ToL1TxCreated(withdrawalId); 28 | return withdrawalId; 29 | } 30 | 31 | /// @notice only l1Target can update greeting 32 | function setGreeting(string memory _greeting) public override { 33 | // To check that message came from L1, we check that the sender is the L1 contract's L2 alias. 34 | require( 35 | msg.sender == AddressAliasHelper.applyL1ToL2Alias(l1Target), 36 | "Greeting only updateable by L1" 37 | ); 38 | Greeter.setGreeting(_greeting); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/redeem-failed-retryable/contracts/ethereum/GreeterL1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity >=0.6.11; 3 | 4 | import "@arbitrum/nitro-contracts/src/bridge/Inbox.sol"; 5 | import "@arbitrum/nitro-contracts/src/bridge/Outbox.sol"; 6 | import "../Greeter.sol"; 7 | 8 | contract GreeterL1 is Greeter { 9 | address public l2Target; 10 | IInbox public inbox; 11 | 12 | event RetryableTicketCreated(uint256 indexed ticketId); 13 | 14 | constructor( 15 | string memory _greeting, 16 | address _l2Target, 17 | address _inbox 18 | ) Greeter(_greeting) { 19 | l2Target = _l2Target; 20 | inbox = IInbox(_inbox); 21 | } 22 | 23 | function updateL2Target(address _l2Target) public { 24 | l2Target = _l2Target; 25 | } 26 | 27 | function setGreetingInL2( 28 | string memory _greeting, 29 | uint256 maxSubmissionCost, 30 | uint256 maxGas, 31 | uint256 gasPriceBid 32 | ) public payable returns (uint256) { 33 | bytes memory data = abi.encodeWithSelector(Greeter.setGreeting.selector, _greeting); 34 | uint256 ticketID = inbox.createRetryableTicket{ value: msg.value }( 35 | l2Target, 36 | 0, 37 | maxSubmissionCost, 38 | msg.sender, 39 | msg.sender, 40 | maxGas, 41 | gasPriceBid, 42 | data 43 | ); 44 | 45 | emit RetryableTicketCreated(ticketID); 46 | return ticketID; 47 | } 48 | 49 | /// @notice only l2Target can update greeting 50 | function setGreeting(string memory _greeting) public override { 51 | IBridge bridge = inbox.bridge(); 52 | // this prevents reentrancies on L2 to L1 txs 53 | require(msg.sender == address(bridge), "NOT_BRIDGE"); 54 | IOutbox outbox = IOutbox(bridge.activeOutbox()); 55 | address l2Sender = outbox.l2ToL1Sender(); 56 | require(l2Sender == l2Target, "Greeting only updateable by L2"); 57 | 58 | Greeter.setGreeting(_greeting); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/redeem-failed-retryable/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | const main = require('./scripts/exec-redeem.js') 3 | const { hardhatConfig } = require('arb-shared-dependencies') 4 | 5 | const { task } = require('hardhat/config') 6 | 7 | task('redeem-failed-retryable') 8 | .addParam('txhash', 'Hash of the L1 txn that created the retryable ticket') 9 | 10 | .setAction(async args => { 11 | await main(args.txhash) 12 | }) 13 | 14 | /** 15 | * @type import('hardhat/config').HardhatUserConfig 16 | */ 17 | module.exports = hardhatConfig 18 | -------------------------------------------------------------------------------- /packages/redeem-failed-retryable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redeem-failed-retryable", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "createFailedRetryable": "hardhat run scripts/exec-createFailedRetryable.js", 8 | "redeemFailedRetryable": "hardhat redeem-failed-retryable" 9 | }, 10 | "devDependencies": { 11 | "@nomiclabs/hardhat-ethers": "^2.0.2", 12 | "ethers": "^5.1.2", 13 | "hardhat": "^2.2.0" 14 | }, 15 | "dependencies": { 16 | "@arbitrum/sdk": "^v3.1.2", 17 | "@arbitrum/nitro-contracts": "v1.0.2", 18 | "dotenv": "^8.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/redeem-failed-retryable/scripts/exec-createFailedRetryable.js: -------------------------------------------------------------------------------- 1 | const { providers, Wallet } = require('ethers') 2 | const hre = require('hardhat') 3 | const ethers = require('ethers') 4 | const { hexDataLength } = require('@ethersproject/bytes') 5 | const { 6 | L1ToL2MessageGasEstimator, 7 | } = require('@arbitrum/sdk/dist/lib/message/L1ToL2MessageGasEstimator') 8 | const { 9 | EthBridger, 10 | getL2Network, 11 | addDefaultLocalNetwork, 12 | } = require('@arbitrum/sdk') 13 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 14 | requireEnvVariables(['DEVNET_PRIVKEY', 'L2RPC', 'L1RPC']) 15 | 16 | /** 17 | * Set up: instantiate L1 / L2 wallets connected to providers 18 | */ 19 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 20 | 21 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 22 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 23 | 24 | const l1Wallet = new Wallet(walletPrivateKey, l1Provider) 25 | const l2Wallet = new Wallet(walletPrivateKey, l2Provider) 26 | 27 | const main = async () => { 28 | await arbLog('Creating Failed Retryables for Cross-chain Greeter') 29 | 30 | /** 31 | * Add the default local network configuration to the SDK 32 | * to allow this script to run on a local node 33 | */ 34 | addDefaultLocalNetwork() 35 | 36 | /** 37 | * Use l2Network to create an Arbitrum SDK EthBridger instance 38 | * We'll use EthBridger to retrieve the Inbox address 39 | */ 40 | 41 | const l2Network = await getL2Network(l2Provider) 42 | const ethBridger = new EthBridger(l2Network) 43 | const inboxAddress = ethBridger.l2Network.ethBridge.inbox 44 | 45 | /** 46 | * We deploy L1 Greeter to L1, L2 greeter to L2, each with a different "greeting" message. 47 | * After deploying, save set each contract's counterparty's address to its state so that they can later talk to each other. 48 | */ 49 | const L1Greeter = await ( 50 | await hre.ethers.getContractFactory('GreeterL1') 51 | ).connect(l1Wallet) // 52 | console.log('Deploying L1 Greeter 👋') 53 | const l1Greeter = await L1Greeter.deploy( 54 | 'Hello world in L1', 55 | ethers.constants.AddressZero, // temp l2 addr 56 | inboxAddress 57 | ) 58 | await l1Greeter.deployed() 59 | console.log(`deployed to ${l1Greeter.address}`) 60 | const L2Greeter = await ( 61 | await hre.ethers.getContractFactory('GreeterL2') 62 | ).connect(l2Wallet) 63 | 64 | console.log('Deploying L2 Greeter 👋👋') 65 | 66 | const l2Greeter = await L2Greeter.deploy( 67 | 'Hello world in L2', 68 | ethers.constants.AddressZero // temp l1 addr 69 | ) 70 | await l2Greeter.deployed() 71 | console.log(`deployed to ${l2Greeter.address}`) 72 | 73 | const updateL1Tx = await l1Greeter.updateL2Target(l2Greeter.address) 74 | await updateL1Tx.wait() 75 | 76 | const updateL2Tx = await l2Greeter.updateL1Target(l1Greeter.address) 77 | await updateL2Tx.wait() 78 | console.log('Counterpart contract addresses set in both greeters 👍') 79 | 80 | /** 81 | * Let's log the L2 greeting string 82 | */ 83 | const currentL2Greeting = await l2Greeter.greet() 84 | console.log(`Current L2 greeting: "${currentL2Greeting}"`) 85 | 86 | console.log('Updating greeting from L1 to L2:') 87 | 88 | /** 89 | * Here we have a new greeting message that we want to set as the L2 greeting; we'll be setting it by sending it as a message from layer 1!!! 90 | */ 91 | const newGreeting = 'Greeting from far, far away' 92 | 93 | /** 94 | * To send an L1-to-L2 message (aka a "retryable ticket"), we need to send ether from L1 to pay for the txn costs on L2. 95 | * There are two costs we need to account for: base submission cost and cost of L2 execution. We'll start with base submission cost. 96 | */ 97 | 98 | /** 99 | * Base submission cost is a special cost for creating a retryable ticket; querying the cost requires us to know how many bytes of calldata out retryable ticket will require, so let's figure that out. 100 | * We'll get the bytes for our greeting data, then add 4 for the 4-byte function signature. 101 | */ 102 | 103 | const newGreetingBytes = ethers.utils.defaultAbiCoder.encode( 104 | ['string'], 105 | [newGreeting] 106 | ) 107 | const newGreetingBytesLength = hexDataLength(newGreetingBytes) + 4 // 4 bytes func identifier 108 | 109 | /** 110 | * Now we can query the submission price using a helper method; the first value returned tells us the best cost of our transaction; that's what we'll be using. 111 | * The second value (nextUpdateTimestamp) tells us when the base cost will next update (base cost changes over time with chain congestion; the value updates every 24 hours). We won't actually use it here, but generally it's useful info to have. 112 | */ 113 | const l1ToL2MessageGasEstimate = new L1ToL2MessageGasEstimator(l2Provider) 114 | 115 | const _submissionPriceWei = 116 | await l1ToL2MessageGasEstimate.estimateSubmissionFee( 117 | l1Provider, 118 | await l1Provider.getGasPrice(), 119 | newGreetingBytesLength 120 | ) 121 | 122 | console.log( 123 | `Current retryable base submission price: ${_submissionPriceWei.toString()}` 124 | ) 125 | 126 | /** 127 | * ...Okay, but on the off chance we end up underpaying, our retryable ticket simply fails. 128 | * This is highly unlikely, but just to be safe, let's increase the amount we'll be paying (the difference between the actual cost and the amount we pay gets refunded to our address on L2 anyway) 129 | * In nitro, submission fee will be charged in L1 based on L1 basefee, revert on L1 side upon insufficient fee. 130 | */ 131 | const submissionPriceWei = _submissionPriceWei.mul(5) 132 | /** 133 | * Now we'll figure out the gas we need to send for L2 execution; this requires the L2 gas price and gas limit for our L2 transaction 134 | */ 135 | 136 | /** 137 | * For the L2 gas price, we simply query it from the L2 provider, as we would when using L1 138 | */ 139 | const gasPriceBid = await l2Provider.getGasPrice() 140 | console.log(`L2 gas price: ${gasPriceBid.toString()}`) 141 | 142 | /** 143 | * With these three values, we can calculate the total callvalue we'll need our L1 transaction to send to L2 144 | * To create a failed retryable ticket, we hardcode a very low number for gas limit (e.g., 10) which leads to a failed auto redeem on L2 145 | */ 146 | const maxGas = 10 147 | const callValue = submissionPriceWei.add(gasPriceBid.mul(maxGas)) 148 | 149 | console.log( 150 | `Sending greeting to L2 with ${callValue.toString()} callValue for L2 fees:` 151 | ) 152 | 153 | const setGreetingTx = await l1Greeter.setGreetingInL2( 154 | newGreeting, 155 | submissionPriceWei, 156 | maxGas, 157 | gasPriceBid, 158 | { 159 | value: callValue, 160 | } 161 | ) 162 | const setGreetingRec = await setGreetingTx.wait() 163 | 164 | console.log( 165 | `Greeting txn confirmed on L1 but will fail to auto redeem on L2! Here's the L1 tx hash: ${setGreetingRec.transactionHash}` 166 | ) 167 | } 168 | main() 169 | .then(() => process.exit(0)) 170 | .catch(error => { 171 | console.error(error) 172 | process.exit(1) 173 | }) 174 | -------------------------------------------------------------------------------- /packages/redeem-failed-retryable/scripts/exec-redeem.js: -------------------------------------------------------------------------------- 1 | const { providers, Wallet } = require('ethers') 2 | const { 3 | addDefaultLocalNetwork, 4 | L1TransactionReceipt, 5 | L1ToL2MessageStatus, 6 | } = require('@arbitrum/sdk') 7 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 8 | require('dotenv').config() 9 | requireEnvVariables(['DEVNET_PRIVKEY', 'L2RPC', 'L1RPC']) 10 | 11 | /** 12 | * Set up: instantiate the L2 wallet connected to provider 13 | */ 14 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 15 | 16 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 17 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 18 | const l2Wallet = new Wallet(walletPrivateKey, l2Provider) 19 | 20 | module.exports = async txnHash => { 21 | await arbLog('Redeem A Failed Retryable Ticket') 22 | 23 | /** 24 | * Add the default local network configuration to the SDK 25 | * to allow this script to run on a local node 26 | */ 27 | addDefaultLocalNetwork() 28 | 29 | /** 30 | * We start with an L1 txn hash; this is a transaction that triggers creating a retryable ticket 31 | */ 32 | if (!txnHash) 33 | throw new Error('Provide a transaction hash of an L1 transaction') 34 | if (!txnHash.startsWith('0x') || txnHash.trim().length != 66) 35 | throw new Error(`Hmm, ${txnHash} doesn't look like a txn hash...`) 36 | 37 | /** 38 | * First, we check if our L1 to L2 message is already redeemed on L2 39 | */ 40 | const receipt = await l1Provider.getTransactionReceipt(txnHash) 41 | const l1Receipt = new L1TransactionReceipt(receipt) 42 | 43 | const messages = await l1Receipt.getL1ToL2Messages(l2Wallet) 44 | const message = await messages[0] 45 | const messageRec = await message.waitForStatus() 46 | const status = await messageRec.status 47 | if (status === L1ToL2MessageStatus.REDEEMED) { 48 | console.log( 49 | `L2 retryable txn is already executed 🥳 ${await messageRec.l2TxReceipt 50 | .transactionHash}` 51 | ) 52 | return 53 | } else { 54 | console.log( 55 | `L2 retryable txn failed with status ${L1ToL2MessageStatus[status]}` 56 | ) 57 | } 58 | 59 | console.log(`Redeeming the ticket now 🥳`) 60 | /** 61 | * We use the redeem() method from Arbitrum SDK to manually redeem our ticket 62 | */ 63 | const l2Tx = await message.redeem() 64 | const rec = await l2Tx.waitForRedeem() 65 | console.log( 66 | 'The L2 side of your transaction is now execeuted 🥳 :', 67 | await rec.transactionHash 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /packages/token-deposit/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC="" -------------------------------------------------------------------------------- /packages/token-deposit/README.md: -------------------------------------------------------------------------------- 1 | # token-deposit Tutorial 2 | 3 | `token-deposit` demonstrates moving a token from Ethereum (Layer 1) into the Arbitrum (Layer 2) chain using the Standard Token Gateway in Arbitrum's token bridging system. 4 | 5 | For info on how it works under the hood, see our [token bridging docs](https://developer.offchainlabs.com/docs/bridging_assets). 6 | 7 | #### **Standard ERC20 Deposit** 8 | 9 | Depositing an ERC20 token into the Arbitrum chain is done via our the Arbitrum token bridge. 10 | 11 | Here, we deploy a [demo token](./contracts/DappToken.sol) and trigger a deposit; by default, the deposit will be routed through the standard ERC20 gateway, where on initial deposit, a standard arb erc20 contract will automatically be deployed to L2. 12 | 13 | We use our [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) library to initiate and verify the deposit. 14 | 15 | See [./exec.js](./scripts/exec.js) for inline explanation. 16 | 17 | ### Config Environment Variables 18 | 19 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 20 | 21 | ```bash 22 | cp .env-sample .env 23 | ``` 24 | 25 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 26 | 27 | ### Run: 28 | 29 | ``` 30 | yarn run token-deposit 31 | ``` 32 | 33 |

34 | 35 |

-------------------------------------------------------------------------------- /packages/token-deposit/contracts/DappToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract DappToken is ERC20 { 7 | /** 8 | * @dev See {ERC20-constructor}. 9 | * 10 | * An initial supply amount is passed, which is pre-minted and sent to the deployer. 11 | */ 12 | constructor(uint256 _initialSupply) ERC20("Dapp Token", "DAPP") { 13 | _mint(msg.sender, _initialSupply * 10 ** decimals()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/token-deposit/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | const { hardhatConfig } = require('arb-shared-dependencies') 3 | 4 | module.exports = hardhatConfig 5 | -------------------------------------------------------------------------------- /packages/token-deposit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "token-deposit", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "token-deposit": "hardhat run scripts/exec.js" 8 | }, 9 | "devDependencies": { 10 | "@nomiclabs/hardhat-ethers": "^2.0.2", 11 | "@openzeppelin/contracts": "^4.8.3", 12 | "chai": "^4.3.4", 13 | "ethers": "^5.1.2", 14 | "hardhat": "^2.2.0" 15 | }, 16 | "dependencies": { 17 | "@arbitrum/sdk": "^v3.1.2", 18 | "dotenv": "^8.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/token-deposit/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require('hardhat') 2 | const { BigNumber, providers, Wallet } = require('ethers') 3 | const { expect } = require('chai') 4 | const { 5 | addDefaultLocalNetwork, 6 | getL2Network, 7 | Erc20Bridger, 8 | L1ToL2MessageStatus, 9 | } = require('@arbitrum/sdk') 10 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 11 | require('dotenv').config() 12 | requireEnvVariables(['DEVNET_PRIVKEY', 'L1RPC', 'L2RPC']) 13 | 14 | /** 15 | * Set up: instantiate L1 / L2 wallets connected to providers 16 | */ 17 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 18 | 19 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 20 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 21 | 22 | const l1Wallet = new Wallet(walletPrivateKey, l1Provider) 23 | const l2Wallet = new Wallet(walletPrivateKey, l2Provider) 24 | 25 | /** 26 | * Set the amount of token to be transferred to L2 27 | */ 28 | const tokenAmount = BigNumber.from(50) 29 | 30 | const main = async () => { 31 | await arbLog('Deposit token using Arbitrum SDK') 32 | 33 | /** 34 | * Add the default local network configuration to the SDK 35 | * to allow this script to run on a local node 36 | */ 37 | addDefaultLocalNetwork() 38 | 39 | /** 40 | * For the purpose of our tests, here we deploy an standard ERC20 token (DappToken) to L1 41 | * It sends its deployer (us) the initial supply of 1000 42 | */ 43 | console.log('Deploying the test DappToken to L1:') 44 | const L1DappToken = await ( 45 | await ethers.getContractFactory('DappToken') 46 | ).connect(l1Wallet) 47 | const l1DappToken = await L1DappToken.deploy(1000) 48 | await l1DappToken.deployed() 49 | console.log(`DappToken is deployed to L1 at ${l1DappToken.address}`) 50 | 51 | /** 52 | * Use l2Network to create an Arbitrum SDK Erc20Bridger instance 53 | * We'll use Erc20Bridger for its convenience methods around transferring token to L2 54 | */ 55 | const l2Network = await getL2Network(l2Provider) 56 | const erc20Bridger = new Erc20Bridger(l2Network) 57 | 58 | /** 59 | * We get the address of L1 Gateway for our DappToken, which later helps us to get the initial token balance of Bridge (before deposit) 60 | */ 61 | const l1Erc20Address = l1DappToken.address 62 | const expectedL1GatewayAddress = await erc20Bridger.getL1GatewayAddress( 63 | l1Erc20Address, 64 | l1Provider 65 | ) 66 | const initialBridgeTokenBalance = await l1DappToken.balanceOf( 67 | expectedL1GatewayAddress 68 | ) 69 | 70 | /** 71 | * Because the token might have decimals, we update the amount to deposit taking into account those decimals 72 | */ 73 | const tokenDecimals = await l1DappToken.decimals() 74 | const tokenDepositAmount = tokenAmount.mul( 75 | BigNumber.from(10).pow(tokenDecimals) 76 | ) 77 | 78 | /** 79 | * The Standard Gateway contract will ultimately be making the token transfer call; thus, that's the contract we need to approve. 80 | * erc20Bridger.approveToken handles this approval 81 | * Arguments required are: 82 | * (1) l1Signer: The L1 address transferring token to L2 83 | * (2) erc20L1Address: L1 address of the ERC20 token to be depositted to L2 84 | */ 85 | console.log('Approving:') 86 | const approveTx = await erc20Bridger.approveToken({ 87 | l1Signer: l1Wallet, 88 | erc20L1Address: l1Erc20Address, 89 | }) 90 | 91 | const approveRec = await approveTx.wait() 92 | console.log( 93 | `You successfully allowed the Arbitrum Bridge to spend DappToken ${approveRec.transactionHash}` 94 | ) 95 | 96 | /** 97 | * Deposit DappToken to L2 using erc20Bridger. This will escrow funds in the Gateway contract on L1, and send a message to mint tokens on L2. 98 | * The erc20Bridge.deposit method handles computing the necessary fees for automatic-execution of retryable tickets — maxSubmission cost & l2 gas price * gas — and will automatically forward the fees to L2 as callvalue 99 | * Also note that since this is the first DappToken deposit onto L2, a standard Arb ERC20 contract will automatically be deployed. 100 | * Arguments required are: 101 | * (1) amount: The amount of tokens to be transferred to L2 102 | * (2) erc20L1Address: L1 address of the ERC20 token to be depositted to L2 103 | * (2) l1Signer: The L1 address transferring token to L2 104 | * (3) l2Provider: An l2 provider 105 | */ 106 | console.log('Transferring DappToken to L2:') 107 | const depositTx = await erc20Bridger.deposit({ 108 | amount: tokenDepositAmount, 109 | erc20L1Address: l1Erc20Address, 110 | l1Signer: l1Wallet, 111 | l2Provider: l2Provider, 112 | }) 113 | 114 | /** 115 | * Now we wait for L1 and L2 side of transactions to be confirmed 116 | */ 117 | console.log( 118 | `Deposit initiated: waiting for L2 retryable (takes 10-15 minutes; current time: ${new Date().toTimeString()}) ` 119 | ) 120 | const depositRec = await depositTx.wait() 121 | const l2Result = await depositRec.waitForL2(l2Provider) 122 | 123 | /** 124 | * The `complete` boolean tells us if the l1 to l2 message was successful 125 | */ 126 | l2Result.complete 127 | ? console.log( 128 | `L2 message successful: status: ${L1ToL2MessageStatus[l2Result.status]}` 129 | ) 130 | : console.log( 131 | `L2 message failed: status ${L1ToL2MessageStatus[l2Result.status]}` 132 | ) 133 | 134 | /** 135 | * Get the Bridge token balance 136 | */ 137 | const finalBridgeTokenBalance = await l1DappToken.balanceOf( 138 | expectedL1GatewayAddress 139 | ) 140 | 141 | /** 142 | * Check if Bridge balance has been updated correctly 143 | */ 144 | expect( 145 | initialBridgeTokenBalance 146 | .add(tokenDepositAmount) 147 | .eq(finalBridgeTokenBalance), 148 | 'bridge balance not updated after L1 token deposit txn' 149 | ).to.be.true 150 | 151 | /** 152 | * Check if our l2Wallet DappToken balance has been updated correctly 153 | * To do so, we use erc20Bridge to get the l2Token address and contract 154 | */ 155 | const l2TokenAddress = await erc20Bridger.getL2ERC20Address( 156 | l1Erc20Address, 157 | l1Provider 158 | ) 159 | const l2Token = erc20Bridger.getL2TokenContract(l2Provider, l2TokenAddress) 160 | 161 | const testWalletL2Balance = ( 162 | await l2Token.functions.balanceOf(l2Wallet.address) 163 | )[0] 164 | expect( 165 | testWalletL2Balance.eq(tokenDepositAmount), 166 | 'l2 wallet not updated after deposit' 167 | ).to.be.true 168 | } 169 | 170 | main() 171 | .then(() => process.exit(0)) 172 | .catch(error => { 173 | console.error(error) 174 | process.exit(1) 175 | }) 176 | -------------------------------------------------------------------------------- /packages/token-withdraw/.env-sample: -------------------------------------------------------------------------------- 1 | # This is a sample .env file for use in local development. 2 | # Duplicate this file as .env here 3 | 4 | # Your Private key 5 | DEVNET_PRIVKEY="0x your key here" 6 | 7 | # Hosted Aggregator Node (JSON-RPC Endpoint). This is Arbitrum Goerli Testnet, can use any Arbitrum chain 8 | L2RPC="https://goerli-rollup.arbitrum.io/rpc" 9 | 10 | # Ethereum RPC; i.e., for Goerli https://goerli.infura.io/v3/ 11 | L1RPC="" -------------------------------------------------------------------------------- /packages/token-withdraw/README.md: -------------------------------------------------------------------------------- 1 | # token-withdraw Tutorial 2 | 3 | `token-withdraw` shows how to move ERC20 tokens from Arbitrum (Layer 2) into Ethereum (Layer 1). 4 | 5 | Note that this repo covers initiating a token withdrawal; for a demo on (later) releasing the funds from the Outbox, see [outbox-execute](../outbox-execute/README.md) 6 | 7 | ## How it works (Under the hood) 8 | 9 | To withdraw a token from Arbitrum, a message is send from a Gateway contract which burns the token on L2, and sends a message to L1, which allow the token to be released from escrow once the dispute period is expired. For more info, see [Outgoing messages documentation](https://developer.offchainlabs.com/docs/l1_l2_messages#l2-to-l1-messages-lifecycle). 10 | 11 | --- 12 | 13 | #### **Standard ERC20 Withdrawal** 14 | 15 | In this demo, we deploy a fresh token and then deposit some to L2. Then, we use these new tokens to trigger a withdrawal back to L1. 16 | 17 | We use our [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) library for the token bridge interactions. 18 | 19 | See [./exec.js](./scripts/exec.js) for inline explanation. 20 | 21 | ## Config Environment Variables 22 | 23 | Set the values shown in `.env-sample` as environmental variables. To copy it into a `.env` file: 24 | 25 | ```bash 26 | cp .env-sample .env 27 | ``` 28 | 29 | (you'll still need to edit some variables, i.e., `DEVNET_PRIVKEY`) 30 | 31 | ### Run 32 | 33 | ``` 34 | yarn withdraw-token 35 | ``` 36 | 37 |

38 | 39 |

40 | -------------------------------------------------------------------------------- /packages/token-withdraw/contracts/DappToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract DappToken is ERC20 { 7 | /** 8 | * @dev See {ERC20-constructor}. 9 | * 10 | * An initial supply amount is passed, which is pre-minted and sent to the deployer. 11 | */ 12 | constructor(uint256 _initialSupply) ERC20("Dapp Token", "DAPP") { 13 | _mint(msg.sender, _initialSupply * 10 ** decimals()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/token-withdraw/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | const { hardhatConfig } = require('arb-shared-dependencies') 3 | 4 | module.exports = hardhatConfig 5 | -------------------------------------------------------------------------------- /packages/token-withdraw/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "token-withdraw", 3 | "license": "Apache-2.0", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "withdraw-token": "hardhat run scripts/exec.js" 8 | }, 9 | "devDependencies": { 10 | "@nomiclabs/hardhat-ethers": "^2.0.2", 11 | "@openzeppelin/contracts": "^4.8.3", 12 | "chai": "^4.3.4", 13 | "ethers": "^5.1.2", 14 | "hardhat": "^2.2.0" 15 | }, 16 | "dependencies": { 17 | "@arbitrum/sdk": "^v3.1.2", 18 | "dotenv": "^8.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/token-withdraw/scripts/exec.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require('hardhat') 2 | const { BigNumber, providers, Wallet } = require('ethers') 3 | const { expect } = require('chai') 4 | const { 5 | addDefaultLocalNetwork, 6 | getL2Network, 7 | Erc20Bridger, 8 | L1ToL2MessageStatus, 9 | } = require('@arbitrum/sdk') 10 | const { arbLog, requireEnvVariables } = require('arb-shared-dependencies') 11 | require('dotenv').config() 12 | requireEnvVariables(['DEVNET_PRIVKEY', 'L1RPC', 'L2RPC']) 13 | 14 | /** 15 | * Set up: instantiate L1 / L2 wallets connected to providers 16 | */ 17 | const walletPrivateKey = process.env.DEVNET_PRIVKEY 18 | 19 | const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC) 20 | const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC) 21 | 22 | const l1Wallet = new Wallet(walletPrivateKey, l1Provider) 23 | const l2Wallet = new Wallet(walletPrivateKey, l2Provider) 24 | 25 | /** 26 | * Set the amount of token to be transferred to L2 and then withdrawn 27 | */ 28 | const tokenAmount = BigNumber.from(50) 29 | const tokenAmountToWithdraw = BigNumber.from(20) 30 | 31 | const main = async () => { 32 | await arbLog('Withdraw token using Arbitrum SDK') 33 | 34 | /** 35 | * Add the default local network configuration to the SDK 36 | * to allow this script to run on a local node 37 | */ 38 | addDefaultLocalNetwork() 39 | 40 | /** 41 | * For the purpose of our tests, here we deploy an standard ERC20 token (DappToken) to L1 42 | * It sends its deployer (us) the initial supply of 1000 43 | */ 44 | console.log('Deploying the test DappToken to L1:') 45 | const L1DappToken = await ( 46 | await ethers.getContractFactory('DappToken') 47 | ).connect(l1Wallet) 48 | const l1DappToken = await L1DappToken.deploy(1000000000000000) 49 | await l1DappToken.deployed() 50 | console.log(`DappToken is deployed to L1 at ${l1DappToken.address}`) 51 | const l1Erc20Address = l1DappToken.address 52 | 53 | /** 54 | * Use l2Network to create an Arbitrum SDK Erc20Bridger instance 55 | * We'll use Erc20Bridger for its convenience methods around transferring token to L2 and back to L1 56 | */ 57 | const l2Network = await getL2Network(l2Provider) 58 | const erc20Bridger = new Erc20Bridger(l2Network) 59 | 60 | /** 61 | * Because the token might have decimals, we update the amounts to deposit and withdraw taking into account those decimals 62 | */ 63 | const tokenDecimals = await l1DappToken.decimals() 64 | const tokenDepositAmount = tokenAmount.mul( 65 | BigNumber.from(10).pow(tokenDecimals) 66 | ) 67 | const tokenWithdrawAmount = tokenAmountToWithdraw.mul( 68 | BigNumber.from(10).pow(tokenDecimals) 69 | ) 70 | 71 | /** 72 | * The Standard Gateway contract will ultimately be making the token transfer call; thus, that's the contract we need to approve. 73 | * erc20Bridger.approveToken handles this approval 74 | * Arguments required are: 75 | * (1) l1Signer: The L1 address transferring token to L2 76 | * (2) erc20L1Address: L1 address of the ERC20 token to be deposited to L2 77 | */ 78 | console.log('Approving:') 79 | const approveTx = await erc20Bridger.approveToken({ 80 | l1Signer: l1Wallet, 81 | erc20L1Address: l1Erc20Address, 82 | }) 83 | 84 | const approveRec = await approveTx.wait() 85 | console.log( 86 | `You successfully allowed the Arbitrum Bridge to spend DappToken ${approveRec.transactionHash}` 87 | ) 88 | 89 | /** 90 | * Deposit DappToken to L2 using Erc20Bridger. This will escrow funds in the Gateway contract on L1, and send a message to mint tokens on L2. 91 | * The erc20Bridger.deposit method handles computing the necessary fees for automatic-execution of retryable tickets — maxSubmission cost & l2 gas price * gas — and will automatically forward the fees to L2 as callvalue 92 | * Also note that since this is the first DappToken deposit onto L2, a standard Arb ERC20 contract will automatically be deployed. 93 | * Arguments required are: 94 | * (1) amount: The amount of tokens to be transferred to L2 95 | * (2) erc20L1Address: L1 address of the ERC20 token to be deposited to L2 96 | * (2) l1Signer: The L1 address transferring token to L2 97 | * (3) l2Provider: An l2 provider 98 | */ 99 | console.log('Transferring DappToken to L2:') 100 | const depositTx = await erc20Bridger.deposit({ 101 | amount: tokenDepositAmount, 102 | erc20L1Address: l1Erc20Address, 103 | l1Signer: l1Wallet, 104 | l2Provider: l2Provider, 105 | }) 106 | 107 | /** 108 | * Now we wait for L1 and L2 side of transactions to be confirmed 109 | */ 110 | console.log( 111 | `Deposit initiated: waiting for L2 retryable (takes 10-15 minutes; current time: ${new Date().toTimeString()}) ` 112 | ) 113 | const depositRec = await depositTx.wait() 114 | const l2Result = await depositRec.waitForL2(l2Provider) 115 | console.log(`Setup complete`) 116 | /** 117 | * The `complete` boolean tells us if the l1 to l2 message was successul 118 | */ 119 | l2Result.complete 120 | ? console.log( 121 | `L2 message successful: status: ${L1ToL2MessageStatus[l2Result.status]}` 122 | ) 123 | : console.log( 124 | `L2 message failed: status ${L1ToL2MessageStatus[l2Result.status]}` 125 | ) 126 | 127 | /** 128 | * ... Okay, Now we begin withdrawing DappToken from L2. To withdraw, we'll use Erc20Bridger helper method withdraw 129 | * withdraw will call our L2 Gateway Router to initiate a withdrawal via the Standard ERC20 gateway 130 | * This transaction is constructed and paid for like any other L2 transaction (it just happens to (ultimately) make a call to ArbSys.sendTxToL1) 131 | * Arguments required are: 132 | * (1) amount: The amount of tokens to be transferred to L1 133 | * (2) erc20L1Address: L1 address of the ERC20 token 134 | * (3) l2Signer: The L2 address transferring token to L1 135 | */ 136 | console.log('Withdrawing:') 137 | const withdrawTx = await erc20Bridger.withdraw({ 138 | amount: tokenWithdrawAmount, 139 | destinationAddress: l2Wallet.address, 140 | erc20l1Address: l1Erc20Address, 141 | l2Signer: l2Wallet, 142 | }) 143 | const withdrawRec = await withdrawTx.wait() 144 | console.log(`Token withdrawal initiated! 🥳 ${withdrawRec.transactionHash}`) 145 | 146 | /** 147 | * And with that, our withdrawal is initiated! No additional time-sensitive actions are required. 148 | * Any time after the transaction's assertion is confirmed (around 7 days), funds can be transferred out of the bridge via the outbox contract 149 | * We'll check our l2Wallet DappToken balance here: 150 | */ 151 | const l2Token = erc20Bridger.getL2TokenContract( 152 | l2Provider, 153 | await erc20Bridger.getL2ERC20Address(l1Erc20Address, l1Provider) 154 | ) 155 | 156 | const l2WalletBalance = ( 157 | await l2Token.functions.balanceOf(await l2Wallet.getAddress()) 158 | )[0] 159 | 160 | expect( 161 | l2WalletBalance.add(tokenWithdrawAmount).eq(tokenDepositAmount), 162 | 'token withdraw balance not deducted' 163 | ).to.be.true 164 | 165 | console.log( 166 | `To to claim funds (after dispute period), see outbox-execute repo 🤞🏻` 167 | ) 168 | } 169 | 170 | main() 171 | .then(() => process.exit(0)) 172 | .catch(error => { 173 | console.error(error) 174 | process.exit(1) 175 | }) 176 | --------------------------------------------------------------------------------