├── .env.example ├── .eslintrc.json ├── .github └── workflows │ ├── fuzz.yml │ ├── lint.yml │ ├── slither.yml │ ├── tests.yml │ └── wait-for-it.sh ├── .gitignore ├── .gitmodules ├── .mocharc-e2e.js ├── .mocharc.js ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .solcover.js ├── LICENSE ├── README.md ├── arbitrum-helpers ├── abis │ ├── ArbRetryableTx.json │ ├── ArbSys.json │ ├── Bridge.json │ ├── IERC677Receiver.json │ ├── IERC677ReceiverAndExitReceiver.json │ ├── ITradeableExitReceiver.json │ ├── Inbox.json │ ├── NodeInterface.json │ └── Outbox.json ├── artifacts │ ├── L1GatewayRouter.json │ └── L2GatewayRouter.json ├── bridge.ts ├── contracts.ts ├── deploy.ts ├── deployment.ts ├── index.ts ├── messaging.ts ├── mocks.ts ├── networks │ ├── RetryProvider.ts │ ├── index.ts │ ├── mainnet.ts │ └── rinkeby.ts └── transactions.ts ├── certora ├── HashHelper.sol └── dai.spec ├── codechecks.yml ├── contracts ├── arbitrum │ ├── ArbSys.sol │ ├── IBridge.sol │ ├── IInbox.sol │ ├── IMessageProvider.sol │ └── IOutbox.sol ├── l1 │ ├── L1CrossDomainEnabled.sol │ ├── L1DaiGateway.sol │ ├── L1Escrow.sol │ ├── L1GovernanceRelay.sol │ └── L1ITokenGateway.sol ├── l2 │ ├── L2CrossDomainEnabled.sol │ ├── L2DaiGateway.sol │ ├── L2GovernanceRelay.sol │ ├── L2ITokenGateway.sol │ └── dai.sol └── test │ ├── BadSpell.sol │ ├── DaiEchidnaTest.sol │ ├── TestBridgeUpgradeSpell.sol │ └── TestDaiMintSpell.sol ├── docs ├── deposit.png └── full.png ├── echidna.config.ci.yml ├── echidna.config.yml ├── hardhat.config.ts ├── package.json ├── scripts ├── deployMainnet.ts └── deployRinkeby.ts ├── slither.config.json ├── slither.db.json ├── test-e2e └── bridge.test.ts ├── test ├── l1 │ ├── L1DaiGateway.ts │ ├── L1Escrow.ts │ └── L1GovernanceRelay.ts └── l2 │ ├── L2DaiGateway.ts │ ├── L2GovernanceRelay.ts │ ├── dai.ts │ └── eth-permit │ ├── eth-permit.ts │ ├── lib.ts │ └── rpc.ts ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | E2E_TESTS_PKEY= 2 | E2E_TESTS_L1_RPC='' 3 | E2E_TESTS_L2_RPC='https://rinkeby.arbitrum.io/rpc' 4 | E2E_TESTS_DEPLOYMENT=optional JSON with addresses if not present new instance will be deployed 5 | 6 | 7 | L1_RINKEBY_DEPLOYER_PRIV_KEY= 8 | L1_RINKEBY_RPC_URL= 9 | L2_RINKEBY_RPC_URL= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "extends": ["typestrict"], 6 | "plugins": ["no-only-tests", "simple-import-sort", "unused-imports"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "project": "./tsconfig.json", 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "unused-imports/no-unused-imports-ts": "error", 14 | "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_" }], 15 | "simple-import-sort/imports": "error", 16 | "simple-import-sort/exports": "error", 17 | "@typescript-eslint/no-use-before-define": "off", 18 | "@typescript-eslint/no-useless-constructor": "error", 19 | "accessor-pairs": "error", 20 | "constructor-super": "error", 21 | "eqeqeq": [ 22 | "error", 23 | "always", 24 | { 25 | "null": "ignore" 26 | } 27 | ], 28 | "handle-callback-err": ["error", "^(err|error)$"], 29 | "new-parens": "error", 30 | "no-array-constructor": "error", 31 | "no-async-promise-executor": "error", 32 | "no-caller": "error", 33 | "no-class-assign": "error", 34 | "no-compare-neg-zero": "error", 35 | "no-cond-assign": "error", 36 | "no-const-assign": "error", 37 | "no-constant-condition": [ 38 | "error", 39 | { 40 | "checkLoops": false 41 | } 42 | ], 43 | "no-control-regex": "error", 44 | "no-debugger": "error", 45 | "no-delete-var": "error", 46 | "no-dupe-args": "error", 47 | "no-dupe-keys": "error", 48 | "no-duplicate-case": "error", 49 | "no-empty-character-class": "error", 50 | "no-empty-pattern": "error", 51 | "no-eval": "error", 52 | "no-ex-assign": "error", 53 | "no-extend-native": "error", 54 | "no-extra-bind": "error", 55 | "no-extra-boolean-cast": "error", 56 | "no-extra-parens": ["error", "functions"], 57 | "no-floating-decimal": "error", 58 | "no-func-assign": "error", 59 | "no-global-assign": "error", 60 | "no-implied-eval": "error", 61 | "no-inner-declarations": ["error", "functions"], 62 | "no-invalid-regexp": "error", 63 | "no-iterator": "error", 64 | "no-label-var": "error", 65 | "no-labels": [ 66 | "error", 67 | { 68 | "allowLoop": false, 69 | "allowSwitch": false 70 | } 71 | ], 72 | "no-lone-blocks": "error", 73 | "no-misleading-character-class": "error", 74 | "no-multi-str": "error", 75 | "no-negated-in-lhs": "error", 76 | "no-new": "error", 77 | "no-new-func": "error", 78 | "no-new-object": "error", 79 | "no-new-require": "error", 80 | "no-new-symbol": "error", 81 | "no-new-wrappers": "error", 82 | "no-obj-calls": "error", 83 | "no-octal": "error", 84 | "no-octal-escape": "error", 85 | "no-path-concat": "error", 86 | "no-proto": "error", 87 | "no-redeclare": [ 88 | "error", 89 | { 90 | "builtinGlobals": false 91 | } 92 | ], 93 | "no-regex-spaces": "error", 94 | "no-return-assign": ["error", "except-parens"], 95 | "no-self-assign": "error", 96 | "no-self-compare": "error", 97 | "no-sequences": "error", 98 | "no-shadow-restricted-names": "error", 99 | "no-sparse-arrays": "error", 100 | "no-tabs": "error", 101 | "no-this-before-super": "error", 102 | "no-throw-literal": "error", 103 | "no-unmodified-loop-condition": "error", 104 | "no-unneeded-ternary": [ 105 | "error", 106 | { 107 | "defaultAssignment": false 108 | } 109 | ], 110 | "no-unreachable": "error", 111 | "no-unsafe-finally": "error", 112 | "no-unsafe-negation": "error", 113 | "no-restricted-imports": ["error"], 114 | "no-use-before-define": [ 115 | "error", 116 | { 117 | "classes": false, 118 | "functions": false, 119 | "variables": false 120 | } 121 | ], 122 | "no-useless-call": "error", 123 | "no-useless-catch": "error", 124 | "no-useless-computed-key": "error", 125 | "no-useless-escape": "error", 126 | "no-useless-rename": "error", 127 | "no-useless-return": "error", 128 | "no-with": "error", 129 | "one-var": [ 130 | "error", 131 | { 132 | "initialized": "never" 133 | } 134 | ], 135 | "prefer-const": [ 136 | "error", 137 | { 138 | "destructuring": "all" 139 | } 140 | ], 141 | "prefer-promise-reject-errors": "error", 142 | "symbol-description": "error", 143 | "use-isnan": "error", 144 | "valid-typeof": [ 145 | "error", 146 | { 147 | "requireStringLiterals": true 148 | } 149 | ], 150 | "yoda": ["error", "never"], 151 | "no-only-tests/no-only-tests": "error" 152 | }, 153 | "overrides": [ 154 | { 155 | "files": ["test/**/*.{js,ts,tsx}"], 156 | "rules": { 157 | "@typescript-eslint/no-non-null-assertion": "off" 158 | } 159 | } 160 | ] 161 | } 162 | -------------------------------------------------------------------------------- /.github/workflows/fuzz.yml: -------------------------------------------------------------------------------- 1 | name: Fuzz 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | echidna: 11 | name: Echidna 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | submodules: recursive 18 | 19 | - name: Set up node 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 12 23 | 24 | - name: Set up Python 3.8 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: 3.8 28 | 29 | - name: Get yarn cache directory path 30 | id: yarn-cache-dir-path 31 | run: echo "::set-output name=dir::$(yarn cache dir)" 32 | 33 | - name: Cache YARN dependencies 34 | uses: actions/cache@v2 35 | with: 36 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 37 | key: yarn-${{ hashFiles('**/yarn.lock') }} 38 | restore-keys: | 39 | yarn- 40 | 41 | - name: Install node dependencies 42 | run: yarn install --frozen-lockfile 43 | 44 | - name: Install pip3 45 | run: | 46 | python -m pip install --upgrade pip 47 | - name: Install slither 48 | run: | 49 | pip3 install slither-analyzer 50 | - name: Install solc-select 51 | run: | 52 | pip3 install solc-select 53 | - name: Set solc v0.6.11 54 | run: | 55 | solc-select install 0.6.11 56 | solc-select use 0.6.11 57 | - name: Install echidna 58 | run: | 59 | sudo wget -O /tmp/echidna-test.tar.gz https://github.com/crytic/echidna/releases/download/v1.7.2/echidna-test-1.7.2-Ubuntu-18.04.tar.gz 60 | sudo tar -xf /tmp/echidna-test.tar.gz -C /usr/bin 61 | sudo chmod +x /usr/bin/echidna-test 62 | - name: Run DaiEchidnaTest 63 | run: echidna-test . --contract DaiEchidnaTest --config echidna.config.ci.yml 64 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | name: Run Linters 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out Git repository 16 | uses: actions/checkout@v2 17 | with: 18 | submodules: recursive 19 | 20 | - name: Set up node 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: 12.x 24 | 25 | - name: Get yarn cache directory path 26 | id: yarn-cache-dir-path 27 | run: echo "::set-output name=dir::$(yarn cache dir)" 28 | 29 | - name: Cache YARN dependencies 30 | uses: actions/cache@v2 31 | with: 32 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 33 | key: yarn-${{ hashFiles('**/yarn.lock') }} 34 | restore-keys: | 35 | yarn- 36 | 37 | - name: Install Dependencies 38 | run: yarn --no-progress --non-interactive --frozen-lockfile 39 | 40 | - run: yarn build 41 | - run: yarn typecheck 42 | - run: yarn lint 43 | - run: yarn format 44 | -------------------------------------------------------------------------------- /.github/workflows/slither.yml: -------------------------------------------------------------------------------- 1 | name: Slither 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | slither: 11 | name: Slither 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Node 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 12 21 | 22 | - name: Set up Python 3.8 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: 3.8 26 | 27 | - name: Get yarn cache directory path 28 | id: yarn-cache-dir-path 29 | run: echo "::set-output name=dir::$(yarn cache dir)" 30 | 31 | - name: Cache YARN dependencies 32 | uses: actions/cache@v2 33 | with: 34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 35 | key: yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | yarn- 38 | 39 | - name: Install node dependencies 40 | run: yarn install --frozen-lockfile 41 | 42 | - name: Install pip3 43 | run: | 44 | python -m pip install --upgrade pip 45 | - name: Install slither 46 | run: | 47 | pip3 install slither-analyzer 48 | - name: Run Slither 49 | run: slither . 50 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Unit Tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out Git repository 16 | uses: actions/checkout@v2 17 | with: 18 | submodules: recursive 19 | 20 | - name: Set up node 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: 12.x 24 | 25 | - name: Get yarn cache directory path 26 | id: yarn-cache-dir-path 27 | run: echo "::set-output name=dir::$(yarn cache dir)" 28 | 29 | - name: Cache YARN dependencies 30 | uses: actions/cache@v2 31 | with: 32 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 33 | key: yarn-${{ hashFiles('**/yarn.lock') }} 34 | restore-keys: | 35 | yarn- 36 | 37 | - run: yarn --no-progress --non-interactive --frozen-lockfile 38 | 39 | - run: yarn build 40 | - run: REPORT_GAS=1 yarn test 41 | - name: Compare gas reports on GitHub 42 | run: yarn codechecks 43 | env: 44 | CC_SECRET: ${{ secrets.CC_SECRET }} 45 | 46 | test-e2e: 47 | name: E2E Tests (on rinkeby) 48 | runs-on: ubuntu-latest 49 | 50 | steps: 51 | - name: Check out Git repository 52 | uses: actions/checkout@v2 53 | with: 54 | submodules: recursive 55 | 56 | - name: Set up node 57 | uses: actions/setup-node@v2 58 | with: 59 | node-version: 12.x 60 | 61 | - name: Get yarn cache directory path 62 | id: yarn-cache-dir-path 63 | run: echo "::set-output name=dir::$(yarn cache dir)" 64 | 65 | - name: Cache YARN dependencies 66 | uses: actions/cache@v2 67 | with: 68 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 69 | key: yarn-${{ hashFiles('**/yarn.lock') }} 70 | restore-keys: | 71 | yarn- 72 | 73 | - run: yarn --no-progress --non-interactive --frozen-lockfile 74 | 75 | - run: yarn build 76 | - run: yarn test-e2e 77 | env: 78 | E2E_TESTS_L1_RPC: ${{ secrets.E2E_TESTS_L1_RPC }} 79 | # this is not really secret + it's not masked in the output which makes debugging easier 80 | E2E_TESTS_L2_RPC: https://rinkeby.arbitrum.io/rpc 81 | E2E_TESTS_PKEY: ${{ secrets.E2E_TESTS_PKEY }} 82 | -------------------------------------------------------------------------------- /.github/workflows/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | cmdname=$(basename $0) 5 | 6 | echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 7 | 8 | usage() 9 | { 10 | cat << USAGE >&2 11 | Usage: 12 | $cmdname host:port [-s] [-t timeout] [-- command args] 13 | -h HOST | --host=HOST Host or IP under test 14 | -p PORT | --port=PORT TCP port under test 15 | Alternatively, you specify the host and port as host:port 16 | -s | --strict Only execute subcommand if the test succeeds 17 | -q | --quiet Don't output any status messages 18 | -t TIMEOUT | --timeout=TIMEOUT 19 | Timeout in seconds, zero for no timeout 20 | -- COMMAND ARGS Execute command with args after the test finishes 21 | USAGE 22 | exit 1 23 | } 24 | 25 | wait_for() 26 | { 27 | if [[ $TIMEOUT -gt 0 ]]; then 28 | echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" 29 | else 30 | echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" 31 | fi 32 | start_ts=$(date +%s) 33 | while : 34 | do 35 | if [[ $ISBUSY -eq 1 ]]; then 36 | nc -z $HOST $PORT 37 | result=$? 38 | else 39 | (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 40 | result=$? 41 | fi 42 | if [[ $result -eq 0 ]]; then 43 | end_ts=$(date +%s) 44 | echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" 45 | break 46 | fi 47 | sleep 1 48 | done 49 | return $result 50 | } 51 | 52 | wait_for_wrapper() 53 | { 54 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 55 | if [[ $QUIET -eq 1 ]]; then 56 | timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 57 | else 58 | timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 59 | fi 60 | PID=$! 61 | trap "kill -INT -$PID" INT 62 | wait $PID 63 | RESULT=$? 64 | if [[ $RESULT -ne 0 ]]; then 65 | echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" 66 | fi 67 | return $RESULT 68 | } 69 | 70 | # process arguments 71 | while [[ $# -gt 0 ]] 72 | do 73 | case "$1" in 74 | *:* ) 75 | hostport=(${1//:/ }) 76 | HOST=${hostport[0]} 77 | PORT=${hostport[1]} 78 | shift 1 79 | ;; 80 | --child) 81 | CHILD=1 82 | shift 1 83 | ;; 84 | -q | --quiet) 85 | QUIET=1 86 | shift 1 87 | ;; 88 | -s | --strict) 89 | STRICT=1 90 | shift 1 91 | ;; 92 | -h) 93 | HOST="$2" 94 | if [[ $HOST == "" ]]; then break; fi 95 | shift 2 96 | ;; 97 | --host=*) 98 | HOST="${1#*=}" 99 | shift 1 100 | ;; 101 | -p) 102 | PORT="$2" 103 | if [[ $PORT == "" ]]; then break; fi 104 | shift 2 105 | ;; 106 | --port=*) 107 | PORT="${1#*=}" 108 | shift 1 109 | ;; 110 | -t) 111 | TIMEOUT="$2" 112 | if [[ $TIMEOUT == "" ]]; then break; fi 113 | shift 2 114 | ;; 115 | --timeout=*) 116 | TIMEOUT="${1#*=}" 117 | shift 1 118 | ;; 119 | --) 120 | shift 121 | CLI="$@" 122 | break 123 | ;; 124 | --help) 125 | usage 126 | ;; 127 | *) 128 | echoerr "Unknown argument: $1" 129 | usage 130 | ;; 131 | esac 132 | done 133 | 134 | if [[ "$HOST" == "" || "$PORT" == "" ]]; then 135 | echoerr "Error: you need to provide a host and port to test." 136 | usage 137 | fi 138 | 139 | TIMEOUT=${TIMEOUT:-15} 140 | STRICT=${STRICT:-0} 141 | CHILD=${CHILD:-0} 142 | QUIET=${QUIET:-0} 143 | 144 | ISBUSY=0 145 | BUSYTIMEFLAG="" 146 | 147 | # Is this macOS? 148 | if [[ "$(uname)" == 'Darwin' ]]; then 149 | # macOS lacks the timeout command -- this function adds an equivalent 150 | function timeout() { perl -e 'alarm shift; exec @ARGV' "$@"; } 151 | else 152 | # check to see if timeout is from busybox? 153 | TIMEOUT_PATH=$(realpath $(which timeout)) 154 | if [[ $TIMEOUT_PATH =~ "busybox" ]]; then 155 | ISBUSY=1 156 | BUSYTIMEFLAG="-t" 157 | fi 158 | fi 159 | 160 | if [[ $CHILD -gt 0 ]]; then 161 | wait_for 162 | RESULT=$? 163 | exit $RESULT 164 | else 165 | if [[ $TIMEOUT -gt 0 ]]; then 166 | wait_for_wrapper 167 | RESULT=$? 168 | else 169 | wait_for 170 | RESULT=$? 171 | fi 172 | fi 173 | 174 | if [[ $CLI != "" ]]; then 175 | if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then 176 | echoerr "$cmdname: strict mode, refusing to execute subprocess" 177 | exit $RESULT 178 | fi 179 | exec $CLI 180 | else 181 | exit $RESULT 182 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /artifacts/ 3 | /cache/ 4 | typechain 5 | crytic-export/ 6 | .env 7 | gasReporterOutput.json 8 | 9 | # echidna 10 | crytic-export/ 11 | corpus/ 12 | 13 | # certora 14 | .*certora* 15 | .last_confs/ 16 | coverage.json 17 | coverage 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makerdao/arbitrum-dai-bridge/ba5e98631307025a74fc03108492b20af57a0d3a/.gitmodules -------------------------------------------------------------------------------- /.mocharc-e2e.js: -------------------------------------------------------------------------------- 1 | // ensure NODE_ENV 2 | process.env.NODE_ENV = 'test' 3 | 4 | // if tsconfig.test.json exists in cwd prefer it 5 | const { existsSync } = require('fs') 6 | const { join } = require('path') 7 | const testTsconfigPath = join(process.cwd(), 'tsconfig.test.json') 8 | if (existsSync(testTsconfigPath)) { 9 | process.env.TS_NODE_PROJECT = testTsconfigPath 10 | } 11 | 12 | // exit test runner on unhandled rejections 13 | process.on('unhandledRejection', (reason, promise) => { 14 | console.error('Unhandled Rejection during test execution:', promise, 'reason:', reason) 15 | process.exit(1) 16 | }) 17 | 18 | module.exports = { 19 | require: ['ts-node/register/transpile-only', 'dotenv/config'], 20 | extension: ['ts'], 21 | watchExtensions: ['ts'], 22 | spec: ['test-e2e/**/*.test.ts'], 23 | timeout: 5000000, 24 | } 25 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | // ensure NODE_ENV 2 | process.env.NODE_ENV = 'test' 3 | 4 | // if tsconfig.test.json exists in cwd prefer it 5 | const { existsSync } = require('fs') 6 | const { join } = require('path') 7 | const testTsconfigPath = join(process.cwd(), 'tsconfig.test.json') 8 | if (existsSync(testTsconfigPath)) { 9 | process.env.TS_NODE_PROJECT = testTsconfigPath 10 | } 11 | 12 | // exit test runner on unhandled rejections 13 | process.on('unhandledRejection', (reason, promise) => { 14 | console.error('Unhandled Rejection during test execution:', promise, 'reason:', reason) 15 | process.exit(1) 16 | }) 17 | 18 | module.exports = { 19 | require: ['ts-node/register/transpile-only'], 20 | extension: ['ts'], 21 | watchExtensions: ['ts'], 22 | spec: ['test/**/*.ts'], 23 | timeout: 80000, 24 | } 25 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.20.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | package.json 4 | dist 5 | typechain 6 | dai.sol -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "proseWrap": "always", 8 | "overrides": [ 9 | { 10 | "files": "*.sol", 11 | "options": { 12 | "printWidth": 100, 13 | "tabWidth": 2, 14 | "useTabs": false, 15 | "singleQuote": false, 16 | "explicitTypes": "always" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: [ 3 | 'test/DaiEchidnaTest.sol', 4 | 'test/BadSpell.sol', 5 | 'test/TestBridgeUpgradeSpell.sol', 6 | 'test/TestDaiMintSpell.sol', 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Lint](https://github.com/makerdao/arbitrum-dai-bridge/actions/workflows/lint.yml/badge.svg)](https://github.com/makerdao/arbitrum-dai-bridge/actions/workflows/lint.yml) 2 | [![Check](https://github.com/makerdao/arbitrum-dai-bridge/actions/workflows/slither.yml/badge.svg)](https://github.com/makerdao/arbitrum-dai-bridge/actions/workflows/slither.yml) 3 | [![Tests](https://github.com/makerdao/arbitrum-dai-bridge/actions/workflows/tests.yml/badge.svg)](https://github.com/makerdao/arbitrum-dai-bridge/actions/workflows/tests.yml) 4 | [![Fuzz](https://github.com/makerdao/arbitrum-dai-bridge/actions/workflows/fuzz.yml/badge.svg)](https://github.com/makerdao/arbitrum-dai-bridge/actions/workflows/fuzz.yml) 5 | 6 | # Arbitrum Dai Bridge 7 | 8 | Arbitrum Dai, upgradable token bridge and governance relay 9 | 10 | ## Contracts 11 | 12 | - `dai.sol` - Improved DAI contract. 13 | - `L1DaiGateway.sol` - L1 side of the bridge. Escrows L1 DAI in `L1Escrow` contract. Unlocks L1 DAI upon withdrawal 14 | message from `L2DaiGateway`. 15 | - `L2DaiGateway.sol` - L2 side of the bridge. Mints new L2 DAI after receiving a message from `L1DaiGateway`. Burns L2 16 | DAI tokens when withdrawals happen. 17 | - `L1Escrow` - Hold funds on L1. Allows having many bridges coexist on L1 and share liquidity. 18 | - `L1GovernanceRelay` & `L2GovernanceRelay` - allows to execute a governance spell on L2. 19 | 20 | ## Diagrams: 21 | 22 | ![Basic deposit](docs/deposit.png?raw=true 'Basic deposit') 23 | 24 | [Full diagram](docs/full.png) 25 | 26 | ## Upgrade guide 27 | 28 | ### Deploying new token bridge 29 | 30 | This bridge stores funds in an external escrow account rather than on the bridge address itself. To upgrade, deploy the 31 | new bridge independently and connect it to the same escrow. Thanks to this, multiple bridges can operate at the same 32 | time (with potentially different interfaces), and no bridge will ever run out of funds. 33 | 34 | ### Closing bridge 35 | 36 | After deploying a new bridge, you might consider closing the old one. The procedure is slightly complicated due to async 37 | messages (`finalizeInboundTransfer`) that can be in progress. 38 | 39 | An owner calls `L2DaiGateway.close()` and `L1DaiGateway.close()` so no new async messages can be sent to the other part 40 | of the bridge. After all async messages are done processing (can take up to 1 week), the bridge is effectively closed. 41 | Now, the owner can consider revoking approval to access funds from escrow on L1 and token minting rights on L2. 42 | 43 | ## Emergency shutdown 44 | 45 | If ES is triggered, ESM contract can be used to `deny` access from the `PauseProxy` (governance). In such scenario the 46 | bridge continues to work as usual and it's impossible to close it. 47 | 48 | ## Known Risks 49 | 50 | ### Wrong parameters for xchain messages 51 | 52 | Arbitrum's xchain messages require 53 | [a couple of arguments](https://developer.offchainlabs.com/docs/l1_l2_messages#parameters). We expose these in our 54 | public interfaces so it's up to the users to select appropriate values. Wrong values will cause a need to manually retry 55 | L1 -> L2 messages or in the worst case can cause a message to be lost. This is especially difficult when interacting 56 | with `L1GovernanceRelay` via MakerDAO governance spells with a long delay (2 days). 57 | 58 | ### Arbitrum bug 59 | 60 | In this section, we describe various risks caused by possible **bugs** in Arbitrum system. 61 | 62 | **L1 -> L2 message passing bug** 63 | 64 | Bug allowing to send arbitrary messages from L1 to L2 ie. This could result in minting of uncollateralized L2 DAI. This 65 | can be done via: 66 | 67 | - sending `finalizeInboundTransfer` messages directly to `L2DaiGateway` 68 | - granting minting rights by executing malicious spell with `L2GovernanceRelay` 69 | 70 | Immediately withdrawing L2 DAI to L1 DAI is not possible because of the dispute period (1 week). In case of such bug, 71 | governance can disconnect `L1DAITokenBridge` from `L1Escrow`, ensuring that no L1 DAI can be stolen. Even with 2 days 72 | delay on governance actions, there should be plenty of time to coordinate action. Later off-chain coordination is 73 | required to send DAI back to rightful owners or redeploy Arbitrum system. 74 | 75 | **L2 -> L1 message passing bug** 76 | 77 | Bug allowing to send arbitrary messages from L2 to L1 is potentially more harmful. This can happen in two ways: 78 | 79 | 1. Bug in `Outbox` allows sending arbitrary messages on L1 bypassing the dispute period, 80 | 2. The fraud proof system stops working which allows submitting incorrect state root. Such state root can be used to 81 | proof an arbitrary message sent from L2 to L1. This will be a subject to a dispute period (1 week). 82 | 83 | If (1) happens, an attacker can immediately drain L1 DAI from `L1Escrow`. 84 | 85 | If (2) happens, governance can disconnect `L1DAITokenBridge` from `L1Escrow` and prevent the theft of L1 DAI. 86 | 87 | **Malicious router** 88 | 89 | `GatewayRouter` developed by Arbitrum team, is a privileged actor in our system and allows explicitly passing addresses 90 | that initiated deposits/withdrawals. It was reviewed by our team but if there is a bug in its implementation it could in 91 | theory be used to steal funds from the escrow (burn arbitrary L2 DAI tokens and withdraw them to any address, or steal 92 | DAI that was already approved on L1). If it's malicious, it could be used to steal funds. 93 | 94 | ### Arbitrum upgrade 95 | 96 | Arbitrum contracts ARE upgradable. A malicious upgrade could result in stealing user funds in many ways. Users need to 97 | trust Arbitrum admins while using this bridge or while interacting with the Arbitrum network. 98 | 99 | ### Governance mistake during upgrade 100 | 101 | Bridge upgrade is not a trivial procedure due to the async messages between L1 and L2. The whole process is described in 102 | _Upgrade guide_ in this document. 103 | 104 | If a governance spell mistakenly revokes old bridge approval to access escrow funds, async withdrawal messages will 105 | fail. Fortunately, reverted messages can be retried at a later date (for one week for L1 -> L2 messages), so governance 106 | has a chance to fix its mistake and process pending messages again. 107 | 108 | ## Invariants 109 | 110 | ### L1 DAI Locked and L2 DAI Minted 111 | 112 | ``` 113 | L1DAI.balanceOf(escrow) ≥ L2DAI.totalSupply() 114 | ``` 115 | 116 | All DAI available on L2 should be locked on L1. This should hold true with more bridges as well. 117 | 118 | It's `>=` because: 119 | 120 | a) when depositing on L1, locking is instant but minting is an async message 121 | 122 | b) when withdrawing from L2, burning is instant but unlocking on L1 is an async message and is subject to a dispute 123 | period (1 week) 124 | 125 | c) someone can send L1 DAI directly to the escrow 126 | 127 | ## Deployments 128 | 129 | ### Mainnet 130 | 131 | ```json 132 | { 133 | "l1DaiGateway": "0xD3B5b60020504bc3489D6949d545893982BA3011", 134 | "l1Escrow": "0xA10c7CE4b876998858b1a9E12b10092229539400", 135 | "l2Dai": "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", 136 | "l2DaiGateway": "0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65", 137 | "l1Dai": "0x6B175474E89094C44Da98b954EedeAC495271d0F", 138 | "l1GovRelay": "0x9ba25c289e351779E0D481Ba37489317c34A899d", 139 | "l2GovRelay": "0x10E6593CDda8c58a1d0f14C5164B376352a55f2F" 140 | } 141 | ``` 142 | 143 | ### Rinkeby 144 | 145 | ```json 146 | { 147 | "l1DaiGateway": "0x10E6593CDda8c58a1d0f14C5164B376352a55f2F", 148 | "l1Escrow": "0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65", 149 | "l2Dai": "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", 150 | "l2DaiGateway": "0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65", 151 | "l1Dai": "0xd9e66A2f546880EA4d800F189d6F12Cc15Bff281", 152 | "l1GovRelay": "0x09B354CDA89203BB7B3131CC728dFa06ab09Ae2F", 153 | "l2GovRelay": "0x10E6593CDda8c58a1d0f14C5164B376352a55f2F" 154 | } 155 | ``` 156 | 157 | ## Running 158 | 159 | ``` 160 | yarn 161 | yarn build 162 | yarn test # runs unit tests 163 | ``` 164 | 165 | ## Running E2E tests 166 | 167 | Arbitrum doesn't provide local dev environment so E2E tests are executed against the Rinkeby network. 168 | 169 | ## Development 170 | 171 | Run `yarn test:fix` to run linting in fix mode, auto-formatting and unit tests. 172 | 173 | Running `yarn test` makes sure that contracts are compiled. Running `yarn test-e2e` doesn't. 174 | 175 | ## Fuzzing 176 | 177 | ### Install Echidna 178 | 179 | - Precompiled Binaries (recommended) 180 | 181 | Before starting, make sure Slither is installed: 182 | 183 | ``` 184 | $ pip3 install slither-analyzer 185 | ``` 186 | 187 | To quickly test Echidna in Linux or MacOS: [release page](https://github.com/crytic/echidna/releases) 188 | 189 | ### Local Dependencies 190 | 191 | - Slither: 192 | ``` 193 | $ pip3 install slither-analyzer 194 | ``` 195 | - solc-select: 196 | ``` 197 | $ pip3 install solc-select 198 | ``` 199 | 200 | ### Run Echidna Tests 201 | 202 | - Install solc version: 203 | ``` 204 | $ solc-select install 0.6.11 205 | ``` 206 | - Select solc version: 207 | ``` 208 | $ solc-select use 0.6.11 209 | ``` 210 | - Run Echidna Tests: 211 | ``` 212 | $ yarn fuzz 213 | ``` 214 | 215 | ## Certora 216 | 217 | ### Install Certora 218 | 219 | - Install Java 220 | ``` 221 | sudo apt install openjdk-14-jdk 222 | ``` 223 | - Install Certora Prover 224 | ``` 225 | pip3 install certora-cli 226 | ``` 227 | - Set Certora Key 228 | ``` 229 | export CERTORAKEY= 230 | ``` 231 | 232 | ### Local Dependencies 233 | 234 | - solc-select: 235 | ``` 236 | pip3 install solc-select 237 | ``` 238 | 239 | ### Run Certora Specs 240 | 241 | - Install solc version: 242 | ``` 243 | solc-select install 0.6.11 244 | ``` 245 | - Run Certora Specs: 246 | ``` 247 | yarn certora 248 | ``` 249 | -------------------------------------------------------------------------------- /arbitrum-helpers/abis/ArbRetryableTx.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "inputs": [ 5 | { 6 | "internalType": "uint256", 7 | "name": "calldataSize", 8 | "type": "uint256" 9 | } 10 | ], 11 | "name": "getSubmissionPrice", 12 | "outputs": [ 13 | { 14 | "internalType": "uint256", 15 | "name": "", 16 | "type": "uint256" 17 | }, 18 | { 19 | "internalType": "uint256", 20 | "name": "", 21 | "type": "uint256" 22 | } 23 | ], 24 | "stateMutability": "view", 25 | "type": "function" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /arbitrum-helpers/abis/IERC677Receiver.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "inputs": [ 5 | { 6 | "internalType": "address", 7 | "name": "_sender", 8 | "type": "address" 9 | }, 10 | { 11 | "name": "_value", 12 | "type": "uint256" 13 | }, 14 | { 15 | "name": "data", 16 | "type": "bytes" 17 | } 18 | ], 19 | "name": "onTokenTransfer", 20 | "outputs": [], 21 | "stateMutability": "nonpayable", 22 | "type": "function" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /arbitrum-helpers/abis/IERC677ReceiverAndExitReceiver.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "inputs": [ 5 | { 6 | "internalType": "address", 7 | "name": "_sender", 8 | "type": "address" 9 | }, 10 | { 11 | "name": "_value", 12 | "type": "uint256" 13 | }, 14 | { 15 | "name": "data", 16 | "type": "bytes" 17 | } 18 | ], 19 | "name": "onTokenTransfer", 20 | "outputs": [], 21 | "stateMutability": "nonpayable", 22 | "type": "function" 23 | }, 24 | { 25 | "inputs": [ 26 | { 27 | "internalType": "address", 28 | "name": "sender", 29 | "type": "address" 30 | }, 31 | { 32 | "name": "exitNum", 33 | "type": "uint256" 34 | }, 35 | { 36 | "name": "data", 37 | "type": "bytes" 38 | } 39 | ], 40 | "name": "onExitTransfer", 41 | "outputs": [ 42 | { 43 | "name": "", 44 | "type": "bool" 45 | } 46 | ], 47 | "stateMutability": "nonpayable", 48 | "type": "function" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /arbitrum-helpers/abis/ITradeableExitReceiver.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "inputs": [ 5 | { 6 | "internalType": "address", 7 | "name": "sender", 8 | "type": "address" 9 | }, 10 | { 11 | "name": "exitNum", 12 | "type": "uint256" 13 | }, 14 | { 15 | "name": "data", 16 | "type": "bytes" 17 | } 18 | ], 19 | "name": "onExitTransfer", 20 | "outputs": [ 21 | { 22 | "name": "", 23 | "type": "bool" 24 | } 25 | ], 26 | "stateMutability": "nonpayable", 27 | "type": "function" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /arbitrum-helpers/abis/NodeInterface.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "inputs": [ 5 | { 6 | "internalType": "address", 7 | "name": "sender", 8 | "type": "address" 9 | }, 10 | { 11 | "internalType": "uint256", 12 | "name": "deposit", 13 | "type": "uint256" 14 | }, 15 | { 16 | "internalType": "address", 17 | "name": "destAddr", 18 | "type": "address" 19 | }, 20 | { 21 | "internalType": "uint256", 22 | "name": "l2CallValue", 23 | "type": "uint256" 24 | }, 25 | { 26 | "internalType": "uint256", 27 | "name": "maxSubmissionCost", 28 | "type": "uint256" 29 | }, 30 | { 31 | "internalType": "address", 32 | "name": "excessFeeRefundAddress", 33 | "type": "address" 34 | }, 35 | { 36 | "internalType": "address", 37 | "name": "callValueRefundAddress", 38 | "type": "address" 39 | }, 40 | { 41 | "internalType": "uint256", 42 | "name": "maxGas", 43 | "type": "uint256" 44 | }, 45 | { 46 | "internalType": "uint256", 47 | "name": "gasPriceBid", 48 | "type": "uint256" 49 | }, 50 | { 51 | "internalType": "bytes", 52 | "name": "data", 53 | "type": "bytes" 54 | } 55 | ], 56 | "name": "estimateRetryableTicket", 57 | "outputs": [ 58 | { 59 | "internalType": "uint256", 60 | "name": "", 61 | "type": "uint256" 62 | }, 63 | { 64 | "internalType": "uint256", 65 | "name": "", 66 | "type": "uint256" 67 | } 68 | ], 69 | "stateMutability": "pure", 70 | "type": "function" 71 | }, 72 | { 73 | "inputs": [ 74 | { 75 | "internalType": "uint256", 76 | "name": "batchNum", 77 | "type": "uint256" 78 | }, 79 | { 80 | "internalType": "uint64", 81 | "name": "index", 82 | "type": "uint64" 83 | } 84 | ], 85 | "name": "lookupMessageBatchProof", 86 | "outputs": [ 87 | { 88 | "internalType": "bytes32[]", 89 | "name": "proof", 90 | "type": "bytes32[]" 91 | }, 92 | { 93 | "internalType": "uint256", 94 | "name": "path", 95 | "type": "uint256" 96 | }, 97 | { 98 | "internalType": "address", 99 | "name": "l2Sender", 100 | "type": "address" 101 | }, 102 | { 103 | "internalType": "address", 104 | "name": "l1Dest", 105 | "type": "address" 106 | }, 107 | { 108 | "internalType": "uint256", 109 | "name": "l2Block", 110 | "type": "uint256" 111 | }, 112 | { 113 | "internalType": "uint256", 114 | "name": "l1Block", 115 | "type": "uint256" 116 | }, 117 | { 118 | "internalType": "uint256", 119 | "name": "timestamp", 120 | "type": "uint256" 121 | }, 122 | { 123 | "internalType": "uint256", 124 | "name": "amount", 125 | "type": "uint256" 126 | }, 127 | { 128 | "internalType": "bytes", 129 | "name": "calldataForL1", 130 | "type": "bytes" 131 | } 132 | ], 133 | "stateMutability": "view", 134 | "type": "function" 135 | } 136 | ] 137 | } 138 | -------------------------------------------------------------------------------- /arbitrum-helpers/bridge.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, ethers, Wallet } from 'ethers' 2 | import { defaultAbiCoder } from 'ethers/lib/utils' 3 | 4 | import { waitForTx, waitToRelayTxsToL2 } from '../arbitrum-helpers' 5 | import { L1DaiGateway } from '../typechain' 6 | import { getArbitrumCoreContracts } from './contracts' 7 | import { BridgeDeployment, NetworkConfig } from './deploy' 8 | 9 | export async function getGasPriceBid(l2: ethers.providers.BaseProvider): Promise { 10 | return await l2.getGasPrice() 11 | } 12 | 13 | export async function getMaxSubmissionPrice( 14 | l2: ethers.providers.BaseProvider, 15 | calldataOrCalldataLength: string | number, 16 | ) { 17 | const calldataLength = 18 | typeof calldataOrCalldataLength === 'string' ? calldataOrCalldataLength.length : calldataOrCalldataLength 19 | const [submissionPrice] = await getArbitrumCoreContracts(l2).arbRetryableTx.getSubmissionPrice(calldataLength) 20 | const maxSubmissionPrice = submissionPrice.mul(4) 21 | return maxSubmissionPrice 22 | } 23 | 24 | export async function getMaxGas( 25 | l2: ethers.providers.BaseProvider, 26 | sender: string, 27 | destination: string, 28 | refundDestination: string, 29 | maxSubmissionPrice: BigNumber, 30 | gasPriceBid: BigNumber, 31 | calldata: string, 32 | ): Promise { 33 | const [estimatedGas] = await getArbitrumCoreContracts(l2).nodeInterface.estimateRetryableTicket( 34 | sender, 35 | ethers.utils.parseEther('0.05'), 36 | destination, 37 | 0, 38 | maxSubmissionPrice, 39 | refundDestination, 40 | refundDestination, 41 | 0, 42 | gasPriceBid, 43 | calldata, 44 | ) 45 | const maxGas = estimatedGas.mul(4) 46 | 47 | return maxGas 48 | } 49 | 50 | export async function depositToStandardBridge({ 51 | from, 52 | to, 53 | l2Provider, 54 | deposit, 55 | l1Gateway, 56 | l1TokenAddress, 57 | l2GatewayAddress, 58 | }: { 59 | from: Wallet 60 | to: string 61 | l2Provider: ethers.providers.BaseProvider 62 | deposit: BigNumber | string 63 | l1Gateway: L1DaiGateway 64 | l1TokenAddress: string 65 | l2GatewayAddress: string 66 | }) { 67 | const gasPriceBid = await getGasPriceBid(l2Provider) 68 | 69 | const onlyData = '0x' 70 | const depositCalldata = await l1Gateway.getOutboundCalldata(l1TokenAddress, from.address, to, deposit, onlyData) 71 | const maxSubmissionPrice = await getMaxSubmissionPrice(l2Provider, depositCalldata) 72 | 73 | const maxGas = await getMaxGas( 74 | l2Provider, 75 | l1Gateway.address, 76 | l2GatewayAddress, 77 | from.address, 78 | maxSubmissionPrice, 79 | gasPriceBid, 80 | depositCalldata, 81 | ) 82 | const defaultData = defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionPrice, onlyData]) 83 | const ethValue = await maxSubmissionPrice.add(gasPriceBid.mul(maxGas)) 84 | 85 | return await waitForTx( 86 | l1Gateway.connect(from).outboundTransfer(l1TokenAddress, to, deposit, maxGas, gasPriceBid, defaultData, { 87 | value: ethValue, 88 | }), 89 | ) 90 | } 91 | 92 | export async function depositToStandardRouter({ 93 | from, 94 | to, 95 | l2Provider, 96 | deposit, 97 | l1Gateway, 98 | l1Router, 99 | l1TokenAddress, 100 | l2GatewayAddress, 101 | }: { 102 | from: Wallet 103 | to: string 104 | l2Provider: ethers.providers.BaseProvider 105 | deposit: BigNumber | string 106 | l1Router: any 107 | l1Gateway: L1DaiGateway 108 | l1TokenAddress: string 109 | l2GatewayAddress: string 110 | }) { 111 | const gasPriceBid = await getGasPriceBid(l2Provider) 112 | 113 | const onlyData = '0x' 114 | const depositCalldata = await l1Gateway.getOutboundCalldata(l1TokenAddress, from.address, to, deposit, onlyData) 115 | const maxSubmissionPrice = await getMaxSubmissionPrice(l2Provider, depositCalldata) 116 | 117 | const maxGas = await getMaxGas( 118 | l2Provider, 119 | l1Gateway.address, 120 | l2GatewayAddress, 121 | from.address, 122 | maxSubmissionPrice, 123 | gasPriceBid, 124 | depositCalldata, 125 | ) 126 | const defaultData = defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionPrice, onlyData]) 127 | const ethValue = await maxSubmissionPrice.add(gasPriceBid.mul(maxGas)) 128 | 129 | return await waitForTx( 130 | l1Router.connect(from).outboundTransfer(l1TokenAddress, to, deposit, maxGas, gasPriceBid, defaultData, { 131 | value: ethValue, 132 | }), 133 | ) 134 | } 135 | 136 | export async function setGatewayForToken({ 137 | l2Provider, 138 | l1Router, 139 | tokenGateway, 140 | }: { 141 | l2Provider: ethers.providers.BaseProvider 142 | l1Router: any 143 | l2Router: any 144 | tokenGateway: L1DaiGateway 145 | }) { 146 | const token = await tokenGateway.l1Dai() 147 | 148 | const calldataLength = 300 + 20 * 2 // fixedOverheadLength + 2 * address 149 | const gasPriceBid = await getGasPriceBid(l2Provider) 150 | const maxSubmissionPrice = await getMaxSubmissionPrice(l2Provider, calldataLength) 151 | await l1Router.setGateways([token], [tokenGateway.address], 0, gasPriceBid, maxSubmissionPrice, { 152 | value: maxSubmissionPrice, 153 | }) 154 | } 155 | 156 | export async function executeSpell( 157 | network: NetworkConfig, 158 | bridgeDeployment: BridgeDeployment, 159 | l2Spell: string, 160 | spellCalldata: string, 161 | ) { 162 | const l2MessageCalldata = bridgeDeployment.l2GovRelay.interface.encodeFunctionData('relay', [l2Spell, spellCalldata]) 163 | const calldataLength = l2MessageCalldata.length 164 | 165 | const gasPriceBid = await getGasPriceBid(network.l2.provider) 166 | const maxSubmissionPrice = await getMaxSubmissionPrice(network.l2.provider, calldataLength) 167 | const maxGas = await getMaxGas( 168 | network.l2.provider, 169 | bridgeDeployment.l1GovRelay.address, 170 | bridgeDeployment.l2GovRelay.address, 171 | bridgeDeployment.l2GovRelay.address, 172 | maxSubmissionPrice, 173 | gasPriceBid, 174 | l2MessageCalldata, 175 | ) 176 | const ethValue = maxSubmissionPrice.add(gasPriceBid.mul(maxGas)) 177 | console.log('ethValue: ', ethValue.toString()) 178 | 179 | await network.l1.deployer.sendTransaction({ to: bridgeDeployment.l1GovRelay.address, value: ethValue }) 180 | 181 | await waitToRelayTxsToL2( 182 | waitForTx( 183 | bridgeDeployment.l1GovRelay 184 | .connect(network.l1.deployer) 185 | .relay(l2Spell, spellCalldata, ethValue, maxGas, gasPriceBid, maxSubmissionPrice), 186 | ), 187 | network.l1.inbox, 188 | network.l1.provider, 189 | network.l2.provider, 190 | ) 191 | } 192 | -------------------------------------------------------------------------------- /arbitrum-helpers/contracts.ts: -------------------------------------------------------------------------------- 1 | import { ContractFactory, ethers } from 'ethers' 2 | import { readFileSync } from 'fs' 3 | import { join } from 'path' 4 | 5 | export const arbitrumL2CoreContracts = { 6 | arbRetryableTx: '0x000000000000000000000000000000000000006E', 7 | nodeInterface: '0x00000000000000000000000000000000000000C8', 8 | } 9 | 10 | export function getArbitrumCoreContracts(l2: ethers.providers.BaseProvider) { 11 | return { 12 | arbRetryableTx: new ethers.Contract( 13 | arbitrumL2CoreContracts.arbRetryableTx, 14 | require('./abis/ArbRetryableTx.json').abi, 15 | l2, 16 | ), 17 | nodeInterface: new ethers.Contract( 18 | arbitrumL2CoreContracts.nodeInterface, 19 | require('./abis/NodeInterface.json').abi, 20 | l2, 21 | ), 22 | } 23 | } 24 | 25 | export function getArbitrumArtifactFactory(name: string): T { 26 | const artifact = getArbitrumArtifact(name) 27 | 28 | return new ethers.ContractFactory(artifact.abi, artifact.bytecode) as any 29 | } 30 | 31 | export function getArbitrumArtifact(name: string): any { 32 | const artifactPath = join(__dirname, './artifacts', `${name}.json`) 33 | const artifactRaw = readFileSync(artifactPath, 'utf-8') 34 | const artifact = JSON.parse(artifactRaw) 35 | 36 | return artifact 37 | } 38 | -------------------------------------------------------------------------------- /arbitrum-helpers/deploy.ts: -------------------------------------------------------------------------------- 1 | import { getActiveWards, getAddressOfNextDeployedContract } from '@makerdao/hardhat-utils' 2 | import { AuthableLike } from '@makerdao/hardhat-utils/dist/auth/AuthableContract' 3 | import { expect } from 'chai' 4 | import { providers, Signer, Wallet } from 'ethers' 5 | import { ethers } from 'hardhat' 6 | import { compact } from 'lodash' 7 | import { assert, Awaited } from 'ts-essentials' 8 | 9 | import { waitForTx } from '../arbitrum-helpers' 10 | import { Dai, L1DaiGateway, L1Escrow, L1GovernanceRelay, L2DaiGateway, L2GovernanceRelay } from '../typechain' 11 | import { getArbitrumArtifact, getArbitrumArtifactFactory } from './contracts' 12 | import { deployUsingFactoryAndVerify } from './deployment' 13 | 14 | export interface NetworkConfig { 15 | l1: { 16 | provider: providers.BaseProvider 17 | deployer: Wallet 18 | dai: string 19 | inbox: string 20 | makerPauseProxy: string 21 | makerESM: string 22 | } 23 | l2: { 24 | provider: providers.BaseProvider 25 | deployer: Wallet 26 | } 27 | } 28 | 29 | interface RouterDependencies { 30 | l1: { 31 | deployer: Signer 32 | dai: string 33 | inbox: string 34 | } 35 | l2: { 36 | deployer: Signer 37 | } 38 | } 39 | 40 | export interface RouterDeployment { 41 | l1GatewayRouter: any 42 | l2GatewayRouter: any 43 | } 44 | 45 | export type BridgeDeployment = Awaited> 46 | 47 | export async function deployRouter(deps: RouterDependencies): Promise { 48 | const zeroAddr = ethers.constants.AddressZero 49 | 50 | const l1GatewayRouter = await deployUsingFactoryAndVerify( 51 | deps.l1.deployer, 52 | getArbitrumArtifactFactory('L1GatewayRouter'), 53 | [], 54 | ) 55 | 56 | const futureAddressOfL2GatewayRouter = await getAddressOfNextDeployedContract(deps.l2.deployer) 57 | 58 | await waitForTx( 59 | l1GatewayRouter.initialize( 60 | await deps.l1.deployer.getAddress(), 61 | zeroAddr, 62 | zeroAddr, 63 | futureAddressOfL2GatewayRouter, 64 | deps.l1.inbox, 65 | ), 66 | ) 67 | 68 | const l2GatewayRouter = await deployUsingFactoryAndVerify( 69 | deps.l2.deployer, 70 | getArbitrumArtifactFactory('L2GatewayRouter'), 71 | [], 72 | ) 73 | expect(l2GatewayRouter.address).to.be.eq(futureAddressOfL2GatewayRouter) 74 | 75 | await waitForTx(l2GatewayRouter.initialize(l1GatewayRouter.address, zeroAddr)) 76 | 77 | return { 78 | l1GatewayRouter, 79 | l2GatewayRouter, 80 | } 81 | } 82 | 83 | // @note: by default the deployment won't be fully secured -- deployer won't be denied 84 | export async function deployBridge( 85 | deps: NetworkConfig, 86 | routerDeployment: RouterDeployment, 87 | desiredL2DaiAddress?: string, 88 | ) { 89 | if (desiredL2DaiAddress) { 90 | const nextAddress = await getAddressOfNextDeployedContract(deps.l2.deployer) 91 | expect(nextAddress.toLowerCase()).to.be.eq( 92 | desiredL2DaiAddress.toLowerCase(), 93 | 'Expected L2DAI address doesnt match with address that will be deployed', 94 | ) 95 | } 96 | expect(await deps.l1.deployer.getBalance()).to.not.be.eq(0, 'Not enough balance on L1') 97 | expect(await deps.l2.deployer.getBalance()).to.not.be.eq(0, 'Not enough balance on L2') 98 | 99 | const l1Escrow = await deployUsingFactoryAndVerify(deps.l1.deployer, await ethers.getContractFactory('L1Escrow'), []) 100 | console.log('Deployed l1Escrow at: ', l1Escrow.address) 101 | 102 | const l2Dai = await deployUsingFactoryAndVerify(deps.l2.deployer, await ethers.getContractFactory('Dai'), []) 103 | console.log('Deployed l2Dai at: ', l2Dai.address) 104 | const l1DaiGatewayFutureAddr = await getAddressOfNextDeployedContract(deps.l1.deployer) 105 | const l2DaiGateway = await deployUsingFactoryAndVerify( 106 | deps.l2.deployer, 107 | await ethers.getContractFactory('L2DaiGateway'), 108 | [l1DaiGatewayFutureAddr, routerDeployment.l2GatewayRouter.address, deps.l1.dai, l2Dai.address], 109 | ) 110 | console.log('Deployed l2DaiGateway at: ', l2DaiGateway.address) 111 | 112 | const l1DaiGateway = await deployUsingFactoryAndVerify( 113 | deps.l1.deployer, 114 | await ethers.getContractFactory('L1DaiGateway'), 115 | [ 116 | l2DaiGateway.address, 117 | routerDeployment.l1GatewayRouter.address, 118 | deps.l1.inbox, 119 | deps.l1.dai, 120 | l2Dai.address, 121 | l1Escrow.address, 122 | ], 123 | ) 124 | console.log('Deployed l1DaiGateway at: ', l1DaiGateway.address) 125 | expect(l1DaiGateway.address).to.be.eq( 126 | l1DaiGatewayFutureAddr, 127 | "Expected future address of l1DaiGateway doesn't match actual address!", 128 | ) 129 | 130 | const l2GovRelayFutureAddr = await getAddressOfNextDeployedContract(deps.l2.deployer) 131 | const l1GovRelay = await deployUsingFactoryAndVerify( 132 | deps.l1.deployer, 133 | await ethers.getContractFactory('L1GovernanceRelay'), 134 | [deps.l1.inbox, l2GovRelayFutureAddr], 135 | ) 136 | console.log('Deployed l1GovernanceRelay at: ', l1GovRelay.address) 137 | const l2GovRelay = await deployUsingFactoryAndVerify( 138 | deps.l2.deployer, 139 | await ethers.getContractFactory('L2GovernanceRelay'), 140 | [l1GovRelay.address], 141 | ) 142 | expect(l2GovRelay.address).to.be.eq(l2GovRelayFutureAddr) 143 | console.log('Deployed l2GovernanceRelay at: ', l2GovRelay.address) 144 | 145 | // permissions 146 | console.log('Setting permissions...') 147 | await waitForTx(l2Dai.rely(l2DaiGateway.address)) // allow minting/burning from the bridge 148 | await waitForTx(l2Dai.rely(l2GovRelay.address)) // allow granting new minting rights by the governance 149 | 150 | await waitForTx(l2DaiGateway.rely(l2GovRelay.address)) // allow closing bridge by the governance 151 | 152 | await waitForTx(l1Escrow.approve(deps.l1.dai, l1DaiGateway.address, ethers.constants.MaxUint256)) // allow l1DaiGateway accessing funds from the bridge for withdrawals 153 | await waitForTx(l1Escrow.rely(deps.l1.makerPauseProxy)) 154 | await waitForTx(l1Escrow.rely(deps.l1.makerESM)) 155 | 156 | await waitForTx(l1DaiGateway.rely(deps.l1.makerPauseProxy)) 157 | await waitForTx(l1DaiGateway.rely(deps.l1.makerESM)) 158 | 159 | await waitForTx(l1GovRelay.rely(deps.l1.makerPauseProxy)) 160 | await waitForTx(l1GovRelay.rely(deps.l1.makerESM)) 161 | 162 | return { 163 | l1DaiGateway, 164 | l1Escrow, 165 | l2Dai, 166 | l2DaiGateway, 167 | l1Dai: (await ethers.getContractAt('Dai', deps.l1.dai, deps.l1.deployer)) as Dai, 168 | l1GovRelay, 169 | l2GovRelay, 170 | } 171 | } 172 | 173 | function normalizeAddresses(addresses: string[]): string[] { 174 | return addresses.map((a) => a.toLowerCase()).sort() 175 | } 176 | 177 | export async function performSanityChecks( 178 | deps: NetworkConfig, 179 | bridgeDeployment: BridgeDeployment, 180 | l1BlockOfBeginningOfDeployment: number, 181 | l2BlockOfBeginningOfDeployment: number, 182 | includeDeployer: boolean, 183 | ) { 184 | console.log('Performing sanity checks...') 185 | 186 | async function checkPermissions(contract: AuthableLike, startBlock: number, _expectedPermissions: string[]) { 187 | const actualPermissions = await getActiveWards(contract, startBlock) 188 | const expectedPermissions = compact([..._expectedPermissions, includeDeployer && deps.l1.deployer.address]) 189 | 190 | expect(normalizeAddresses(actualPermissions)).to.deep.eq(normalizeAddresses(expectedPermissions)) 191 | } 192 | 193 | await checkPermissions(bridgeDeployment.l1Escrow, l1BlockOfBeginningOfDeployment, [ 194 | deps.l1.makerPauseProxy, 195 | deps.l1.makerESM, 196 | ]) 197 | await checkPermissions(bridgeDeployment.l1DaiGateway, l1BlockOfBeginningOfDeployment, [ 198 | deps.l1.makerPauseProxy, 199 | deps.l1.makerESM, 200 | ]) 201 | await checkPermissions(bridgeDeployment.l1GovRelay, l1BlockOfBeginningOfDeployment, [ 202 | deps.l1.makerPauseProxy, 203 | deps.l1.makerESM, 204 | ]) 205 | await checkPermissions(bridgeDeployment.l2DaiGateway, l2BlockOfBeginningOfDeployment, [ 206 | bridgeDeployment.l2GovRelay.address, 207 | ]) 208 | await checkPermissions(bridgeDeployment.l2Dai, l2BlockOfBeginningOfDeployment, [ 209 | bridgeDeployment.l2DaiGateway.address, 210 | bridgeDeployment.l2GovRelay.address, 211 | ]) 212 | 213 | expect(await bridgeDeployment.l1DaiGateway.l1Escrow()).to.be.eq(bridgeDeployment.l1Escrow.address) 214 | expect(await bridgeDeployment.l1GovRelay.l2GovernanceRelay()).to.be.eq(bridgeDeployment.l2GovRelay.address) 215 | expect(await bridgeDeployment.l1GovRelay.inbox()).to.be.eq(await bridgeDeployment.l1DaiGateway.inbox()) 216 | } 217 | 218 | export async function denyDeployer(deps: NetworkConfig, bridgeDeployment: BridgeDeployment) { 219 | console.log('Denying deployer access') 220 | await waitForTx(bridgeDeployment.l2Dai.deny(await deps.l2.deployer.getAddress())) 221 | await waitForTx(bridgeDeployment.l2DaiGateway.deny(await deps.l2.deployer.getAddress())) 222 | await waitForTx(bridgeDeployment.l1Escrow.deny(await deps.l1.deployer.getAddress())) 223 | await waitForTx(bridgeDeployment.l1DaiGateway.deny(await deps.l1.deployer.getAddress())) 224 | await waitForTx(bridgeDeployment.l1GovRelay.deny(await deps.l1.deployer.getAddress())) 225 | } 226 | 227 | export async function useStaticDeployment( 228 | network: NetworkConfig, 229 | addresses: { 230 | l1DaiGateway: string 231 | l1Escrow: string 232 | l2Dai: string 233 | l2DaiGateway: string 234 | l1Dai: string 235 | l1GovRelay: string 236 | l2GovRelay: string 237 | }, 238 | ): ReturnType { 239 | return { 240 | l1DaiGateway: (await ethers.getContractAt( 241 | 'L1DaiGateway', 242 | throwIfUndefined(addresses.l1DaiGateway), 243 | network.l1.deployer, 244 | )) as L1DaiGateway, 245 | l1Escrow: (await ethers.getContractAt( 246 | 'L1Escrow', 247 | throwIfUndefined(addresses.l1Escrow), 248 | network.l1.deployer, 249 | )) as L1Escrow, 250 | l2Dai: (await ethers.getContractAt('Dai', throwIfUndefined(addresses.l2Dai), network.l2.deployer)) as Dai, 251 | l2DaiGateway: (await ethers.getContractAt( 252 | 'L2DaiGateway', 253 | throwIfUndefined(addresses.l2DaiGateway), 254 | network.l2.deployer, 255 | )) as L2DaiGateway, 256 | l1Dai: (await ethers.getContractAt('Dai', throwIfUndefined(addresses.l1Dai), network.l1.deployer)) as Dai, 257 | l1GovRelay: (await ethers.getContractAt( 258 | 'L1GovernanceRelay', 259 | throwIfUndefined(addresses.l1GovRelay), 260 | network.l1.deployer, 261 | )) as L1GovernanceRelay, 262 | l2GovRelay: (await ethers.getContractAt( 263 | 'L2GovernanceRelay', 264 | throwIfUndefined(addresses.l2GovRelay), 265 | network.l2.deployer, 266 | )) as L2GovernanceRelay, 267 | } 268 | } 269 | 270 | export async function useStaticRouterDeployment( 271 | network: NetworkConfig, 272 | addresses: { 273 | l1GatewayRouter: string 274 | l2GatewayRouter: string 275 | }, 276 | ): ReturnType { 277 | return { 278 | l1GatewayRouter: (await ethers.getContractAt( 279 | getArbitrumArtifact('L1GatewayRouter').abi as any, 280 | throwIfUndefined(addresses.l1GatewayRouter), 281 | network.l1.deployer, 282 | )) as any, 283 | l2GatewayRouter: (await ethers.getContractAt( 284 | getArbitrumArtifact('L2GatewayRouter').abi as any, 285 | throwIfUndefined(addresses.l2GatewayRouter), 286 | network.l1.deployer, 287 | )) as any, // todo types for router 288 | } 289 | } 290 | 291 | function throwIfUndefined(val: any): any { 292 | assert(val !== undefined, 'val is undefined! Static config incorrect!') 293 | 294 | return val 295 | } 296 | -------------------------------------------------------------------------------- /arbitrum-helpers/deployment.ts: -------------------------------------------------------------------------------- 1 | import { ContractFactory, Signer } from 'ethers' 2 | import { ethers } from 'hardhat' 3 | import { isEmpty } from 'lodash' 4 | 5 | import { waitForTx } from '.' 6 | 7 | export async function deployUsingFactory( 8 | signer: Signer, 9 | factory: T, 10 | args: Parameters, 11 | ): Promise> { 12 | const contractFactory = new ethers.ContractFactory(factory.interface, factory.bytecode, signer) 13 | const contractInitCode = contractFactory.getDeployTransaction(...(args as any)) 14 | const deployTx = signer.sendTransaction(contractInitCode) 15 | // note: we don't use factory directly here b/c it's not possible to wait until tx is finalized 16 | const minedTx = await waitForTx(deployTx) 17 | 18 | const contractDeployed = contractFactory.attach(minedTx.contractAddress) 19 | 20 | return contractDeployed as any 21 | } 22 | 23 | export async function deployUsingFactoryAndVerify( 24 | signer: Signer, 25 | factory: T, 26 | args: Parameters, 27 | ): Promise> { 28 | const contractDeployed = await deployUsingFactory(signer, factory, args) 29 | 30 | console.log( 31 | `npx hardhat verify ${contractDeployed.address} ${args 32 | .filter((a: any) => a.gasPrice === undefined && !isEmpty(a)) 33 | .join(' ')}`, 34 | ) 35 | 36 | return contractDeployed as any 37 | } 38 | -------------------------------------------------------------------------------- /arbitrum-helpers/index.ts: -------------------------------------------------------------------------------- 1 | export { depositToStandardBridge } from './bridge' 2 | export * from './deploy' 3 | export { waitToRelayTxsToL2 } from './messaging' 4 | export { deployArbitrumContractMock } from './mocks' 5 | export * from './networks' 6 | export * from './transactions' 7 | -------------------------------------------------------------------------------- /arbitrum-helpers/messaging.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { BigNumber, ethers, providers, Signer, utils } from 'ethers' 3 | import hre from 'hardhat' 4 | 5 | export async function waitToRelayTxsToL2( 6 | inProgressL1Tx: Promise, 7 | inboxAddress: string, 8 | l1: ethers.providers.BaseProvider, 9 | l2: ethers.providers.BaseProvider, 10 | ) { 11 | const l1Tx = await inProgressL1Tx 12 | const seqNums = await getInboxSeqNumFromContractTransaction(l1Tx, inboxAddress, l1) 13 | const seqNum = seqNums && seqNums[0] 14 | if (!seqNum) { 15 | throw new Error('Seq num not found') 16 | } 17 | const retryableTicket = await calculateL2TransactionHash(seqNum, l2) 18 | const autoRedeem = calculateRetryableAutoRedeemTxnHash(retryableTicket) 19 | const redeemTransaction = calculateL2RetryableTransactionHash(retryableTicket) 20 | 21 | console.log( 22 | `Waiting for xchain messages to be relayed... L1 hash: ${l1Tx.transactionHash}, L2 tx hash: ${retryableTicket}, L2 auto redeem tx: ${redeemTransaction}`, 23 | ) 24 | 25 | const retryableTicketReceipt = await l2.waitForTransaction(retryableTicket, undefined, 1000 * 60 * 15) 26 | expect(retryableTicketReceipt.status).to.equal(1) 27 | 28 | const autoRedeemReceipt = await l2.waitForTransaction(autoRedeem, undefined, 1000 * 60) 29 | expect(autoRedeemReceipt.status).to.equal(1) 30 | 31 | const redemptionReceipt = await l2.getTransactionReceipt(redeemTransaction) 32 | expect(redemptionReceipt.status).equals(1) 33 | console.log('Xchain message arrived') 34 | } 35 | 36 | async function getInboxSeqNumFromContractTransaction( 37 | l1Transaction: providers.TransactionReceipt, 38 | inboxAddress: string, 39 | provider: ethers.providers.BaseProvider, 40 | ) { 41 | const contract = new ethers.Contract(inboxAddress, require('./abis/Inbox.json').abi, provider) 42 | const iface = contract.interface 43 | const messageDelivered = iface.getEvent('InboxMessageDelivered') 44 | const messageDeliveredFromOrigin = iface.getEvent('InboxMessageDeliveredFromOrigin') 45 | 46 | const eventTopics = { 47 | InboxMessageDelivered: iface.getEventTopic(messageDelivered), 48 | InboxMessageDeliveredFromOrigin: iface.getEventTopic(messageDeliveredFromOrigin), 49 | } 50 | 51 | const logs = l1Transaction.logs.filter( 52 | (log) => 53 | log.topics[0] === eventTopics.InboxMessageDelivered || 54 | log.topics[0] === eventTopics.InboxMessageDeliveredFromOrigin, 55 | ) 56 | 57 | if (logs.length === 0) return undefined 58 | return logs.map((log) => BigNumber.from(log.topics[1])) 59 | } 60 | 61 | async function calculateL2TransactionHash(inboxSequenceNumber: BigNumber, provider: ethers.providers.BaseProvider) { 62 | const l2ChainId = BigNumber.from((await provider.getNetwork()).chainId) 63 | 64 | return utils.keccak256( 65 | utils.concat([ 66 | utils.zeroPad(l2ChainId.toHexString(), 32), 67 | utils.zeroPad(bitFlipSeqNum(inboxSequenceNumber).toHexString(), 32), 68 | ]), 69 | ) 70 | } 71 | 72 | function bitFlipSeqNum(seqNum: BigNumber) { 73 | return seqNum.or(BigNumber.from(1).shl(255)) 74 | } 75 | 76 | function calculateRetryableAutoRedeemTxnHash(requestID: string) { 77 | return utils.keccak256( 78 | utils.concat([utils.zeroPad(requestID, 32), utils.zeroPad(BigNumber.from(1).toHexString(), 32)]), 79 | ) 80 | } 81 | 82 | function calculateL2RetryableTransactionHash(requestID: string) { 83 | return utils.keccak256( 84 | utils.concat([utils.zeroPad(requestID, 32), utils.zeroPad(BigNumber.from(0).toHexString(), 32)]), 85 | ) 86 | } 87 | 88 | export function applyL1ToL2Alias(l1Address: string): string { 89 | const offset = ethers.BigNumber.from('0x1111000000000000000000000000000000001111') 90 | const l1AddressAsNumber = ethers.BigNumber.from(l1Address) 91 | 92 | const l2AddressAsNumber = l1AddressAsNumber.add(offset) 93 | 94 | const mask = ethers.BigNumber.from(2).pow(160) 95 | return l2AddressAsNumber.mod(mask).toHexString() 96 | } 97 | 98 | export async function getL2SignerFromL1(l1Signer: Signer): Promise { 99 | const l2Address = applyL1ToL2Alias(await l1Signer.getAddress()) 100 | 101 | await hre.network.provider.request({ 102 | method: 'hardhat_impersonateAccount', 103 | params: [l2Address], 104 | }) 105 | 106 | const l2Signer = await hre.ethers.getSigner(l2Address) 107 | 108 | return l2Signer 109 | } 110 | -------------------------------------------------------------------------------- /arbitrum-helpers/mocks.ts: -------------------------------------------------------------------------------- 1 | import { deployContractMock } from '@makerdao/hardhat-utils' 2 | import { ethers } from 'ethers' 3 | import { join } from 'path' 4 | 5 | export function deployArbitrumContractMock( 6 | name: string, 7 | opts?: { 8 | provider?: ethers.providers.BaseProvider 9 | address?: string 10 | }, 11 | ) { 12 | const abiPath = join(__dirname, `./abis/${name}.json`) 13 | 14 | return deployContractMock(abiPath, opts) as any 15 | } 16 | -------------------------------------------------------------------------------- /arbitrum-helpers/networks/RetryProvider.ts: -------------------------------------------------------------------------------- 1 | import { providers, utils } from 'ethers' 2 | 3 | export function delay(time: number): Promise { 4 | return new Promise((resolve) => setTimeout(resolve, time)) 5 | } 6 | 7 | /** 8 | * Custom ethers.js provider automatically retrying any errors coming from node 9 | */ 10 | export class RetryProvider extends providers.JsonRpcProvider { 11 | public maxAttempts: number 12 | 13 | constructor(attempts: number, url?: utils.ConnectionInfo | string, network?: string) { 14 | super(url, network) 15 | this.maxAttempts = attempts 16 | } 17 | 18 | public async perform(method: string, params: any): Promise { 19 | let attempt = 0 20 | 21 | // do not retry txs 22 | if (method === 'eth_sendRawTransaction' || method === 'sendTransaction') { 23 | return await super.perform(method, params) 24 | } 25 | 26 | return utils.poll(async () => { 27 | attempt++ 28 | 29 | try { 30 | return await super.perform(method, params) 31 | } catch (error: any) { 32 | console.log( 33 | `Got ${error.statusCode}, ${JSON.stringify({ 34 | attempts: attempt, 35 | method, 36 | params, 37 | error, 38 | })}`, 39 | ) 40 | 41 | await this.handleError(attempt, error) 42 | } 43 | }) 44 | } 45 | 46 | private async handleError(attempt: number, error: any): Promise { 47 | // do not retry sendTransaction calls 48 | if (attempt >= this.maxAttempts) { 49 | console.log('Got error, failing...', JSON.stringify(error)) 50 | throw error 51 | } else if (error && error.statusCode) { 52 | // if we are hitting the api limit retry faster 53 | console.log('Retrying 429...') 54 | await delay(500) 55 | } else { 56 | // just retry if error is not critical 57 | console.log('Retrying...') 58 | await delay(1000) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /arbitrum-helpers/networks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mainnet' 2 | export * from './rinkeby' 3 | -------------------------------------------------------------------------------- /arbitrum-helpers/networks/mainnet.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'console' 2 | import { ethers } from 'hardhat' 3 | 4 | import { NetworkConfig, useStaticRouterDeployment } from '..' 5 | import { RetryProvider } from './RetryProvider' 6 | 7 | // maker contracts: https://changelog.makerdao.com/releases/mainnet/1.9.5/contracts.json 8 | // arbitrum contracts: https://github.com/OffchainLabs/arbitrum/blob/master/packages/arb-bridge-eth/_deployments/1_current_deployment.json 9 | 10 | export async function getMainnetNetworkConfig({ 11 | pkey, 12 | l1Rpc, 13 | l2Rpc, 14 | }: { 15 | pkey: string 16 | l1Rpc: string 17 | l2Rpc: string 18 | }): Promise { 19 | const l1 = new ethers.providers.JsonRpcProvider(l1Rpc) 20 | const l2 = new RetryProvider(5, l2Rpc) // arbitrum l2 can be very unstable so we use RetryProvider 21 | const l1Deployer = new ethers.Wallet(pkey, l1) 22 | const l2Deployer = new ethers.Wallet(pkey, l2) 23 | 24 | assert((await l1.getNetwork()).chainId === 1, 'Not mainnet!') 25 | assert((await l2.getNetwork()).chainId === 42161, 'Not arbitrum one!') 26 | 27 | return { 28 | l1: { 29 | provider: l1, 30 | deployer: l1Deployer, 31 | dai: '0x6B175474E89094C44Da98b954EedeAC495271d0F', 32 | inbox: '0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f', 33 | makerPauseProxy: '0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB', 34 | makerESM: '0x29CfBd381043D00a98fD9904a431015Fef07af2f', 35 | }, 36 | l2: { 37 | provider: l2, 38 | deployer: l2Deployer, 39 | }, 40 | } 41 | } 42 | 43 | export async function getMainnetRouterDeployment(network: NetworkConfig) { 44 | return await useStaticRouterDeployment(network, { 45 | l1GatewayRouter: '0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef', 46 | l2GatewayRouter: '0x5288c571Fd7aD117beA99bF60FE0846C4E84F933', 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /arbitrum-helpers/networks/rinkeby.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'console' 2 | import { ethers } from 'hardhat' 3 | 4 | import { NetworkConfig, useStaticRouterDeployment } from '..' 5 | import { RetryProvider } from './RetryProvider' 6 | 7 | export async function getRinkebyNetworkConfig({ 8 | pkey, 9 | l1Rpc, 10 | l2Rpc, 11 | }: { 12 | pkey: string 13 | l1Rpc: string 14 | l2Rpc: string 15 | }): Promise { 16 | const l1 = new ethers.providers.JsonRpcProvider(l1Rpc) 17 | const l2 = new RetryProvider(5, l2Rpc) // arbitrum l2 testnet is very unstable so we use RetryProvider 18 | const l1Deployer = new ethers.Wallet(pkey, l1) 19 | const l2Deployer = new ethers.Wallet(pkey, l2) 20 | 21 | assert((await l1.getNetwork()).chainId === 4, 'Not rinkeby!') 22 | assert((await l2.getNetwork()).chainId === 421611, 'Not arbitrum testnet!') 23 | 24 | return { 25 | l1: { 26 | provider: l1, 27 | deployer: l1Deployer, 28 | dai: '0xd9e66A2f546880EA4d800F189d6F12Cc15Bff281', // our own deployment 29 | inbox: '0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e', 30 | makerPauseProxy: '0x00edb63bc6f36a45fc18c98e03ad2d707652ab5c', // dummy EOA controlled by us 31 | makerESM: ethers.constants.AddressZero, 32 | }, 33 | l2: { 34 | provider: l2, 35 | deployer: l2Deployer, 36 | }, 37 | } 38 | } 39 | 40 | export async function getRinkebyRouterDeployment(network: NetworkConfig) { 41 | return await useStaticRouterDeployment(network, { 42 | l1GatewayRouter: '0x70C143928eCfFaf9F5b406f7f4fC28Dc43d68380', 43 | l2GatewayRouter: '0x9413AD42910c1eA60c737dB5f58d1C504498a3cD', 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /arbitrum-helpers/transactions.ts: -------------------------------------------------------------------------------- 1 | import { ContractTransaction, providers } from 'ethers' 2 | 3 | export async function waitForTx( 4 | tx: Promise, 5 | _confirmations?: number, 6 | ): Promise { 7 | const resolvedTx = await tx 8 | const confirmations = _confirmations ?? chainIdToConfirmationsNeededForFinalization(resolvedTx.chainId) 9 | 10 | // we retry .wait b/c sometimes it fails for the first time 11 | try { 12 | return await resolvedTx.wait(confirmations) 13 | } catch (e) {} 14 | return await resolvedTx.wait(confirmations) 15 | } 16 | 17 | function chainIdToConfirmationsNeededForFinalization(chainId: number): number { 18 | const defaultWhenReorgsPossible = 3 19 | const defaultForInstantFinality = 0 20 | 21 | // covers mainnet and public testnets 22 | if (chainId < 6) { 23 | return defaultWhenReorgsPossible 24 | } else { 25 | return defaultForInstantFinality 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /certora/HashHelper.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.6.11; 2 | 3 | // HashHelper for permit spec 4 | 5 | contract HashHelper { 6 | function computeDigestForDai( 7 | bytes32 domain_separator, 8 | bytes32 permit_typehash, 9 | address owner, 10 | address spender, 11 | uint256 value, 12 | uint256 nonce, 13 | uint256 deadline 14 | ) public pure returns (bytes32 digest) { 15 | digest = keccak256( 16 | abi.encodePacked( 17 | "\x19\x01", 18 | domain_separator, 19 | keccak256(abi.encode(permit_typehash, owner, spender, value, nonce, deadline)) 20 | ) 21 | ); 22 | } 23 | 24 | function call_ecrecover( 25 | bytes32 digest, 26 | uint8 v, 27 | bytes32 r, 28 | bytes32 s 29 | ) public pure returns (address signer) { 30 | signer = ecrecover(digest, v, r, s); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /certora/dai.spec: -------------------------------------------------------------------------------- 1 | // dai.spec 2 | 3 | // certoraRun contracts/l2/dai.sol:Dai certora/HashHelper.sol --verify Dai:certora/dai.spec --rule_sanity --solc_args "['--optimize','--optimize-runs','200']" 4 | 5 | using HashHelper as hashHelper 6 | 7 | methods { 8 | wards(address) returns (uint256) envfree 9 | name() returns (string) envfree 10 | symbol() returns (string) envfree 11 | version() returns (string) envfree 12 | decimals() returns (uint8) envfree 13 | totalSupply() returns (uint256) envfree 14 | balanceOf(address) returns (uint256) envfree 15 | allowance(address, address) returns (uint256) envfree 16 | nonces(address) returns (uint256) envfree 17 | deploymentChainId() returns (uint256) envfree 18 | DOMAIN_SEPARATOR() returns (bytes32) envfree 19 | PERMIT_TYPEHASH() returns (bytes32) envfree 20 | hashHelper.call_ecrecover(bytes32, uint8, bytes32, bytes32) returns (address) envfree 21 | hashHelper.computeDigestForDai(bytes32, bytes32, address, address, uint256, uint256, uint256) returns (bytes32) envfree 22 | } 23 | 24 | ghost balanceSum() returns mathint { 25 | init_state axiom balanceSum() == 0; 26 | } 27 | 28 | hook Sstore balanceOf[KEY address a] uint256 balance (uint256 old_balance) STORAGE { 29 | havoc balanceSum assuming balanceSum@new() == balanceSum@old() + balance - old_balance && balanceSum@new() >= 0; 30 | } 31 | 32 | invariant balanceSum_equals_totalSupply() balanceSum() == totalSupply() 33 | 34 | // Verify that wards behaves correctly on rely 35 | rule rely(address usr) { 36 | env e; 37 | 38 | rely(e, usr); 39 | 40 | assert(wards(usr) == 1, "rely did not set the wards as expected"); 41 | } 42 | 43 | // Verify revert rules on rely 44 | rule rely_revert(address usr) { 45 | env e; 46 | 47 | uint256 ward = wards(e.msg.sender); 48 | 49 | rely@withrevert(e, usr); 50 | 51 | bool revert1 = e.msg.value > 0; 52 | bool revert2 = ward != 1; 53 | 54 | assert(revert1 => lastReverted, "Sending ETH did not revert"); 55 | assert(revert2 => lastReverted, "Lack of auth did not revert"); 56 | assert(lastReverted => revert1 || revert2, "Revert rules are not covering all the cases"); 57 | } 58 | 59 | // Verify that wards behaves correctly on deny 60 | rule deny(address usr) { 61 | env e; 62 | 63 | deny(e, usr); 64 | 65 | assert(wards(usr) == 0, "deny did not set the wards as expected"); 66 | } 67 | 68 | // Verify revert rules on deny 69 | rule deny_revert(address usr) { 70 | env e; 71 | 72 | uint256 ward = wards(e.msg.sender); 73 | 74 | deny@withrevert(e, usr); 75 | 76 | bool revert1 = e.msg.value > 0; 77 | bool revert2 = ward != 1; 78 | 79 | assert(revert1 => lastReverted, "Sending ETH did not revert"); 80 | assert(revert2 => lastReverted, "Lack of auth did not revert"); 81 | assert(lastReverted => revert1 || revert2, "Revert rules are not covering all the cases"); 82 | } 83 | 84 | // Verify that balance behaves correctly on transfer 85 | rule transfer(address to, uint256 value) { 86 | env e; 87 | 88 | requireInvariant balanceSum_equals_totalSupply(); 89 | 90 | uint256 senderBalanceBefore = balanceOf(e.msg.sender); 91 | uint256 toBalanceBefore = balanceOf(to); 92 | uint256 supplyBefore = totalSupply(); 93 | bool senderSameAsTo = e.msg.sender == to; 94 | 95 | transfer(e, to, value); 96 | 97 | uint256 senderBalanceAfter = balanceOf(e.msg.sender); 98 | uint256 toBalanceAfter = balanceOf(to); 99 | uint256 supplyAfter = totalSupply(); 100 | 101 | assert(supplyAfter == supplyBefore, "supply changed"); 102 | 103 | assert(!senderSameAsTo => 104 | senderBalanceAfter == senderBalanceBefore - value && 105 | toBalanceAfter == toBalanceBefore + value, 106 | "transfer did not change balances as expected" 107 | ); 108 | 109 | assert(senderSameAsTo => 110 | senderBalanceAfter == senderBalanceBefore, 111 | "transfer changed the balance when sender and receiver are the same" 112 | ); 113 | } 114 | 115 | // Verify revert rules on transfer 116 | rule transfer_revert(address to, uint256 value) { 117 | env e; 118 | 119 | uint256 senderBalance = balanceOf(e.msg.sender); 120 | 121 | transfer@withrevert(e, to, value); 122 | 123 | bool revert1 = e.msg.value > 0; 124 | bool revert2 = to == 0 || to == currentContract; 125 | bool revert3 = senderBalance < value; 126 | 127 | assert(revert1 => lastReverted, "Sending ETH did not revert"); 128 | assert(revert2 => lastReverted, "Forbidden address didn't revert"); 129 | assert(revert3 => lastReverted, "Insufficient balance didn't revert"); 130 | assert(lastReverted => revert1 || revert2 || revert3, "Revert rules are not covering all the cases"); 131 | } 132 | 133 | // Verify that balance and allowance behave correctly on transferFrom 134 | rule transferFrom(address from, address to, uint256 value) { 135 | env e; 136 | 137 | requireInvariant balanceSum_equals_totalSupply(); 138 | 139 | uint256 fromBalanceBefore = balanceOf(from); 140 | uint256 toBalanceBefore = balanceOf(to); 141 | uint256 supplyBefore = totalSupply(); 142 | uint256 allowanceBefore = allowance(from, e.msg.sender); 143 | bool deductAllowance = e.msg.sender != from && allowanceBefore != max_uint256; 144 | bool fromSameAsTo = from == to; 145 | 146 | transferFrom(e, from, to, value); 147 | 148 | uint256 fromBalanceAfter = balanceOf(from); 149 | uint256 toBalanceAfter = balanceOf(to); 150 | uint256 supplyAfter = totalSupply(); 151 | uint256 allowanceAfter = allowance(from, e.msg.sender); 152 | 153 | assert(supplyAfter == supplyBefore, "supply changed"); 154 | assert(deductAllowance => allowanceAfter == allowanceBefore - value, "allowance did not decrease in value"); 155 | assert(!deductAllowance => allowanceAfter == allowanceBefore, "allowance did not remain the same"); 156 | assert(!fromSameAsTo => fromBalanceAfter == fromBalanceBefore - value, "transferFrom did not decrease the balance as expected"); 157 | assert(!fromSameAsTo => toBalanceAfter == toBalanceBefore + value, "transferFrom did not increase the balance as expected"); 158 | assert(fromSameAsTo => fromBalanceAfter == fromBalanceBefore, "transferFrom did not keep the balance the same as expected"); 159 | } 160 | 161 | // Verify revert rules on transferFrom 162 | rule transferFrom_revert(address from, address to, uint256 value) { 163 | env e; 164 | 165 | uint256 fromBalance = balanceOf(from); 166 | uint256 allowed = allowance(from, e.msg.sender); 167 | 168 | transferFrom@withrevert(e, from, to, value); 169 | 170 | bool revert1 = e.msg.value > 0; 171 | bool revert2 = to == 0 || to == currentContract; 172 | bool revert3 = fromBalance < value; 173 | bool revert4 = allowed < value && e.msg.sender != from; 174 | 175 | assert(revert1 => lastReverted, "Sending ETH did not revert"); 176 | assert(revert2 => lastReverted, "Incorrect address did not revert"); 177 | assert(revert3 => lastReverted, "Insufficient balance did not revert"); 178 | assert(revert4 => lastReverted, "Insufficient allowance did not revert"); 179 | assert(lastReverted => revert1 || revert2 || revert3 || revert4, "Revert rules are not covering all the cases"); 180 | } 181 | 182 | // Verify that allowance behaves correctly on approve 183 | rule approve(address spender, uint256 value) { 184 | env e; 185 | 186 | approve(e, spender, value); 187 | 188 | assert(allowance(e.msg.sender, spender) == value, "approve did not set the allowance as expected"); 189 | } 190 | 191 | // Verify revert rules on approve 192 | rule approve_revert(address spender, uint256 value) { 193 | env e; 194 | 195 | approve@withrevert(e, spender, value); 196 | 197 | bool revert1 = e.msg.value > 0; 198 | 199 | assert(revert1 => lastReverted, "Sending ETH did not revert"); 200 | assert(lastReverted => revert1, "Revert rules are not covering all the cases"); 201 | } 202 | 203 | // Verify that allowance behaves correctly on increaseAllowance 204 | rule increaseAllowance(address spender, uint256 value) { 205 | env e; 206 | 207 | uint256 spenderAllowance = allowance(e.msg.sender, spender); 208 | 209 | increaseAllowance(e, spender, value); 210 | 211 | assert(allowance(e.msg.sender, spender) == spenderAllowance + value, "increaseAllowance did not increase the allowance as expected"); 212 | } 213 | 214 | // Verify revert rules on increaseAllowance 215 | rule increaseAllowance_revert(address spender, uint256 value) { 216 | env e; 217 | 218 | uint256 spenderAllowance = allowance(e.msg.sender, spender); 219 | 220 | increaseAllowance@withrevert(e, spender, value); 221 | 222 | bool revert1 = e.msg.value > 0; 223 | bool revert2 = spenderAllowance + value > max_uint256; 224 | 225 | assert(revert1 => lastReverted, "Sending ETH did not revert"); 226 | assert(revert2 => lastReverted, "Overflow allowance did not revert"); 227 | assert(lastReverted => revert1 || revert2, "Revert rules are not covering all the cases"); 228 | } 229 | 230 | // Verify that allowance behaves correctly on decreaseAllowance 231 | rule decreaseAllowance(address spender, uint256 value) { 232 | env e; 233 | 234 | uint256 spenderAllowance = allowance(e.msg.sender, spender); 235 | 236 | decreaseAllowance(e, spender, value); 237 | 238 | assert(allowance(e.msg.sender, spender) == spenderAllowance - value, "decreaseAllowance did not decrease the allowance as expected"); 239 | } 240 | 241 | // Verify revert rules on decreaseAllowance 242 | rule decreaseAllowance_revert(address spender, uint256 value) { 243 | env e; 244 | 245 | uint256 spenderAllowance = allowance(e.msg.sender, spender); 246 | 247 | decreaseAllowance@withrevert(e, spender, value); 248 | 249 | bool revert1 = e.msg.value > 0; 250 | bool revert2 = spenderAllowance - value < 0; 251 | 252 | assert(revert1 => lastReverted, "Sending ETH did not revert"); 253 | assert(revert2 => lastReverted, "Underflow allowance did not revert"); 254 | assert(lastReverted => revert1 || revert2, "Revert rules are not covering all the cases"); 255 | } 256 | 257 | // Verify that supply and balance behave correctly on mint 258 | rule mint(address to, uint256 value) { 259 | env e; 260 | 261 | requireInvariant balanceSum_equals_totalSupply(); 262 | 263 | // Save the totalSupply and sender balance before minting 264 | uint256 supply = totalSupply(); 265 | uint256 toBalance = balanceOf(to); 266 | 267 | mint(e, to, value); 268 | 269 | assert(balanceOf(to) == toBalance + value, "mint did not increase the balance as expected"); 270 | assert(totalSupply() == supply + value, "mint did not increase the supply as expected"); 271 | } 272 | 273 | // Verify revert rules on mint 274 | rule mint_revert(address to, uint256 value) { 275 | env e; 276 | 277 | // Save the totalSupply and sender balance before minting 278 | uint256 supply = totalSupply(); 279 | uint256 ward = wards(e.msg.sender); 280 | 281 | mint@withrevert(e, to, value); 282 | 283 | bool revert1 = e.msg.value > 0; 284 | bool revert2 = ward != 1; 285 | bool revert3 = supply + value > max_uint256; 286 | bool revert4 = to == 0 || to == currentContract; 287 | 288 | assert(revert1 => lastReverted, "Sending ETH did not revert"); 289 | assert(revert2 => lastReverted, "Lack of auth did not revert"); 290 | assert(revert3 => lastReverted, "Overflow supply did not revert"); 291 | assert(revert4 => lastReverted, "Incorrect address did not revert"); 292 | assert(lastReverted => revert1 || revert2 || revert3 || revert4, "Revert rules are not covering all the cases"); 293 | } 294 | 295 | // Verify that supply and balance behave correctly on burn 296 | rule burn(address from, uint256 value) { 297 | env e; 298 | 299 | requireInvariant balanceSum_equals_totalSupply(); 300 | 301 | uint256 supply = totalSupply(); 302 | uint256 fromBalance = balanceOf(from); 303 | uint256 allowed = allowance(from, e.msg.sender); 304 | uint256 ward = wards(e.msg.sender); 305 | bool senderSameAsFrom = e.msg.sender == from; 306 | bool wardsEqOne = wards(e.msg.sender) == 1; 307 | bool allowedEqMaxUint = allowed == max_uint256; 308 | 309 | burn(e, from, value); 310 | 311 | assert(!senderSameAsFrom && !wardsEqOne && !allowedEqMaxUint => allowance(from, e.msg.sender) == allowed - value, "burn did not decrease the allowance as expected" ); 312 | assert(senderSameAsFrom || wardsEqOne || allowedEqMaxUint => allowance(from, e.msg.sender) == allowed, "burn did not keep the allowance as expected"); 313 | assert(balanceOf(from) == fromBalance - value, "burn did not decrease the balance as expected"); 314 | assert(totalSupply() == supply - value, "burn did not decrease the supply as expected"); 315 | } 316 | 317 | // Verify revert rules on burn 318 | rule burn_revert(address from, uint256 value) { 319 | env e; 320 | 321 | uint256 supply = totalSupply(); 322 | uint256 fromBalance = balanceOf(from); 323 | uint256 allowed = allowance(from, e.msg.sender); 324 | uint256 ward = wards(e.msg.sender); 325 | 326 | burn@withrevert(e, from, value); 327 | 328 | bool revert1 = e.msg.value > 0; 329 | bool revert2 = fromBalance < value; 330 | bool revert3 = from != e.msg.sender && ward !=1 && allowed < value; 331 | 332 | assert(revert1 => lastReverted, "Sending ETH did not revert"); 333 | assert(revert2 => lastReverted, "Underflow balance did not revert"); 334 | assert(revert3 => lastReverted, "Underflow allowance did not revert"); 335 | assert(lastReverted => revert1 || revert2 || revert3, "Revert rules are not covering all the cases"); 336 | } 337 | 338 | // Verify that allowance behaves correctly on permit 339 | rule permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) { 340 | env e; 341 | 342 | permit(e, owner, spender, value, deadline, v, r, s); 343 | 344 | assert(allowance(owner, spender) == value, "permit did not set the allowance as expected"); 345 | } 346 | 347 | // Verify revert rules on permit 348 | rule permit_revert(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) { 349 | env e; 350 | 351 | uint256 ownerNonce = nonces(owner); 352 | address ownerRecover = hashHelper.call_ecrecover( 353 | hashHelper.computeDigestForDai(DOMAIN_SEPARATOR(), PERMIT_TYPEHASH(), owner, spender, value, ownerNonce, deadline), 354 | v, 355 | r, 356 | s); 357 | 358 | permit@withrevert(e, owner, spender, value, deadline, v, r, s); 359 | 360 | bool revert1 = e.msg.value > 0; 361 | bool revert2 = e.block.timestamp > deadline; 362 | bool revert3 = owner == 0 || owner != ownerRecover; 363 | 364 | assert(revert1 => lastReverted, "Sending ETH did not revert"); 365 | assert(revert2 => lastReverted, "Deadline exceed did not revert"); 366 | assert(revert3 => lastReverted, "Invalid permit did not revert"); 367 | assert(lastReverted => revert1 || revert2 || revert3, "Revert rules are not covering all the cases"); 368 | } 369 | -------------------------------------------------------------------------------- /codechecks.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | - name: eth-gas-reporter/codechecks 3 | -------------------------------------------------------------------------------- /contracts/arbitrum/ArbSys.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.21 <0.7.0; 2 | 3 | /** 4 | * @title Precompiled contract that exists in every Arbitrum chain at address(100), 0x0000000000000000000000000000000000000064. Exposes a variety of system-level functionality. 5 | */ 6 | interface ArbSys { 7 | /** 8 | * @notice Get internal version number identifying an ArbOS build 9 | * @return version number as int 10 | */ 11 | function arbOSVersion() external pure returns (uint256); 12 | 13 | function arbChainID() external view returns (uint256); 14 | 15 | /** 16 | * @notice Get Arbitrum block number (distinct from L1 block number; Arbitrum genesis block has block number 0) 17 | * @return block number as int 18 | */ 19 | function arbBlockNumber() external view returns (uint256); 20 | 21 | /** 22 | * @notice Send given amount of Eth to dest from sender. 23 | * This is a convenience function, which is equivalent to calling sendTxToL1 with empty calldataForL1. 24 | * @param destination recipient address on L1 25 | * @return unique identifier for this L2-to-L1 transaction. 26 | */ 27 | function withdrawEth(address destination) external payable returns (uint256); 28 | 29 | /** 30 | * @notice Send a transaction to L1 31 | * @param destination recipient address on L1 32 | * @param calldataForL1 (optional) calldata for L1 contract call 33 | * @return a unique identifier for this L2-to-L1 transaction. 34 | */ 35 | function sendTxToL1(address destination, bytes calldata calldataForL1) 36 | external 37 | payable 38 | returns (uint256); 39 | 40 | /** 41 | * @notice get the number of transactions issued by the given external account or the account sequence number of the given contract 42 | * @param account target account 43 | * @return the number of transactions issued by the given external account or the account sequence number of the given contract 44 | */ 45 | function getTransactionCount(address account) external view returns (uint256); 46 | 47 | /** 48 | * @notice get the value of target L2 storage slot 49 | * This function is only callable from address 0 to prevent contracts from being able to call it 50 | * @param account target account 51 | * @param index target index of storage slot 52 | * @return stotage value for the given account at the given index 53 | */ 54 | function getStorageAt(address account, uint256 index) external view returns (uint256); 55 | 56 | /** 57 | * @notice check if current call is coming from l1 58 | * @return true if the caller of this was called directly from L1 59 | */ 60 | function isTopLevelCall() external view returns (bool); 61 | 62 | event EthWithdrawal(address indexed destAddr, uint256 amount); 63 | 64 | event L2ToL1Transaction( 65 | address caller, 66 | address indexed destination, 67 | uint256 indexed uniqueId, 68 | uint256 indexed batchNumber, 69 | uint256 indexInBatch, 70 | uint256 arbBlockNum, 71 | uint256 ethBlockNum, 72 | uint256 timestamp, 73 | uint256 callvalue, 74 | bytes data 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /contracts/arbitrum/IBridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2021, 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 | pragma solidity ^0.6.11; 20 | 21 | interface IBridge { 22 | event MessageDelivered( 23 | uint256 indexed messageIndex, 24 | bytes32 indexed beforeInboxAcc, 25 | address inbox, 26 | uint8 kind, 27 | address sender, 28 | bytes32 messageDataHash 29 | ); 30 | 31 | event BridgeCallTriggered( 32 | address indexed outbox, 33 | address indexed destAddr, 34 | uint256 amount, 35 | bytes data 36 | ); 37 | 38 | event InboxToggle(address indexed inbox, bool enabled); 39 | 40 | event OutboxToggle(address indexed outbox, bool enabled); 41 | 42 | function deliverMessageToInbox( 43 | uint8 kind, 44 | address sender, 45 | bytes32 messageDataHash 46 | ) external payable returns (uint256); 47 | 48 | function executeCall( 49 | address destAddr, 50 | uint256 amount, 51 | bytes calldata data 52 | ) external returns (bool success, bytes memory returnData); 53 | 54 | // These are only callable by the admin 55 | function setInbox(address inbox, bool enabled) external; 56 | 57 | function setOutbox(address inbox, bool enabled) external; 58 | 59 | // View functions 60 | 61 | function activeOutbox() external view returns (address); 62 | 63 | function allowedInboxes(address inbox) external view returns (bool); 64 | 65 | function allowedOutboxes(address outbox) external view returns (bool); 66 | 67 | function inboxAccs(uint256 index) external view returns (bytes32); 68 | 69 | function messageCount() external view returns (uint256); 70 | } 71 | -------------------------------------------------------------------------------- /contracts/arbitrum/IInbox.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2021, 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 | pragma solidity ^0.6.11; 20 | 21 | import "./IMessageProvider.sol"; 22 | 23 | interface IInbox is IMessageProvider { 24 | function sendL2Message(bytes calldata messageData) external returns (uint256); 25 | 26 | function sendUnsignedTransaction( 27 | uint256 maxGas, 28 | uint256 gasPriceBid, 29 | uint256 nonce, 30 | address destAddr, 31 | uint256 amount, 32 | bytes calldata data 33 | ) external returns (uint256); 34 | 35 | function sendContractTransaction( 36 | uint256 maxGas, 37 | uint256 gasPriceBid, 38 | address destAddr, 39 | uint256 amount, 40 | bytes calldata data 41 | ) external returns (uint256); 42 | 43 | function sendL1FundedUnsignedTransaction( 44 | uint256 maxGas, 45 | uint256 gasPriceBid, 46 | uint256 nonce, 47 | address destAddr, 48 | bytes calldata data 49 | ) external payable returns (uint256); 50 | 51 | function sendL1FundedContractTransaction( 52 | uint256 maxGas, 53 | uint256 gasPriceBid, 54 | address destAddr, 55 | bytes calldata data 56 | ) external payable returns (uint256); 57 | 58 | function createRetryableTicket( 59 | address destAddr, 60 | uint256 arbTxCallValue, 61 | uint256 maxSubmissionCost, 62 | address submissionRefundAddress, 63 | address valueRefundAddress, 64 | uint256 maxGas, 65 | uint256 gasPriceBid, 66 | bytes calldata data 67 | ) external payable returns (uint256); 68 | 69 | function createRetryableTicketNoRefundAliasRewrite( 70 | address destAddr, 71 | uint256 arbTxCallValue, 72 | uint256 maxSubmissionCost, 73 | address submissionRefundAddress, 74 | address valueRefundAddress, 75 | uint256 maxGas, 76 | uint256 gasPriceBid, 77 | bytes calldata data 78 | ) external payable returns (uint256); 79 | 80 | function depositEth(uint256 maxSubmissionCost) external payable returns (uint256); 81 | 82 | function bridge() external view returns (address); 83 | 84 | function pauseCreateRetryables() external; 85 | 86 | function unpauseCreateRetryables() external; 87 | 88 | function startRewriteAddress() external; 89 | 90 | function stopRewriteAddress() external; 91 | } 92 | -------------------------------------------------------------------------------- /contracts/arbitrum/IMessageProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2021, 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 | pragma solidity ^0.6.11; 20 | 21 | interface IMessageProvider { 22 | event InboxMessageDelivered(uint256 indexed messageNum, bytes data); 23 | 24 | event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); 25 | } 26 | -------------------------------------------------------------------------------- /contracts/arbitrum/IOutbox.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2021, 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 | pragma solidity ^0.6.11; 20 | 21 | interface IOutbox { 22 | event OutboxEntryCreated( 23 | uint256 indexed batchNum, 24 | uint256 outboxEntryIndex, 25 | bytes32 outputRoot, 26 | uint256 numInBatch 27 | ); 28 | event OutBoxTransactionExecuted( 29 | address indexed destAddr, 30 | address indexed l2Sender, 31 | uint256 indexed outboxEntryIndex, 32 | uint256 transactionIndex 33 | ); 34 | 35 | function l2ToL1Sender() external view returns (address); 36 | 37 | function l2ToL1Block() external view returns (uint256); 38 | 39 | function l2ToL1EthBlock() external view returns (uint256); 40 | 41 | function l2ToL1Timestamp() external view returns (uint256); 42 | 43 | function l2ToL1BatchNum() external view returns (uint256); 44 | 45 | function l2ToL1OutputId() external view returns (bytes32); 46 | 47 | function processOutgoingMessages(bytes calldata sendsData, uint256[] calldata sendLengths) 48 | external; 49 | 50 | function outboxEntryExists(uint256 batchNum) external view returns (bool); 51 | } 52 | -------------------------------------------------------------------------------- /contracts/l1/L1CrossDomainEnabled.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Affero General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Affero General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Affero General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.6.11; 17 | 18 | import "../arbitrum/IBridge.sol"; 19 | import "../arbitrum/IInbox.sol"; 20 | import "../arbitrum/IOutbox.sol"; 21 | 22 | abstract contract L1CrossDomainEnabled { 23 | IInbox public immutable inbox; 24 | 25 | event TxToL2(address indexed from, address indexed to, uint256 indexed seqNum, bytes data); 26 | 27 | constructor(address _inbox) public { 28 | inbox = IInbox(_inbox); 29 | } 30 | 31 | modifier onlyL2Counterpart(address l2Counterpart) { 32 | // a message coming from the counterpart gateway was executed by the bridge 33 | address bridge = inbox.bridge(); 34 | require(msg.sender == bridge, "NOT_FROM_BRIDGE"); 35 | 36 | // and the outbox reports that the L2 address of the sender is the counterpart gateway 37 | address l2ToL1Sender = IOutbox(IBridge(bridge).activeOutbox()).l2ToL1Sender(); 38 | require(l2ToL1Sender == l2Counterpart, "ONLY_COUNTERPART_GATEWAY"); 39 | _; 40 | } 41 | 42 | function sendTxToL2( 43 | address target, 44 | address user, 45 | uint256 maxSubmissionCost, 46 | uint256 maxGas, 47 | uint256 gasPriceBid, 48 | bytes memory data 49 | ) internal returns (uint256) { 50 | uint256 seqNum = inbox.createRetryableTicket{value: msg.value}( 51 | target, 52 | 0, // we always assume that l2CallValue = 0 53 | maxSubmissionCost, 54 | user, 55 | user, 56 | maxGas, 57 | gasPriceBid, 58 | data 59 | ); 60 | emit TxToL2(user, target, seqNum, data); 61 | return seqNum; 62 | } 63 | 64 | function sendTxToL2NoAliasing( 65 | address target, 66 | address user, 67 | uint256 l1CallValue, 68 | uint256 maxSubmissionCost, 69 | uint256 maxGas, 70 | uint256 gasPriceBid, 71 | bytes memory data 72 | ) internal returns (uint256) { 73 | uint256 seqNum = inbox.createRetryableTicketNoRefundAliasRewrite{value: l1CallValue}( 74 | target, 75 | 0, // we always assume that l2CallValue = 0 76 | maxSubmissionCost, 77 | user, 78 | user, 79 | maxGas, 80 | gasPriceBid, 81 | data 82 | ); 83 | emit TxToL2(user, target, seqNum, data); 84 | return seqNum; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /contracts/l1/L1DaiGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Affero General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Affero General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Affero General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.6.11; 17 | 18 | import "./L1ITokenGateway.sol"; 19 | import "../l2/L2ITokenGateway.sol"; 20 | import "./L1CrossDomainEnabled.sol"; 21 | 22 | interface TokenLike { 23 | function transferFrom( 24 | address _from, 25 | address _to, 26 | uint256 _value 27 | ) external returns (bool success); 28 | } 29 | 30 | contract L1DaiGateway is L1CrossDomainEnabled, L1ITokenGateway { 31 | // --- Auth --- 32 | mapping(address => uint256) public wards; 33 | 34 | function rely(address usr) external auth { 35 | wards[usr] = 1; 36 | emit Rely(usr); 37 | } 38 | 39 | function deny(address usr) external auth { 40 | wards[usr] = 0; 41 | emit Deny(usr); 42 | } 43 | 44 | modifier auth() { 45 | require(wards[msg.sender] == 1, "L1DaiGateway/not-authorized"); 46 | _; 47 | } 48 | 49 | event Rely(address indexed usr); 50 | event Deny(address indexed usr); 51 | 52 | address public immutable l1Dai; 53 | address public immutable l2Dai; 54 | address public immutable l1Escrow; 55 | address public immutable l1Router; 56 | address public immutable l2Counterpart; 57 | uint256 public isOpen = 1; 58 | 59 | event Closed(); 60 | 61 | constructor( 62 | address _l2Counterpart, 63 | address _l1Router, 64 | address _inbox, 65 | address _l1Dai, 66 | address _l2Dai, 67 | address _l1Escrow 68 | ) public L1CrossDomainEnabled(_inbox) { 69 | wards[msg.sender] = 1; 70 | emit Rely(msg.sender); 71 | 72 | l1Dai = _l1Dai; 73 | l2Dai = _l2Dai; 74 | l1Escrow = _l1Escrow; 75 | l1Router = _l1Router; 76 | l2Counterpart = _l2Counterpart; 77 | } 78 | 79 | function close() external auth { 80 | isOpen = 0; 81 | 82 | emit Closed(); 83 | } 84 | 85 | function outboundTransfer( 86 | address l1Token, 87 | address to, 88 | uint256 amount, 89 | uint256 maxGas, 90 | uint256 gasPriceBid, 91 | bytes calldata data 92 | ) external payable override returns (bytes memory) { 93 | // do not allow initiating new xchain messages if bridge is closed 94 | require(isOpen == 1, "L1DaiGateway/closed"); 95 | require(l1Token == l1Dai, "L1DaiGateway/token-not-dai"); 96 | 97 | // we use nested scope to avoid stack too deep errors 98 | address from; 99 | uint256 seqNum; 100 | bytes memory extraData; 101 | { 102 | uint256 maxSubmissionCost; 103 | (from, maxSubmissionCost, extraData) = parseOutboundData(data); 104 | require(extraData.length == 0, "L1DaiGateway/call-hook-data-not-allowed"); 105 | 106 | TokenLike(l1Token).transferFrom(from, l1Escrow, amount); 107 | 108 | bytes memory outboundCalldata = getOutboundCalldata(l1Token, from, to, amount, extraData); 109 | seqNum = sendTxToL2( 110 | l2Counterpart, 111 | from, 112 | maxSubmissionCost, 113 | maxGas, 114 | gasPriceBid, 115 | outboundCalldata 116 | ); 117 | } 118 | 119 | emit DepositInitiated(l1Token, from, to, seqNum, amount); 120 | 121 | return abi.encode(seqNum); 122 | } 123 | 124 | function getOutboundCalldata( 125 | address l1Token, 126 | address from, 127 | address to, 128 | uint256 amount, 129 | bytes memory data 130 | ) public pure returns (bytes memory outboundCalldata) { 131 | bytes memory emptyBytes = ""; 132 | 133 | outboundCalldata = abi.encodeWithSelector( 134 | L2ITokenGateway.finalizeInboundTransfer.selector, 135 | l1Token, 136 | from, 137 | to, 138 | amount, 139 | abi.encode(emptyBytes, data) 140 | ); 141 | 142 | return outboundCalldata; 143 | } 144 | 145 | function finalizeInboundTransfer( 146 | address l1Token, 147 | address from, 148 | address to, 149 | uint256 amount, 150 | bytes calldata data 151 | ) external override onlyL2Counterpart(l2Counterpart) { 152 | require(l1Token == l1Dai, "L1DaiGateway/token-not-dai"); 153 | (uint256 exitNum, ) = abi.decode(data, (uint256, bytes)); 154 | 155 | TokenLike(l1Token).transferFrom(l1Escrow, to, amount); 156 | 157 | emit WithdrawalFinalized(l1Token, from, to, exitNum, amount); 158 | } 159 | 160 | function parseOutboundData(bytes memory data) 161 | internal 162 | view 163 | returns ( 164 | address from, 165 | uint256 maxSubmissionCost, 166 | bytes memory extraData 167 | ) 168 | { 169 | if (msg.sender == l1Router) { 170 | // router encoded 171 | (from, extraData) = abi.decode(data, (address, bytes)); 172 | } else { 173 | from = msg.sender; 174 | extraData = data; 175 | } 176 | // user encoded 177 | (maxSubmissionCost, extraData) = abi.decode(extraData, (uint256, bytes)); 178 | } 179 | 180 | function calculateL2TokenAddress(address l1Token) external view override returns (address) { 181 | if (l1Token != l1Dai) { 182 | return address(0); 183 | } 184 | 185 | return l2Dai; 186 | } 187 | 188 | function counterpartGateway() external view override returns (address) { 189 | return l2Counterpart; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /contracts/l1/L1Escrow.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Affero General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Affero General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Affero General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.6.11; 17 | 18 | interface ApproveLike { 19 | function approve(address, uint256) external; 20 | } 21 | 22 | // Escrow funds on L1, manage approval rights 23 | 24 | contract L1Escrow { 25 | // --- Auth --- 26 | mapping(address => uint256) public wards; 27 | 28 | function rely(address usr) external auth { 29 | wards[usr] = 1; 30 | emit Rely(usr); 31 | } 32 | 33 | function deny(address usr) external auth { 34 | wards[usr] = 0; 35 | emit Deny(usr); 36 | } 37 | 38 | modifier auth() { 39 | require(wards[msg.sender] == 1, "L1Escrow/not-authorized"); 40 | _; 41 | } 42 | 43 | event Rely(address indexed usr); 44 | event Deny(address indexed usr); 45 | 46 | event Approve(address indexed token, address indexed spender, uint256 value); 47 | 48 | constructor() public { 49 | wards[msg.sender] = 1; 50 | emit Rely(msg.sender); 51 | } 52 | 53 | function approve( 54 | address token, 55 | address spender, 56 | uint256 value 57 | ) external auth { 58 | emit Approve(token, spender, value); 59 | 60 | ApproveLike(token).approve(spender, value); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /contracts/l1/L1GovernanceRelay.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Affero General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Affero General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Affero General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.6.11; 17 | 18 | import "../arbitrum/IInbox.sol"; 19 | 20 | import "./L1CrossDomainEnabled.sol"; 21 | import "../l2/L2GovernanceRelay.sol"; 22 | 23 | // Relay a message from L1 to L2GovernanceRelay 24 | // Sending L1->L2 message on arbitrum requires ETH balance. That's why this contract can receive ether. 25 | // Excessive ether can be reclaimed by governance by calling reclaim function. 26 | 27 | contract L1GovernanceRelay is L1CrossDomainEnabled { 28 | // --- Auth --- 29 | mapping(address => uint256) public wards; 30 | 31 | function rely(address usr) external auth { 32 | wards[usr] = 1; 33 | emit Rely(usr); 34 | } 35 | 36 | function deny(address usr) external auth { 37 | wards[usr] = 0; 38 | emit Deny(usr); 39 | } 40 | 41 | modifier auth() { 42 | require(wards[msg.sender] == 1, "L1GovernanceRelay/not-authorized"); 43 | _; 44 | } 45 | 46 | address public immutable l2GovernanceRelay; 47 | 48 | event Rely(address indexed usr); 49 | event Deny(address indexed usr); 50 | 51 | constructor(address _inbox, address _l2GovernanceRelay) public L1CrossDomainEnabled(_inbox) { 52 | wards[msg.sender] = 1; 53 | emit Rely(msg.sender); 54 | 55 | l2GovernanceRelay = _l2GovernanceRelay; 56 | } 57 | 58 | // Allow contract to receive ether 59 | receive() external payable {} 60 | 61 | // Allow governance to reclaim stored ether 62 | function reclaim(address receiver, uint256 amount) external auth { 63 | (bool sent, ) = receiver.call{value: amount}(""); 64 | require(sent, "L1GovernanceRelay/failed-to-send-ether"); 65 | } 66 | 67 | // Forward a call to be repeated on L2 68 | function relay( 69 | address target, 70 | bytes calldata targetData, 71 | uint256 l1CallValue, 72 | uint256 maxGas, 73 | uint256 gasPriceBid, 74 | uint256 maxSubmissionCost 75 | ) external payable auth { 76 | bytes memory data = abi.encodeWithSelector( 77 | L2GovernanceRelay.relay.selector, 78 | target, 79 | targetData 80 | ); 81 | 82 | sendTxToL2NoAliasing( 83 | l2GovernanceRelay, 84 | l2GovernanceRelay, // send any excess ether to the L2 counterpart 85 | l1CallValue, 86 | maxSubmissionCost, 87 | maxGas, 88 | gasPriceBid, 89 | data 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /contracts/l1/L1ITokenGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Affero General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Affero General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Affero General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.6.11; 17 | 18 | // differences between L1 and L2 version of this interface: 19 | // - payable modifier on outboundTransfer 20 | // - events 21 | interface L1ITokenGateway { 22 | event DepositInitiated( 23 | address l1Token, 24 | address indexed from, 25 | address indexed to, 26 | uint256 indexed sequenceNumber, 27 | uint256 amount 28 | ); 29 | 30 | event WithdrawalFinalized( 31 | address l1Token, 32 | address indexed from, 33 | address indexed to, 34 | uint256 indexed exitNum, 35 | uint256 amount 36 | ); 37 | 38 | function outboundTransfer( 39 | address token, 40 | address to, 41 | uint256 amount, 42 | uint256 maxGas, 43 | uint256 gasPriceBid, 44 | bytes calldata data 45 | ) external payable returns (bytes memory); 46 | 47 | function finalizeInboundTransfer( 48 | address token, 49 | address from, 50 | address to, 51 | uint256 amount, 52 | bytes calldata data 53 | ) external; 54 | 55 | // if token is not supported this should return 0x0 address 56 | function calculateL2TokenAddress(address l1Token) external view returns (address); 57 | 58 | // used by router 59 | function counterpartGateway() external view returns (address); 60 | } 61 | -------------------------------------------------------------------------------- /contracts/l2/L2CrossDomainEnabled.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Affero General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Affero General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Affero General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.6.11; 17 | 18 | import "../arbitrum/ArbSys.sol"; 19 | 20 | abstract contract L2CrossDomainEnabled { 21 | event TxToL1(address indexed from, address indexed to, uint256 indexed id, bytes data); 22 | 23 | function sendTxToL1( 24 | address user, 25 | address to, 26 | bytes memory data 27 | ) internal returns (uint256) { 28 | // note: this method doesn't support sending ether to L1 together with a call 29 | uint256 id = ArbSys(address(100)).sendTxToL1(to, data); 30 | 31 | emit TxToL1(user, to, id, data); 32 | 33 | return id; 34 | } 35 | 36 | modifier onlyL1Counterpart(address l1Counterpart) { 37 | require(msg.sender == applyL1ToL2Alias(l1Counterpart), "ONLY_COUNTERPART_GATEWAY"); 38 | _; 39 | } 40 | 41 | uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); 42 | 43 | // l1 addresses are transformed durng l1->l2 calls 44 | function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { 45 | l2Address = address(uint160(l1Address) + offset); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/l2/L2DaiGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Affero General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Affero General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Affero General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.6.11; 17 | 18 | import "./L2ITokenGateway.sol"; 19 | import "../l1/L1ITokenGateway.sol"; 20 | import "./L2CrossDomainEnabled.sol"; 21 | 22 | interface Mintable { 23 | function mint(address usr, uint256 wad) external; 24 | 25 | function burn(address usr, uint256 wad) external; 26 | } 27 | 28 | contract L2DaiGateway is L2CrossDomainEnabled, L2ITokenGateway { 29 | // --- Auth --- 30 | mapping(address => uint256) public wards; 31 | 32 | function rely(address usr) external auth { 33 | wards[usr] = 1; 34 | emit Rely(usr); 35 | } 36 | 37 | function deny(address usr) external auth { 38 | wards[usr] = 0; 39 | emit Deny(usr); 40 | } 41 | 42 | modifier auth() { 43 | require(wards[msg.sender] == 1, "L2DaiGateway/not-authorized"); 44 | _; 45 | } 46 | 47 | event Rely(address indexed usr); 48 | event Deny(address indexed usr); 49 | 50 | address public immutable l1Dai; 51 | address public immutable l2Dai; 52 | address public immutable l1Counterpart; 53 | address public immutable l2Router; 54 | uint256 public isOpen = 1; 55 | 56 | event Closed(); 57 | 58 | constructor( 59 | address _l1Counterpart, 60 | address _l2Router, 61 | address _l1Dai, 62 | address _l2Dai 63 | ) public { 64 | wards[msg.sender] = 1; 65 | emit Rely(msg.sender); 66 | 67 | l1Dai = _l1Dai; 68 | l2Dai = _l2Dai; 69 | l1Counterpart = _l1Counterpart; 70 | l2Router = _l2Router; 71 | } 72 | 73 | function close() external auth { 74 | isOpen = 0; 75 | 76 | emit Closed(); 77 | } 78 | 79 | function outboundTransfer( 80 | address l1Token, 81 | address to, 82 | uint256 amount, 83 | bytes calldata data 84 | ) external returns (bytes memory) { 85 | return outboundTransfer(l1Token, to, amount, 0, 0, data); 86 | } 87 | 88 | function outboundTransfer( 89 | address l1Token, 90 | address to, 91 | uint256 amount, 92 | uint256, // maxGas 93 | uint256, // gasPriceBid 94 | bytes calldata data 95 | ) public override returns (bytes memory res) { 96 | require(isOpen == 1, "L2DaiGateway/closed"); 97 | require(l1Token == l1Dai, "L2DaiGateway/token-not-dai"); 98 | 99 | (address from, bytes memory extraData) = parseOutboundData(data); 100 | require(extraData.length == 0, "L2DaiGateway/call-hook-data-not-allowed"); 101 | 102 | Mintable(l2Dai).burn(from, amount); 103 | 104 | uint256 id = sendTxToL1( 105 | from, 106 | l1Counterpart, 107 | getOutboundCalldata(l1Token, from, to, amount, extraData) 108 | ); 109 | 110 | // we don't need to track exitNums (b/c we have no fast exits) so we always use 0 111 | emit WithdrawalInitiated(l1Token, from, to, id, 0, amount); 112 | 113 | return abi.encode(id); 114 | } 115 | 116 | function getOutboundCalldata( 117 | address token, 118 | address from, 119 | address to, 120 | uint256 amount, 121 | bytes memory data 122 | ) public pure returns (bytes memory outboundCalldata) { 123 | outboundCalldata = abi.encodeWithSelector( 124 | L1ITokenGateway.finalizeInboundTransfer.selector, 125 | token, 126 | from, 127 | to, 128 | amount, 129 | abi.encode(0, data) // we don't need to track exitNums (b/c we have no fast exits) so we always use 0 130 | ); 131 | 132 | return outboundCalldata; 133 | } 134 | 135 | function finalizeInboundTransfer( 136 | address l1Token, 137 | address from, 138 | address to, 139 | uint256 amount, 140 | bytes calldata // data -- unsused 141 | ) external override onlyL1Counterpart(l1Counterpart) { 142 | require(l1Token == l1Dai, "L2DaiGateway/token-not-dai"); 143 | 144 | Mintable(l2Dai).mint(to, amount); 145 | 146 | emit DepositFinalized(l1Token, from, to, amount); 147 | } 148 | 149 | function calculateL2TokenAddress(address l1Token) external view override returns (address) { 150 | if (l1Token != l1Dai) { 151 | return address(0); 152 | } 153 | 154 | return l2Dai; 155 | } 156 | 157 | function parseOutboundData(bytes memory data) 158 | internal 159 | view 160 | returns (address from, bytes memory extraData) 161 | { 162 | if (msg.sender == l2Router) { 163 | (from, extraData) = abi.decode(data, (address, bytes)); 164 | } else { 165 | from = msg.sender; 166 | extraData = data; 167 | } 168 | } 169 | 170 | function counterpartGateway() external view override returns (address) { 171 | return l1Counterpart; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /contracts/l2/L2GovernanceRelay.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Affero General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Affero General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Affero General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.6.11; 17 | 18 | import "./L2CrossDomainEnabled.sol"; 19 | 20 | // Receive xchain message from L1 counterpart and execute given spell 21 | 22 | contract L2GovernanceRelay is L2CrossDomainEnabled { 23 | address public immutable l1GovernanceRelay; 24 | 25 | constructor(address _l1GovernanceRelay) public { 26 | l1GovernanceRelay = _l1GovernanceRelay; 27 | } 28 | 29 | // Allow contract to receive ether 30 | receive() external payable {} 31 | 32 | function relay(address target, bytes calldata targetData) 33 | external 34 | onlyL1Counterpart(l1GovernanceRelay) 35 | { 36 | (bool ok, ) = target.delegatecall(targetData); 37 | // note: even if a retryable call fails, it can be retried 38 | require(ok, "L2GovernanceRelay/delegatecall-error"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/l2/L2ITokenGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Affero General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Affero General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Affero General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.6.11; 17 | 18 | interface L2ITokenGateway { 19 | event DepositFinalized( 20 | address indexed l1Token, 21 | address indexed from, 22 | address indexed to, 23 | uint256 amount 24 | ); 25 | 26 | event WithdrawalInitiated( 27 | address l1Token, 28 | address indexed from, 29 | address indexed to, 30 | uint256 indexed l2ToL1Id, 31 | uint256 exitNum, 32 | uint256 amount 33 | ); 34 | 35 | function outboundTransfer( 36 | address token, 37 | address to, 38 | uint256 amount, 39 | uint256 maxGas, 40 | uint256 gasPriceBid, 41 | bytes calldata data 42 | ) external returns (bytes memory); 43 | 44 | function finalizeInboundTransfer( 45 | address token, 46 | address from, 47 | address to, 48 | uint256 amount, 49 | bytes calldata data 50 | ) external; 51 | 52 | // if token is not supported this should return 0x0 address 53 | function calculateL2TokenAddress(address l1Token) external view returns (address); 54 | 55 | // used by router 56 | function counterpartGateway() external view returns (address); 57 | } 58 | -------------------------------------------------------------------------------- /contracts/l2/dai.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | // Copyright (C) 2017, 2018, 2019 dbrock, rain, mrchico 4 | // Copyright (C) 2021 Dai Foundation 5 | 6 | // This program is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU Affero General Public License as published by 8 | // the Free Software Foundation, either version 3 of the License, or 9 | // (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU Affero General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Affero General Public License 17 | // along with this program. If not, see . 18 | 19 | pragma solidity ^0.6.11; 20 | 21 | // Improved Dai token 22 | 23 | contract Dai { 24 | 25 | // --- Auth --- 26 | mapping (address => uint256) public wards; 27 | function rely(address usr) external auth { 28 | wards[usr] = 1; 29 | emit Rely(usr); 30 | } 31 | function deny(address usr) external auth { 32 | wards[usr] = 0; 33 | emit Deny(usr); 34 | } 35 | modifier auth { 36 | require(wards[msg.sender] == 1, "Dai/not-authorized"); 37 | _; 38 | } 39 | 40 | // --- ERC20 Data --- 41 | string public constant name = "Dai Stablecoin"; 42 | string public constant symbol = "DAI"; 43 | string public constant version = "2"; 44 | uint8 public constant decimals = 18; 45 | uint256 public totalSupply; 46 | 47 | mapping (address => uint256) public balanceOf; 48 | mapping (address => mapping (address => uint256)) public allowance; 49 | mapping (address => uint256) public nonces; 50 | 51 | event Approval(address indexed owner, address indexed spender, uint256 value); 52 | event Transfer(address indexed from, address indexed to, uint256 value); 53 | event Rely(address indexed usr); 54 | event Deny(address indexed usr); 55 | 56 | // --- Math --- 57 | function _add(uint256 x, uint256 y) internal pure returns (uint256 z) { 58 | require((z = x + y) >= x); 59 | } 60 | function _sub(uint256 x, uint256 y) internal pure returns (uint256 z) { 61 | require((z = x - y) <= x); 62 | } 63 | 64 | // --- EIP712 niceties --- 65 | uint256 public immutable deploymentChainId; 66 | bytes32 private immutable _DOMAIN_SEPARATOR; 67 | bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 68 | 69 | constructor() public { 70 | wards[msg.sender] = 1; 71 | emit Rely(msg.sender); 72 | 73 | uint256 chainId; 74 | assembly {chainId := chainid()} 75 | deploymentChainId = chainId; 76 | _DOMAIN_SEPARATOR = _calculateDomainSeparator(chainId); 77 | } 78 | 79 | function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) { 80 | return keccak256( 81 | abi.encode( 82 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 83 | keccak256(bytes(name)), 84 | keccak256(bytes(version)), 85 | chainId, 86 | address(this) 87 | ) 88 | ); 89 | } 90 | function DOMAIN_SEPARATOR() external view returns (bytes32) { 91 | uint256 chainId; 92 | assembly {chainId := chainid()} 93 | return chainId == deploymentChainId ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId); 94 | } 95 | 96 | // --- ERC20 Mutations --- 97 | function transfer(address to, uint256 value) external returns (bool) { 98 | require(to != address(0) && to != address(this), "Dai/invalid-address"); 99 | uint256 balance = balanceOf[msg.sender]; 100 | require(balance >= value, "Dai/insufficient-balance"); 101 | 102 | balanceOf[msg.sender] = balance - value; 103 | balanceOf[to] += value; 104 | 105 | emit Transfer(msg.sender, to, value); 106 | 107 | return true; 108 | } 109 | function transferFrom(address from, address to, uint256 value) external returns (bool) { 110 | require(to != address(0) && to != address(this), "Dai/invalid-address"); 111 | uint256 balance = balanceOf[from]; 112 | require(balance >= value, "Dai/insufficient-balance"); 113 | 114 | if (from != msg.sender) { 115 | uint256 allowed = allowance[from][msg.sender]; 116 | if (allowed != type(uint256).max) { 117 | require(allowed >= value, "Dai/insufficient-allowance"); 118 | 119 | allowance[from][msg.sender] = allowed - value; 120 | } 121 | } 122 | 123 | balanceOf[from] = balance - value; 124 | balanceOf[to] += value; 125 | 126 | emit Transfer(from, to, value); 127 | 128 | return true; 129 | } 130 | function approve(address spender, uint256 value) external returns (bool) { 131 | allowance[msg.sender][spender] = value; 132 | 133 | emit Approval(msg.sender, spender, value); 134 | 135 | return true; 136 | } 137 | function increaseAllowance(address spender, uint256 addedValue) external returns (bool) { 138 | uint256 newValue = _add(allowance[msg.sender][spender], addedValue); 139 | allowance[msg.sender][spender] = newValue; 140 | 141 | emit Approval(msg.sender, spender, newValue); 142 | 143 | return true; 144 | } 145 | function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) { 146 | uint256 allowed = allowance[msg.sender][spender]; 147 | require(allowed >= subtractedValue, "Dai/insufficient-allowance"); 148 | allowed = allowed - subtractedValue; 149 | allowance[msg.sender][spender] = allowed; 150 | 151 | emit Approval(msg.sender, spender, allowed); 152 | 153 | return true; 154 | } 155 | 156 | // --- Mint/Burn --- 157 | function mint(address to, uint256 value) external auth { 158 | require(to != address(0) && to != address(this), "Dai/invalid-address"); 159 | balanceOf[to] = balanceOf[to] + value; // note: we don't need an overflow check here b/c balanceOf[to] <= totalSupply and there is an overflow check below 160 | totalSupply = _add(totalSupply, value); 161 | 162 | emit Transfer(address(0), to, value); 163 | } 164 | function burn(address from, uint256 value) external { 165 | uint256 balance = balanceOf[from]; 166 | require(balance >= value, "Dai/insufficient-balance"); 167 | 168 | if (from != msg.sender && wards[msg.sender] != 1) { 169 | uint256 allowed = allowance[from][msg.sender]; 170 | if (allowed != type(uint256).max) { 171 | require(allowed >= value, "Dai/insufficient-allowance"); 172 | 173 | allowance[from][msg.sender] = allowed - value; 174 | } 175 | } 176 | 177 | balanceOf[from] = balance - value; // note: we don't need overflow checks b/c require(balance >= value) and balance <= totalSupply 178 | totalSupply = totalSupply - value; 179 | 180 | emit Transfer(from, address(0), value); 181 | } 182 | 183 | // --- Approve by signature --- 184 | function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external { 185 | require(block.timestamp <= deadline, "Dai/permit-expired"); 186 | 187 | uint256 chainId; 188 | assembly {chainId := chainid()} 189 | 190 | bytes32 digest = 191 | keccak256(abi.encodePacked( 192 | "\x19\x01", 193 | chainId == deploymentChainId ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId), 194 | keccak256(abi.encode( 195 | PERMIT_TYPEHASH, 196 | owner, 197 | spender, 198 | value, 199 | nonces[owner]++, 200 | deadline 201 | )) 202 | )); 203 | 204 | require(owner != address(0) && owner == ecrecover(digest, v, r, s), "Dai/invalid-permit"); 205 | 206 | allowance[owner][spender] = value; 207 | emit Approval(owner, spender, value); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /contracts/test/BadSpell.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | pragma solidity ^0.6.11; 4 | 5 | contract BadSpell { 6 | uint256 public someVar; 7 | 8 | function abort() external pure { 9 | require(false, "ABORT!"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/test/DaiEchidnaTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | pragma solidity 0.6.11; 3 | 4 | import "../l2/dai.sol"; 5 | 6 | /// @dev A contract that will receive Dai, and allows for it to be retrieved. 7 | contract Alice { 8 | constructor(address dai, address usr) public { 9 | Dai(dai).approve(usr, type(uint256).max); 10 | } 11 | } 12 | 13 | /// @dev Dai Echidna Testing 14 | contract DaiEchidnaTest { 15 | Dai internal dai; 16 | address internal alice; 17 | 18 | uint256 internal constant WAD = 10**18; 19 | uint256 internal constant MAX_SUPPLY = 10**15 * WAD; 20 | 21 | /// @dev Instantiate the Dai contract, and alice address that will return dai when asked to. 22 | constructor() public { 23 | dai = new Dai(); 24 | alice = address(new Alice(address(dai), address(this))); 25 | } 26 | 27 | // --- Math --- 28 | function add(uint256 x, uint256 y) internal pure returns (uint256 z) { 29 | z = x + y; 30 | assert(z >= x); // check for addition overflow 31 | } 32 | 33 | function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { 34 | z = x - y; 35 | assert(z <= x); // check for subtraction underflow 36 | } 37 | 38 | /// @dev Test that supply and balance hold on mint 39 | function mint(uint256 wad) public { 40 | uint256 supply = dai.totalSupply(); 41 | uint256 aliceBalance = dai.balanceOf(alice); 42 | wad = 1 + (wad % sub(MAX_SUPPLY, supply)); 43 | dai.mint(alice, wad); 44 | assert(dai.balanceOf(alice) == add(aliceBalance, wad)); 45 | assert(dai.totalSupply() == add(supply, wad)); 46 | } 47 | 48 | /// @dev Test that supply and balance hold on burn 49 | function burn(uint256 wad) public { 50 | uint256 supply = dai.totalSupply(); 51 | uint256 aliceBalance = dai.balanceOf(alice); 52 | wad = aliceBalance == 0 ? 0 : 1 + (wad % aliceBalance); 53 | dai.burn(alice, wad); 54 | assert(dai.balanceOf(alice) == sub(aliceBalance, wad)); 55 | assert(dai.totalSupply() == sub(supply, wad)); 56 | } 57 | 58 | /// @dev Test that supply and balance hold on transfer 59 | function transfer(uint256 wad) public { 60 | uint256 thisBalance = dai.balanceOf(address(this)); 61 | uint256 aliceBalance = dai.balanceOf(alice); 62 | wad = thisBalance == 0 ? 0 : 1 + (wad % thisBalance); 63 | dai.transfer(alice, wad); 64 | assert(dai.balanceOf(address(this)) == sub(thisBalance, wad)); 65 | assert(dai.balanceOf(alice) == add(aliceBalance, wad)); 66 | } 67 | 68 | /// @dev Test that supply and balance hold on transferFrom 69 | function transferFrom(uint256 wad) public { 70 | uint256 aliceBalance = dai.balanceOf(alice); 71 | uint256 thisBalance = dai.balanceOf(address(this)); 72 | wad = aliceBalance == 0 ? 0 : 1 + (wad % aliceBalance); 73 | dai.transferFrom(alice, address(this), wad); 74 | assert(dai.balanceOf(alice) == sub(aliceBalance, wad)); 75 | assert(dai.balanceOf(address(this)) == add(thisBalance, wad)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/test/TestBridgeUpgradeSpell.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Affero General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Affero General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Affero General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.6.11; 17 | 18 | interface BridgeLike { 19 | function close() external; 20 | 21 | function l2Dai() external view returns (address); 22 | } 23 | 24 | interface AuthLike { 25 | function rely(address usr) external; 26 | 27 | function deny(address usr) external; 28 | } 29 | 30 | /** 31 | * An example spell to transfer from the old bridge to the new one. 32 | */ 33 | contract TestBridgeUpgradeSpell { 34 | function upgradeBridge(address _oldBridge, address _newBridge) external { 35 | BridgeLike oldBridge = BridgeLike(_oldBridge); 36 | AuthLike dai = AuthLike(oldBridge.l2Dai()); 37 | oldBridge.close(); 38 | 39 | // note: ususally you wouldn't "deny" right away b/c of async messages 40 | dai.deny(_oldBridge); 41 | dai.rely(_newBridge); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/test/TestDaiMintSpell.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | pragma solidity ^0.6.11; 4 | 5 | interface MintLike { 6 | function mint(address to, uint256 value) external; 7 | } 8 | 9 | /** 10 | * An example spell to mint some dai. 11 | */ 12 | contract TestDaiMintSpell { 13 | function mintDai( 14 | address _dai, 15 | address _user, 16 | uint256 _amount 17 | ) external { 18 | MintLike(_dai).mint(_user, _amount); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/deposit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makerdao/arbitrum-dai-bridge/ba5e98631307025a74fc03108492b20af57a0d3a/docs/deposit.png -------------------------------------------------------------------------------- /docs/full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makerdao/arbitrum-dai-bridge/ba5e98631307025a74fc03108492b20af57a0d3a/docs/full.png -------------------------------------------------------------------------------- /echidna.config.ci.yml: -------------------------------------------------------------------------------- 1 | #format can be "text" or "json" for different output (human or machine readable) 2 | format: 'text' 3 | #checkAsserts checks assertions 4 | checkAsserts: true 5 | #coverage controls coverage guided testing 6 | coverage: false 7 | #deployer is address of the contract deployer (who often is privileged owner, etc.) 8 | deployer: '0x41414141' 9 | #sender is set of addresses transactions may originate from 10 | sender: ['0x42424242', '0x43434343'] 11 | -------------------------------------------------------------------------------- /echidna.config.yml: -------------------------------------------------------------------------------- 1 | #format can be "text" or "json" for different output (human or machine readable) 2 | #format: 'text' 3 | #checkAsserts checks assertions 4 | checkAsserts: true 5 | #seqLen defines how many transactions are in a test sequence 6 | seqLen: 200 7 | #testLimit is the number of test sequences to run 8 | testLimit: 1000000 9 | #estimateGas makes echidna perform analysis of maximum gas costs for functions (experimental) 10 | #estimateGas: true 11 | #directory to save the corpus; by default is disabled 12 | corpusDir: 'corpus' 13 | #deployer is address of the contract deployer (who often is privileged owner, etc.) 14 | deployer: '0x41414141' 15 | #sender is set of addresses transactions may originate from 16 | sender: ['0x42424242', '0x43434343'] 17 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import 'solidity-coverage' 2 | import 'hardhat-gas-reporter' 3 | import '@nomiclabs/hardhat-etherscan' 4 | import '@nomiclabs/hardhat-ethers' 5 | import '@nomiclabs/hardhat-waffle' 6 | import '@nomiclabs/hardhat-web3' 7 | import '@typechain/hardhat' 8 | 9 | import { HardhatUserConfig } from 'hardhat/config' 10 | 11 | const testDir = process.env.TESTS_DIR ?? 'test' 12 | 13 | const config: HardhatUserConfig = { 14 | mocha: { 15 | timeout: 50000, 16 | }, 17 | solidity: { 18 | // note: we run optimizer only for dai.sol 19 | compilers: [ 20 | { 21 | version: '0.6.11', 22 | settings: { 23 | optimizer: { 24 | enabled: false, 25 | }, 26 | }, 27 | }, 28 | ], 29 | overrides: { 30 | 'contracts/l2/dai.sol': { 31 | version: '0.6.11', 32 | settings: { 33 | optimizer: { 34 | enabled: true, 35 | runs: 200, 36 | }, 37 | }, 38 | }, 39 | }, 40 | }, 41 | paths: { 42 | tests: testDir, 43 | }, 44 | gasReporter: { 45 | enabled: process.env.REPORT_GAS === '1', 46 | currency: 'USD', 47 | gasPrice: 50, 48 | }, 49 | } 50 | 51 | export default config 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arbitrum-bridge", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "clean": "hardhat clean && rm -rf artifacts cache crytic-export corpus", 8 | "format": "prettier --check \"./**/*.{ts,sol}\"", 9 | "format:fix": "prettier --write \"./**/*.{ts,sol}\"", 10 | "lint": "eslint --ext .ts test test-e2e arbitrum-helpers scripts hardhat.config.ts", 11 | "lint:fix": "yarn lint --fix", 12 | "typecheck": "tsc --noEmit", 13 | "test": "yarn test:unit", 14 | "test:unit": "hardhat test", 15 | "test-e2e": "mocha --config .mocharc-e2e.js", 16 | "test:fix": "yarn lint:fix && yarn format:fix && yarn test && yarn typecheck", 17 | "fuzz": "echidna-test . --contract DaiEchidnaTest --config echidna.config.yml", 18 | "certora": "certoraRun --solc ~/.solc-select/artifacts/solc-0.6.11 contracts/l2/dai.sol:Dai certora/HashHelper.sol --verify Dai:certora/dai.spec --rule_sanity --solc_args \"['--optimize','--optimize-runs','200']\"", 19 | "deploy:mainnet": "TS_NODE_TRANSPILE_ONLY=1 ts-node ./scripts/deployMainnet.ts", 20 | "deploy:rinkeby": "TS_NODE_TRANSPILE_ONLY=1 ts-node ./scripts/deployRinkeby.ts" 21 | }, 22 | "devDependencies": { 23 | "@codechecks/client": "^0.1.11", 24 | "@eth-optimism/smock": "=1.1.9", 25 | "@makerdao/hardhat-utils": "^0.1.3", 26 | "@nomiclabs/ethereumjs-vm": "^4.2.2", 27 | "@nomiclabs/hardhat-ethers": "^2.0.2", 28 | "@nomiclabs/hardhat-etherscan": "^2.1.6", 29 | "@nomiclabs/hardhat-waffle": "^2.0.0", 30 | "@nomiclabs/hardhat-web3": "^2.0.0", 31 | "@typechain/ethers-v5": "^7.0.0", 32 | "@typechain/hardhat": "^2.0.0", 33 | "@types/chai": "^4.2.12", 34 | "@types/lodash": "^4.14.170", 35 | "@types/mocha": "^8.0.3", 36 | "@types/node": "^14.6.0", 37 | "@types/utf8": "^2.1.6", 38 | "@typescript-eslint/eslint-plugin": "^4.9.0", 39 | "@typescript-eslint/parser": "^4.9.0", 40 | "chai": "^4.2.0", 41 | "chai-as-promised": "^7.1.1", 42 | "dotenv": "^9.0.2", 43 | "eslint": "^7.15.0", 44 | "eslint-config-typestrict": "^1.0.1", 45 | "eslint-plugin-no-only-tests": "^2.4.0", 46 | "eslint-plugin-simple-import-sort": "^6.0.1", 47 | "eslint-plugin-sonarjs": "^0.5.0", 48 | "eslint-plugin-unused-imports": "^1.0.1", 49 | "eth-permit": "^0.1.9", 50 | "ethereum-waffle": "^3.4.0", 51 | "ethers": "^5.4.0", 52 | "hardhat": "^2.4.1", 53 | "hardhat-gas-reporter": "^1.0.4", 54 | "lodash": "^4.17.21", 55 | "mocha": "^8.1.1", 56 | "prettier": "^2.2.1", 57 | "prettier-plugin-solidity": "^1.0.0-beta.17", 58 | "ts-essentials": "^7.0.1", 59 | "ts-node": "^9.0.0", 60 | "typechain": "^5.0.0", 61 | "typescript": "^4.0.2", 62 | "utf8": "^3.0.0", 63 | "solidity-coverage": "^0.7.17" 64 | }, 65 | "resolutions": { 66 | "ethers": "5.4.0", 67 | "@ethersproject/providers": "5.4.0", 68 | "@ethersproject/contracts": "5.4.0", 69 | "@ethersproject/abi": "5.4.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /scripts/deployMainnet.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | import { getRequiredEnv } from '@makerdao/hardhat-utils' 3 | import { mapValues } from 'lodash' 4 | 5 | import { 6 | deployBridge, 7 | getMainnetNetworkConfig, 8 | getMainnetRouterDeployment, 9 | performSanityChecks, 10 | } from '../arbitrum-helpers' 11 | 12 | async function main() { 13 | const pkey = getRequiredEnv('L1_MAINNET_DEPLOYER_PRIV_KEY') 14 | const l1Rpc = getRequiredEnv('L1_MAINNET_RPC_URL') 15 | const l2Rpc = getRequiredEnv('L2_MAINNET_RPC_URL') 16 | 17 | const network = await getMainnetNetworkConfig({ pkey, l1Rpc, l2Rpc }) 18 | console.log(`Deploying to Mainnet using: ${network.l1.deployer.address}`) 19 | const routerDeployment = await getMainnetRouterDeployment(network) 20 | 21 | const l1BlockOfBeginningOfDeployment = await network.l1.provider.getBlockNumber() 22 | const l2BlockOfBeginningOfDeployment = await network.l2.provider.getBlockNumber() 23 | 24 | const bridgeDeployment = await deployBridge(network, routerDeployment, '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1') 25 | 26 | await performSanityChecks( 27 | network, 28 | bridgeDeployment, 29 | l1BlockOfBeginningOfDeployment, 30 | l2BlockOfBeginningOfDeployment, 31 | true, 32 | ) 33 | 34 | console.log( 35 | JSON.stringify( 36 | mapValues(bridgeDeployment, (v) => v.address), 37 | null, 38 | 2, 39 | ), 40 | ) 41 | } 42 | 43 | main() 44 | .then(() => console.log('DONE')) 45 | .catch((error) => { 46 | console.error(error) 47 | process.exit(1) 48 | }) 49 | -------------------------------------------------------------------------------- /scripts/deployRinkeby.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | import { getRequiredEnv } from '@makerdao/hardhat-utils' 3 | import { mapValues } from 'lodash' 4 | 5 | import { 6 | deployBridge, 7 | getRinkebyNetworkConfig, 8 | getRinkebyRouterDeployment, 9 | performSanityChecks, 10 | } from '../arbitrum-helpers' 11 | 12 | async function main() { 13 | const pkey = getRequiredEnv('L1_RINKEBY_DEPLOYER_PRIV_KEY') 14 | const l1Rpc = getRequiredEnv('L1_RINKEBY_RPC_URL') 15 | const l2Rpc = getRequiredEnv('L2_RINKEBY_RPC_URL') 16 | 17 | const network = await getRinkebyNetworkConfig({ pkey, l1Rpc, l2Rpc }) 18 | console.log(`Deploying to Rinkeby testnet using: ${network.l1.deployer.address}`) 19 | const routerDeployment = await getRinkebyRouterDeployment(network) 20 | 21 | const l1BlockOfBeginningOfDeployment = await network.l1.provider.getBlockNumber() 22 | const l2BlockOfBeginningOfDeployment = await network.l2.provider.getBlockNumber() 23 | 24 | const bridgeDeployment = await deployBridge(network, routerDeployment, '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1') 25 | 26 | await performSanityChecks( 27 | network, 28 | bridgeDeployment, 29 | l1BlockOfBeginningOfDeployment, 30 | l2BlockOfBeginningOfDeployment, 31 | true, 32 | ) 33 | 34 | console.log( 35 | JSON.stringify( 36 | mapValues(bridgeDeployment, (v) => v.address), 37 | null, 38 | 2, 39 | ), 40 | ) 41 | } 42 | 43 | main() 44 | .then(() => console.log('DONE')) 45 | .catch((error) => { 46 | console.error(error) 47 | process.exit(1) 48 | }) 49 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "detectors_to_exclude": "pragma,naming-convention,solc-version,redundant-statements,shadowing-local,uninitialized-return,empty-functions,unused-return,missing-zero-check", 3 | "solc_disable_warnings": false, 4 | "filter_paths": "node_modules", 5 | "exclude_informational": true, 6 | "exclude_low": false, 7 | "exclude_medium": false, 8 | "exclude_high": false, 9 | "disable_color": false 10 | } 11 | -------------------------------------------------------------------------------- /test-e2e/bridge.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | deployUsingFactory, 3 | getAddressOfNextDeployedContract, 4 | getOptionalEnv, 5 | getRequiredEnv, 6 | waitForTx, 7 | } from '@makerdao/hardhat-utils' 8 | import { expect } from 'chai' 9 | import { parseUnits } from 'ethers/lib/utils' 10 | import { ethers } from 'hardhat' 11 | import { mapValues } from 'lodash' 12 | 13 | import { 14 | BridgeDeployment, 15 | deployBridge, 16 | deployRouter, 17 | getRinkebyNetworkConfig, 18 | NetworkConfig, 19 | RouterDeployment, 20 | useStaticDeployment, 21 | useStaticRouterDeployment, 22 | waitToRelayTxsToL2, 23 | } from '../arbitrum-helpers' 24 | import { 25 | depositToStandardBridge, 26 | depositToStandardRouter, 27 | executeSpell, 28 | setGatewayForToken, 29 | } from '../arbitrum-helpers/bridge' 30 | 31 | const amount = parseUnits('7', 'ether') 32 | 33 | describe('bridge', () => { 34 | let routerDeployment: RouterDeployment 35 | let bridgeDeployment: BridgeDeployment 36 | let network: NetworkConfig 37 | before(async () => { 38 | // bridge deployment is quite time consuming so we do it only once 39 | ;({ bridgeDeployment, network, routerDeployment } = await setupTest()) 40 | }) 41 | 42 | it('deposits funds', async () => { 43 | const initialL1Balance = await bridgeDeployment.l1Dai.balanceOf(network.l1.deployer.address) 44 | const initialEscrowBalance = await bridgeDeployment.l1Dai.balanceOf(bridgeDeployment.l1Escrow.address) 45 | const initialL2Balance = await bridgeDeployment.l2Dai.balanceOf(network.l1.deployer.address) 46 | 47 | await waitForTx(bridgeDeployment.l1Dai.approve(bridgeDeployment.l1DaiGateway.address, amount)) 48 | 49 | await waitToRelayTxsToL2( 50 | depositToStandardBridge({ 51 | l2Provider: network.l2.provider, 52 | from: network.l1.deployer, 53 | to: network.l1.deployer.address, 54 | l1Gateway: bridgeDeployment.l1DaiGateway, 55 | l1TokenAddress: bridgeDeployment.l1Dai.address, 56 | l2GatewayAddress: bridgeDeployment.l2DaiGateway.address, 57 | deposit: amount, 58 | }), 59 | network.l1.inbox, 60 | network.l1.provider, 61 | network.l2.provider, 62 | ) 63 | 64 | expect(await bridgeDeployment.l1Dai.balanceOf(network.l1.deployer.address)).to.be.eq(initialL1Balance.sub(amount)) 65 | expect(await bridgeDeployment.l1Dai.balanceOf(bridgeDeployment.l1Escrow.address)).to.be.eq( 66 | initialEscrowBalance.add(amount), 67 | ) 68 | expect(await bridgeDeployment.l2Dai.balanceOf(network.l1.deployer.address)).to.be.eq(initialL2Balance.add(amount)) 69 | 70 | await waitForTx( 71 | bridgeDeployment.l2DaiGateway 72 | .connect(network.l2.deployer) 73 | ['outboundTransfer(address,address,uint256,bytes)']( 74 | bridgeDeployment.l1Dai.address, 75 | network.l1.deployer.address, 76 | amount, 77 | '0x', 78 | ), 79 | ) 80 | 81 | expect(await bridgeDeployment.l2Dai.balanceOf(network.l1.deployer.address)).to.be.eq(initialL2Balance) // burn is immediate 82 | // @todo ensure that withdrawal was successful 83 | }) 84 | 85 | it('deposits funds using gateway', async () => { 86 | const initialL1Balance = await bridgeDeployment.l1Dai.balanceOf(network.l1.deployer.address) 87 | const initialEscrowBalance = await bridgeDeployment.l1Dai.balanceOf(bridgeDeployment.l1Escrow.address) 88 | const initialL2Balance = await bridgeDeployment.l2Dai.balanceOf(network.l1.deployer.address) 89 | 90 | await waitForTx(bridgeDeployment.l1Dai.approve(bridgeDeployment.l1DaiGateway.address, amount)) 91 | await waitToRelayTxsToL2( 92 | depositToStandardRouter({ 93 | l2Provider: network.l2.provider, 94 | from: network.l1.deployer, 95 | to: network.l1.deployer.address, 96 | l1Gateway: bridgeDeployment.l1DaiGateway, 97 | l1Router: routerDeployment.l1GatewayRouter, 98 | l1TokenAddress: bridgeDeployment.l1Dai.address, 99 | l2GatewayAddress: bridgeDeployment.l2DaiGateway.address, 100 | deposit: amount, 101 | }), 102 | network.l1.inbox, 103 | network.l1.provider, 104 | network.l2.provider, 105 | ) 106 | 107 | expect(await bridgeDeployment.l1Dai.balanceOf(network.l1.deployer.address)).to.be.eq(initialL1Balance.sub(amount)) 108 | expect(await bridgeDeployment.l1Dai.balanceOf(bridgeDeployment.l1Escrow.address)).to.be.eq( 109 | initialEscrowBalance.add(amount), 110 | ) 111 | expect(await bridgeDeployment.l2Dai.balanceOf(network.l1.deployer.address)).to.be.eq(initialL2Balance.add(amount)) 112 | 113 | await waitForTx( 114 | bridgeDeployment.l2DaiGateway 115 | .connect(network.l2.deployer) 116 | ['outboundTransfer(address,address,uint256,bytes)']( 117 | bridgeDeployment.l1Dai.address, 118 | network.l1.deployer.address, 119 | amount, 120 | '0x', 121 | ), 122 | ) 123 | 124 | expect(await bridgeDeployment.l2Dai.balanceOf(network.l1.deployer.address)).to.be.eq(initialL2Balance) // burn is immediate 125 | }) 126 | 127 | it('upgrades bridge using governance spell', async () => { 128 | const initialL1Balance = await bridgeDeployment.l1Dai.balanceOf(network.l1.deployer.address) 129 | const initialEscrowBalance = await bridgeDeployment.l1Dai.balanceOf(bridgeDeployment.l1Escrow.address) 130 | const initialL2Balance = await bridgeDeployment.l2Dai.balanceOf(network.l1.deployer.address) 131 | 132 | const l1DaiGatewayV2FutureAddr = await getAddressOfNextDeployedContract(network.l1.deployer) 133 | const l2DaiGatewayV2 = await deployUsingFactory( 134 | network.l2.deployer, 135 | await ethers.getContractFactory('L2DaiGateway'), 136 | [ 137 | l1DaiGatewayV2FutureAddr, 138 | routerDeployment.l2GatewayRouter.address, 139 | network.l1.dai, 140 | bridgeDeployment.l2Dai.address, 141 | ], 142 | ) 143 | console.log('Deployed l2DaiGatewayV2 at: ', l2DaiGatewayV2.address) 144 | 145 | const l1DaiGatewayV2 = await deployUsingFactory( 146 | network.l1.deployer, 147 | await ethers.getContractFactory('L1DaiGateway'), 148 | [ 149 | l2DaiGatewayV2.address, 150 | routerDeployment.l1GatewayRouter.address, 151 | network.l1.inbox, 152 | network.l1.dai, 153 | bridgeDeployment.l2Dai.address, 154 | bridgeDeployment.l1Escrow.address, 155 | ], 156 | ) 157 | console.log('Deployed l1DaiGatewayV2 at: ', l1DaiGatewayV2.address) 158 | expect(l1DaiGatewayV2.address).to.be.eq( 159 | l1DaiGatewayV2FutureAddr, 160 | "Expected future address of l1DaiGateway doesn't match actual address!", 161 | ) 162 | await waitForTx( 163 | bridgeDeployment.l1Escrow.approve( 164 | bridgeDeployment.l1Dai.address, 165 | l1DaiGatewayV2.address, 166 | ethers.constants.MaxUint256, 167 | ), 168 | ) 169 | 170 | const l2UpgradeSpell = await deployUsingFactory( 171 | network.l2.deployer, 172 | await ethers.getContractFactory('TestBridgeUpgradeSpell'), 173 | [], 174 | ) 175 | console.log('L2 Bridge Upgrade Spell: ', l2UpgradeSpell.address) 176 | 177 | // Close L2 bridge V1 178 | console.log('Executing spell to close L2 Bridge v1 and grant minting permissions to L2 Bridge v2') 179 | 180 | const spellCalldata = l2UpgradeSpell.interface.encodeFunctionData('upgradeBridge', [ 181 | bridgeDeployment.l2DaiGateway.address, 182 | l2DaiGatewayV2.address, 183 | ]) 184 | 185 | await executeSpell(network, bridgeDeployment, l2UpgradeSpell.address, spellCalldata) 186 | 187 | console.log('Bridge upgraded!') 188 | 189 | await waitForTx(bridgeDeployment.l1Dai.approve(l1DaiGatewayV2.address, amount)) 190 | await waitToRelayTxsToL2( 191 | depositToStandardBridge({ 192 | l2Provider: network.l2.provider, 193 | from: network.l1.deployer, 194 | to: network.l1.deployer.address, 195 | l1Gateway: l1DaiGatewayV2, 196 | l1TokenAddress: bridgeDeployment.l1Dai.address, 197 | l2GatewayAddress: l2DaiGatewayV2.address, 198 | deposit: amount, 199 | }), 200 | network.l1.inbox, 201 | network.l1.provider, 202 | network.l2.provider, 203 | ) 204 | 205 | expect(await bridgeDeployment.l1Dai.balanceOf(network.l1.deployer.address)).to.be.eq(initialL1Balance.sub(amount)) 206 | expect(await bridgeDeployment.l1Dai.balanceOf(bridgeDeployment.l1Escrow.address)).to.be.eq( 207 | initialEscrowBalance.add(amount), 208 | ) 209 | expect(await bridgeDeployment.l2Dai.balanceOf(network.l1.deployer.address)).to.be.eq(initialL2Balance.add(amount)) 210 | 211 | await waitForTx( 212 | l2DaiGatewayV2 213 | .connect(network.l2.deployer) 214 | ['outboundTransfer(address,address,uint256,bytes)']( 215 | bridgeDeployment.l1Dai.address, 216 | network.l1.deployer.address, 217 | amount, 218 | '0x', 219 | ), 220 | ) 221 | 222 | expect(await bridgeDeployment.l2Dai.balanceOf(network.l1.deployer.address)).to.be.eq(initialL2Balance) // burn is immediate 223 | }) 224 | }) 225 | 226 | export async function setupTest() { 227 | const pkey = getRequiredEnv('E2E_TESTS_PKEY') 228 | const l1Rpc = getRequiredEnv('E2E_TESTS_L1_RPC') 229 | const l2Rpc = getRequiredEnv('E2E_TESTS_L2_RPC') 230 | const network = await getRinkebyNetworkConfig({ pkey, l1Rpc, l2Rpc }) 231 | 232 | let bridgeDeployment: BridgeDeployment 233 | let routerDeployment: RouterDeployment 234 | 235 | // this is a mechanism to reuse old deployment -- speeds up development 236 | const staticDeploymentString = getOptionalEnv('E2E_TESTS_DEPLOYMENT') 237 | if (staticDeploymentString) { 238 | console.log('Using static deployment...') 239 | const deployment = JSON.parse(staticDeploymentString) 240 | routerDeployment = await useStaticRouterDeployment(network, deployment) 241 | bridgeDeployment = await useStaticDeployment(network, deployment) 242 | } else { 243 | routerDeployment = await deployRouter(network) 244 | bridgeDeployment = await deployBridge(network, routerDeployment) 245 | 246 | await setGatewayForToken({ 247 | l1Router: routerDeployment.l1GatewayRouter, 248 | l2Router: routerDeployment.l2GatewayRouter, 249 | l2Provider: network.l2.provider, 250 | tokenGateway: bridgeDeployment.l1DaiGateway, 251 | }) 252 | } 253 | 254 | console.log( 255 | 'Bridge deployment: ', 256 | JSON.stringify( 257 | mapValues(bridgeDeployment, (v) => v.address), 258 | null, 259 | 2, 260 | ), 261 | ) 262 | 263 | console.log( 264 | 'Router deployment: ', 265 | JSON.stringify( 266 | mapValues(routerDeployment, (v) => v.address), 267 | null, 268 | 2, 269 | ), 270 | ) 271 | 272 | return { 273 | bridgeDeployment, 274 | routerDeployment, 275 | network, 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /test/l1/L1Escrow.ts: -------------------------------------------------------------------------------- 1 | import { assertPublicMutableMethods, getRandomAddresses, simpleDeploy, testAuth } from '@makerdao/hardhat-utils' 2 | import { expect } from 'chai' 3 | import { ethers } from 'hardhat' 4 | 5 | import { Dai__factory, L1Escrow__factory } from '../../typechain' 6 | 7 | const allowanceLimit = 100 8 | 9 | const errorMessages = { 10 | notAuthed: 'L1Escrow/not-authorized', 11 | } 12 | 13 | describe('L1Escrow', () => { 14 | describe('approve()', () => { 15 | it('sets approval on erc20 tokens', async () => { 16 | const [_deployer, spender] = await ethers.getSigners() 17 | const { l1Dai, l1Escrow } = await setupTest() 18 | 19 | expect(await l1Dai.allowance(l1Escrow.address, spender.address)).to.be.eq(0) 20 | 21 | await l1Escrow.approve(l1Dai.address, spender.address, allowanceLimit) 22 | 23 | expect(await l1Dai.allowance(l1Escrow.address, spender.address)).to.be.eq(allowanceLimit) 24 | }) 25 | 26 | it('emits Approval event', async () => { 27 | const [_deployer, spender] = await ethers.getSigners() 28 | const { l1Dai, l1Escrow } = await setupTest() 29 | 30 | await expect(l1Escrow.approve(l1Dai.address, spender.address, allowanceLimit)) 31 | .to.emit(l1Escrow, 'Approve') 32 | .withArgs(l1Dai.address, spender.address, allowanceLimit) 33 | }) 34 | 35 | it('reverts when called by unauthed user', async () => { 36 | const [_deployer, spender, notDeployer] = await ethers.getSigners() 37 | const { l1Dai, l1Escrow } = await setupTest() 38 | 39 | await expect( 40 | l1Escrow.connect(notDeployer).approve(l1Dai.address, spender.address, allowanceLimit), 41 | ).to.be.revertedWith(errorMessages.notAuthed) 42 | }) 43 | }) 44 | 45 | it('has correct public interface', async () => { 46 | await assertPublicMutableMethods('L1Escrow', ['rely(address)', 'deny(address)', 'approve(address,address,uint256)']) 47 | }) 48 | 49 | testAuth({ 50 | name: 'L1Escrow', 51 | getDeployArgs: async () => [], 52 | authedMethods: [ 53 | async (c) => { 54 | const [a, b] = await getRandomAddresses() 55 | return c.approve(a, b, 1) 56 | }, 57 | ], 58 | }) 59 | }) 60 | 61 | async function setupTest() { 62 | const l1Dai = await simpleDeploy('Dai', []) 63 | const l1Escrow = await simpleDeploy('L1Escrow', []) 64 | 65 | return { l1Dai, l1Escrow } 66 | } 67 | -------------------------------------------------------------------------------- /test/l1/L1GovernanceRelay.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertPublicMutableMethods, 3 | getRandomAddress, 4 | getRandomAddresses, 5 | simpleDeploy, 6 | testAuth, 7 | } from '@makerdao/hardhat-utils' 8 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' 9 | import { expect } from 'chai' 10 | import { parseUnits } from 'ethers/lib/utils' 11 | import { ethers } from 'hardhat' 12 | 13 | import { deployArbitrumContractMock } from '../../arbitrum-helpers/mocks' 14 | import { L1GovernanceRelay__factory, L2GovernanceRelay__factory } from '../../typechain' 15 | 16 | const errorMessages = { 17 | invalidMessenger: 'OVM_XCHAIN: messenger contract unauthenticated', 18 | invalidXDomainMessageOriginator: 'OVM_XCHAIN: wrong sender of cross-domain message', 19 | notAuthed: 'L1GovernanceRelay/not-authorized', 20 | } 21 | 22 | const MAX_GAS = 5000000 23 | const GAS_PRICE_BID = 42 24 | const MAX_SUBMISSION_COST = 69 25 | const defaultEthValue = parseUnits('0.1', 'ether') 26 | 27 | describe('L1GovernanceRelay', () => { 28 | describe('relay()', () => { 29 | it('sends xchain message with eth passed in tx', async () => { 30 | const [deployer, l2GovernanceRelay, l2spell] = await ethers.getSigners() 31 | const { l1GovernanceRelay, inboxMock } = await setupTest({ 32 | l2GovernanceRelay, 33 | }) 34 | 35 | await l1GovernanceRelay 36 | .connect(deployer) 37 | .relay(l2spell.address, [], defaultEthValue, MAX_GAS, GAS_PRICE_BID, MAX_SUBMISSION_COST, { 38 | value: defaultEthValue, 39 | }) 40 | const inboxCall = inboxMock.smocked.createRetryableTicketNoRefundAliasRewrite.calls[0] 41 | 42 | expect(await deployer.provider?.getBalance(inboxMock.address)).to.equal(defaultEthValue) 43 | expect(inboxCall.destAddr).to.equal(l2GovernanceRelay.address) 44 | expect(inboxCall.l2CallValue).to.equal(0) 45 | expect(inboxCall.maxSubmissionCost).to.equal(MAX_SUBMISSION_COST) 46 | expect(inboxCall.excessFeeRefundAddress).to.equal(l2GovernanceRelay.address) 47 | expect(inboxCall.callValueRefundAddress).to.equal(l2GovernanceRelay.address) 48 | expect(inboxCall.maxGas).to.equal(MAX_GAS) 49 | expect(inboxCall.gasPriceBid).to.equal(GAS_PRICE_BID) 50 | expect(inboxCall.data).to.equal( 51 | new L2GovernanceRelay__factory().interface.encodeFunctionData('relay', [l2spell.address, []]), 52 | ) 53 | }) 54 | 55 | it('sends xchain message on relay with eth send before', async () => { 56 | const [deployer, l2GovernanceRelay, l2spell] = await ethers.getSigners() 57 | const { l1GovernanceRelay, inboxMock } = await setupTest({ 58 | l2GovernanceRelay, 59 | }) 60 | await deployer.sendTransaction({ to: l1GovernanceRelay.address, value: defaultEthValue }) 61 | 62 | await l1GovernanceRelay 63 | .connect(deployer) 64 | .relay(l2spell.address, [], defaultEthValue, MAX_GAS, GAS_PRICE_BID, MAX_SUBMISSION_COST) 65 | const inboxCall = inboxMock.smocked.createRetryableTicketNoRefundAliasRewrite.calls[0] 66 | 67 | expect(await deployer.provider?.getBalance(inboxMock.address)).to.equal(defaultEthValue) 68 | expect(inboxCall.destAddr).to.equal(l2GovernanceRelay.address) 69 | expect(inboxCall.l2CallValue).to.equal(0) 70 | expect(inboxCall.maxSubmissionCost).to.equal(MAX_SUBMISSION_COST) 71 | expect(inboxCall.excessFeeRefundAddress).to.equal(l2GovernanceRelay.address) 72 | expect(inboxCall.callValueRefundAddress).to.equal(l2GovernanceRelay.address) 73 | expect(inboxCall.maxGas).to.equal(MAX_GAS) 74 | expect(inboxCall.gasPriceBid).to.equal(GAS_PRICE_BID) 75 | expect(inboxCall.data).to.equal( 76 | new L2GovernanceRelay__factory().interface.encodeFunctionData('relay', [l2spell.address, []]), 77 | ) 78 | }) 79 | 80 | it('reverts when not authed', async () => { 81 | const [_deployer, l2GovernanceRelay, l2spell, notAdmin] = await ethers.getSigners() 82 | const { l1GovernanceRelay } = await setupTest({ 83 | l2GovernanceRelay, 84 | }) 85 | 86 | await expect( 87 | l1GovernanceRelay 88 | .connect(notAdmin) 89 | .relay(l2spell.address, [], defaultEthValue, MAX_GAS, GAS_PRICE_BID, MAX_SUBMISSION_COST), 90 | ).to.be.revertedWith(errorMessages.notAuthed) 91 | }) 92 | }) 93 | 94 | describe('reclaim', () => { 95 | it('allows sending out eth from the balance', async () => { 96 | const [deployer, l2GovernanceRelay] = await ethers.getSigners() 97 | const provider = deployer.provider! 98 | const randomReceiver = await getRandomAddress() 99 | const { l1GovernanceRelay } = await setupTest({ 100 | l2GovernanceRelay, 101 | }) 102 | await deployer.sendTransaction({ to: l1GovernanceRelay.address, value: defaultEthValue }) 103 | 104 | await l1GovernanceRelay.connect(deployer).reclaim(randomReceiver, defaultEthValue) 105 | 106 | expect(await provider.getBalance(randomReceiver)).to.eq(defaultEthValue) 107 | }) 108 | 109 | it('reverts when not authed', async () => { 110 | const [deployer, l2GovernanceRelay, other] = await ethers.getSigners() 111 | const randomReceiver = await getRandomAddress() 112 | const { l1GovernanceRelay } = await setupTest({ 113 | l2GovernanceRelay, 114 | }) 115 | await deployer.sendTransaction({ to: l1GovernanceRelay.address, value: defaultEthValue }) 116 | 117 | await expect(l1GovernanceRelay.connect(other).reclaim(randomReceiver, defaultEthValue)).to.be.revertedWith( 118 | errorMessages.notAuthed, 119 | ) 120 | }) 121 | }) 122 | 123 | describe('receives', () => { 124 | it('receives eth', async () => { 125 | const [deployer, l2GovernanceRelay, other] = await ethers.getSigners() 126 | const provider = deployer.provider! 127 | const { l1GovernanceRelay } = await setupTest({ 128 | l2GovernanceRelay, 129 | }) 130 | 131 | await other.sendTransaction({ to: l1GovernanceRelay.address, value: defaultEthValue }) 132 | 133 | expect(await provider.getBalance(l1GovernanceRelay.address)).to.eq(defaultEthValue) 134 | }) 135 | }) 136 | 137 | describe('constructor', () => { 138 | it('assigns all variables properly', async () => { 139 | const [l2GovernanceRelay, inbox] = await ethers.getSigners() 140 | 141 | const l1GovRelay = await simpleDeploy('L1GovernanceRelay', [ 142 | inbox.address, 143 | l2GovernanceRelay.address, 144 | ]) 145 | 146 | expect(await l1GovRelay.l2GovernanceRelay()).to.eq(l2GovernanceRelay.address) 147 | expect(await l1GovRelay.inbox()).to.eq(inbox.address) 148 | }) 149 | }) 150 | 151 | it('has correct public interface', async () => { 152 | await assertPublicMutableMethods('L1GovernanceRelay', [ 153 | 'rely(address)', 154 | 'deny(address)', 155 | 'reclaim(address,uint256)', 156 | 'relay(address,bytes,uint256,uint256,uint256,uint256)', 157 | ]) 158 | }) 159 | 160 | testAuth({ 161 | name: 'L1GovernanceRelay', 162 | getDeployArgs: async () => { 163 | const [l2GovernanceRelay, l1CrossDomainMessengerMock] = await getRandomAddresses() 164 | 165 | return [l2GovernanceRelay, l1CrossDomainMessengerMock] 166 | }, 167 | authedMethods: [ 168 | async (c) => { 169 | const [target] = await getRandomAddresses() 170 | return c.relay(target, '0x', 0, 0, 0, 0) 171 | }, 172 | async (c) => { 173 | const [target] = await getRandomAddresses() 174 | return c.reclaim(target, '100') 175 | }, 176 | ], 177 | }) 178 | }) 179 | 180 | async function setupTest(signers: { l2GovernanceRelay: SignerWithAddress }) { 181 | const inboxMock = await deployArbitrumContractMock('Inbox') 182 | const l1GovernanceRelay = await simpleDeploy('L1GovernanceRelay', [ 183 | inboxMock.address, 184 | signers.l2GovernanceRelay.address, 185 | ]) 186 | 187 | return { l1GovernanceRelay, inboxMock } 188 | } 189 | -------------------------------------------------------------------------------- /test/l2/L2DaiGateway.ts: -------------------------------------------------------------------------------- 1 | import { defaultAbiCoder } from '@ethersproject/abi' 2 | import { 3 | assertPublicMutableMethods, 4 | assertPublicNotMutableMethods, 5 | getRandomAddress, 6 | getRandomAddresses, 7 | simpleDeploy, 8 | testAuth, 9 | } from '@makerdao/hardhat-utils' 10 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' 11 | import { expect } from 'chai' 12 | import { ethers } from 'hardhat' 13 | 14 | import { getL2SignerFromL1 } from '../../arbitrum-helpers/messaging' 15 | import { deployArbitrumContractMock } from '../../arbitrum-helpers/mocks' 16 | import { Dai__factory, L1DaiGateway__factory, L2DaiGateway__factory } from '../../typechain' 17 | 18 | const initialTotalL2Supply = 3000 19 | const errorMessages = { 20 | closed: 'L2DaiGateway/closed', 21 | tokenMismatch: 'L2DaiGateway/token-not-dai', 22 | callHookDataNotAllowed: 'L2DaiGateway/call-hook-data-not-allowed', 23 | insufficientAllowance: 'Dai/insufficient-allowance', 24 | insufficientFunds: 'Dai/insufficient-balance', 25 | notOwner: 'L2DaiGateway/not-authorized', 26 | notOwnerOfDai: 'Dai/not-authorized', 27 | l1CounterpartMismatch: 'ONLY_COUNTERPART_GATEWAY', 28 | inboundEscrowAndCallGuard: 'Mint can only be called by self', 29 | } 30 | 31 | describe('L2DaiGateway', () => { 32 | describe('finalizeInboundTransfer', () => { 33 | const depositAmount = 100 34 | const defaultData = ethers.utils.defaultAbiCoder.encode(['bytes', 'bytes'], ['0x12', '0x']) 35 | 36 | it('mints tokens', async () => { 37 | const [sender, l1Dai, router] = await ethers.getSigners() 38 | const { l2Dai, l2DaiGateway, l2Deployer } = await setupTest({ 39 | l1Dai, 40 | l1DaiBridge: sender, 41 | router, 42 | deployer: sender, 43 | }) 44 | const receiverAddress = sender.address 45 | 46 | const tx = await l2DaiGateway 47 | .connect(l2Deployer) 48 | .finalizeInboundTransfer(l1Dai.address, sender.address, receiverAddress, depositAmount, defaultData) 49 | 50 | expect(await l2Dai.balanceOf(receiverAddress)).to.be.eq(depositAmount) 51 | expect(await l2Dai.totalSupply()).to.be.eq(depositAmount) 52 | await expect(tx) 53 | .to.emit(l2DaiGateway, 'DepositFinalized') 54 | .withArgs(l1Dai.address, sender.address, receiverAddress, depositAmount) 55 | // await expect(tx).not.to.emit(l2DaiGateway, 'TransferAndCallTriggered') 56 | }) 57 | 58 | it('mints tokens for a 3rd party', async () => { 59 | const [sender, receiver, l1Dai, router] = await ethers.getSigners() 60 | const { l2Dai, l2DaiGateway, l2Deployer } = await setupTest({ 61 | l1Dai, 62 | l1DaiBridge: sender, 63 | router, 64 | deployer: sender, 65 | }) 66 | 67 | const tx = await l2DaiGateway 68 | .connect(l2Deployer) 69 | .finalizeInboundTransfer(l1Dai.address, sender.address, receiver.address, depositAmount, defaultData) 70 | 71 | expect(await l2Dai.balanceOf(receiver.address)).to.be.eq(depositAmount) 72 | expect(await l2Dai.totalSupply()).to.be.eq(depositAmount) 73 | await expect(tx) 74 | .to.emit(l2DaiGateway, 'DepositFinalized') 75 | .withArgs(l1Dai.address, sender.address, receiver.address, depositAmount) 76 | // await expect(tx).not.to.emit(l2DaiGateway, 'TransferAndCallTriggered') 77 | }) 78 | 79 | it('mints tokens even when closed', async () => { 80 | const [sender, l1Dai, router] = await ethers.getSigners() 81 | const { l2Dai, l2DaiGateway, l2Deployer } = await setupTest({ 82 | l1Dai, 83 | l1DaiBridge: sender, 84 | router, 85 | deployer: sender, 86 | }) 87 | const receiverAddress = sender.address 88 | 89 | await l2DaiGateway.close() 90 | const tx = await l2DaiGateway 91 | .connect(l2Deployer) 92 | .finalizeInboundTransfer(l1Dai.address, sender.address, receiverAddress, depositAmount, defaultData) 93 | 94 | expect(await l2Dai.balanceOf(receiverAddress)).to.be.eq(depositAmount) 95 | expect(await l2Dai.totalSupply()).to.be.eq(depositAmount) 96 | await expect(tx) 97 | .to.emit(l2DaiGateway, 'DepositFinalized') 98 | .withArgs(l1Dai.address, sender.address, receiverAddress, depositAmount) 99 | }) 100 | 101 | it('reverts when withdrawing not supported tokens', async () => { 102 | const [sender, l1Dai, router, dummyAcc] = await ethers.getSigners() 103 | const { l2DaiGateway, l2Deployer } = await setupTest({ l1Dai, l1DaiBridge: sender, router, deployer: sender }) 104 | const receiverAddress = sender.address 105 | 106 | await expect( 107 | l2DaiGateway 108 | .connect(l2Deployer) 109 | .finalizeInboundTransfer(dummyAcc.address, sender.address, receiverAddress, depositAmount, defaultData), 110 | ).to.be.revertedWith(errorMessages.tokenMismatch) 111 | }) 112 | 113 | it('reverts when DAI minting access was revoked', async () => { 114 | const [sender, l1Dai, router] = await ethers.getSigners() 115 | const { l2DaiGateway, l2Dai, l2Deployer } = await setupTest({ 116 | l1Dai, 117 | l1DaiBridge: sender, 118 | router, 119 | deployer: sender, 120 | }) 121 | const receiverAddress = sender.address 122 | 123 | await l2Dai.deny(l2DaiGateway.address) 124 | 125 | await expect( 126 | l2DaiGateway 127 | .connect(l2Deployer) 128 | .finalizeInboundTransfer(l1Dai.address, sender.address, receiverAddress, depositAmount, defaultData), 129 | ).to.be.revertedWith(errorMessages.notOwnerOfDai) 130 | }) 131 | 132 | it('reverts when called not relying message from l1DaiGateway', async () => { 133 | const [sender, l1Dai, router, dummyAcc] = await ethers.getSigners() 134 | const { l2DaiGateway } = await setupTest({ l1Dai, l1DaiBridge: sender, router, deployer: sender }) 135 | 136 | await expect( 137 | l2DaiGateway 138 | .connect(dummyAcc) 139 | .finalizeInboundTransfer(dummyAcc.address, sender.address, sender.address, depositAmount, defaultData), 140 | ).to.be.revertedWith(errorMessages.l1CounterpartMismatch) 141 | }) 142 | 143 | it('reverts when called directly by l1 counterpart', async () => { 144 | // this should fail b/c we require address translation 145 | const [sender, l1Dai, router] = await ethers.getSigners() 146 | const { l2DaiGateway } = await setupTest({ l1Dai, l1DaiBridge: sender, router, deployer: sender }) 147 | const receiverAddress = sender.address 148 | 149 | await expect( 150 | l2DaiGateway.finalizeInboundTransfer( 151 | l1Dai.address, 152 | sender.address, 153 | receiverAddress, 154 | depositAmount, 155 | defaultData, 156 | ), 157 | ).to.be.revertedWith(errorMessages.l1CounterpartMismatch) 158 | }) 159 | }) 160 | 161 | describe('outboundTransfer(address,address,uint256,bytes)', () => { 162 | const withdrawAmount = 100 163 | const defaultData = '0x' 164 | const defaultDataWithNotEmptyCallHookData = '0x12' 165 | const expectedWithdrawalId = 0 166 | 167 | it('sends xdomain message and burns tokens', async () => { 168 | const [deployer, l1DaiBridge, l1Dai, router, sender] = await ethers.getSigners() 169 | const { l2Dai, l2DaiGateway, arbSysMock } = await setupWithdrawalTest({ 170 | l1Dai, 171 | l1DaiBridge, 172 | router, 173 | user1: sender, 174 | deployer, 175 | }) 176 | 177 | const tx = await l2DaiGateway 178 | .connect(sender) 179 | ['outboundTransfer(address,address,uint256,bytes)'](l1Dai.address, sender.address, withdrawAmount, defaultData) 180 | const withdrawCrossChainCall = arbSysMock.smocked.sendTxToL1.calls[0] 181 | 182 | expect(await l2Dai.balanceOf(sender.address)).to.be.eq(initialTotalL2Supply - withdrawAmount) 183 | expect(await l2Dai.totalSupply()).to.be.eq(initialTotalL2Supply - withdrawAmount) 184 | await expect(tx) 185 | .to.emit(l2DaiGateway, 'WithdrawalInitiated') 186 | .withArgs( 187 | l1Dai.address, 188 | sender.address, 189 | sender.address, 190 | expectedWithdrawalId, 191 | expectedWithdrawalId, 192 | withdrawAmount, 193 | ) 194 | expect(withdrawCrossChainCall.destAddr).to.eq(l1DaiBridge.address) 195 | expect(withdrawCrossChainCall.calldataForL1).to.eq( 196 | new L1DaiGateway__factory().interface.encodeFunctionData('finalizeInboundTransfer', [ 197 | l1Dai.address, 198 | sender.address, 199 | sender.address, 200 | withdrawAmount, 201 | ethers.utils.defaultAbiCoder.encode(['uint256', 'bytes'], [expectedWithdrawalId, defaultData]), 202 | ]), 203 | ) 204 | }) 205 | 206 | it('sends xdomain message and burns tokens for 3rd party', async () => { 207 | const [deployer, , l1DaiBridge, l1Dai, router, sender, receiver] = await ethers.getSigners() 208 | const { l2Dai, l2DaiGateway, arbSysMock } = await setupWithdrawalTest({ 209 | l1Dai, 210 | l1DaiBridge, 211 | router, 212 | user1: sender, 213 | deployer, 214 | }) 215 | 216 | const tx = await l2DaiGateway 217 | .connect(sender) 218 | ['outboundTransfer(address,address,uint256,bytes)']( 219 | l1Dai.address, 220 | receiver.address, 221 | withdrawAmount, 222 | defaultData, 223 | ) 224 | const withdrawCrossChainCall = arbSysMock.smocked.sendTxToL1.calls[0] 225 | 226 | expect(await l2Dai.balanceOf(sender.address)).to.be.eq(initialTotalL2Supply - withdrawAmount) 227 | expect(await l2Dai.balanceOf(receiver.address)).to.be.eq(0) 228 | expect(await l2Dai.totalSupply()).to.be.eq(initialTotalL2Supply - withdrawAmount) 229 | await expect(tx) 230 | .to.emit(l2DaiGateway, 'WithdrawalInitiated') 231 | .withArgs( 232 | l1Dai.address, 233 | sender.address, 234 | receiver.address, 235 | expectedWithdrawalId, 236 | expectedWithdrawalId, 237 | withdrawAmount, 238 | ) 239 | expect(withdrawCrossChainCall.destAddr).to.eq(l1DaiBridge.address) 240 | expect(withdrawCrossChainCall.calldataForL1).to.eq( 241 | new L1DaiGateway__factory().interface.encodeFunctionData('finalizeInboundTransfer', [ 242 | l1Dai.address, 243 | sender.address, 244 | receiver.address, 245 | withdrawAmount, 246 | ethers.utils.defaultAbiCoder.encode(['uint256', 'bytes'], [expectedWithdrawalId, defaultData]), 247 | ]), 248 | ) 249 | }) 250 | 251 | it('sends xdomain message and burns tokens when called through router', async () => { 252 | const [deployer, , l1DaiBridge, l1Dai, router, sender, receiver] = await ethers.getSigners() 253 | const { l2Dai, l2DaiGateway, arbSysMock } = await setupWithdrawalTest({ 254 | l1Dai, 255 | l1DaiBridge, 256 | router, 257 | user1: sender, 258 | deployer, 259 | }) 260 | const routerEncodedData = defaultAbiCoder.encode(['address', 'bytes'], [sender.address, defaultData]) 261 | 262 | const tx = await l2DaiGateway 263 | .connect(router) 264 | ['outboundTransfer(address,address,uint256,bytes)']( 265 | l1Dai.address, 266 | receiver.address, 267 | withdrawAmount, 268 | routerEncodedData, 269 | ) 270 | const withdrawCrossChainCall = arbSysMock.smocked.sendTxToL1.calls[0] 271 | 272 | expect(await l2Dai.balanceOf(sender.address)).to.be.eq(initialTotalL2Supply - withdrawAmount) 273 | expect(await l2Dai.balanceOf(receiver.address)).to.be.eq(0) 274 | expect(await l2Dai.totalSupply()).to.be.eq(initialTotalL2Supply - withdrawAmount) 275 | await expect(tx) 276 | .to.emit(l2DaiGateway, 'WithdrawalInitiated') 277 | .withArgs( 278 | l1Dai.address, 279 | sender.address, 280 | receiver.address, 281 | expectedWithdrawalId, 282 | expectedWithdrawalId, 283 | withdrawAmount, 284 | ) 285 | expect(withdrawCrossChainCall.destAddr).to.eq(l1DaiBridge.address) 286 | expect(withdrawCrossChainCall.calldataForL1).to.eq( 287 | new L1DaiGateway__factory().interface.encodeFunctionData('finalizeInboundTransfer', [ 288 | l1Dai.address, 289 | sender.address, 290 | receiver.address, 291 | withdrawAmount, 292 | ethers.utils.defaultAbiCoder.encode(['uint256', 'bytes'], [expectedWithdrawalId, defaultData]), 293 | ]), 294 | ) 295 | }) 296 | 297 | it('reverts when called with a different token', async () => { 298 | const [sender, l1DaiBridge, l1Dai, router] = await ethers.getSigners() 299 | const { l2Dai, l2DaiGateway } = await setupWithdrawalTest({ 300 | l1Dai, 301 | l1DaiBridge, 302 | router, 303 | user1: sender, 304 | deployer: sender, 305 | }) 306 | 307 | await expect( 308 | l2DaiGateway['outboundTransfer(address,address,uint256,bytes)']( 309 | l2Dai.address, 310 | sender.address, 311 | withdrawAmount, 312 | defaultData, 313 | ), 314 | ).to.be.revertedWith(errorMessages.tokenMismatch) 315 | }) 316 | 317 | it('reverts when called with callHookData', async () => { 318 | const [sender, l1DaiBridge, l1Dai, router] = await ethers.getSigners() 319 | const { l2DaiGateway } = await setupWithdrawalTest({ 320 | l1Dai, 321 | l1DaiBridge, 322 | router, 323 | user1: sender, 324 | deployer: sender, 325 | }) 326 | 327 | await expect( 328 | l2DaiGateway['outboundTransfer(address,address,uint256,bytes)']( 329 | l1Dai.address, 330 | sender.address, 331 | withdrawAmount, 332 | defaultDataWithNotEmptyCallHookData, 333 | ), 334 | ).to.be.revertedWith(errorMessages.callHookDataNotAllowed) 335 | }) 336 | 337 | it('reverts when bridge closed', async () => { 338 | const [sender, l1DaiBridge, l1Dai, router] = await ethers.getSigners() 339 | const { l2DaiGateway } = await setupWithdrawalTest({ 340 | l1Dai, 341 | l1DaiBridge, 342 | router, 343 | user1: sender, 344 | deployer: sender, 345 | }) 346 | 347 | await l2DaiGateway.connect(sender).close() 348 | 349 | await expect( 350 | l2DaiGateway['outboundTransfer(address,address,uint256,bytes)']( 351 | l1Dai.address, 352 | sender.address, 353 | withdrawAmount, 354 | defaultData, 355 | ), 356 | ).to.be.revertedWith(errorMessages.closed) 357 | }) 358 | 359 | it('reverts when bridge doesnt have burn permissions on DAI', async () => { 360 | const [sender, l1DaiBridge, l1Dai, router] = await ethers.getSigners() 361 | const { l2Dai, l2DaiGateway } = await setupWithdrawalTest({ 362 | l1Dai, 363 | l1DaiBridge, 364 | router, 365 | user1: sender, 366 | deployer: sender, 367 | }) 368 | 369 | // remove burn permissions 370 | await l2Dai.deny(l2DaiGateway.address) 371 | 372 | await expect( 373 | l2DaiGateway['outboundTransfer(address,address,uint256,bytes)']( 374 | l1Dai.address, 375 | sender.address, 376 | withdrawAmount, 377 | defaultData, 378 | ), 379 | ).to.be.revertedWith(errorMessages.insufficientAllowance) 380 | }) 381 | 382 | it('reverts when user funds too low', async () => { 383 | const [sender, l1DaiBridge, l1Dai, router, user2] = await ethers.getSigners() 384 | const { l2DaiGateway } = await setupWithdrawalTest({ 385 | l1Dai, 386 | l1DaiBridge, 387 | router, 388 | user1: sender, 389 | deployer: sender, 390 | }) 391 | 392 | await expect( 393 | l2DaiGateway 394 | .connect(user2) 395 | ['outboundTransfer(address,address,uint256,bytes)']( 396 | l1Dai.address, 397 | sender.address, 398 | withdrawAmount, 399 | defaultData, 400 | ), 401 | ).to.be.revertedWith(errorMessages.insufficientFunds) 402 | }) 403 | }) 404 | 405 | describe('outboundTransfer(address,address,uint256,uint256,uint256,bytes)', () => { 406 | const withdrawAmount = 100 407 | const defaultData = '0x' 408 | const expectedWithdrawalId = 0 409 | const maxGas = 100 410 | const gasPriceBid = 200 411 | 412 | it('sends xdomain message and burns tokens', async () => { 413 | const [deployer, l1DaiBridge, l1Dai, router, sender] = await ethers.getSigners() 414 | const { l2Dai, l2DaiGateway, arbSysMock } = await setupWithdrawalTest({ 415 | l1Dai, 416 | l1DaiBridge, 417 | router, 418 | user1: sender, 419 | deployer, 420 | }) 421 | 422 | const tx = await l2DaiGateway 423 | .connect(sender) 424 | ['outboundTransfer(address,address,uint256,uint256,uint256,bytes)']( 425 | l1Dai.address, 426 | sender.address, 427 | withdrawAmount, 428 | maxGas, 429 | gasPriceBid, 430 | defaultData, 431 | ) 432 | const withdrawCrossChainCall = arbSysMock.smocked.sendTxToL1.calls[0] 433 | 434 | expect(await l2Dai.balanceOf(sender.address)).to.be.eq(initialTotalL2Supply - withdrawAmount) 435 | expect(await l2Dai.totalSupply()).to.be.eq(initialTotalL2Supply - withdrawAmount) 436 | await expect(tx) 437 | .to.emit(l2DaiGateway, 'WithdrawalInitiated') 438 | .withArgs( 439 | l1Dai.address, 440 | sender.address, 441 | sender.address, 442 | expectedWithdrawalId, 443 | expectedWithdrawalId, 444 | withdrawAmount, 445 | ) 446 | expect(withdrawCrossChainCall.destAddr).to.eq(l1DaiBridge.address) 447 | expect(withdrawCrossChainCall.calldataForL1).to.eq( 448 | new L1DaiGateway__factory().interface.encodeFunctionData('finalizeInboundTransfer', [ 449 | l1Dai.address, 450 | sender.address, 451 | sender.address, 452 | withdrawAmount, 453 | ethers.utils.defaultAbiCoder.encode(['uint256', 'bytes'], [expectedWithdrawalId, defaultData]), 454 | ]), 455 | ) 456 | }) 457 | }) 458 | 459 | describe('close', () => { 460 | it('can be called by owner', async () => { 461 | const [owner, l1Dai, router] = await ethers.getSigners() 462 | const { l2DaiGateway } = await setupTest({ l1Dai, l1DaiBridge: owner, router, deployer: owner }) 463 | 464 | expect(await l2DaiGateway.isOpen()).to.be.eq(1) 465 | const closeTx = await l2DaiGateway.connect(owner).close() 466 | 467 | await expect(closeTx).to.emit(l2DaiGateway, 'Closed') 468 | 469 | expect(await l2DaiGateway.isOpen()).to.be.eq(0) 470 | }) 471 | 472 | it('can be called multiple times by the owner but nothing changes', async () => { 473 | const [owner, l1Dai, router] = await ethers.getSigners() 474 | const { l2DaiGateway } = await setupTest({ l1Dai, l1DaiBridge: owner, router, deployer: owner }) 475 | 476 | await l2DaiGateway.connect(owner).close() 477 | expect(await l2DaiGateway.isOpen()).to.be.eq(0) 478 | 479 | await l2DaiGateway.connect(owner).close() 480 | expect(await l2DaiGateway.isOpen()).to.be.eq(0) 481 | }) 482 | 483 | it('reverts when called not by the owner', async () => { 484 | const [owner, l1Dai, router, user1] = await ethers.getSigners() 485 | const { l2DaiGateway } = await setupTest({ l1Dai, l1DaiBridge: owner, router, deployer: owner }) 486 | 487 | await expect(l2DaiGateway.connect(user1).close()).to.be.revertedWith(errorMessages.notOwner) 488 | }) 489 | }) 490 | 491 | describe('calculateL2TokenAddress', () => { 492 | it('return l2Dai address when asked about dai', async () => { 493 | const [owner, l1Dai, router] = await ethers.getSigners() 494 | const { l2DaiGateway, l2Dai } = await setupTest({ l1Dai, l1DaiBridge: owner, router, deployer: owner }) 495 | 496 | expect(await l2DaiGateway.calculateL2TokenAddress(l1Dai.address)).to.eq(l2Dai.address) 497 | }) 498 | 499 | it('returns zero address for unknown tokens', async () => { 500 | const [owner, l1Dai, router] = await ethers.getSigners() 501 | const randomToken = await getRandomAddress() 502 | const { l2DaiGateway } = await setupTest({ l1Dai, l1DaiBridge: owner, router, deployer: owner }) 503 | 504 | expect(await l2DaiGateway.calculateL2TokenAddress(randomToken)).to.eq(ethers.constants.AddressZero) 505 | }) 506 | }) 507 | 508 | describe('constructor', () => { 509 | it('assigns all variables properly', async () => { 510 | const [l1Counterpart, router, l1Dai, l2Dai] = await getRandomAddresses() 511 | 512 | const l2DaiGateway = await simpleDeploy('L2DaiGateway', [ 513 | l1Counterpart, 514 | router, 515 | l1Dai, 516 | l2Dai, 517 | ]) 518 | 519 | expect(await l2DaiGateway.l1Counterpart()).to.be.eq(l1Counterpart) 520 | expect(await l2DaiGateway.l2Router()).to.be.eq(router) 521 | expect(await l2DaiGateway.l1Dai()).to.be.eq(l1Dai) 522 | expect(await l2DaiGateway.l2Dai()).to.be.eq(l2Dai) 523 | expect(await l2DaiGateway.isOpen()).to.be.eq(1) 524 | }) 525 | }) 526 | 527 | it('has correct public interface', async () => { 528 | await assertPublicMutableMethods('L2DaiGateway', [ 529 | 'finalizeInboundTransfer(address,address,address,uint256,bytes)', // finalize deposit 530 | 'outboundTransfer(address,address,uint256,bytes)', // withdraw 531 | 'outboundTransfer(address,address,uint256,uint256,uint256,bytes)', // withdrawTo 532 | 'close()', 533 | 'rely(address)', 534 | 'deny(address)', 535 | ]) 536 | await assertPublicNotMutableMethods('L2DaiGateway', [ 537 | 'getOutboundCalldata(address,address,address,uint256,bytes)', 538 | 'calculateL2TokenAddress(address)', 539 | 540 | // storage variables: 541 | 'l1Counterpart()', 542 | 'isOpen()', 543 | 'l1Dai()', 544 | 'l2Dai()', 545 | 'l2Router()', 546 | 'wards(address)', 547 | 'counterpartGateway()', 548 | ]) 549 | }) 550 | 551 | testAuth({ 552 | name: 'L2DaiGateway', 553 | getDeployArgs: async () => { 554 | const [l1Counterpart, router, l1Dai, l2Dai] = await getRandomAddresses() 555 | 556 | return [l1Counterpart, router, l1Dai, l2Dai] 557 | }, 558 | authedMethods: [(c) => c.close()], 559 | }) 560 | }) 561 | 562 | async function setupTest(signers: { 563 | l1Dai: SignerWithAddress 564 | l1DaiBridge: SignerWithAddress 565 | router: SignerWithAddress 566 | deployer: SignerWithAddress 567 | }) { 568 | const l2Dai = await simpleDeploy('Dai', []) 569 | const l2DaiGateway = await simpleDeploy('L2DaiGateway', [ 570 | signers.l1DaiBridge.address, 571 | signers.router.address, 572 | signers.l1Dai.address, 573 | l2Dai.address, 574 | ]) 575 | await l2Dai.rely(l2DaiGateway.address) 576 | 577 | const l2Deployer = await getL2SignerFromL1(signers.deployer) 578 | await signers.deployer.sendTransaction({ 579 | to: await l2Deployer.getAddress(), 580 | value: ethers.utils.parseUnits('0.1', 'ether'), 581 | }) 582 | 583 | return { 584 | l2Dai, 585 | l2DaiGateway, 586 | l2Deployer, 587 | } 588 | } 589 | 590 | async function setupWithdrawalTest(signers: { 591 | l1Dai: SignerWithAddress 592 | l1DaiBridge: SignerWithAddress 593 | router: SignerWithAddress 594 | user1: SignerWithAddress 595 | deployer: SignerWithAddress 596 | }) { 597 | const harness = await setupTest(signers) 598 | const arbSysMock = await deployArbitrumContractMock('ArbSys', { 599 | address: '0x0000000000000000000000000000000000000064', 600 | }) 601 | await harness.l2Dai.mint(signers.user1.address, initialTotalL2Supply) 602 | 603 | return { 604 | ...harness, 605 | arbSysMock, 606 | } 607 | } 608 | -------------------------------------------------------------------------------- /test/l2/L2GovernanceRelay.ts: -------------------------------------------------------------------------------- 1 | import { assertPublicMutableMethods, simpleDeploy } from '@makerdao/hardhat-utils' 2 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' 3 | import { expect } from 'chai' 4 | import { ethers } from 'hardhat' 5 | 6 | import { getL2SignerFromL1 } from '../../arbitrum-helpers/messaging' 7 | import { BadSpell__factory, Dai__factory, L2GovernanceRelay__factory, TestDaiMintSpell__factory } from '../../typechain' 8 | 9 | const errorMessages = { 10 | l1CounterpartMismatch: 'ONLY_COUNTERPART_GATEWAY', 11 | delegatecallError: 'L2GovernanceRelay/delegatecall-error', 12 | illegalStorageChange: 'L2GovernanceRelay/illegal-storage-change', 13 | } 14 | 15 | describe('L2GovernanceRelay', () => { 16 | describe('relay', () => { 17 | const depositAmount = 100 18 | 19 | it('mints new tokens', async () => { 20 | const [deployer, l1GovernanceRelayImpersonator, l1Dai, user1] = await ethers.getSigners() 21 | const { l2GovernanceRelay, l2Dai, l2daiMintSpell, l2GovernanceRelayImpersonator } = await setupTest({ 22 | l1Dai, 23 | l1GovernanceRelay: l1GovernanceRelayImpersonator, 24 | deployer, 25 | }) 26 | 27 | await l2GovernanceRelay 28 | .connect(l2GovernanceRelayImpersonator) 29 | .relay( 30 | l2daiMintSpell.address, 31 | l2daiMintSpell.interface.encodeFunctionData('mintDai', [l2Dai.address, user1.address, depositAmount]), 32 | ) 33 | 34 | expect(await l2Dai.balanceOf(user1.address)).to.be.eq(depositAmount) 35 | expect(await l2Dai.totalSupply()).to.be.eq(depositAmount) 36 | }) 37 | 38 | it('reverts when called not relying message from l1DaiGateway', async () => { 39 | const [deployer, l1GovernanceRelayImpersonator, l1Dai, randomAcc, user1] = await ethers.getSigners() 40 | const { l2GovernanceRelay, l2daiMintSpell, l2Dai } = await setupTest({ 41 | l1Dai, 42 | l1GovernanceRelay: l1GovernanceRelayImpersonator, 43 | deployer, 44 | }) 45 | 46 | await expect( 47 | l2GovernanceRelay 48 | .connect(randomAcc) 49 | .relay( 50 | l2daiMintSpell.address, 51 | l2daiMintSpell.interface.encodeFunctionData('mintDai', [l2Dai.address, user1.address, depositAmount]), 52 | ), 53 | ).to.be.revertedWith(errorMessages.l1CounterpartMismatch) 54 | }) 55 | 56 | it('reverts when called directly by l1 counterpart', async () => { 57 | // this should fail b/c we require address translation 58 | const [deployer, l1GovernanceRelayImpersonator, l1Dai, user1] = await ethers.getSigners() 59 | const { l2GovernanceRelay, l2daiMintSpell, l2Dai } = await setupTest({ 60 | l1Dai, 61 | l1GovernanceRelay: l1GovernanceRelayImpersonator, 62 | deployer, 63 | }) 64 | 65 | await expect( 66 | l2GovernanceRelay 67 | .connect(l1GovernanceRelayImpersonator) 68 | .relay( 69 | l2daiMintSpell.address, 70 | l2daiMintSpell.interface.encodeFunctionData('mintDai', [l2Dai.address, user1.address, depositAmount]), 71 | ), 72 | ).to.be.revertedWith(errorMessages.l1CounterpartMismatch) 73 | }) 74 | 75 | it('reverts when spell reverts', async () => { 76 | const [deployer, l1GovernanceRelayImpersonator, l1Dai] = await ethers.getSigners() 77 | const { l2GovernanceRelay, l2GovernanceRelayImpersonator } = await setupTest({ 78 | l1Dai, 79 | l1GovernanceRelay: l1GovernanceRelayImpersonator, 80 | deployer, 81 | }) 82 | const badSpell = await simpleDeploy('BadSpell', []) 83 | 84 | await expect( 85 | l2GovernanceRelay 86 | .connect(l2GovernanceRelayImpersonator) 87 | .relay(badSpell.address, badSpell.interface.encodeFunctionData('abort')), 88 | ).to.be.revertedWith(errorMessages.delegatecallError) 89 | }) 90 | }) 91 | 92 | describe('constructor', () => { 93 | it('assigns all variables properly', async () => { 94 | const [l1GovRelay] = await ethers.getSigners() 95 | 96 | const l2GovRelay = await simpleDeploy('L2GovernanceRelay', [l1GovRelay.address]) 97 | 98 | expect(await l2GovRelay.l1GovernanceRelay()).to.eq(l1GovRelay.address) 99 | }) 100 | }) 101 | 102 | it('has correct public interface', async () => { 103 | await assertPublicMutableMethods('L2GovernanceRelay', ['relay(address,bytes)']) 104 | }) 105 | }) 106 | 107 | async function setupTest(signers: { 108 | l1Dai: SignerWithAddress 109 | l1GovernanceRelay: SignerWithAddress 110 | deployer: SignerWithAddress 111 | }) { 112 | const l2Dai = await simpleDeploy('Dai', []) 113 | 114 | const l2GovernanceRelay = await simpleDeploy('L2GovernanceRelay', [ 115 | signers.l1GovernanceRelay.address, 116 | ]) 117 | await l2Dai.rely(l2GovernanceRelay.address) 118 | await l2Dai.deny(signers.deployer.address) 119 | 120 | const l2daiMintSpell = await simpleDeploy('TestDaiMintSpell', []) 121 | 122 | const l2GovernanceRelayImpersonator = await getL2SignerFromL1(signers.l1GovernanceRelay) 123 | await signers.deployer.sendTransaction({ 124 | to: await l2GovernanceRelayImpersonator.getAddress(), 125 | value: ethers.utils.parseUnits('0.1', 'ether'), 126 | }) 127 | 128 | return { l2Dai, l2GovernanceRelay, l2daiMintSpell, l2GovernanceRelayImpersonator } 129 | } 130 | -------------------------------------------------------------------------------- /test/l2/dai.ts: -------------------------------------------------------------------------------- 1 | import { assertPublicMutableMethods, getRandomAddresses, testAuth } from '@makerdao/hardhat-utils' 2 | import { expect } from 'chai' 3 | import { ethers, web3 } from 'hardhat' 4 | 5 | import { Dai, Dai__factory } from '../../typechain' 6 | 7 | const { signERC2612Permit } = require('./eth-permit/eth-permit') 8 | 9 | describe('Dai', () => { 10 | let signers: any 11 | let dai: Dai 12 | 13 | beforeEach(async () => { 14 | const [deployer, user1, user2, user3] = await ethers.getSigners() 15 | signers = { deployer, user1, user2, user3 } 16 | const daiFactory = (await ethers.getContractFactory('Dai', deployer)) as Dai__factory 17 | dai = await daiFactory.deploy() 18 | }) 19 | 20 | describe('deployment', async () => { 21 | it('returns the name', async () => { 22 | expect(await dai.name()).to.be.eq('Dai Stablecoin') 23 | }) 24 | 25 | it('returns the symbol', async () => { 26 | expect(await dai.symbol()).to.be.eq('DAI') 27 | }) 28 | 29 | it('returns the decimals', async () => { 30 | expect(await dai.decimals()).to.be.eq(18) 31 | }) 32 | 33 | describe('with a positive balance', async () => { 34 | beforeEach(async () => { 35 | await dai.mint(signers.user1.address, 10) 36 | }) 37 | 38 | it('returns the dai balance as total supply', async () => { 39 | expect(await dai.totalSupply()).to.be.eq('10') 40 | }) 41 | 42 | it('transfers dai', async () => { 43 | const balanceBefore = await dai.balanceOf(signers.user2.address) 44 | await dai.connect(signers.user1).transfer(signers.user2.address, 1) 45 | const balanceAfter = await dai.balanceOf(signers.user2.address) 46 | expect(balanceAfter).to.be.eq(balanceBefore.add(1)) 47 | }) 48 | 49 | it('transfers dai to yourself', async () => { 50 | const balanceBefore = await dai.balanceOf(signers.user1.address) 51 | await dai.connect(signers.user1).transfer(signers.user1.address, 1) 52 | const balanceAfter = await dai.balanceOf(signers.user1.address) 53 | expect(balanceAfter).to.be.eq(balanceBefore) 54 | }) 55 | 56 | it('transfers dai using transferFrom', async () => { 57 | const balanceBefore = await dai.balanceOf(signers.user2.address) 58 | await dai.connect(signers.user1).transferFrom(signers.user1.address, signers.user2.address, 1) 59 | const balanceAfter = await dai.balanceOf(signers.user2.address) 60 | expect(balanceAfter).to.be.eq(balanceBefore.add(1)) 61 | }) 62 | 63 | it('transfers dai to yourself using transferFrom', async () => { 64 | const balanceBefore = await dai.balanceOf(signers.user1.address) 65 | await dai.connect(signers.user1).transferFrom(signers.user1.address, signers.user1.address, 1) 66 | const balanceAfter = await dai.balanceOf(signers.user1.address) 67 | expect(balanceAfter).to.be.eq(balanceBefore) 68 | }) 69 | 70 | it('should not transfer beyond balance', async () => { 71 | await expect(dai.connect(signers.user1).transfer(signers.user2.address, 100)).to.be.revertedWith( 72 | 'Dai/insufficient-balance', 73 | ) 74 | await expect( 75 | dai.connect(signers.user1).transferFrom(signers.user1.address, signers.user2.address, 100), 76 | ).to.be.revertedWith('Dai/insufficient-balance') 77 | }) 78 | 79 | it('should not transfer to zero address', async () => { 80 | await expect(dai.connect(signers.user1).transfer(ethers.constants.AddressZero, 1)).to.be.revertedWith( 81 | 'Dai/invalid-address', 82 | ) 83 | await expect( 84 | dai.connect(signers.user1).transferFrom(signers.user1.address, ethers.constants.AddressZero, 1), 85 | ).to.be.revertedWith('Dai/invalid-address') 86 | }) 87 | 88 | it('should not transfer to dai address', async () => { 89 | await expect(dai.connect(signers.user1).transfer(dai.address, 1)).to.be.revertedWith('Dai/invalid-address') 90 | await expect(dai.connect(signers.user1).transferFrom(signers.user1.address, dai.address, 1)).to.be.revertedWith( 91 | 'Dai/invalid-address', 92 | ) 93 | }) 94 | 95 | it('should not allow minting to zero address', async () => { 96 | await expect(dai.mint(ethers.constants.AddressZero, 1)).to.be.revertedWith('Dai/invalid-address') 97 | }) 98 | 99 | it('should not allow minting to dai address', async () => { 100 | await expect(dai.mint(dai.address, 1)).to.be.revertedWith('Dai/invalid-address') 101 | }) 102 | 103 | it('should not allow minting to address beyond MAX', async () => { 104 | await expect(dai.mint(signers.user1.address, ethers.constants.MaxUint256)).to.be.reverted 105 | }) 106 | 107 | it('burns own dai', async () => { 108 | const balanceBefore = await dai.balanceOf(signers.user1.address) 109 | await dai.connect(signers.user1).burn(signers.user1.address, 1) 110 | const balanceAfter = await dai.balanceOf(signers.user1.address) 111 | expect(balanceAfter).to.be.eq(balanceBefore.sub(1)) 112 | }) 113 | 114 | it('should not burn beyond balance', async () => { 115 | await expect(dai.connect(signers.user1).burn(signers.user1.address, 100)).to.be.revertedWith( 116 | 'Dai/insufficient-balance', 117 | ) 118 | }) 119 | 120 | it('should not burn other', async () => { 121 | await expect(dai.connect(signers.user2).burn(signers.user1.address, 1)).to.be.revertedWith( 122 | 'Dai/insufficient-allowance', 123 | ) 124 | }) 125 | 126 | it('deployer can burn other', async () => { 127 | const balanceBefore = await dai.balanceOf(signers.user1.address) 128 | await dai.connect(signers.deployer).burn(signers.user1.address, 1) 129 | const balanceAfter = await dai.balanceOf(signers.user1.address) 130 | expect(balanceAfter).to.be.eq(balanceBefore.sub(1)) 131 | }) 132 | 133 | it('can burn other if approved', async () => { 134 | const balanceBefore = await dai.balanceOf(signers.user1.address) 135 | await dai.connect(signers.user1).approve(signers.user2.address, 1) 136 | 137 | await dai.connect(signers.user2).burn(signers.user1.address, 1) 138 | 139 | const balanceAfter = await dai.balanceOf(signers.user1.address) 140 | expect(balanceAfter).to.be.eq(balanceBefore.sub(1)) 141 | }) 142 | 143 | it('approves to increase allowance', async () => { 144 | const allowanceBefore = await dai.allowance(signers.user1.address, signers.user2.address) 145 | await dai.connect(signers.user1).approve(signers.user2.address, 1) 146 | const allowanceAfter = await dai.allowance(signers.user1.address, signers.user2.address) 147 | expect(allowanceAfter).to.be.eq(allowanceBefore.add(1)) 148 | }) 149 | 150 | it('increaseAllowance to increase allowance', async () => { 151 | const allowanceBefore = await dai.allowance(signers.user1.address, signers.user2.address) 152 | await dai.connect(signers.user1).increaseAllowance(signers.user2.address, 1) 153 | const allowanceAfter = await dai.allowance(signers.user1.address, signers.user2.address) 154 | expect(allowanceAfter).to.be.eq(allowanceBefore.add(1)) 155 | }) 156 | 157 | it('approves to increase allowance with permit', async () => { 158 | const permitResult = await signERC2612Permit( 159 | web3.currentProvider, 160 | dai.address, 161 | signers.user1.address, 162 | signers.user2.address, 163 | '1', 164 | null, 165 | null, 166 | '2', 167 | ) 168 | await dai.permit( 169 | signers.user1.address, 170 | signers.user2.address, 171 | '1', 172 | permitResult.deadline, 173 | permitResult.v, 174 | permitResult.r, 175 | permitResult.s, 176 | ) 177 | const allowanceAfter = await dai.allowance(signers.user1.address, signers.user2.address) 178 | expect(allowanceAfter).to.be.eq('1') 179 | }) 180 | 181 | it('does not approve with expired permit', async () => { 182 | const permitResult = await signERC2612Permit( 183 | web3.currentProvider, 184 | dai.address, 185 | signers.user1.address, 186 | signers.user2.address, 187 | '1', 188 | null, 189 | null, 190 | '2', 191 | ) 192 | await expect( 193 | dai.permit( 194 | signers.user1.address, 195 | signers.user2.address, 196 | '1', 197 | 0, 198 | permitResult.v, 199 | permitResult.r, 200 | permitResult.s, 201 | ), 202 | ).to.be.revertedWith('Dai/permit-expired') 203 | }) 204 | 205 | it('does not approve with invalid permit', async () => { 206 | const permitResult = await signERC2612Permit( 207 | web3.currentProvider, 208 | dai.address, 209 | signers.user1.address, 210 | signers.user2.address, 211 | '1', 212 | null, 213 | null, 214 | '2', 215 | ) 216 | await expect( 217 | dai.permit( 218 | signers.user1.address, 219 | signers.user2.address, 220 | '2', 221 | permitResult.deadline, 222 | permitResult.v, 223 | permitResult.r, 224 | permitResult.s, 225 | ), 226 | 'Dai/invalid-permit', 227 | ).to.be.revertedWith('Dai/invalid-permit') 228 | }) 229 | 230 | describe('with a positive allowance', async () => { 231 | beforeEach(async () => { 232 | await dai.connect(signers.user1).approve(signers.user2.address, 1) 233 | }) 234 | 235 | it('transfers dai using transferFrom and allowance', async () => { 236 | const balanceBefore = await dai.balanceOf(signers.user2.address) 237 | await dai.connect(signers.user2).transferFrom(signers.user1.address, signers.user2.address, 1) 238 | const balanceAfter = await dai.balanceOf(signers.user2.address) 239 | expect(balanceAfter).to.be.eq(balanceBefore.add(1)) 240 | }) 241 | 242 | it('should not transfer beyond allowance', async () => { 243 | await expect( 244 | dai.connect(signers.user2).transferFrom(signers.user1.address, signers.user2.address, 2), 245 | ).to.be.revertedWith('Dai/insufficient-allowance') 246 | }) 247 | 248 | it('burns dai using burn and allowance', async () => { 249 | const balanceBefore = await dai.balanceOf(signers.user1.address) 250 | await dai.connect(signers.user2).burn(signers.user1.address, 1) 251 | const balanceAfter = await dai.balanceOf(signers.user1.address) 252 | expect(balanceAfter).to.be.eq(balanceBefore.sub(1)) 253 | }) 254 | 255 | it('should not burn beyond allowance', async () => { 256 | await expect(dai.connect(signers.user2).burn(signers.user1.address, 2)).to.be.revertedWith( 257 | 'Dai/insufficient-allowance', 258 | ) 259 | }) 260 | 261 | it('increaseAllowance should increase allowance', async () => { 262 | const balanceBefore = await dai.allowance(signers.user1.address, signers.user2.address) 263 | await dai.connect(signers.user1).increaseAllowance(signers.user2.address, 1) 264 | const balanceAfter = await dai.allowance(signers.user1.address, signers.user2.address) 265 | expect(balanceAfter).to.be.eq(balanceBefore.add(1)) 266 | }) 267 | 268 | it('should not increaseAllowance beyond MAX', async () => { 269 | await expect(dai.connect(signers.user1).increaseAllowance(signers.user2.address, ethers.constants.MaxUint256)) 270 | .to.be.reverted 271 | }) 272 | 273 | it('decreaseAllowance should decrease allowance', async () => { 274 | const balanceBefore = await dai.allowance(signers.user1.address, signers.user2.address) 275 | await dai.connect(signers.user1).decreaseAllowance(signers.user2.address, 1) 276 | const balanceAfter = await dai.allowance(signers.user1.address, signers.user2.address) 277 | expect(balanceAfter).to.be.eq(balanceBefore.sub(1)) 278 | }) 279 | 280 | it('should not decreaseAllowance beyond allowance', async () => { 281 | await expect(dai.connect(signers.user1).decreaseAllowance(signers.user2.address, 2)).to.be.revertedWith( 282 | 'Dai/insufficient-allowance', 283 | ) 284 | }) 285 | }) 286 | 287 | describe('with a maximum allowance', async () => { 288 | beforeEach(async () => { 289 | await dai.connect(signers.user1).approve(signers.user2.address, ethers.constants.MaxUint256) 290 | }) 291 | 292 | it('does not decrease allowance using transferFrom', async () => { 293 | await dai.connect(signers.user2).transferFrom(signers.user1.address, signers.user2.address, 1) 294 | const allowanceAfter = await dai.allowance(signers.user1.address, signers.user2.address) 295 | expect(allowanceAfter).to.be.eq(ethers.constants.MaxUint256) 296 | }) 297 | 298 | it('does not decrease allowance using burn', async () => { 299 | await dai.connect(signers.user2).burn(signers.user1.address, 1) 300 | const allowanceAfter = await dai.allowance(signers.user1.address, signers.user2.address) 301 | expect(allowanceAfter).to.be.eq(ethers.constants.MaxUint256) 302 | }) 303 | }) 304 | 305 | describe('events', async () => { 306 | it('emits Transfer event on mint', async () => { 307 | await expect(dai.mint(signers.user1.address, 10)) 308 | .to.emit(dai, 'Transfer') 309 | .withArgs(ethers.constants.AddressZero, signers.user1.address, 10) 310 | }) 311 | 312 | it('emits Transfer event on transfer', async () => { 313 | await expect(dai.connect(signers.user1).transfer(signers.user2.address, 1)) 314 | .to.emit(dai, 'Transfer') 315 | .withArgs(signers.user1.address, signers.user2.address, 1) 316 | }) 317 | 318 | it('emits Transfer event on transferFrom', async () => { 319 | await expect(dai.connect(signers.user1).transferFrom(signers.user1.address, signers.user2.address, 1)) 320 | .to.emit(dai, 'Transfer') 321 | .withArgs(signers.user1.address, signers.user2.address, 1) 322 | }) 323 | 324 | it('emits Transfer event on burn', async () => { 325 | await expect(dai.connect(signers.user1).burn(signers.user1.address, 1)) 326 | .to.emit(dai, 'Transfer') 327 | .withArgs(signers.user1.address, ethers.constants.AddressZero, 1) 328 | }) 329 | 330 | it('emits Approval event on approve', async () => { 331 | await expect(dai.connect(signers.user1).approve(signers.user2.address, 1)) 332 | .to.emit(dai, 'Approval') 333 | .withArgs(signers.user1.address, signers.user2.address, 1) 334 | }) 335 | 336 | it('emits Approval event on increaseAllowance', async () => { 337 | await expect(dai.connect(signers.user1).increaseAllowance(signers.user2.address, 1)) 338 | .to.emit(dai, 'Approval') 339 | .withArgs(signers.user1.address, signers.user2.address, 1) 340 | }) 341 | 342 | it('emits Approval event on decreaseAllowance', async () => { 343 | await dai.connect(signers.user1).approve(signers.user2.address, 1) 344 | await expect(dai.connect(signers.user1).decreaseAllowance(signers.user2.address, 1)) 345 | .to.emit(dai, 'Approval') 346 | .withArgs(signers.user1.address, signers.user2.address, 0) 347 | }) 348 | 349 | it('emits Approval event on permit', async () => { 350 | const permitResult = await signERC2612Permit( 351 | web3.currentProvider, 352 | dai.address, 353 | signers.user1.address, 354 | signers.user2.address, 355 | '1', 356 | null, 357 | null, 358 | '2', 359 | ) 360 | await expect( 361 | dai.permit( 362 | signers.user1.address, 363 | signers.user2.address, 364 | '1', 365 | permitResult.deadline, 366 | permitResult.v, 367 | permitResult.r, 368 | permitResult.s, 369 | ), 370 | ) 371 | .to.emit(dai, 'Approval') 372 | .withArgs(signers.user1.address, signers.user2.address, 1) 373 | }) 374 | }) 375 | }) 376 | }) 377 | 378 | it('has correct public interface', async () => { 379 | await assertPublicMutableMethods('Dai', [ 380 | 'rely(address)', 381 | 'deny(address)', 382 | 'approve(address,uint256)', 383 | 'burn(address,uint256)', 384 | 'decreaseAllowance(address,uint256)', 385 | 'increaseAllowance(address,uint256)', 386 | 'mint(address,uint256)', 387 | 'permit(address,address,uint256,uint256,uint8,bytes32,bytes32)', 388 | 'transfer(address,uint256)', 389 | 'transferFrom(address,address,uint256)', 390 | ]) 391 | }) 392 | 393 | testAuth({ 394 | name: 'Dai', 395 | getDeployArgs: async () => [], 396 | authedMethods: [ 397 | async (c) => { 398 | const [to] = await getRandomAddresses() 399 | return c.mint(to, 1) 400 | }, 401 | ], 402 | }) 403 | }) 404 | -------------------------------------------------------------------------------- /test/l2/eth-permit/eth-permit.ts: -------------------------------------------------------------------------------- 1 | import { hexToUtf8 } from './lib' 2 | import { call, getChainId, RSV, signData } from './rpc' 3 | 4 | const MAX_INT = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' 5 | 6 | interface DaiPermitMessage { 7 | holder: string 8 | spender: string 9 | nonce: number 10 | expiry: number | string 11 | allowed?: boolean 12 | } 13 | 14 | interface ERC2612PermitMessage { 15 | owner: string 16 | spender: string 17 | value: number | string 18 | nonce: number | string 19 | deadline: number | string 20 | } 21 | 22 | interface Domain { 23 | name: string 24 | version: string 25 | chainId: number 26 | verifyingContract: string 27 | } 28 | 29 | const EIP712Domain = [ 30 | { name: 'name', type: 'string' }, 31 | { name: 'version', type: 'string' }, 32 | { name: 'chainId', type: 'uint256' }, 33 | { name: 'verifyingContract', type: 'address' }, 34 | ] 35 | 36 | const createTypedDaiData = (message: DaiPermitMessage, domain: Domain) => { 37 | const typedData = { 38 | types: { 39 | EIP712Domain, 40 | Permit: [ 41 | { name: 'holder', type: 'address' }, 42 | { name: 'spender', type: 'address' }, 43 | { name: 'nonce', type: 'uint256' }, 44 | { name: 'expiry', type: 'uint256' }, 45 | { name: 'allowed', type: 'bool' }, 46 | ], 47 | }, 48 | primaryType: 'Permit', 49 | domain, 50 | message, 51 | } 52 | 53 | return typedData 54 | } 55 | 56 | const createTypedERC2612Data = (message: ERC2612PermitMessage, domain: Domain) => { 57 | const typedData = { 58 | types: { 59 | EIP712Domain, 60 | Permit: [ 61 | { name: 'owner', type: 'address' }, 62 | { name: 'spender', type: 'address' }, 63 | { name: 'value', type: 'uint256' }, 64 | { name: 'nonce', type: 'uint256' }, 65 | { name: 'deadline', type: 'uint256' }, 66 | ], 67 | }, 68 | primaryType: 'Permit', 69 | domain, 70 | message, 71 | } 72 | 73 | return typedData 74 | } 75 | 76 | const NONCES_FN = '0x7ecebe00' 77 | const NAME_FN = '0x06fdde03' 78 | 79 | const zeros = (numZeros: number) => ''.padEnd(numZeros, '0') 80 | 81 | const getTokenName = async (provider: any, address: string) => 82 | hexToUtf8((await call(provider, address, NAME_FN)).substr(130)) 83 | 84 | const getDomain = async (provider: any, token: string | Domain, version: string): Promise => { 85 | if (typeof token !== 'string') { 86 | return token as Domain 87 | } 88 | 89 | const tokenAddress = token as string 90 | 91 | const [name, chainId] = await Promise.all([getTokenName(provider, tokenAddress), getChainId(provider)]) 92 | 93 | const domain: Domain = { name, version, chainId, verifyingContract: tokenAddress } 94 | return domain 95 | } 96 | 97 | export const signDaiPermit = async ( 98 | provider: any, 99 | token: string | Domain, 100 | holder: string, 101 | spender: string, 102 | expiry?: number, 103 | nonce?: number, 104 | version: string = '1', 105 | ): Promise => { 106 | const tokenAddress = (token as Domain).verifyingContract || (token as string) 107 | 108 | const message: DaiPermitMessage = { 109 | holder, 110 | spender, 111 | nonce: nonce || (await call(provider, tokenAddress, `${NONCES_FN}${zeros(24)}${holder.substr(2)}`)), 112 | expiry: expiry || MAX_INT, 113 | allowed: true, 114 | } 115 | 116 | const domain = await getDomain(provider, token, version) 117 | const typedData = createTypedDaiData(message, domain) 118 | const sig = await signData(provider, holder, typedData) 119 | 120 | return { ...sig, ...message } 121 | } 122 | 123 | export const signERC2612Permit = async ( 124 | provider: any, 125 | token: string | Domain, 126 | owner: string, 127 | spender: string, 128 | value: string | number = MAX_INT, 129 | deadline?: number, 130 | nonce?: number, 131 | version: string = '1', 132 | ): Promise => { 133 | const tokenAddress = (token as Domain).verifyingContract || (token as string) 134 | 135 | const message: ERC2612PermitMessage = { 136 | owner, 137 | spender, 138 | value, 139 | nonce: nonce || (await call(provider, tokenAddress, `${NONCES_FN}${zeros(24)}${owner.substr(2)}`)), 140 | deadline: deadline || MAX_INT, 141 | } 142 | 143 | const domain = await getDomain(provider, token, version) 144 | const typedData = createTypedERC2612Data(message, domain) 145 | const sig = await signData(provider, owner, typedData) 146 | 147 | return { ...sig, ...message } 148 | } 149 | -------------------------------------------------------------------------------- /test/l2/eth-permit/lib.ts: -------------------------------------------------------------------------------- 1 | import { decode } from 'utf8' 2 | 3 | export const hexToUtf8 = function (hex: string) { 4 | // if (!isHexStrict(hex)) 5 | // throw new Error('The parameter "'+ hex +'" must be a valid HEX string.'); 6 | 7 | let str = '' 8 | let code = 0 9 | hex = hex.replace(/^0x/i, '') 10 | 11 | // remove 00 padding from either side 12 | hex = hex.replace(/^(?:00)*/, '') 13 | hex = hex.split('').reverse().join('') 14 | hex = hex.replace(/^(?:00)*/, '') 15 | hex = hex.split('').reverse().join('') 16 | 17 | const l = hex.length 18 | 19 | for (let i = 0; i < l; i += 2) { 20 | code = parseInt(hex.substr(i, 2), 16) 21 | // if (code !== 0) { 22 | str += String.fromCharCode(code) 23 | // } 24 | } 25 | 26 | return decode(str) 27 | } 28 | -------------------------------------------------------------------------------- /test/l2/eth-permit/rpc.ts: -------------------------------------------------------------------------------- 1 | const randomId = () => Math.floor(Math.random() * 10000000000) 2 | 3 | export const send = (provider: any, method: string, params?: any[]) => 4 | new Promise((resolve, reject) => { 5 | const payload = { 6 | id: randomId(), 7 | method, 8 | params, 9 | } 10 | const callback = (err: any, result: any) => { 11 | if (err) { 12 | reject(err) 13 | } else if (result.error) { 14 | console.error(result.error) 15 | reject(result.error) 16 | } else { 17 | resolve(result.result) 18 | } 19 | } 20 | 21 | const _provider = provider.provider || provider 22 | 23 | if (_provider.sendAsync) { 24 | _provider.sendAsync(payload, callback) 25 | } else { 26 | _provider.send(payload, callback) 27 | } 28 | }) 29 | 30 | export interface RSV { 31 | r: string 32 | s: string 33 | v: number 34 | } 35 | 36 | export const signData = async (provider: any, fromAddress: string, typeData: any): Promise => { 37 | const typeDataString = typeof typeData === 'string' ? typeData : JSON.stringify(typeData) 38 | const result = await send(provider, 'eth_signTypedData_v4', [fromAddress, typeDataString]).catch((error: any) => { 39 | if (error.message === 'Method eth_signTypedData_v4 not supported.') { 40 | return send(provider, 'eth_signTypedData', [fromAddress, typeData]) 41 | } else { 42 | throw error 43 | } 44 | }) 45 | 46 | return { 47 | r: result.slice(0, 66), 48 | s: `0x${result.slice(66, 130)}`, 49 | v: parseInt(result.slice(130, 132), 16), 50 | } 51 | } 52 | 53 | let chainIdOverride: null | number = null 54 | export const setChainIdOverride = (id: number) => { 55 | chainIdOverride = id 56 | } 57 | export const getChainId = async (provider: any): Promise => chainIdOverride || send(provider, 'eth_chainId') 58 | 59 | export const call = (provider: any, to: string, data: string) => 60 | send(provider, 'eth_call', [ 61 | { 62 | to, 63 | data, 64 | }, 65 | 'latest', 66 | ]) 67 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "sourceMap": true, 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "module": "commonjs", 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist" 11 | }, 12 | "include": ["*.ts", "**/*.ts", "artifacts/*.json"], 13 | "files": ["./hardhat.config.ts"], 14 | "exclude": ["./build", "node_modules"] 15 | } 16 | --------------------------------------------------------------------------------