Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Base Token Bridge #1

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9f209db
Add bridging functions
Jul 9, 2024
cfbdcc0
Block remoteToken == 0
Jul 15, 2024
130be81
Add init lib
Jul 18, 2024
ca1fc25
Mitigate domain persistence issue in test
Jul 19, 2024
f5c545b
Add bridge unit tests
Jul 19, 2024
99d8669
Use internal for common bridgeERC20 logic
Jul 22, 2024
544254c
Bubble up undecoded revert
Jul 22, 2024
8a366da
Add gov relay unit tests
Jul 22, 2024
2c39c49
Add escrow unit tests
Jul 22, 2024
f071a37
Test paused withdraw
Jul 23, 2024
43bcbb0
Add Deploy.s.sol
Jul 24, 2024
f0ffd35
Add testnet init script
Jul 24, 2024
12dc476
Add deposit/withdraw scripts
Jul 24, 2024
3c464fd
Complete README
Aug 5, 2024
b78c006
Add minGasLimit bound check
Aug 6, 2024
e98b020
Update dss-test
Aug 20, 2024
2ec7b56
Update deploy/mocks/ChainLog.sol
telome Aug 20, 2024
a2b29e2
Rearrange L2GovRelay test
Aug 20, 2024
9d82c7c
Update test/L1TokenBridge.t.sol
telome Aug 20, 2024
a9afb22
Update test/Integration.t.sol
telome Aug 20, 2024
0f71468
Add ,
Aug 20, 2024
1e289a3
Update test/L2TokenBridge.t.sol
telome Aug 20, 2024
2c9c30c
Update README.md
telome Aug 20, 2024
b7e2252
Update README.md
telome Aug 20, 2024
ca7cfe1
Update README.md
telome Aug 20, 2024
603dafa
Fix exportContracts issue
Aug 21, 2024
687d9bd
Use new dss-test functions
Aug 21, 2024
22bcee7
Use gas estimate multiplier for Deposit.s.sol
Aug 21, 2024
4b6cd2a
Update CI
Aug 23, 2024
bae891c
Address Cantina audit findings (#2)
telome Sep 2, 2024
6226a76
Add Cantina Audit Report (#3)
telome Sep 10, 2024
5037eb4
Add ChainSecurity Report (#4)
oldchili Sep 16, 2024
a78371b
Implement fileable escrow (#5)
sunbreak1211 Sep 17, 2024
a01b872
Remove unused function from interface
sunbreak1211 Sep 19, 2024
d0c8983
Add maxWithdraws to L2TokenBridge (#6)
telome Oct 1, 2024
0f93550
Upgradable L1TokenBridge and L2TokenBridge (#7)
telome Oct 8, 2024
16f33b5
Update CS audit report (#8)
telome Oct 14, 2024
262383f
Update sepolia deployment
Oct 15, 2024
fb4bfb1
Add Cantina report (#10)
oldchili Nov 13, 2024
8133c00
Add Certora specs (#9)
sunbreak1211 Nov 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export FOUNDRY_SCRIPT_DEPS=deployed
export FOUNDRY_EXPORTS_OVERWRITE_LATEST=true
export L1="sepolia"
export L2="base_sepolia"
export MAINNET_RPC_URL=
export BASE_RPC_URL=
export SEPOLIA_RPC_URL=
export BASE_SEPOLIA_RPC_URL=
export L1_PRIVATE_KEY="0x$(cat /path/to/pkey1)"
export L2_PRIVATE_KEY="0x$(cat /path/to/pkey2)"
export ETHERSCAN_KEY=
export BASESCAN_KEY=
48 changes: 48 additions & 0 deletions .github/workflows/certora.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Certora

on: [push, pull_request]

jobs:
certora:
name: Certora
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
op-token-bridge:
- escrow
- l1-governance-relay
- l2-governance-relay
- l1-token-bridge
- l2-token-bridge

steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: recursive

- uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '11'
java-package: jre

- name: Set up Python 3.8
uses: actions/setup-python@v3
with:
python-version: 3.8

- name: Install solc-select
run: pip3 install solc-select

- name: Solc Select 0.8.21
run: solc-select install 0.8.21

- name: Install Certora
run: pip3 install certora-cli-beta

- name: Verify ${{ matrix.op-token-bridge }}
run: make certora-${{ matrix.op-token-bridge }} results=1
env:
CERTORAKEY: ${{ secrets.CERTORAKEY }}
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: test

on: workflow_dispatch
on: [push, pull_request]

env:
FOUNDRY_PROFILE: ci
Expand Down Expand Up @@ -32,3 +32,6 @@ jobs:
run: |
forge test -vvv
id: test
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
BASE_RPC_URL: ${{ secrets.BASE_RPC_URL }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ docs/

# Dotenv file
.env

# Certora
.certora_internal
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[submodule "lib/dss-test"]
path = lib/dss-test
url = https://github.com/makerdao/dss-test
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
[submodule "lib/openzeppelin-foundry-upgrades"]
path = lib/openzeppelin-foundry-upgrades
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
PATH := ~/.solc-select/artifacts/:~/.solc-select/artifacts/solc-0.8.21:$(PATH)
certora-escrow :; PATH=${PATH} certoraRun certora/Escrow.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
certora-l1-governance-relay :; PATH=${PATH} certoraRun certora/L1GovernanceRelay.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
certora-l2-governance-relay :; PATH=${PATH} certoraRun certora/L2GovernanceRelay.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
certora-l1-token-bridge :; PATH=${PATH} certoraRun certora/L1TokenBridge.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
certora-l2-token-bridge :; PATH=${PATH} certoraRun certora/L2TokenBridge.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
112 changes: 112 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# MakerDAO OP Token Bridge

## Overview

The OP Token Bridge is a [custom bridge](https://docs.optimism.io/builders/app-developers/bridging/custom-bridge) to an OP Stack L2 that allows users to deposit a supported token to the L2 and withdraw it back to Ethereum. It operates similarly to the previously deployed [Optimism Dai Bridge](https://github.com/makerdao/optimism-dai-bridge) and relies on the same security model but allows MakerDAO governance to update the set of tokens supported by the bridge.

## Contracts

- `L1TokenBridge.sol` - L1 side of the bridge. Transfers the deposited tokens into an escrow contract. Transfer them back to the user upon receiving a withdrawal message from the `L2TokenBridge`.
- `L2TokenBridge.sol` - L2 side of the bridge. Mints new L2 tokens after receiving a deposit message from `L1TokenBridge`. Burns L2 tokens when withdrawing them to L1.
- `Escrow.sol` - Escrow contract that holds the bridged tokens on L1.
- `L1GovernanceRelay.sol` - L1 side of the governance relay, which allows governance to exert admin control over the deployed L2 contracts.
- `L2GovernanceRelay.sol` - L2 side of the governance relay.

The `L1TokenBridge` and `L2TokenBridge` contracts use the ERC-1822 UUPS pattern for upgradeability and the ERC-1967 proxy storage slots standard. It is important that the `TokenBridgeDeploy` library sequences be used for deploying.

### External dependencies

- The L2 implementations of the bridged tokens are not provided as part of this repository and are assumed to exist in external repositories. It is assumed that only simple, regular ERC20 tokens will be used with this bridge. In particular, the supported tokens are assumed to revert on failure (instead of returning false) and do not execute any hook on transfer.

## User flows

### L1 to L2 deposits

To deposit a given amount of a supported token to the L2, Alice calls `bridgeERC20[To]()` on the `L1TokenBridge`. This call locks Alice's tokens into the `Escrow` contract and calls the [L1CrossDomainMessenger](https://github.com/ethereum-optimism/optimism/blob/9001eef4784dc2950d0bdcda29752cb2939bae2b/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol) which instructs the sequencer to asynchroneously relay a cross-chain message on L2. This will involve a call to `finalizeBridgeERC20()` on `L2TokenBridge`, which mints an equivalent amount of L2 tokens for Alice (or `to`).

### L2 to L1 withdrawals

To withdraw her tokens back to L1, Alice calls `bridgeERC20[To]()` on the `L2TokenBridge`. This call burns Alice's tokens and calls the [L2CrossDomainMessenger](https://github.com/ethereum-optimism/optimism/blob/9001eef4784dc2950d0bdcda29752cb2939bae2b/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol), which will eventually (after the ~7 days security period) allow the permissionless finalization of the withdrawal on L1. This will involve a call to `finalizeBridgeERC20()` on the `L1TokenBridge`, which releases an equivalent amount of L1 tokens from the `Escrow` to Alice (or `to`).

## Upgrades

### Upgrade the bridge implementation(s)

`L1TokenBridge` and/or `L2TokenBridge` implementations can be upgraded by calling the `upgradeToAndCall` function of their inherited `UUPSUpgradeable` parent. Special care must be taken to ensure any deposit or withdrawal that is in transit at the time of the upgrade will still be able to get confirmed on the destination side.

### Upgrade to a new bridge (and deprecate this bridge)

As an alternative upgrade mechanism, a new bridge can be deployed to be used with the escrow.

1. Deploy the new token bridge and connect it to the same escrow as the one used by this bridge. The old and new bridges can operate in parallel.
2. Optionally, deprecate the old bridge by closing it. This involves calling `close()` on both the `L1TokenBridge` and `L2TokenBridge` so that no new outbound message can be sent to the other side of the bridge. After all cross-chain messages are done processing (can take ~1 week), the bridge is effectively closed and governance can consider revoking the approval to transfer funds from the escrow on L1 and the token minting rights on L2.

### Upgrade a single token to a new bridge

To migrate a single token to a new bridge, follow the steps below:

1. Deploy the new token bridge and connect it to the same escrow as the one used by this bridge.
2. Unregister the token on both `L1TokenBridge` and `L2TokenBridge`, so that no new outbound message can be sent to the other side of the bridge for that token.
oldchili marked this conversation as resolved.
Show resolved Hide resolved

## Tests

### OZ upgradeability validations

The OZ validations can be run alongside the existing tests:
`VALIDATE=true forge test --ffi --build-info --extra-output storageLayout`

## Deployment

### Declare env variables

Add the required env variables listed in `.env.example` to your `.env` file, and run `source .env`.

Make sure to set the `L1` and `L2` env variables according to your desired deployment environment. To deploy the bridge on Base, use the following values:

Mainnet deployment:

```
L1=mainnet
L2=base
```

Testnet deployment:

```
L1=sepolia
L2=base_sepolia
```

### Deploy the bridge

Fill in the required variables into your domain config in `script/input/{chainId}/config.json` by using `base` or `base_sepolia` as an example. Deploy the L1 and L2 tokens (not included in this repo) that must be supported by the bridge then fill in the addresses of these tokens in `script/input/{chainId}/config.json` as two arrays of address strings under the `tokens` key for both the L1 and L2 domains. On testnet, if the `tokens` key is missing for a domain, mock tokens will automatically be deployed for that domain.

The following command deploys the L1 and L2 sides of the bridge:

```
forge script script/Deploy.s.sol:Deploy --slow --multi --broadcast --verify
```

### Initialize the bridge

On mainnet, the bridge should be initialized via the spell process. Importantly, the spell caster should add at least 20% gas on top of the estimated gas limit to account for the possibility of a sudden spike in the amount of gas burned to pay for the L1 to L2 message. On testnet, the bridge initialization can be performed via the following command:

```
forge script script/Init.s.sol:Init --slow --multi --broadcast
```

### Test the deployment

Make sure the L1 deployer account holds at least 10^18 units of the first token listed under `"l1Tokens"` in `script/output/{chainId}/deployed-latest.json`. To perform a test deposit of that token, use the following command (which includes a buffer to the gas estimation per Optimism's [recommendation](https://docs.optimism.io/builders/app-developers/bridging/messaging#for-l1-to-l2-transactions-1) for L1 => L2 transactions).

```
forge script script/Deposit.s.sol:Deposit --slow --multi --broadcast --gas-estimate-multiplier 120
```

To subsequently perform a test withdrawal, use the following command:

```
forge script script/Withdraw.s.sol:Withdraw --slow --multi --broadcast
```

The message can be relayed manually to L1 using the [Superchain Relayer](https://superchainrelayer.xyz/).
Binary file not shown.
Binary file not shown.
Binary file not shown.
14 changes: 14 additions & 0 deletions certora/Escrow.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"files": [
"src/Escrow.sol",
"test/mocks/GemMock.sol"
],
"solc": "solc-0.8.21",
"solc_optimize": "200",
"verify": "Escrow:certora/Escrow.spec",
"rule_sanity": "basic",
"multi_assert_check": true,
"parametric_contracts": ["Escrow"],
"build_cache": true,
"msg": "Escrow"
}
121 changes: 121 additions & 0 deletions certora/Escrow.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Escrow.spec

using GemMock as gem;

methods {
// storage variables
function wards(address) external returns (uint256) envfree;
//
function gem.allowance(address,address) external returns (uint256) envfree;
//
function _.approve(address,uint256) external => DISPATCHER(true);
}

// Verify that each storage layout is only modified in the corresponding functions
rule storageAffected(method f) {
env e;

address anyAddr;

mathint wardsBefore = wards(anyAddr);

calldataarg args;
f(e, args);

mathint wardsAfter = wards(anyAddr);

assert wardsAfter != wardsBefore => f.selector == sig:rely(address).selector || f.selector == sig:deny(address).selector, "Assert 1";
}

// Verify correct storage changes for non reverting rely
rule rely(address usr) {
env e;

address other;
require other != usr;

mathint wardsOtherBefore = wards(other);

rely(e, usr);

mathint wardsUsrAfter = wards(usr);
mathint wardsOtherAfter = wards(other);

assert wardsUsrAfter == 1, "Assert 1";
assert wardsOtherAfter == wardsOtherBefore, "Assert 2";
}

// Verify revert rules on rely
rule rely_revert(address usr) {
env e;

mathint wardsSender = wards(e.msg.sender);

rely@withrevert(e, usr);

bool revert1 = e.msg.value > 0;
bool revert2 = wardsSender != 1;

assert lastReverted <=> revert1 || revert2, "Revert rules failed";
}

// Verify correct storage changes for non reverting deny
rule deny(address usr) {
env e;

address other;
require other != usr;

mathint wardsOtherBefore = wards(other);

deny(e, usr);

mathint wardsUsrAfter = wards(usr);
mathint wardsOtherAfter = wards(other);

assert wardsUsrAfter == 0, "Assert 1";
assert wardsOtherAfter == wardsOtherBefore, "Assert 2";
}

// Verify revert rules on deny
rule deny_revert(address usr) {
env e;

mathint wardsSender = wards(e.msg.sender);

deny@withrevert(e, usr);

bool revert1 = e.msg.value > 0;
bool revert2 = wardsSender != 1;

assert lastReverted <=> revert1 || revert2, "Revert rules failed";
}

// Verify correct storage changes for non reverting approve
rule approve(address token, address spender, uint256 value) {
env e;

require token == gem;

approve(e, token, spender, value);

mathint allowance = gem.allowance(currentContract, spender);

assert allowance == to_mathint(value), "Assert 1";
}

// Verify revert rules on approve
rule approve_revert(address token, address spender, uint256 value) {
env e;

require token == gem;

mathint wardsSender = wards(e.msg.sender);

approve@withrevert(e, token, spender, value);

bool revert1 = e.msg.value > 0;
bool revert2 = wardsSender != 1;

assert lastReverted <=> revert1 || revert2, "Revert rules failed";
}
23 changes: 23 additions & 0 deletions certora/L1GovernanceRelay.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"files": [
"src/L1GovernanceRelay.sol",
"certora/harness/Auxiliar.sol",
"test/mocks/MessengerMock.sol",
],
"solc": "solc-0.8.21",
"solc_optimize_map": {
"L1GovernanceRelay": "200",
"Auxiliar": "0",
"MessengerMock": "0"
},
"link": [
"L1GovernanceRelay:messenger=MessengerMock"
],
"verify": "L1GovernanceRelay:certora/L1GovernanceRelay.spec",
"rule_sanity": "basic",
"multi_assert_check": true,
"parametric_contracts": ["L1GovernanceRelay"],
"build_cache": true,
"optimistic_hashing": true,
"msg": "L1GovernanceRelay"
}
Loading
Loading