diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c376036 --- /dev/null +++ b/.env.example @@ -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= diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml new file mode 100644 index 0000000..d5a9e7f --- /dev/null +++ b/.github/workflows/certora.yml @@ -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 }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9282e82..b7cea14 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: test -on: workflow_dispatch +on: [push, pull_request] env: FOUNDRY_PROFILE: ci @@ -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 }} diff --git a/.gitignore b/.gitignore index 85198aa..8c30259 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ docs/ # Dotenv file .env + +# Certora +.certora_internal diff --git a/.gitmodules b/.gitmodules index a2df3f1..69a4d3e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..792cdea --- /dev/null +++ b/Makefile @@ -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,) diff --git a/README.md b/README.md index e69de29..09d2f7f 100644 --- a/README.md +++ b/README.md @@ -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. + +## 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/). diff --git a/audit/20240909-cantina-report-review-makerdao-op-token-bridge.pdf b/audit/20240909-cantina-report-review-makerdao-op-token-bridge.pdf new file mode 100644 index 0000000..28206c4 Binary files /dev/null and b/audit/20240909-cantina-report-review-makerdao-op-token-bridge.pdf differ diff --git a/audit/20241009-ChainSecurity_MakerDAO_OP_Token_Bridge_audit.pdf b/audit/20241009-ChainSecurity_MakerDAO_OP_Token_Bridge_audit.pdf new file mode 100644 index 0000000..2d457ea Binary files /dev/null and b/audit/20241009-ChainSecurity_MakerDAO_OP_Token_Bridge_audit.pdf differ diff --git a/audit/20241023-cantina-report-review-makerdao-op-token-bridge.pdf b/audit/20241023-cantina-report-review-makerdao-op-token-bridge.pdf new file mode 100644 index 0000000..21b456d Binary files /dev/null and b/audit/20241023-cantina-report-review-makerdao-op-token-bridge.pdf differ diff --git a/certora/Escrow.conf b/certora/Escrow.conf new file mode 100644 index 0000000..bfd8c9e --- /dev/null +++ b/certora/Escrow.conf @@ -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" +} diff --git a/certora/Escrow.spec b/certora/Escrow.spec new file mode 100644 index 0000000..8be9bd3 --- /dev/null +++ b/certora/Escrow.spec @@ -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"; +} diff --git a/certora/L1GovernanceRelay.conf b/certora/L1GovernanceRelay.conf new file mode 100644 index 0000000..39e7b1f --- /dev/null +++ b/certora/L1GovernanceRelay.conf @@ -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" +} diff --git a/certora/L1GovernanceRelay.spec b/certora/L1GovernanceRelay.spec new file mode 100644 index 0000000..968bb51 --- /dev/null +++ b/certora/L1GovernanceRelay.spec @@ -0,0 +1,132 @@ +// L1GovernanceRelay.spec + +using Auxiliar as aux; +using MessengerMock as l1messenger; + +methods { + // storage variables + function wards(address) external returns (uint256) envfree; + // immutables + function l2GovernanceRelay() external returns (address) envfree; + function messenger() external returns (address) envfree; + // + function aux.getGovMessageHash(address,bytes) external returns (bytes32) envfree; + function l1messenger.lastTarget() external returns (address) envfree; + function l1messenger.lastMessageHash() external returns (bytes32) envfree; + function l1messenger.lastMinGasLimit() external returns (uint32) envfree; + // + function _.sendMessage(address,bytes,uint32) 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 relay +rule relay(address target, bytes targetData, uint32 minGasLimit) { + env e; + + address l2GovernanceRelay = l2GovernanceRelay(); + + bytes32 message = aux.getGovMessageHash(target, targetData); + + relay(e, target, targetData, minGasLimit); + + address lastTargetAfter = l1messenger.lastTarget(); + bytes32 lastMessageHashAfter = l1messenger.lastMessageHash(); + uint32 lastMinGasLimitAfter = l1messenger.lastMinGasLimit(); + + assert lastTargetAfter == l2GovernanceRelay, "Assert 1"; + assert lastMessageHashAfter == message, "Assert 2"; + assert lastMinGasLimitAfter == minGasLimit, "Assert 3"; +} + +// Verify revert rules on relay +rule relay_revert(address target, bytes targetData, uint32 minGasLimit) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + relay@withrevert(e, target, targetData, minGasLimit); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} diff --git a/certora/L1TokenBridge.conf b/certora/L1TokenBridge.conf new file mode 100644 index 0000000..d0cf7cb --- /dev/null +++ b/certora/L1TokenBridge.conf @@ -0,0 +1,31 @@ +{ + "files": [ + "src/L1TokenBridge.sol", + "certora/harness/Auxiliar.sol", + "test/mocks/MessengerMock.sol", + "test/mocks/GemMock.sol", + "certora/harness/ImplementationMock.sol" + ], + "solc": "solc-0.8.21", + "solc_optimize_map": { + "L1TokenBridge": "200", + "Auxiliar": "0", + "MessengerMock": "0", + "GemMock": "0", + "ImplementationMock": "0" + }, + "link": [ + "L1TokenBridge:messenger=MessengerMock" + ], + "verify": "L1TokenBridge:certora/L1TokenBridge.spec", + "rule_sanity": "basic", + "multi_assert_check": true, + "parametric_contracts": ["L1TokenBridge"], + "build_cache": true, + "optimistic_hashing": true, + "hashing_length_bound": "512", + "prover_args": [ + "-enableStorageSplitting false" + ], + "msg": "L1TokenBridge" +} diff --git a/certora/L1TokenBridge.spec b/certora/L1TokenBridge.spec new file mode 100644 index 0000000..a69eb9a --- /dev/null +++ b/certora/L1TokenBridge.spec @@ -0,0 +1,487 @@ +// L1TokenBridge.spec + +using Auxiliar as aux; +using MessengerMock as l1messenger; +using GemMock as gem; + +methods { + // storage variables + function wards(address) external returns (uint256) envfree; + function l1ToL2Token(address) external returns (address) envfree; + function isOpen() external returns (uint256) envfree; + function escrow() external returns (address) envfree; + // immutables + function otherBridge() external returns (address) envfree; + function messenger() external returns (address) envfree; + // getter + function getImplementation() external returns (address) envfree; + // + function gem.allowance(address,address) external returns (uint256) envfree; + function gem.totalSupply() external returns (uint256) envfree; + function gem.balanceOf(address) external returns (uint256) envfree; + function aux.getBridgeMessageHash(address,address,address,address,uint256,bytes) external returns (bytes32) envfree; + function l1messenger.xDomainMessageSender() external returns (address) envfree; + function l1messenger.lastTarget() external returns (address) envfree; + function l1messenger.lastMessageHash() external returns (bytes32) envfree; + function l1messenger.lastMinGasLimit() external returns (uint32) envfree; + // + function _.proxiableUUID() external => DISPATCHER(true); + function _.transferFrom(address,address,uint256) external => DISPATCHER(true); +} + +definition INITIALIZABLE_STORAGE() returns uint256 = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; +definition IMPLEMENTATION_SLOT() returns uint256 = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + +persistent ghost bool firstRead; +persistent ghost mathint initializedBefore; +persistent ghost bool initializingBefore; +persistent ghost mathint initializedAfter; +persistent ghost bool initializingAfter; +hook ALL_SLOAD(uint256 slot) uint256 val { + if (slot == INITIALIZABLE_STORAGE() && firstRead) { + firstRead = false; + initializedBefore = val % (max_uint64 + 1); + initializingBefore = (val / 2^64) % (max_uint8 + 1) != 0; + } else if (slot == INITIALIZABLE_STORAGE()) { + initializedAfter = val % (max_uint64 + 1); + initializingAfter = (val / 2^64) % (max_uint8 + 1) != 0; + } +} +hook ALL_SSTORE(uint256 slot, uint256 val) { + if (slot == INITIALIZABLE_STORAGE()) { + initializedAfter = val % (max_uint64 + 1); + initializingAfter = (val / 2^64) % (max_uint8 + 1) != 0; + } +} + +// Verify no more entry points exist +rule entryPoints(method f) filtered { f -> !f.isView } { + env e; + + calldataarg args; + f(e, args); + + assert f.selector == sig:initialize().selector || + f.selector == sig:upgradeToAndCall(address,bytes).selector || + f.selector == sig:rely(address).selector || + f.selector == sig:deny(address).selector || + f.selector == sig:file(bytes32,address).selector || + f.selector == sig:close().selector || + f.selector == sig:registerToken(address,address).selector || + f.selector == sig:bridgeERC20(address,address,uint256,uint32,bytes).selector || + f.selector == sig:bridgeERC20To(address,address,address,uint256,uint32,bytes).selector || + f.selector == sig:finalizeBridgeERC20(address,address,address,address,uint256,bytes).selector; +} + +// Verify that each storage layout is only modified in the corresponding functions +rule storageAffected(method f) filtered { f -> f.selector != sig:upgradeToAndCall(address, bytes).selector } { + env e; + + address anyAddr; + + initializedAfter = initializedBefore; + + mathint wardsBefore = wards(anyAddr); + address l1ToL2TokenBefore = l1ToL2Token(anyAddr); + mathint isOpenBefore = isOpen(); + address escrowBefore = escrow(); + + calldataarg args; + f(e, args); + + mathint wardsAfter = wards(anyAddr); + address l1ToL2TokenAfter = l1ToL2Token(anyAddr); + mathint isOpenAfter = isOpen(); + address escrowAfter = escrow(); + + assert initializedAfter != initializedBefore => f.selector == sig:initialize().selector, "Assert 1"; + assert wardsAfter != wardsBefore => f.selector == sig:rely(address).selector || f.selector == sig:deny(address).selector || f.selector == sig:initialize().selector, "Assert 2"; + assert l1ToL2TokenAfter != l1ToL2TokenBefore => f.selector == sig:registerToken(address,address).selector, "Assert 3"; + assert isOpenAfter != isOpenBefore => f.selector == sig:close().selector || f.selector == sig:initialize().selector, "Assert 4"; + assert escrowAfter != escrowBefore => f.selector == sig:file(bytes32,address).selector, "Assert 5"; +} + +// Verify correct storage changes for non reverting initialize +rule initialize() { + env e; + + address other; + require other != e.msg.sender; + + mathint wardsOtherBefore = wards(other); + + initialize(e); + + mathint wardsSenderAfter = wards(e.msg.sender); + mathint wardsOtherAfter = wards(other); + mathint isOpenAfter = isOpen(); + + assert initializedAfter == 1, "Assert 1"; + assert !initializingAfter, "Assert 2"; + assert wardsSenderAfter == 1, "Assert 3"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 4"; + assert isOpenAfter == 1, "Assert 5"; +} + +// Verify revert rules on initialize +rule initialize_revert() { + env e; + + firstRead = true; + mathint bridgeCodeSize = nativeCodesize[currentContract]; // This should actually be always > 0 + + initialize@withrevert(e); + + bool initialSetup = initializedBefore == 0 && !initializingBefore; + bool construction = initializedBefore == 1 && bridgeCodeSize == 0; + + bool revert1 = e.msg.value > 0; + bool revert2 = !initialSetup && !construction; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting initialize +rule upgradeToAndCall(address newImplementation, bytes data) { + env e; + + require data.length == 0; // Avoid evaluating the delegatecCall part + + upgradeToAndCall(e, newImplementation, data); + + address implementationAfter = getImplementation(); + + assert implementationAfter == newImplementation, "Assert 1"; +} + +// Verify revert rules on upgradeToAndCall +rule upgradeToAndCall_revert(address newImplementation, bytes data) { + env e; + + require data.length == 0; // Avoid evaluating the delegatecCall part + + address self = currentContract.__self; + address implementation = getImplementation(); + mathint wardsSender = wards(e.msg.sender); + bytes32 newImplementationProxiableUUID = newImplementation.proxiableUUID(e); + + upgradeToAndCall@withrevert(e, newImplementation, data); + + bool revert1 = e.msg.value > 0; + bool revert2 = self == currentContract || implementation != self; + bool revert3 = wardsSender != 1; + bool revert4 = newImplementationProxiableUUID != to_bytes32(IMPLEMENTATION_SLOT()); + + assert lastReverted <=> revert1 || revert2 || revert3 || + revert4, "Revert rules failed"; +} + +// 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 file +rule file(bytes32 what, address data) { + env e; + + file(e, what, data); + + address escrowAfter = escrow(); + + assert escrowAfter == data, "Assert 1"; +} + +// Verify revert rules on file +rule file_revert(bytes32 what, address data) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + file@withrevert(e, what, data); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + bool revert3 = what != to_bytes32(0x657363726f770000000000000000000000000000000000000000000000000000); + + assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting close +rule close() { + env e; + + close(e); + + mathint isOpenAfter = isOpen(); + + assert isOpenAfter == 0, "Assert 1"; +} + +// Verify revert rules on close +rule close_revert() { + env e; + + mathint wardsSender = wards(e.msg.sender); + + close@withrevert(e); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting registerToken +rule registerToken(address l1Token, address l2Token) { + env e; + + registerToken(e, l1Token, l2Token); + + address l1ToL2TokenAfter = l1ToL2Token(l1Token); + + assert l1ToL2TokenAfter == l2Token, "Assert 1"; +} + +// Verify revert rules on registerToken +rule registerToken_revert(address l1Token, address l2Token) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + registerToken@withrevert(e, l1Token, l2Token); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting bridgeERC20 +rule bridgeERC20(address _localToken, address _remoteToken, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + require _localToken == gem; + + address otherBridge = otherBridge(); + address escrow = escrow(); + require e.msg.sender != escrow; + + bytes32 message = aux.getBridgeMessageHash(_localToken, _remoteToken, e.msg.sender, e.msg.sender, _amount, _extraData); + mathint localTokenBalanceOfSenderBefore = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrowBefore = gem.balanceOf(escrow); + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfSenderBefore + localTokenBalanceOfEscrowBefore; + + bridgeERC20(e, _localToken, _remoteToken, _amount, _minGasLimit, _extraData); + + address lastTargetAfter = l1messenger.lastTarget(); + bytes32 lastMessageHashAfter = l1messenger.lastMessageHash(); + uint32 lastMinGasLimitAfter = l1messenger.lastMinGasLimit(); + mathint localTokenBalanceOfSenderAfter = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrowAfter = gem.balanceOf(escrow); + + assert lastTargetAfter == otherBridge, "Assert 1"; + assert lastMessageHashAfter == message, "Assert 2"; + assert lastMinGasLimitAfter == _minGasLimit, "Assert 3"; + assert localTokenBalanceOfSenderAfter == localTokenBalanceOfSenderBefore - _amount, "Assert 4"; + assert localTokenBalanceOfEscrowAfter == localTokenBalanceOfEscrowBefore + _amount, "Assert 5"; +} + +// Verify revert rules on bridgeERC20 +rule bridgeERC20_revert(address _localToken, address _remoteToken, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + mathint isOpen = isOpen(); + address l1ToL2TokenLocalToken = l1ToL2Token(_localToken); + + address escrow = escrow(); + + mathint localTokenBalanceOfSender = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrow = gem.balanceOf(escrow); + + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfSender + localTokenBalanceOfEscrow; + // User assumptions + require localTokenBalanceOfSender >= _amount; + require gem.allowance(e.msg.sender, currentContract) >= _amount; + + bridgeERC20@withrevert(e, _localToken, _remoteToken, _amount, _minGasLimit, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = nativeCodesize[e.msg.sender] != 0; + bool revert3 = isOpen != 1; + bool revert4 = _remoteToken == 0 || l1ToL2TokenLocalToken != _remoteToken; + + assert lastReverted <=> revert1 || revert2 || revert3 || + revert4, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting bridgeERC20To +rule bridgeERC20To(address _localToken, address _remoteToken, address _to, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + require _localToken == gem; + + address otherBridge = otherBridge(); + address escrow = escrow(); + require e.msg.sender != escrow; + + bytes32 message = aux.getBridgeMessageHash(_localToken, _remoteToken, e.msg.sender, _to, _amount, _extraData); + mathint localTokenBalanceOfSenderBefore = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrowBefore = gem.balanceOf(escrow); + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfSenderBefore + localTokenBalanceOfEscrowBefore; + + bridgeERC20To(e, _localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); + + address lastTargetAfter = l1messenger.lastTarget(); + bytes32 lastMessageHashAfter = l1messenger.lastMessageHash(); + uint32 lastMinGasLimitAfter = l1messenger.lastMinGasLimit(); + mathint localTokenBalanceOfSenderAfter = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrowAfter = gem.balanceOf(escrow); + + assert lastTargetAfter == otherBridge, "Assert 1"; + assert lastMessageHashAfter == message, "Assert 2"; + assert lastMinGasLimitAfter == _minGasLimit, "Assert 3"; + assert localTokenBalanceOfSenderAfter == localTokenBalanceOfSenderBefore - _amount, "Assert 4"; + assert localTokenBalanceOfEscrowAfter == localTokenBalanceOfEscrowBefore + _amount, "Assert 5"; +} + +// Verify revert rules on bridgeERC20To +rule bridgeERC20To_revert(address _localToken, address _remoteToken, address _to, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + mathint isOpen = isOpen(); + address l1ToL2TokenLocalToken = l1ToL2Token(_localToken); + + address escrow = escrow(); + + mathint localTokenBalanceOfSender = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrow = gem.balanceOf(escrow); + + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfSender + localTokenBalanceOfEscrow; + // User assumptions + require localTokenBalanceOfSender >= _amount; + require gem.allowance(e.msg.sender, currentContract) >= _amount; + + bridgeERC20To@withrevert(e, _localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = isOpen != 1; + bool revert3 = _remoteToken == 0 || l1ToL2TokenLocalToken != _remoteToken; + + assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting finalizeBridgeERC20 +rule finalizeBridgeERC20(address _localToken, address _remoteToken, address _from, address _to, uint256 _amount, bytes _extraData) { + env e; + + require _localToken == gem; + + address escrow = escrow(); + + mathint localTokenBalanceOfEscrowBefore = gem.balanceOf(escrow); + mathint localTokenBalanceOfToBefore = gem.balanceOf(_to); + + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfEscrowBefore + localTokenBalanceOfToBefore; + + finalizeBridgeERC20(e, _localToken, _remoteToken, _from, _to, _amount, _extraData); + + mathint localTokenBalanceOfEscrowAfter = gem.balanceOf(escrow); + mathint localTokenBalanceOfToAfter = gem.balanceOf(_to); + + assert escrow != _to => localTokenBalanceOfEscrowAfter == localTokenBalanceOfEscrowBefore - _amount, "Assert 1"; + assert escrow != _to => localTokenBalanceOfToAfter == localTokenBalanceOfToBefore + _amount, "Assert 2"; + assert escrow == _to => localTokenBalanceOfEscrowAfter == localTokenBalanceOfEscrowBefore, "Assert 3"; +} + +// Verify revert rules on finalizeBridgeERC20 +rule finalizeBridgeERC20_revert(address _localToken, address _remoteToken, address _from, address _to, uint256 _amount, bytes _extraData) { + env e; + + require _localToken == gem; + + address messenger = messenger(); + address otherBridge = otherBridge(); + address xDomainMessageSender = l1messenger.xDomainMessageSender(); + address escrow = escrow(); + + mathint localTokenBalanceOfEscrow = gem.balanceOf(escrow); + + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfEscrow + gem.balanceOf(_to); + // Bridge assumption + require localTokenBalanceOfEscrow >= _amount; + // Set up assumption + require gem.allowance(escrow, currentContract) == max_uint256; + + finalizeBridgeERC20@withrevert(e, _localToken, _remoteToken, _from, _to, _amount, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.msg.sender != messenger || xDomainMessageSender != otherBridge; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} diff --git a/certora/L2GovernanceRelay.conf b/certora/L2GovernanceRelay.conf new file mode 100644 index 0000000..fa08736 --- /dev/null +++ b/certora/L2GovernanceRelay.conf @@ -0,0 +1,21 @@ +{ + "files": [ + "src/L2GovernanceRelay.sol", + "test/mocks/MessengerMock.sol", + ], + "solc": "solc-0.8.21", + "solc_optimize_map": { + "L2GovernanceRelay": "200", + "MessengerMock": "0" + }, + "link": [ + "L2GovernanceRelay:messenger=MessengerMock" + ], + "verify": "L2GovernanceRelay:certora/L2GovernanceRelay.spec", + "rule_sanity": "basic", + "multi_assert_check": true, + "parametric_contracts": ["L2GovernanceRelay"], + "build_cache": true, + "optimistic_hashing": true, + "msg": "L2GovernanceRelay" +} diff --git a/certora/L2GovernanceRelay.spec b/certora/L2GovernanceRelay.spec new file mode 100644 index 0000000..725971d --- /dev/null +++ b/certora/L2GovernanceRelay.spec @@ -0,0 +1,51 @@ +// L2GovernanceRelay.spec + +using MessengerMock as l2messenger; + +methods { + // immutables + function l1GovernanceRelay() external returns (address) envfree; + function messenger() external returns (address) envfree; + // + function l2messenger.xDomainMessageSender() external returns (address) envfree; +} + +persistent ghost bool called; +persistent ghost address calledAddr; +persistent ghost mathint dataLength; +persistent ghost bool success; +hook DELEGATECALL(uint256 g, address addr, uint256 argsOffset, uint256 argsLength, uint256 retOffset, uint256 retLength) uint256 rc { + called = true; + calledAddr = addr; + dataLength = argsLength; + success = rc != 0; +} + +// Verify correct storage changes for non reverting relay +rule relay(address target, bytes targetData) { + env e; + + relay(e, target, targetData); + + assert called, "Assert 1"; + assert calledAddr == target, "Assert 2"; + assert dataLength == targetData.length, "Assert 3"; + assert success, "Assert 4"; +} + +// Verify revert rules on relay +rule relay_revert(address target, bytes targetData) { + env e; + + address l1GovernanceRelay = l1GovernanceRelay(); + address messenger = messenger(); + address xDomainMessageSender = l2messenger.xDomainMessageSender(); + + relay@withrevert(e, target, targetData); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.msg.sender != messenger || xDomainMessageSender != l1GovernanceRelay; + bool revert3 = !success; + + assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed"; +} diff --git a/certora/L2TokenBridge.conf b/certora/L2TokenBridge.conf new file mode 100644 index 0000000..f0db0b9 --- /dev/null +++ b/certora/L2TokenBridge.conf @@ -0,0 +1,31 @@ +{ + "files": [ + "src/L2TokenBridge.sol", + "certora/harness/Auxiliar.sol", + "test/mocks/MessengerMock.sol", + "test/mocks/GemMock.sol", + "certora/harness/ImplementationMock.sol" + ], + "solc": "solc-0.8.21", + "solc_optimize_map": { + "L2TokenBridge": "200", + "Auxiliar": "0", + "MessengerMock": "0", + "GemMock": "0", + "ImplementationMock": "0" + }, + "link": [ + "L2TokenBridge:messenger=MessengerMock" + ], + "verify": "L2TokenBridge:certora/L2TokenBridge.spec", + "rule_sanity": "basic", + "multi_assert_check": true, + "parametric_contracts": ["L2TokenBridge"], + "build_cache": true, + "optimistic_hashing": true, + "hashing_length_bound": "512", + "prover_args": [ + "-enableStorageSplitting false" + ], + "msg": "L2TokenBridge" +} diff --git a/certora/L2TokenBridge.spec b/certora/L2TokenBridge.spec new file mode 100644 index 0000000..5d6eb93 --- /dev/null +++ b/certora/L2TokenBridge.spec @@ -0,0 +1,478 @@ +// L2TokenBridge.spec + +using Auxiliar as aux; +using MessengerMock as l2messenger; +using GemMock as gem; + +methods { + // storage variables + function wards(address) external returns (uint256) envfree; + function l1ToL2Token(address) external returns (address) envfree; + function maxWithdraws(address) external returns (uint256) envfree; + function isOpen() external returns (uint256) envfree; + // immutables + function otherBridge() external returns (address) envfree; + function messenger() external returns (address) envfree; + // getter + function getImplementation() external returns (address) envfree; + // + function gem.wards(address) external returns (uint256) envfree; + function gem.allowance(address,address) external returns (uint256) envfree; + function gem.totalSupply() external returns (uint256) envfree; + function gem.balanceOf(address) external returns (uint256) envfree; + function aux.getBridgeMessageHash(address,address,address,address,uint256,bytes) external returns (bytes32) envfree; + function l2messenger.xDomainMessageSender() external returns (address) envfree; + function l2messenger.lastTarget() external returns (address) envfree; + function l2messenger.lastMessageHash() external returns (bytes32) envfree; + function l2messenger.lastMinGasLimit() external returns (uint32) envfree; + // + function _.proxiableUUID() external => DISPATCHER(true); + function _.burn(address,uint256) external => DISPATCHER(true); + function _.mint(address,uint256) external => DISPATCHER(true); +} + +definition INITIALIZABLE_STORAGE() returns uint256 = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; +definition IMPLEMENTATION_SLOT() returns uint256 = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + +persistent ghost bool firstRead; +persistent ghost mathint initializedBefore; +persistent ghost bool initializingBefore; +persistent ghost mathint initializedAfter; +persistent ghost bool initializingAfter; +hook ALL_SLOAD(uint256 slot) uint256 val { + if (slot == INITIALIZABLE_STORAGE() && firstRead) { + firstRead = false; + initializedBefore = val % (max_uint64 + 1); + initializingBefore = (val / 2^64) % (max_uint8 + 1) != 0; + } else if (slot == INITIALIZABLE_STORAGE()) { + initializedAfter = val % (max_uint64 + 1); + initializingAfter = (val / 2^64) % (max_uint8 + 1) != 0; + } +} +hook ALL_SSTORE(uint256 slot, uint256 val) { + if (slot == INITIALIZABLE_STORAGE()) { + initializedAfter = val % (max_uint64 + 1); + initializingAfter = (val / 2^64) % (max_uint8 + 1) != 0; + } +} + +// Verify no more entry points exist +rule entryPoints(method f) filtered { f -> !f.isView } { + env e; + + calldataarg args; + f(e, args); + + assert f.selector == sig:initialize().selector || + f.selector == sig:upgradeToAndCall(address,bytes).selector || + f.selector == sig:rely(address).selector || + f.selector == sig:deny(address).selector || + f.selector == sig:close().selector || + f.selector == sig:registerToken(address,address).selector || + f.selector == sig:setMaxWithdraw(address,uint256).selector || + f.selector == sig:bridgeERC20(address,address,uint256,uint32,bytes).selector || + f.selector == sig:bridgeERC20To(address,address,address,uint256,uint32,bytes).selector || + f.selector == sig:finalizeBridgeERC20(address,address,address,address,uint256,bytes).selector; +} + +// Verify that each storage layout is only modified in the corresponding functions +rule storageAffected(method f) filtered { f -> f.selector != sig:upgradeToAndCall(address, bytes).selector } { + env e; + + address anyAddr; + + initializedAfter = initializedBefore; + + mathint wardsBefore = wards(anyAddr); + address l1ToL2TokenBefore = l1ToL2Token(anyAddr); + mathint maxWithdrawsBefore = maxWithdraws(anyAddr); + mathint isOpenBefore = isOpen(); + + calldataarg args; + f(e, args); + + mathint wardsAfter = wards(anyAddr); + address l1ToL2TokenAfter = l1ToL2Token(anyAddr); + mathint maxWithdrawsAfter = maxWithdraws(anyAddr); + mathint isOpenAfter = isOpen(); + + assert initializedAfter != initializedBefore => f.selector == sig:initialize().selector, "Assert 1"; + assert wardsAfter != wardsBefore => f.selector == sig:rely(address).selector || f.selector == sig:deny(address).selector || f.selector == sig:initialize().selector, "Assert 2"; + assert l1ToL2TokenAfter != l1ToL2TokenBefore => f.selector == sig:registerToken(address,address).selector, "Assert 3"; + assert maxWithdrawsAfter != maxWithdrawsBefore => f.selector == sig:setMaxWithdraw(address,uint256).selector, "Assert 4"; + assert isOpenAfter != isOpenBefore => f.selector == sig:close().selector || f.selector == sig:initialize().selector, "Assert 5"; +} + +// Verify correct storage changes for non reverting initialize +rule initialize() { + env e; + + address other; + require other != e.msg.sender; + + mathint wardsOtherBefore = wards(other); + + initialize(e); + + mathint wardsSenderAfter = wards(e.msg.sender); + mathint wardsOtherAfter = wards(other); + mathint isOpenAfter = isOpen(); + + assert initializedAfter == 1, "Assert 1"; + assert !initializingAfter, "Assert 2"; + assert wardsSenderAfter == 1, "Assert 3"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 4"; + assert isOpenAfter == 1, "Assert 5"; +} + +// Verify revert rules on initialize +rule initialize_revert() { + env e; + + firstRead = true; + mathint bridgeCodeSize = nativeCodesize[currentContract]; // This should actually be always > 0 + + initialize@withrevert(e); + + bool initialSetup = initializedBefore == 0 && !initializingBefore; + bool construction = initializedBefore == 1 && bridgeCodeSize == 0; + + bool revert1 = e.msg.value > 0; + bool revert2 = !initialSetup && !construction; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting initialize +rule upgradeToAndCall(address newImplementation, bytes data) { + env e; + + require data.length == 0; // Avoid evaluating the delegatecCall part + + upgradeToAndCall(e, newImplementation, data); + + address implementationAfter = getImplementation(); + + assert implementationAfter == newImplementation, "Assert 1"; +} + +// Verify revert rules on upgradeToAndCall +rule upgradeToAndCall_revert(address newImplementation, bytes data) { + env e; + + require data.length == 0; // Avoid evaluating the delegatecCall part + + address self = currentContract.__self; + address implementation = getImplementation(); + mathint wardsSender = wards(e.msg.sender); + bytes32 newImplementationProxiableUUID = newImplementation.proxiableUUID(e); + + upgradeToAndCall@withrevert(e, newImplementation, data); + + bool revert1 = e.msg.value > 0; + bool revert2 = self == currentContract || implementation != self; + bool revert3 = wardsSender != 1; + bool revert4 = newImplementationProxiableUUID != to_bytes32(IMPLEMENTATION_SLOT()); + + assert lastReverted <=> revert1 || revert2 || revert3 || + revert4, "Revert rules failed"; +} + +// 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 close +rule close() { + env e; + + close(e); + + mathint isOpenAfter = isOpen(); + + assert isOpenAfter == 0, "Assert 1"; +} + +// Verify revert rules on close +rule close_revert() { + env e; + + mathint wardsSender = wards(e.msg.sender); + + close@withrevert(e); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting registerToken +rule registerToken(address l1Token, address l2Token) { + env e; + + registerToken(e, l1Token, l2Token); + + address l1ToL2TokenAfter = l1ToL2Token(l1Token); + + assert l1ToL2TokenAfter == l2Token, "Assert 1"; +} + +// Verify revert rules on registerToken +rule registerToken_revert(address l1Token, address l2Token) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + registerToken@withrevert(e, l1Token, l2Token); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting setMaxWithdraw +rule setMaxWithdraw(address l2Token, uint256 maxWithdraw) { + env e; + + setMaxWithdraw(e, l2Token, maxWithdraw); + + mathint maxWithdrawsAfter = maxWithdraws(l2Token); + + assert maxWithdrawsAfter == maxWithdraw, "Assert 1"; +} + +// Verify revert rules on setMaxWithdraw +rule setMaxWithdraw_revert(address l2Token, uint256 maxWithdraw) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + setMaxWithdraw@withrevert(e, l2Token, maxWithdraw); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting bridgeERC20 +rule bridgeERC20(address _localToken, address _remoteToken, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + require _localToken == gem; + + address otherBridge = otherBridge(); + + bytes32 message = aux.getBridgeMessageHash(_localToken, _remoteToken, e.msg.sender, e.msg.sender, _amount, _extraData); + mathint localTokenTotalSupplyBefore = gem.totalSupply(); + mathint localTokenBalanceOfSenderBefore = gem.balanceOf(e.msg.sender); + // ERC20 assumption + require localTokenTotalSupplyBefore >= localTokenBalanceOfSenderBefore; + + bridgeERC20(e, _localToken, _remoteToken, _amount, _minGasLimit, _extraData); + + address lastTargetAfter = l2messenger.lastTarget(); + bytes32 lastMessageHashAfter = l2messenger.lastMessageHash(); + uint32 lastMinGasLimitAfter = l2messenger.lastMinGasLimit(); + mathint localTokenTotalSupplyAfter = gem.totalSupply(); + mathint localTokenBalanceOfSenderAfter = gem.balanceOf(e.msg.sender); + + assert lastTargetAfter == otherBridge, "Assert 1"; + assert lastMessageHashAfter == message, "Assert 2"; + assert lastMinGasLimitAfter == _minGasLimit, "Assert 3"; + assert localTokenTotalSupplyAfter == localTokenTotalSupplyBefore - _amount, "Assert 4"; + assert localTokenBalanceOfSenderAfter == localTokenBalanceOfSenderBefore - _amount, "Assert 5"; +} + +// Verify revert rules on bridgeERC20 +rule bridgeERC20_revert(address _localToken, address _remoteToken, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + mathint isOpen = isOpen(); + address l1ToL2TokenRemoteToken = l1ToL2Token(_remoteToken); + mathint maxWithdrawsLocatOken = maxWithdraws(_localToken); + + mathint localTokenTotalSupply = gem.totalSupply(); + mathint localTokenBalanceOfSender = gem.balanceOf(e.msg.sender); + // ERC20 assumption + require localTokenTotalSupply >= localTokenBalanceOfSender; + // User assumptions + require localTokenBalanceOfSender >= _amount; + require gem.allowance(e.msg.sender, currentContract) >= _amount; + + bridgeERC20@withrevert(e, _localToken, _remoteToken, _amount, _minGasLimit, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = nativeCodesize[e.msg.sender] != 0; + bool revert3 = isOpen != 1; + bool revert4 = _localToken == 0 || l1ToL2TokenRemoteToken != _localToken; + bool revert5 = _amount > maxWithdrawsLocatOken; + + assert lastReverted <=> revert1 || revert2 || revert3 || + revert4 || revert5, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting bridgeERC20To +rule bridgeERC20To(address _localToken, address _remoteToken, address _to, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + require _localToken == gem; + + address otherBridge = otherBridge(); + + bytes32 message = aux.getBridgeMessageHash(_localToken, _remoteToken, e.msg.sender, _to, _amount, _extraData); + mathint localTokenTotalSupplyBefore = gem.totalSupply(); + mathint localTokenBalanceOfSenderBefore = gem.balanceOf(e.msg.sender); + // ERC20 assumption + require localTokenTotalSupplyBefore >= localTokenBalanceOfSenderBefore; + + bridgeERC20To(e, _localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); + + address lastTargetAfter = l2messenger.lastTarget(); + bytes32 lastMessageHashAfter = l2messenger.lastMessageHash(); + uint32 lastMinGasLimitAfter = l2messenger.lastMinGasLimit(); + mathint localTokenTotalSupplyAfter = gem.totalSupply(); + mathint localTokenBalanceOfSenderAfter = gem.balanceOf(e.msg.sender); + + assert lastTargetAfter == otherBridge, "Assert 1"; + assert lastMessageHashAfter == message, "Assert 2"; + assert lastMinGasLimitAfter == _minGasLimit, "Assert 3"; + assert localTokenTotalSupplyAfter == localTokenTotalSupplyBefore - _amount, "Assert 4"; + assert localTokenBalanceOfSenderAfter == localTokenBalanceOfSenderBefore - _amount, "Assert 5"; +} + +// Verify revert rules on bridgeERC20To +rule bridgeERC20To_revert(address _localToken, address _remoteToken, address _to, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + mathint isOpen = isOpen(); + address l1ToL2TokenRemoteToken = l1ToL2Token(_remoteToken); + mathint maxWithdrawsLocatOken = maxWithdraws(_localToken); + + mathint localTokenTotalSupply = gem.totalSupply(); + mathint localTokenBalanceOfSender = gem.balanceOf(e.msg.sender); + // ERC20 assumption + require localTokenTotalSupply >= localTokenBalanceOfSender; + // User assumptions + require localTokenBalanceOfSender >= _amount; + require gem.allowance(e.msg.sender, currentContract) >= _amount; + + bridgeERC20To@withrevert(e, _localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = isOpen != 1; + bool revert3 = _localToken == 0 || l1ToL2TokenRemoteToken != _localToken; + bool revert4 = _amount > maxWithdrawsLocatOken; + + assert lastReverted <=> revert1 || revert2 || revert3 || + revert4, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting finalizeBridgeERC20 +rule finalizeBridgeERC20(address _localToken, address _remoteToken, address _from, address _to, uint256 _amount, bytes _extraData) { + env e; + + require _localToken == gem; + + mathint localTokenTotalSupplyBefore = gem.totalSupply(); + mathint localTokenBalanceOfToBefore = gem.balanceOf(_to); + // ERC20 assumption + require localTokenTotalSupplyBefore >= localTokenBalanceOfToBefore; + + finalizeBridgeERC20(e, _localToken, _remoteToken, _from, _to, _amount, _extraData); + + mathint localTokenTotalSupplyAfter = gem.totalSupply(); + mathint localTokenBalanceOfToAfter = gem.balanceOf(_to); + + assert localTokenTotalSupplyAfter == localTokenTotalSupplyBefore + _amount, "Assert 1"; + assert localTokenBalanceOfToAfter == localTokenBalanceOfToBefore + _amount, "Assert 2"; +} + +// Verify revert rules on finalizeBridgeERC20 +rule finalizeBridgeERC20_revert(address _localToken, address _remoteToken, address _from, address _to, uint256 _amount, bytes _extraData) { + env e; + + require _localToken == gem; + + address messenger = messenger(); + address otherBridge = otherBridge(); + address xDomainMessageSender = l2messenger.xDomainMessageSender(); + + mathint localTokenTotalSupply = gem.totalSupply(); + mathint localTokenBalanceOfTo = gem.balanceOf(_to); + // ERC20 assumption + require localTokenTotalSupply >= localTokenBalanceOfTo; + // Practical assumption + require localTokenTotalSupply + _amount <= max_uint256; + // Set up assumption + require gem.wards(currentContract) == 1; + + finalizeBridgeERC20@withrevert(e, _localToken, _remoteToken, _from, _to, _amount, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.msg.sender != messenger || xDomainMessageSender != otherBridge; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} diff --git a/certora/harness/Auxiliar.sol b/certora/harness/Auxiliar.sol new file mode 100644 index 0000000..03ca10d --- /dev/null +++ b/certora/harness/Auxiliar.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.21; + +interface GovernanceRelayLike { + function relay(address, bytes calldata) external; +} + +interface BridgeLike { + function finalizeBridgeERC20(address, address, address, address, uint256, bytes calldata) external; +} + +contract Auxiliar { + function getGovMessageHash(address target, bytes calldata targetData) public pure returns (bytes32) { + return keccak256(abi.encodeCall(GovernanceRelayLike.relay, (target, targetData))); + } + + function getBridgeMessageHash(address token1, address token2, address sender, address to, uint256 amount, bytes calldata extraData) public pure returns (bytes32) { + return keccak256(abi.encodeCall(BridgeLike.finalizeBridgeERC20, ( + token1, + token2, + sender, + to, + amount, + extraData + ))); + } +} diff --git a/certora/harness/ImplementationMock.sol b/certora/harness/ImplementationMock.sol new file mode 100644 index 0000000..ec2e28d --- /dev/null +++ b/certora/harness/ImplementationMock.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.21; + +contract ImplementationMock { + bytes32 public proxiableUUID; +} diff --git a/deploy/L1TokenBridgeInstance.sol b/deploy/L1TokenBridgeInstance.sol new file mode 100644 index 0000000..044a93b --- /dev/null +++ b/deploy/L1TokenBridgeInstance.sol @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.0; + +struct L1TokenBridgeInstance { + address govRelay; + address escrow; + address bridge; + address bridgeImp; +} diff --git a/deploy/L2TokenBridgeInstance.sol b/deploy/L2TokenBridgeInstance.sol new file mode 100644 index 0000000..4305e6d --- /dev/null +++ b/deploy/L2TokenBridgeInstance.sol @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.0; + +struct L2TokenBridgeInstance { + address govRelay; + address bridge; + address bridgeImp; + address spell; +} diff --git a/deploy/L2TokenBridgeSpell.sol b/deploy/L2TokenBridgeSpell.sol new file mode 100644 index 0000000..c3e125f --- /dev/null +++ b/deploy/L2TokenBridgeSpell.sol @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.0; + +interface L2GovRelayLike { + function l1GovernanceRelay() external view returns (address); + function messenger() external view returns (address); +} + +interface L2TokenBridgeLike { + function isOpen() external view returns (uint256); + function otherBridge() external view returns (address); + function messenger() external view returns (address); + function version() external view returns (string memory); + function getImplementation() external view returns (address); + function upgradeToAndCall(address, bytes memory) external; + function rely(address) external; + function deny(address) external; + function close() external; + function registerToken(address, address) external; + function setMaxWithdraw(address, uint256) external; +} + +interface AuthLike { + function rely(address usr) external; +} + +// A reusable L2 spell to be used by the L2GovernanceRelay to exert admin control over L2TokenBridge +contract L2TokenBridgeSpell { + L2TokenBridgeLike public immutable l2Bridge; + + constructor(address l2Bridge_) { + l2Bridge = L2TokenBridgeLike(l2Bridge_); + } + + function upgradeToAndCall(address newImp, bytes memory data) external { l2Bridge.upgradeToAndCall(newImp, data); } + function rely(address usr) external { l2Bridge.rely(usr); } + function deny(address usr) external { l2Bridge.deny(usr); } + function close() external { l2Bridge.close(); } + + function registerTokens(address[] memory l1Tokens, address[] memory l2Tokens) public { + for (uint256 i; i < l2Tokens.length;) { + l2Bridge.registerToken(l1Tokens[i], l2Tokens[i]); + AuthLike(l2Tokens[i]).rely(address(l2Bridge)); + unchecked { ++i; } + } + } + + function setMaxWithdraws(address[] memory l2Tokens, uint256[] memory maxWithdraws) public { + for (uint256 i; i < l2Tokens.length;) { + l2Bridge.setMaxWithdraw(l2Tokens[i], maxWithdraws[i]); + unchecked { ++i; } + } + } + + function init( + address l2GovRelay_, + address l2Bridge_, + address l2BridgeImp, + address l1GovRelay, + address l1Bridge, + address l2Messenger, + address[] calldata l1Tokens, + address[] calldata l2Tokens, + uint256[] calldata maxWithdraws + ) external { + L2GovRelayLike l2GovRelay = L2GovRelayLike(l2GovRelay_); + + // sanity checks + require(address(l2Bridge) == l2Bridge_, "L2TokenBridgeSpell/l2-bridge-mismatch"); + require(keccak256(bytes(l2Bridge.version())) == keccak256("1"), "L2TokenBridgeSpell/version-does-not-match"); + require(l2Bridge.getImplementation() == l2BridgeImp, "L2TokenBridgeSpell/imp-does-not-match"); + require(l2Bridge.isOpen() == 1, "L2TokenBridgeSpell/not-open"); + require(l2Bridge.otherBridge() == l1Bridge, "L2TokenBridgeSpell/other-bridge-mismatch"); + require(l2Bridge.messenger() == l2Messenger, "L2TokenBridgeSpell/l2-bridge-messenger-mismatch"); + require(l2GovRelay.l1GovernanceRelay() == l1GovRelay, "L2TokenBridgeSpell/l1-gov-relay-mismatch"); + require(l2GovRelay.messenger() == l2Messenger, "L2TokenBridgeSpell/l2-gov-relay-messenger-mismatch"); + + registerTokens(l1Tokens, l2Tokens); + setMaxWithdraws(l2Tokens, maxWithdraws); + } +} diff --git a/deploy/TokenBridgeDeploy.sol b/deploy/TokenBridgeDeploy.sol new file mode 100644 index 0000000..80dfcaa --- /dev/null +++ b/deploy/TokenBridgeDeploy.sol @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.0; + +import { ScriptTools } from "dss-test/ScriptTools.sol"; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { L1TokenBridgeInstance } from "./L1TokenBridgeInstance.sol"; +import { L2TokenBridgeInstance } from "./L2TokenBridgeInstance.sol"; +import { L2TokenBridgeSpell } from "./L2TokenBridgeSpell.sol"; +import { L1GovernanceRelay } from "src/L1GovernanceRelay.sol"; +import { L2GovernanceRelay } from "src/L2GovernanceRelay.sol"; +import { Escrow } from "src/Escrow.sol"; +import { L1TokenBridge } from "src/L1TokenBridge.sol"; +import { L2TokenBridge } from "src/L2TokenBridge.sol"; + +library TokenBridgeDeploy { + function deployL1( + address deployer, + address owner, + address l2GovRelay, + address l2Bridge, + address l1Messenger + ) internal returns (L1TokenBridgeInstance memory l1BridgeInstance) { + l1BridgeInstance.govRelay = address(new L1GovernanceRelay(l2GovRelay, l1Messenger)); + l1BridgeInstance.escrow = address(new Escrow()); + l1BridgeInstance.bridgeImp = address(new L1TokenBridge(l2Bridge, l1Messenger)); + l1BridgeInstance.bridge = address(new ERC1967Proxy(l1BridgeInstance.bridgeImp, abi.encodeCall(L1TokenBridge.initialize, ()))); + + ScriptTools.switchOwner(l1BridgeInstance.govRelay, deployer, owner); + ScriptTools.switchOwner(l1BridgeInstance.escrow, deployer, owner); + ScriptTools.switchOwner(l1BridgeInstance.bridge, deployer, owner); + } + + function deployL2( + address deployer, + address l1GovRelay, + address l1Bridge, + address l2Messenger + ) internal returns (L2TokenBridgeInstance memory l2BridgeInstance) { + l2BridgeInstance.govRelay = address(new L2GovernanceRelay(l1GovRelay, l2Messenger)); + l2BridgeInstance.bridgeImp = address(new L2TokenBridge(l1Bridge, l2Messenger)); + l2BridgeInstance.bridge = address(new ERC1967Proxy(l2BridgeInstance.bridgeImp, abi.encodeCall(L2TokenBridge.initialize, ()))); + l2BridgeInstance.spell = address(new L2TokenBridgeSpell(l2BridgeInstance.bridge)); + ScriptTools.switchOwner(l2BridgeInstance.bridge, deployer, l2BridgeInstance.govRelay); + } +} diff --git a/deploy/TokenBridgeInit.sol b/deploy/TokenBridgeInit.sol new file mode 100644 index 0000000..920894c --- /dev/null +++ b/deploy/TokenBridgeInit.sol @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.0; + +import { DssInstance } from "dss-test/MCD.sol"; +import { L1TokenBridgeInstance } from "./L1TokenBridgeInstance.sol"; +import { L2TokenBridgeInstance } from "./L2TokenBridgeInstance.sol"; +import { L2TokenBridgeSpell } from "./L2TokenBridgeSpell.sol"; + +interface L1TokenBridgeLike { + function l1ToL2Token(address) external view returns (address); + function isOpen() external view returns (uint256); + function otherBridge() external view returns (address); + function messenger() external view returns (address); + function version() external view returns (string memory); + function getImplementation() external view returns (address); + function file(bytes32, address) external; + function registerToken(address, address) external; +} + +interface L1RelayLike { + function l2GovernanceRelay() external view returns (address); + function messenger() external view returns (address); + function relay( + address target, + bytes calldata targetData, + uint32 minGasLimit + ) external; +} + +interface EscrowLike { + function approve(address, address, uint256) external; +} + +struct BridgesConfig { + address l1Messenger; + address l2Messenger; + address[] l1Tokens; + address[] l2Tokens; + uint256[] maxWithdraws; + uint32 minGasLimit; + bytes32 govRelayCLKey; + bytes32 escrowCLKey; + bytes32 l1BridgeCLKey; + bytes32 l1BridgeImpCLKey; +} + +library TokenBridgeInit { + function initBridges( + DssInstance memory dss, + L1TokenBridgeInstance memory l1BridgeInstance, + L2TokenBridgeInstance memory l2BridgeInstance, + BridgesConfig memory cfg + ) internal { + L1RelayLike l1GovRelay = L1RelayLike(l1BridgeInstance.govRelay); + EscrowLike escrow = EscrowLike(l1BridgeInstance.escrow); + L1TokenBridgeLike l1Bridge = L1TokenBridgeLike(l1BridgeInstance.bridge); + + // sanity checks + require(keccak256(bytes(l1Bridge.version())) == keccak256("1"), "TokenBridgeInit/version-does-not-match"); + require(l1Bridge.getImplementation() == l1BridgeInstance.bridgeImp, "TokenBridgeInit/imp-does-not-match"); + require(l1Bridge.isOpen() == 1, "TokenBridgeInit/not-open"); + require(l1Bridge.otherBridge() == l2BridgeInstance.bridge, "TokenBridgeInit/other-bridge-mismatch"); + require(l1Bridge.messenger() == cfg.l1Messenger, "TokenBridgeInit/l1-bridge-messenger-mismatch"); + require(l1GovRelay.l2GovernanceRelay() == l2BridgeInstance.govRelay, "TokenBridgeInit/l2-gov-relay-mismatch"); + require(l1GovRelay.messenger() == cfg.l1Messenger, "TokenBridgeInit/l1-gov-relay-messenger-mismatch"); + require(cfg.l1Tokens.length == cfg.l2Tokens.length, "TokenBridgeInit/token-arrays-mismatch"); + require(cfg.maxWithdraws.length == cfg.l2Tokens.length, "TokenBridgeInit/max-withdraws-length-mismatch"); + require(cfg.minGasLimit <= 1_000_000_000, "TokenBridgeInit/min-gas-limit-out-of-bounds"); + + l1Bridge.file("escrow", address(escrow)); + + for (uint256 i; i < cfg.l1Tokens.length; ++i) { + (address l1Token, address l2Token) = (cfg.l1Tokens[i], cfg.l2Tokens[i]); + require(l1Token != address(0), "TokenBridgeInit/invalid-l1-token"); + require(l2Token != address(0), "TokenBridgeInit/invalid-l2-token"); + require(cfg.maxWithdraws[i] > 0, "TokenBridgeInit/max-withdraw-not-set"); + require(l1Bridge.l1ToL2Token(l1Token) == address(0), "TokenBridgeInit/existing-l1-token"); + + l1Bridge.registerToken(l1Token, l2Token); + escrow.approve(l1Token, address(l1Bridge), type(uint256).max); + } + + l1GovRelay.relay({ + target: l2BridgeInstance.spell, + targetData: abi.encodeCall(L2TokenBridgeSpell.init, ( + l2BridgeInstance.govRelay, + l2BridgeInstance.bridge, + l2BridgeInstance.bridgeImp, + address(l1GovRelay), + address(l1Bridge), + cfg.l2Messenger, + cfg.l1Tokens, + cfg.l2Tokens, + cfg.maxWithdraws + )), + minGasLimit: cfg.minGasLimit + }); + + dss.chainlog.setAddress(cfg.govRelayCLKey, address(l1GovRelay)); + dss.chainlog.setAddress(cfg.escrowCLKey, address(escrow)); + dss.chainlog.setAddress(cfg.l1BridgeCLKey, address(l1Bridge)); + dss.chainlog.setAddress(cfg.l1BridgeImpCLKey, l1BridgeInstance.bridgeImp); + } +} diff --git a/deploy/mocks/ChainLog.sol b/deploy/mocks/ChainLog.sol new file mode 100644 index 0000000..0b48909 --- /dev/null +++ b/deploy/mocks/ChainLog.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/// ChainLog.sol - An on-chain governance-managed contract registry + +// Copyright (C) 2020 Maker Ecosystem Growth Holdings, INC. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +/// @title An on-chain governance-managed contract registry +/// @notice Publicly readable data; mutating functions must be called by an authorized user +contract ChainLog { + + event Rely(address usr); + event Deny(address usr); + event UpdateVersion(string version); + event UpdateSha256sum(string sha256sum); + event UpdateIPFS(string ipfs); + event UpdateAddress(bytes32 key, address addr); + event RemoveAddress(bytes32 key); + + // --- Auth --- + mapping (address => uint) public wards; + function rely(address usr) external auth { wards[usr] = 1; emit Rely(usr); } + function deny(address usr) external auth { wards[usr] = 0; emit Deny(usr); } + modifier auth { + require(wards[msg.sender] == 1, "ChainLog/not-authorized"); + _; + } + + struct Location { + uint256 pos; + address addr; + } + mapping (bytes32 => Location) location; + + bytes32[] public keys; + + string public version; + string public sha256sum; + string public ipfs; + + constructor() { + wards[msg.sender] = 1; + setVersion("0.0.0"); + setAddress("CHANGELOG", address(this)); + } + + /// @notice Set the "version" of the current changelog + /// @param _version The version string (optional) + function setVersion(string memory _version) public auth { + version = _version; + emit UpdateVersion(_version); + } + + /// @notice Set the "sha256sum" of some current external changelog + /// @dev designed to store sha256 of changelog.makerdao.com hosted log + /// @param _sha256sum The sha256 sum (optional) + function setSha256sum(string memory _sha256sum) public auth { + sha256sum = _sha256sum; + emit UpdateSha256sum(_sha256sum); + } + + /// @notice Set the IPFS hash of a pinned changelog + /// @dev designed to store IPFS pin hash that can retreive changelog json + /// @param _ipfs The ipfs pin hash of an ipfs hosted log (optional) + function setIPFS(string memory _ipfs) public auth { + ipfs = _ipfs; + emit UpdateIPFS(_ipfs); + } + + /// @notice Set the key-value pair for a changelog item + /// @param _key the changelog key (ex. MCD_VAT) + /// @param _addr the address to the contract + function setAddress(bytes32 _key, address _addr) public auth { + if (count() > 0 && _key == keys[location[_key].pos]) { + location[_key].addr = _addr; // Key exists in keys (update) + } else { + _addAddress(_key, _addr); // Add key to keys array + } + emit UpdateAddress(_key, _addr); + } + + /// @notice Removes the key from the keys list() + /// @dev removes the item from the array but moves the last element to it's place + // WARNING: To save the expense of shifting an array on-chain, + // this will replace the key to be deleted with the last key + // in the array, and can therefore result in keys being out + // of order. Use this only if you intend to reorder the list(), + // otherwise consider using `setAddress("KEY", address(0));` + /// @param _key the key to be removed + function removeAddress(bytes32 _key) public auth { + _removeAddress(_key); + emit RemoveAddress(_key); + } + + /// @notice Returns the number of keys being tracked in the keys array + /// @return the number of keys as uint256 + function count() public view returns (uint256) { + return keys.length; + } + + /// @notice Returns the key and address of an item in the changelog array (for enumeration) + /// @dev _index is 0-indexed to the underlying array + /// @return a tuple containing the key and address associated with that key + function get(uint256 _index) public view returns (bytes32, address) { + return (keys[_index], location[keys[_index]].addr); + } + + /// @notice Returns the list of keys being tracked by the changelog + /// @dev May fail if keys is too large, if so, call count() and iterate with get() + function list() public view returns (bytes32[] memory) { + return keys; + } + + /// @notice Returns the address for a particular key + /// @param _key a bytes32 key (ex. MCD_VAT) + /// @return addr the contract address associated with the key + function getAddress(bytes32 _key) public view returns (address addr) { + addr = location[_key].addr; + require(addr != address(0), "dss-chain-log/invalid-key"); + } + + function _addAddress(bytes32 _key, address _addr) internal { + keys.push(_key); + location[keys[keys.length - 1]] = Location( + keys.length - 1, + _addr + ); + } + + function _removeAddress(bytes32 _key) internal { + uint256 index = location[_key].pos; // Get pos in array + require(keys[index] == _key, "dss-chain-log/invalid-key"); + bytes32 move = keys[keys.length - 1]; // Get last key + keys[index] = move; // Replace + location[move].pos = index; // Update array pos + keys.pop(); // Trim last key + delete location[_key]; // Delete struct data + } +} diff --git a/foundry.toml b/foundry.toml index 25b918f..dda1b1a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,15 @@ src = "src" out = "out" libs = ["lib"] +solc = "0.8.21" +fs_permissions = [ + { access = "read", path = "./script/input/"}, + { access = "read", path = "./out/"}, + { access = "read-write", path = "./script/output/"} +] -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +[etherscan] +mainnet = { key = "${ETHERSCAN_KEY}" } +sepolia = { key = "${ETHERSCAN_KEY}", chain = 11155111 } +base = { key = "${BASESCAN_KEY}", chain = 8453, url = "https://api.basescan.org/api" } +base_sepolia = { key = "${BASESCAN_KEY}", chain = 84532, url = "https://api-sepolia.basescan.org/api" } diff --git a/lib/dss-test b/lib/dss-test index 41066f6..f2a2b2b 160000 --- a/lib/dss-test +++ b/lib/dss-test @@ -1 +1 @@ -Subproject commit 41066f6d18202c61208d8cf09b38532a6f5b0d0a +Subproject commit f2a2b2bbea71921103c5b7cf3cb1d241b957bec7 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..723f8ca --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1 diff --git a/lib/openzeppelin-foundry-upgrades b/lib/openzeppelin-foundry-upgrades new file mode 160000 index 0000000..332bd33 --- /dev/null +++ b/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit 332bd3306242e09520df2685b2edb99ebd7f5d37 diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..7761e9f --- /dev/null +++ b/remappings.txt @@ -0,0 +1,3 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +forge-std/=lib/dss-test/lib/forge-std/src/ \ No newline at end of file diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..7fa4c2c --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import "forge-std/Script.sol"; + +import { ScriptTools } from "dss-test/ScriptTools.sol"; +import { Domain } from "dss-test/domains/Domain.sol"; +import { TokenBridgeDeploy, L1TokenBridgeInstance, L2TokenBridgeInstance } from "deploy/TokenBridgeDeploy.sol"; +import { ChainLog } from "deploy/mocks/ChainLog.sol"; +import { GemMock } from "test/mocks/GemMock.sol"; + +contract Deploy is Script { + address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; + + uint256 l1PrivKey = vm.envUint("L1_PRIVATE_KEY"); + uint256 l2PrivKey = vm.envUint("L2_PRIVATE_KEY"); + address l1Deployer = vm.addr(l1PrivKey); + address l2Deployer = vm.addr(l2PrivKey); + + Domain l1Domain; + Domain l2Domain; + + function run() external { + StdChains.Chain memory l1Chain = getChain(string(vm.envOr("L1", string("mainnet")))); + StdChains.Chain memory l2Chain = getChain(string(vm.envOr("L2", string("base")))); + vm.setEnv("FOUNDRY_ROOT_CHAINID", vm.toString(l1Chain.chainId)); // used by ScriptTools to determine config path + string memory config = ScriptTools.loadConfig("config"); + l1Domain = new Domain(config, l1Chain); + l2Domain = new Domain(config, l2Chain); + + address l1Messenger = l2Domain.readConfigAddress("l1Messenger"); + address l2Messenger = l2Domain.readConfigAddress("l2Messenger"); + + l2Domain.selectFork(); + address l2GovRelay = vm.computeCreateAddress(l2Deployer, vm.getNonce(l2Deployer)); + address l2Bridge = vm.computeCreateAddress(l2Deployer, vm.getNonce(l2Deployer) + 2); + + // Deploy chainlog, L1 gov relay, escrow and L1 bridge + + l1Domain.selectFork(); + ChainLog chainlog; + address owner; + if (LOG.code.length > 0) { + chainlog = ChainLog(LOG); + owner = chainlog.getAddress("MCD_PAUSE_PROXY"); + } else { + vm.startBroadcast(l1PrivKey); + chainlog = new ChainLog(); + vm.stopBroadcast(); + owner = l1Deployer; + } + + vm.startBroadcast(l1PrivKey); + L1TokenBridgeInstance memory l1BridgeInstance = TokenBridgeDeploy.deployL1(l1Deployer, owner, l2GovRelay, l2Bridge, l1Messenger); + vm.stopBroadcast(); + + address l1GovRelay = l1BridgeInstance.govRelay; + address l1Bridge = l1BridgeInstance.bridge; + + // Deploy L2 gov relay, L2 bridge and L2 spell + + l2Domain.selectFork(); + vm.startBroadcast(l2PrivKey); + L2TokenBridgeInstance memory l2BridgeInstance = TokenBridgeDeploy.deployL2(l2Deployer, l1GovRelay, l1Bridge, l2Messenger); + vm.stopBroadcast(); + + require(l2BridgeInstance.govRelay == l2GovRelay, "l2GovRelay address mismatch"); + require(l2BridgeInstance.bridge == l2Bridge, "l2Bridge address mismatch"); + + // Deploy mock tokens + + address[] memory l1Tokens; + address[] memory l2Tokens; + if (LOG.code.length > 0) { + l1Tokens = l1Domain.readConfigAddresses("tokens"); + l2Tokens = l2Domain.readConfigAddresses("tokens"); + } else { + l1Domain.selectFork(); + vm.startBroadcast(l1PrivKey); + if (l1Domain.hasConfigKey("tokens")) { + l1Tokens = l1Domain.readConfigAddresses("tokens"); + } else { + uint256 count = l2Domain.hasConfigKey("tokens") ? l2Domain.readConfigAddresses("tokens").length : 2; + l1Tokens = new address[](count); + for (uint256 i; i < count; ++i) { + l1Tokens[i] = address(new GemMock(1_000_000_000 ether)); + } + } + vm.stopBroadcast(); + + l2Domain.selectFork(); + vm.startBroadcast(l2PrivKey); + if (l2Domain.hasConfigKey("tokens")) { + l2Tokens = l2Domain.readConfigAddresses("tokens"); + } else { + uint256 count = l1Domain.hasConfigKey("tokens") ? l1Domain.readConfigAddresses("tokens").length : 2; + l2Tokens = new address[](count); + for (uint256 i; i < count; ++i) { + l2Tokens[i] = address(new GemMock(0)); + GemMock(l2Tokens[i]).rely(l2GovRelay); + GemMock(l2Tokens[i]).deny(l2Deployer); + } + } + vm.stopBroadcast(); + } + + // Export contract addresses + + ScriptTools.exportContract("deployed", "chainlog", address(chainlog)); + ScriptTools.exportContract("deployed", "owner", owner); + ScriptTools.exportContract("deployed", "l1Messenger", l1Messenger); + ScriptTools.exportContract("deployed", "l2Messenger", l2Messenger); + ScriptTools.exportContract("deployed", "escrow", l1BridgeInstance.escrow); + ScriptTools.exportContract("deployed", "l1GovRelay", l1GovRelay); + ScriptTools.exportContract("deployed", "l2GovRelay", l2GovRelay); + ScriptTools.exportContract("deployed", "l1Bridge", l1Bridge); + ScriptTools.exportContract("deployed", "l1BridgeImp", l1BridgeInstance.bridgeImp); + ScriptTools.exportContract("deployed", "l2Bridge", l2Bridge); + ScriptTools.exportContract("deployed", "l2BridgeImp", l2BridgeInstance.bridgeImp); + ScriptTools.exportContract("deployed", "l2BridgeSpell", l2BridgeInstance.spell); + ScriptTools.exportContracts("deployed", "l1Tokens", l1Tokens); + ScriptTools.exportContracts("deployed", "l2Tokens", l2Tokens); + } +} diff --git a/script/Deposit.s.sol b/script/Deposit.s.sol new file mode 100644 index 0000000..9c72113 --- /dev/null +++ b/script/Deposit.s.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import "forge-std/Script.sol"; + +import { ScriptTools } from "dss-test/ScriptTools.sol"; +import { Domain } from "dss-test/domains/Domain.sol"; + +interface GemLike { + function approve(address, uint256) external; +} + +interface BridgeLike { + function bridgeERC20To( + address _localToken, + address _remoteToken, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) external; +} + +// Test deployment in config.json +contract Deposit is Script { + using stdJson for string; + + uint256 l1PrivKey = vm.envUint("L1_PRIVATE_KEY"); + uint256 l2PrivKey = vm.envUint("L2_PRIVATE_KEY"); + address l2Deployer = vm.addr(l2PrivKey); + + function run() external { + StdChains.Chain memory l1Chain = getChain(string(vm.envOr("L1", string("mainnet")))); + vm.setEnv("FOUNDRY_ROOT_CHAINID", vm.toString(l1Chain.chainId)); // used by ScriptTools to determine config path + string memory config = ScriptTools.loadConfig("config"); + string memory deps = ScriptTools.loadDependencies(); + Domain l1Domain = new Domain(config, l1Chain); + l1Domain.selectFork(); + + address l1Bridge = deps.readAddress(".l1Bridge"); + address l1Token = deps.readAddressArray(".l1Tokens")[0]; + address l2Token = deps.readAddressArray(".l2Tokens")[0]; + uint256 amount = 1 ether; + + vm.startBroadcast(l1PrivKey); + GemLike(l1Token).approve(l1Bridge, type(uint256).max); + BridgeLike(l1Bridge).bridgeERC20To({ + _localToken: l1Token, + _remoteToken: l2Token, + _to: l2Deployer, + _amount: amount, + _minGasLimit: 100_000, + _extraData: "" + }); + vm.stopBroadcast(); + } +} diff --git a/script/Init.s.sol b/script/Init.s.sol new file mode 100644 index 0000000..e519ec9 --- /dev/null +++ b/script/Init.s.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import "forge-std/Script.sol"; + +import { ScriptTools } from "dss-test/ScriptTools.sol"; +import { Domain } from "dss-test/domains/Domain.sol"; +import { MCD, DssInstance } from "dss-test/MCD.sol"; +import { TokenBridgeInit, BridgesConfig } from "deploy/TokenBridgeInit.sol"; +import { L1TokenBridgeInstance } from "deploy/L1TokenBridgeInstance.sol"; +import { L2TokenBridgeInstance } from "deploy/L2TokenBridgeInstance.sol"; +import { L2TokenBridgeSpell } from "deploy/L2TokenBridgeSpell.sol"; +import { L2GovernanceRelay } from "src/L2GovernanceRelay.sol"; + + +contract Init is Script { + using stdJson for string; + + uint256 l1PrivKey = vm.envUint("L1_PRIVATE_KEY"); + + function run() external { + StdChains.Chain memory l1Chain = getChain(string(vm.envOr("L1", string("mainnet")))); + StdChains.Chain memory l2Chain = getChain(string(vm.envOr("L2", string("base")))); + vm.setEnv("FOUNDRY_ROOT_CHAINID", vm.toString(l1Chain.chainId)); // used by ScriptTools to determine config path + string memory config = ScriptTools.loadConfig("config"); + string memory deps = ScriptTools.loadDependencies(); + Domain l1Domain = new Domain(config, l1Chain); + Domain l2Domain = new Domain(config, l2Chain); + l1Domain.selectFork(); + + DssInstance memory dss = MCD.loadFromChainlog(deps.readAddress(".chainlog")); + + BridgesConfig memory cfg; + cfg.l1Messenger = deps.readAddress(".l1Messenger"); + cfg.l2Messenger = deps.readAddress(".l2Messenger"); + cfg.l1Tokens = deps.readAddressArray(".l1Tokens"); + cfg.l2Tokens = deps.readAddressArray(".l2Tokens"); + cfg.maxWithdraws = new uint256[](cfg.l2Tokens.length); + cfg.minGasLimit = 100_000; + cfg.govRelayCLKey = l2Domain.readConfigBytes32FromString("govRelayCLKey"); + cfg.escrowCLKey = l2Domain.readConfigBytes32FromString("escrowCLKey"); + cfg.l1BridgeCLKey = l2Domain.readConfigBytes32FromString("l1BridgeCLKey"); + cfg.l1BridgeImpCLKey = l2Domain.readConfigBytes32FromString("l1BridgeImpCLKey"); + for (uint256 i; i < cfg.maxWithdraws.length; ++i) { + cfg.maxWithdraws[i] = 10_000_000 ether; + } + + L1TokenBridgeInstance memory l1BridgeInstance = L1TokenBridgeInstance({ + govRelay: deps.readAddress(".l1GovRelay"), + escrow: deps.readAddress(".escrow"), + bridge: deps.readAddress(".l1Bridge"), + bridgeImp: deps.readAddress(".l1BridgeImp") + }); + L2TokenBridgeInstance memory l2BridgeInstance = L2TokenBridgeInstance({ + govRelay: deps.readAddress(".l2GovRelay"), + spell: deps.readAddress(".l2BridgeSpell"), + bridge: deps.readAddress(".l2Bridge"), + bridgeImp: deps.readAddress(".l2BridgeImp") + }); + + vm.startBroadcast(l1PrivKey); + TokenBridgeInit.initBridges(dss, l1BridgeInstance, l2BridgeInstance, cfg); + vm.stopBroadcast(); + } +} diff --git a/script/Withdraw.s.sol b/script/Withdraw.s.sol new file mode 100644 index 0000000..8a303b6 --- /dev/null +++ b/script/Withdraw.s.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import "forge-std/Script.sol"; + +import { ScriptTools } from "dss-test/ScriptTools.sol"; +import { Domain } from "dss-test/domains/Domain.sol"; + +interface GemLike { + function approve(address, uint256) external; +} + +interface BridgeLike { + function bridgeERC20To( + address _localToken, + address _remoteToken, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) external; +} + +// Test deployment in config.json +contract Withdraw is Script { + using stdJson for string; + + uint256 l1PrivKey = vm.envUint("L1_PRIVATE_KEY"); + uint256 l2PrivKey = vm.envUint("L2_PRIVATE_KEY"); + address l1Deployer = vm.addr(l1PrivKey); + + function run() external { + StdChains.Chain memory l1Chain = getChain(string(vm.envOr("L1", string("mainnet")))); + StdChains.Chain memory l2Chain = getChain(string(vm.envOr("L2", string("arbitrum_one")))); + vm.setEnv("FOUNDRY_ROOT_CHAINID", vm.toString(l1Chain.chainId)); // used by ScriptTools to determine config path + string memory config = ScriptTools.loadConfig("config"); + string memory deps = ScriptTools.loadDependencies(); + Domain l2Domain = new Domain(config, l2Chain); + l2Domain.selectFork(); + + address l2Bridge = deps.readAddress(".l2Bridge"); + address l1Token = deps.readAddressArray(".l1Tokens")[0]; + address l2Token = deps.readAddressArray(".l2Tokens")[0]; + uint256 amount = 0.01 ether; + + vm.startBroadcast(l2PrivKey); + GemLike(l2Token).approve(l2Bridge, type(uint256).max); + BridgeLike(l2Bridge).bridgeERC20To({ + _localToken: l2Token, + _remoteToken: l1Token, + _to: l1Deployer, + _amount: amount, + _minGasLimit: 100_000, + _extraData: "" + }); + vm.stopBroadcast(); + + // The message can be relayed manually on https://superchainrelayer.xyz/ + } +} diff --git a/script/input/1/config.json b/script/input/1/config.json new file mode 100644 index 0000000..23fb2d7 --- /dev/null +++ b/script/input/1/config.json @@ -0,0 +1,17 @@ +{ + "domains": { + "mainnet": { + "chainlog": "0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F", + "tokens": [] + }, + "base": { + "l1Messenger": "0x866E82a600A1414e583f7F13623F1aC5d58b0Afa", + "l2Messenger": "0x4200000000000000000000000000000000000007", + "tokens": [], + "govRelayCLKey": "BASE_GOV_RELAY", + "escrowCLKey": "BASE_ESCROW", + "l1BridgeCLKey": "BASE_TOKEN_BRIDGE", + "l1BridgeImpCLKey": "BASE_TOKEN_BRIDGE_IMP" + } + } +} diff --git a/script/input/11155111/config.json b/script/input/11155111/config.json new file mode 100644 index 0000000..7e32d11 --- /dev/null +++ b/script/input/11155111/config.json @@ -0,0 +1,18 @@ +{ + "domains": { + "sepolia": {}, + "base_sepolia": { + "l1Messenger": "0xC34855F4De64F1840e5686e64278da901e261f20", + "l2Messenger": "0x4200000000000000000000000000000000000007", + "govRelayCLKey": "BASE_GOV_RELAY", + "escrowCLKey": "BASE_ESCROW", + "l1BridgeCLKey": "BASE_TOKEN_BRIDGE", + "l1BridgeImpCLKey": "BASE_TOKEN_BRIDGE_IMP", + "tokens": [ + "0xa5Af64FA3c30aE30AEDf743C0f20ad6577579301", + "0x04e4EeEeA48E56ac25c1Ae6FbcE264E1D26B96B1", + "0xC856384aD60D8e1Ae7F2dD4e8bd0CcEA763B1409" + ] + } + } +} diff --git a/script/output/1/deployed-latest.json b/script/output/1/deployed-latest.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/script/output/1/deployed-latest.json @@ -0,0 +1 @@ +{} diff --git a/script/output/11155111/deployed-latest.json b/script/output/11155111/deployed-latest.json new file mode 100644 index 0000000..f77324c --- /dev/null +++ b/script/output/11155111/deployed-latest.json @@ -0,0 +1,24 @@ +{ + "chainlog": "0xFc1B1D39d7F3851524F69222e400E994C62A1c5b", + "escrow": "0x36a6A22a50954075740c8aB0BA69206Cf6634c65", + "l1Bridge": "0x01A15Af5320DA92BEE6c0E3F2780ae31278D3088", + "l1BridgeImp": "0x844fb5cda302860A83b6FbDd6c6D7A1Bb7A1B879", + "l1GovRelay": "0xbeA2e637F4Fd85648a14A1bbe2cC39BacB079cdc", + "l1Messenger": "0xC34855F4De64F1840e5686e64278da901e261f20", + "l1Tokens": [ + "0xe65D84A5E7976763e06907BdD88584B7502Fb8d5", + "0x711659B0b058b7D42bE6260A62B69cf9A6e688Bc", + "0x6EdDC740AC580139a0E633CC0c492c2369C32958" + ], + "l2Bridge": "0x61Dace31889855e28373E90EC1e70850004B751f", + "l2BridgeImp": "0xdAB4E8462D01C9bAdF5fea22dB16BA360Cad2bF1", + "l2BridgeSpell": "0x91BBe1dEcf0a4E35733B470A2b77C701403B62C2", + "l2GovRelay": "0x52b88E6Ee3D9Da89172162610a8a59223c3A3eAC", + "l2Messenger": "0x4200000000000000000000000000000000000007", + "l2Tokens": [ + "0xa5Af64FA3c30aE30AEDf743C0f20ad6577579301", + "0x04e4EeEeA48E56ac25c1Ae6FbcE264E1D26B96B1", + "0xC856384aD60D8e1Ae7F2dD4e8bd0CcEA763B1409" + ], + "owner": "0x8aD7ce270a5c53541d8A7be460fC42F31D5D51EB" +} diff --git a/src/Escrow.sol b/src/Escrow.sol new file mode 100644 index 0000000..7edc697 --- /dev/null +++ b/src/Escrow.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024 Dai Foundation +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +interface GemLike { + function approve(address, uint256) external; +} + +// Escrow funds on L1, manage approval rights + +contract Escrow { + // --- storage variables --- + + mapping(address => uint256) public wards; + + // --- events --- + + event Rely(address indexed usr); + event Deny(address indexed usr); + event Approve(address indexed token, address indexed spender, uint256 value); + + // --- modifiers --- + + modifier auth() { + require(wards[msg.sender] == 1, "Escrow/not-authorized"); + _; + } + + // --- constructor --- + + constructor() { + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + // --- administration --- + + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + // --- approve --- + + function approve(address token, address spender, uint256 value) external auth { + GemLike(token).approve(spender, value); + emit Approve(token, spender, value); + } +} diff --git a/src/L1GovernanceRelay.sol b/src/L1GovernanceRelay.sol new file mode 100644 index 0000000..e172a25 --- /dev/null +++ b/src/L1GovernanceRelay.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +interface CrossDomainMessengerLike { + function sendMessage(address _target, bytes calldata _message, uint32 _minGasLimit) external payable; +} + +interface L2GovernanceRelayLike { + function relay(address target, bytes calldata targetData) external; +} + +// Relay a message from L1 to L2GovernanceRelay +contract L1GovernanceRelay { + // --- storage variables --- + + mapping(address => uint256) public wards; + + // --- immutables --- + + address public immutable l2GovernanceRelay; + CrossDomainMessengerLike public immutable messenger; + + // --- events --- + + event Rely(address indexed usr); + event Deny(address indexed usr); + + // --- modifiers --- + + modifier auth() { + require(wards[msg.sender] == 1, "L1GovernanceRelay/not-authorized"); + _; + } + + // --- constructor --- + + constructor( + address _l2GovernanceRelay, + address _l1Messenger + ) { + l2GovernanceRelay = _l2GovernanceRelay; + messenger = CrossDomainMessengerLike(_l1Messenger); + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + // --- administration --- + + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + // --- relay --- + + function relay(address target, bytes calldata targetData, uint32 minGasLimit) external auth { + messenger.sendMessage({ + _target: l2GovernanceRelay, + _message: abi.encodeCall(L2GovernanceRelayLike.relay, (target, targetData)), + _minGasLimit: minGasLimit + }); + } +} diff --git a/src/L1TokenBridge.sol b/src/L1TokenBridge.sol new file mode 100644 index 0000000..49c2756 --- /dev/null +++ b/src/L1TokenBridge.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import { UUPSUpgradeable, ERC1967Utils } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +interface TokenLike { + function transferFrom(address, address, uint256) external; +} + +interface CrossDomainMessengerLike { + function xDomainMessageSender() external view returns (address); + function sendMessage(address _target, bytes calldata _message, uint32 _minGasLimit) external payable; +} + +contract L1TokenBridge is UUPSUpgradeable { + // --- storage variables --- + + mapping(address => uint256) public wards; + mapping(address => address) public l1ToL2Token; + uint256 public isOpen; + address public escrow; + + // --- immutables and const --- + + address public immutable otherBridge; + CrossDomainMessengerLike public immutable messenger; + string public constant version = "1"; + + // --- events --- + + event Rely(address indexed usr); + event Deny(address indexed usr); + event File(bytes32 indexed what, address data); + event Closed(); + event TokenSet(address indexed l1Token, address indexed l2Token); + event ERC20BridgeInitiated( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + event ERC20BridgeFinalized( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + + // --- modifiers --- + + modifier auth() { + require(wards[msg.sender] == 1, "L1TokenBridge/not-authorized"); + _; + } + + modifier onlyOtherBridge() { + require( + msg.sender == address(messenger) && messenger.xDomainMessageSender() == otherBridge, + "L1TokenBridge/not-from-other-bridge" + ); + _; + } + + // --- constructor --- + + constructor( + address _otherBridge, + address _messenger + ) { + _disableInitializers(); // Avoid initializing in the context of the implementation + + otherBridge = _otherBridge; + messenger = CrossDomainMessengerLike(_messenger); + } + + // --- upgradability --- + + function initialize() initializer external { + __UUPSUpgradeable_init(); + + isOpen = 1; + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + function _authorizeUpgrade(address newImplementation) internal override auth {} + + function getImplementation() external view returns (address) { + return ERC1967Utils.getImplementation(); + } + + // --- administration --- + + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + function file(bytes32 what, address data) external auth { + if (what == "escrow") { + escrow = data; + } else revert("L1TokenBridge/file-unrecognized-param"); + emit File(what, data); + } + + function close() external auth { + isOpen = 0; + emit Closed(); + } + + function registerToken(address l1Token, address l2Token) external auth { + l1ToL2Token[l1Token] = l2Token; + emit TokenSet(l1Token, l2Token); + } + + // -- bridging -- + + function _initiateBridgeERC20( + address _localToken, + address _remoteToken, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes memory _extraData + ) internal { + require(isOpen == 1, "L1TokenBridge/closed"); // do not allow initiating new xchain messages if bridge is closed + require(_remoteToken != address(0) && l1ToL2Token[_localToken] == _remoteToken, "L1TokenBridge/invalid-token"); + + TokenLike(_localToken).transferFrom(msg.sender, escrow, _amount); + + messenger.sendMessage({ + _target: address(otherBridge), + _message: abi.encodeCall(this.finalizeBridgeERC20, ( + // Because this call will be executed on the remote chain, we reverse the order of + // the remote and local token addresses relative to their order in the + // finalizeBridgeERC20 function. + _remoteToken, + _localToken, + msg.sender, + _to, + _amount, + _extraData + )), + _minGasLimit: _minGasLimit + }); + + emit ERC20BridgeInitiated(_localToken, _remoteToken, msg.sender, _to, _amount, _extraData); + } + + /// @notice Sends ERC20 tokens to the sender's address on L2. + /// @param _localToken Address of the ERC20 on L1. + /// @param _remoteToken Address of the corresponding token on L2. + /// @param _amount Amount of local tokens to deposit. + /// @param _minGasLimit Minimum amount of gas that the bridge can be relayed with. + /// @param _extraData Extra data to be sent with the transaction. Note that the recipient will + /// not be triggered with this data, but it will be emitted and can be used + /// to identify the transaction. + function bridgeERC20( + address _localToken, + address _remoteToken, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) external { + require(msg.sender.code.length == 0, "L1TokenBridge/sender-not-eoa"); + _initiateBridgeERC20(_localToken, _remoteToken, msg.sender, _amount, _minGasLimit, _extraData); + } + + /// @notice Sends ERC20 tokens to a receiver's address on L2. + /// @param _localToken Address of the ERC20 on L1. + /// @param _remoteToken Address of the corresponding token on L2. + /// @param _to Address of the receiver. + /// @param _amount Amount of local tokens to deposit. + /// @param _minGasLimit Minimum amount of gas that the bridge can be relayed with. + /// @param _extraData Extra data to be sent with the transaction. Note that the recipient will + /// not be triggered with this data, but it will be emitted and can be used + /// to identify the transaction. + function bridgeERC20To( + address _localToken, + address _remoteToken, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) external { + _initiateBridgeERC20(_localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); + } + + /// @notice Finalizes an ERC20 bridge on L1. Can only be triggered by the L2TokenBridge. + /// @param _localToken Address of the ERC20 on L1. + /// @param _remoteToken Address of the corresponding token on L2. + /// @param _from Address of the sender. + /// @param _to Address of the receiver. + /// @param _amount Amount of the ERC20 being bridged. + /// @param _extraData Extra data to be sent with the transaction. Note that the recipient will + /// not be triggered with this data, but it will be emitted and can be used + /// to identify the transaction. + function finalizeBridgeERC20( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + bytes calldata _extraData + ) + external + onlyOtherBridge + { + TokenLike(_localToken).transferFrom(escrow, _to, _amount); + + emit ERC20BridgeFinalized(_localToken, _remoteToken, _from, _to, _amount, _extraData); + } +} diff --git a/src/L2GovernanceRelay.sol b/src/L2GovernanceRelay.sol new file mode 100644 index 0000000..4e392b5 --- /dev/null +++ b/src/L2GovernanceRelay.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +interface CrossDomainMessengerLike { + function xDomainMessageSender() external view returns (address); +} + +// Receive xchain message from L1GovernanceRelay and execute given spell +contract L2GovernanceRelay { + + // --- immutables --- + + address public immutable l1GovernanceRelay; + CrossDomainMessengerLike public immutable messenger; + + // --- modifiers --- + + modifier onlyL1GovRelay() { + require( + msg.sender == address(messenger) && messenger.xDomainMessageSender() == l1GovernanceRelay, + "L2GovernanceRelay/not-from-l1-gov-relay" + ); + _; + } + + // --- constructor --- + + constructor( + address _l1GovernanceRelay, + address _l2Messenger + ) { + l1GovernanceRelay = _l1GovernanceRelay; + messenger = CrossDomainMessengerLike(_l2Messenger); + } + + // --- relay --- + + function relay(address target, bytes calldata targetData) external onlyL1GovRelay { + (bool success, bytes memory result) = target.delegatecall(targetData); + if (!success) { + if (result.length == 0) revert("L2GovernanceRelay/delegatecall-error"); + assembly ("memory-safe") { + revert(add(32, result), mload(result)) + } + } + } +} diff --git a/src/L2TokenBridge.sol b/src/L2TokenBridge.sol new file mode 100644 index 0000000..d5de9e5 --- /dev/null +++ b/src/L2TokenBridge.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import { UUPSUpgradeable, ERC1967Utils } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +interface TokenLike { + function mint(address, uint256) external; + function burn(address, uint256) external; +} + +interface CrossDomainMessengerLike { + function xDomainMessageSender() external view returns (address); + function sendMessage(address _target, bytes calldata _message, uint32 _minGasLimit) external payable; +} + +contract L2TokenBridge is UUPSUpgradeable { + // --- storage variables --- + + mapping(address => uint256) public wards; + mapping(address => address) public l1ToL2Token; + mapping(address => uint256) public maxWithdraws; + uint256 public isOpen; + + // --- immutables and const --- + + address public immutable otherBridge; + CrossDomainMessengerLike public immutable messenger; + string public constant version = "1"; + + // --- events --- + + event Rely(address indexed usr); + event Deny(address indexed usr); + event Closed(); + event TokenSet(address indexed l1Token, address indexed l2Token); + event MaxWithdrawSet(address indexed l2Token, uint256 maxWithdraw); + event ERC20BridgeInitiated( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + event ERC20BridgeFinalized( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + + // --- modifiers --- + + modifier auth() { + require(wards[msg.sender] == 1, "L2TokenBridge/not-authorized"); + _; + } + + modifier onlyOtherBridge() { + require( + msg.sender == address(messenger) && messenger.xDomainMessageSender() == otherBridge, + "L2TokenBridge/not-from-other-bridge" + ); + _; + } + + // --- constructor --- + + constructor( + address _otherBridge, + address _messenger + ) { + _disableInitializers(); // Avoid initializing in the context of the implementation + + otherBridge = _otherBridge; + messenger = CrossDomainMessengerLike(_messenger); + } + + // --- upgradability --- + + function initialize() initializer external { + __UUPSUpgradeable_init(); + + isOpen = 1; + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + function _authorizeUpgrade(address newImplementation) internal override auth {} + + function getImplementation() external view returns (address) { + return ERC1967Utils.getImplementation(); + } + + // --- administration --- + + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + function close() external auth { + isOpen = 0; + emit Closed(); + } + + function registerToken(address l1Token, address l2Token) external auth { + l1ToL2Token[l1Token] = l2Token; + emit TokenSet(l1Token, l2Token); + } + + function setMaxWithdraw(address l2Token, uint256 maxWithdraw) external auth { + maxWithdraws[l2Token] = maxWithdraw; + emit MaxWithdrawSet(l2Token, maxWithdraw); + } + + // -- bridging -- + + function _initiateBridgeERC20( + address _localToken, + address _remoteToken, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes memory _extraData + ) internal { + require(isOpen == 1, "L2TokenBridge/closed"); // do not allow initiating new xchain messages if bridge is closed + require(_localToken != address(0) && l1ToL2Token[_remoteToken] == _localToken, "L2TokenBridge/invalid-token"); + require(_amount <= maxWithdraws[_localToken], "L2TokenBridge/amount-too-large"); + + TokenLike(_localToken).burn(msg.sender, _amount); + + messenger.sendMessage({ + _target: address(otherBridge), + _message: abi.encodeCall(this.finalizeBridgeERC20, ( + // Because this call will be executed on the remote chain, we reverse the order of + // the remote and local token addresses relative to their order in the + // finalizeBridgeERC20 function. + _remoteToken, + _localToken, + msg.sender, + _to, + _amount, + _extraData + )), + _minGasLimit: _minGasLimit + }); + + emit ERC20BridgeInitiated(_localToken, _remoteToken, msg.sender, _to, _amount, _extraData); + } + + /// @notice Sends ERC20 tokens to the sender's address on L1. + /// @param _localToken Address of the ERC20 on L2. + /// @param _remoteToken Address of the corresponding token on L1. + /// @param _amount Amount of local tokens to deposit. + /// @param _minGasLimit Minimum amount of gas that the bridge can be relayed with. + /// @param _extraData Extra data to be sent with the transaction. Note that the recipient will + /// not be triggered with this data, but it will be emitted and can be used + /// to identify the transaction. + function bridgeERC20( + address _localToken, + address _remoteToken, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) external { + require(msg.sender.code.length == 0, "L2TokenBridge/sender-not-eoa"); + _initiateBridgeERC20(_localToken, _remoteToken, msg.sender, _amount, _minGasLimit, _extraData); + } + + /// @notice Sends ERC20 tokens to a receiver's address on L1. + /// @param _localToken Address of the ERC20 on L2. + /// @param _remoteToken Address of the corresponding token on L1. + /// @param _to Address of the receiver. + /// @param _amount Amount of local tokens to deposit. + /// @param _minGasLimit Minimum amount of gas that the bridge can be relayed with. + /// @param _extraData Extra data to be sent with the transaction. Note that the recipient will + /// not be triggered with this data, but it will be emitted and can be used + /// to identify the transaction. + function bridgeERC20To( + address _localToken, + address _remoteToken, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) external { + _initiateBridgeERC20(_localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); + } + + /// @notice Finalizes an ERC20 bridge on L2. Can only be triggered by the L1TokenBridge. + /// @param _localToken Address of the ERC20 on L2. + /// @param _remoteToken Address of the corresponding token on L1. + /// @param _from Address of the sender. + /// @param _to Address of the receiver. + /// @param _amount Amount of the ERC20 being bridged. + /// @param _extraData Extra data to be sent with the transaction. Note that the recipient will + /// not be triggered with this data, but it will be emitted and can be used + /// to identify the transaction. + function finalizeBridgeERC20( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + bytes calldata _extraData + ) + external + onlyOtherBridge + { + TokenLike(_localToken).mint(_to, _amount); + + emit ERC20BridgeFinalized(_localToken, _remoteToken, _from, _to, _amount, _extraData); + } +} diff --git a/test/Escrow.t.sol b/test/Escrow.t.sol new file mode 100644 index 0000000..0003963 --- /dev/null +++ b/test/Escrow.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import "dss-test/DssTest.sol"; + +import { Escrow } from "src/Escrow.sol"; +import { GemMock } from "test/mocks/GemMock.sol"; + +contract EscrowTest is DssTest { + + Escrow escrow; + GemMock token; + + event Approve(address indexed token, address indexed spender, uint256 value); + + function setUp() public { + escrow = new Escrow(); + token = new GemMock(0); + } + + function testConstructor() public { + vm.expectEmit(true, true, true, true); + emit Rely(address(this)); + Escrow e = new Escrow(); + + assertEq(e.wards(address(this)), 1); + } + + function testAuth() public { + checkAuth(address(escrow), "Escrow"); + } + + function testAuthModifiers() public virtual { + escrow.deny(address(this)); + + checkModifier(address(escrow), string(abi.encodePacked("Escrow", "/not-authorized")), [ + escrow.approve.selector + ]); + } + + function testApprove() public { + address spender = address(0xb0b); + uint256 value = 10 ether; + + vm.expectEmit(true, true, true, true); + emit Approve(address(token), spender, value); + escrow.approve(address(token), spender, value); + + assertEq(token.allowance(address(escrow), spender), value); + } +} diff --git a/test/Integration.t.sol b/test/Integration.t.sol new file mode 100644 index 0000000..1328f86 --- /dev/null +++ b/test/Integration.t.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import "dss-test/DssTest.sol"; + +import { Domain } from "dss-test/domains/Domain.sol"; +import { OptimismDomain } from "dss-test/domains/OptimismDomain.sol"; +import { TokenBridgeDeploy } from "deploy/TokenBridgeDeploy.sol"; +import { L2TokenBridgeSpell } from "deploy/L2TokenBridgeSpell.sol"; +import { L1TokenBridgeInstance } from "deploy/L1TokenBridgeInstance.sol"; +import { L2TokenBridgeInstance } from "deploy/L2TokenBridgeInstance.sol"; +import { TokenBridgeInit, BridgesConfig } from "deploy/TokenBridgeInit.sol"; +import { L1TokenBridge } from "src/L1TokenBridge.sol"; +import { L2TokenBridge } from "src/L2TokenBridge.sol"; +import { L1GovernanceRelay } from "src/L1GovernanceRelay.sol"; +import { GemMock } from "test/mocks/GemMock.sol"; +import { L1TokenBridgeV2Mock } from "test/mocks/L1TokenBridgeV2Mock.sol"; +import { L2TokenBridgeV2Mock } from "test/mocks/L2TokenBridgeV2Mock.sol"; + +interface SuperChainConfigLike { + function guardian() external returns (address); + function paused() external view returns (bool); + function pause(string memory) external; +} + +interface L1CrossDomainMessengerLike { + function superchainConfig() external returns (address); + function paused() external view returns (bool); +} + +contract IntegrationTest is DssTest { + + Domain l1Domain; + OptimismDomain l2Domain; + + // L1-side + DssInstance dss; + address PAUSE_PROXY; + address L1_MESSENGER; + address l1GovRelay; + address escrow; + L1TokenBridge l1Bridge; + GemMock l1Token; + + // L2-side + address l2GovRelay; + GemMock l2Token; + L2TokenBridge l2Bridge; + address l2Spell; + address L2_MESSENGER; + + constructor() { + vm.setEnv("FOUNDRY_ROOT_CHAINID", "1"); // used by ScriptTools to determine config path + // Note: need to set the domains here instead of in setUp() to make sure their storages are actually persistent + string memory config = ScriptTools.loadConfig("config"); + l1Domain = new Domain(config, getChain("mainnet")); + l2Domain = new OptimismDomain(config, getChain("base"), l1Domain); + } + + function setUp() public { + l1Domain.selectFork(); + l1Domain.loadDssFromChainlog(); + dss = l1Domain.dss(); + PAUSE_PROXY = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); + vm.label(address(PAUSE_PROXY), "PAUSE_PROXY"); + + L1_MESSENGER = l2Domain.readConfigAddress("l1Messenger"); + L2_MESSENGER = l2Domain.readConfigAddress("l2Messenger"); + vm.label(L1_MESSENGER, "L1_MESSENGER"); + vm.label(L2_MESSENGER, "L2_MESSENGER"); + + address l1GovRelay_ = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 4); // foundry increments a global nonce across domains + address l1Bridge_ = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 7); + l2Domain.selectFork(); + L2TokenBridgeInstance memory l2BridgeInstance = TokenBridgeDeploy.deployL2({ + deployer: address(this), + l1GovRelay: l1GovRelay_, + l1Bridge: l1Bridge_, + l2Messenger: L2_MESSENGER + }); + l2GovRelay = l2BridgeInstance.govRelay; + l2Bridge = L2TokenBridge(l2BridgeInstance.bridge); + l2Spell = l2BridgeInstance.spell; + assertEq(address(L2TokenBridgeSpell(l2Spell).l2Bridge()), address(l2Bridge)); + assertEq(l2Bridge.version(), "1"); + assertEq(l2Bridge.getImplementation(), l2BridgeInstance.bridgeImp); + + l1Domain.selectFork(); + L1TokenBridgeInstance memory l1BridgeInstance = TokenBridgeDeploy.deployL1({ + deployer: address(this), + owner: PAUSE_PROXY, + l2GovRelay: l2GovRelay, + l2Bridge: address(l2Bridge), + l1Messenger: L1_MESSENGER + }); + l1GovRelay = l1BridgeInstance.govRelay; + escrow = l1BridgeInstance.escrow; + l1Bridge = L1TokenBridge(l1BridgeInstance.bridge); + assertEq(l1GovRelay, l1GovRelay_); + assertEq(address(l1Bridge), l1Bridge_); + assertEq(l1Bridge.version(), "1"); + assertEq(l1Bridge.getImplementation(), l1BridgeInstance.bridgeImp); + + l1Token = new GemMock(100 ether); + vm.label(address(l1Token), "l1Token"); + + l2Domain.selectFork(); + l2Token = new GemMock(0); + l2Token.rely(l2GovRelay); + l2Token.deny(address(this)); + vm.label(address(l2Token), "l2Token"); + + address[] memory l1Tokens = new address[](1); + l1Tokens[0] = address(l1Token); + address[] memory l2Tokens = new address[](1); + l2Tokens[0] = address(l2Token); + uint256[] memory maxWithdraws = new uint256[](1); + maxWithdraws[0] = 10_000_000 ether; + BridgesConfig memory cfg = BridgesConfig({ + l1Messenger: L1_MESSENGER, + l2Messenger: L2_MESSENGER, + l1Tokens: l1Tokens, + l2Tokens: l2Tokens, + maxWithdraws: maxWithdraws, + minGasLimit: 1_000_000, + govRelayCLKey: "BASE_GOV_RELAY", + escrowCLKey: "BASE_ESCROW", + l1BridgeCLKey: "BASE_TOKEN_BRIDGE", + l1BridgeImpCLKey: "BASE_TOKEN_BRIDGE_IMP" + }); + + l1Domain.selectFork(); + vm.startPrank(PAUSE_PROXY); + TokenBridgeInit.initBridges(dss, l1BridgeInstance, l2BridgeInstance, cfg); + vm.stopPrank(); + + // test L1 side of initBridges + assertEq(l1Token.allowance(escrow, l1Bridge_), type(uint256).max); + assertEq(l1Bridge.l1ToL2Token(address(l1Token)), address(l2Token)); + assertEq(dss.chainlog.getAddress("BASE_GOV_RELAY"), l1GovRelay); + assertEq(dss.chainlog.getAddress("BASE_ESCROW"), escrow); + assertEq(dss.chainlog.getAddress("BASE_TOKEN_BRIDGE"), l1Bridge_); + assertEq(dss.chainlog.getAddress("BASE_TOKEN_BRIDGE_IMP"), l1BridgeInstance.bridgeImp); + + l2Domain.relayFromHost(true); + + // test L2 side of initBridges + assertEq(l2Bridge.l1ToL2Token(address(l1Token)), address(l2Token)); + assertEq(l2Bridge.maxWithdraws(address(l2Token)), 10_000_000 ether); + assertEq(l2Token.wards(address(l2Bridge)), 1); + } + + function testDeposit() public { + l1Domain.selectFork(); + l1Token.approve(address(l1Bridge), 100 ether); + uint256 escrowBefore = l1Token.balanceOf(escrow); + + L1TokenBridge(l1Bridge).bridgeERC20To( + address(l1Token), + address(l2Token), + address(0xb0b), + 100 ether, + 1_000_000, + "" + ); + + assertEq(l1Token.balanceOf(escrow), escrowBefore + 100 ether); + + l2Domain.relayFromHost(true); + + assertEq(l2Token.balanceOf(address(0xb0b)), 100 ether); + } + + + function testWithdraw() public { + testDeposit(); + + vm.startPrank(address(0xb0b)); + l2Token.approve(address(l2Bridge), 100 ether); + L2TokenBridge(l2Bridge).bridgeERC20To( + address(l2Token), + address(l1Token), + address(0xced), + 100 ether, + 1_000_000, + "" + ); + vm.stopPrank(); + + assertEq(l2Token.balanceOf(address(0xb0b)), 0); + + l2Domain.relayToHost(true); + + assertEq(l1Token.balanceOf(address(0xced)), 100 ether); + } + + function testPausedWithdraw() public { + testDeposit(); + + l1Domain.selectFork(); + L1CrossDomainMessengerLike l1Messenger = L1CrossDomainMessengerLike(L1_MESSENGER); + SuperChainConfigLike cfg = SuperChainConfigLike(l1Messenger.superchainConfig()); + vm.prank(cfg.guardian()); cfg.pause(""); + assertTrue(cfg.paused()); + assertTrue(l1Messenger.paused()); + + l2Domain.selectFork(); + vm.startPrank(address(0xb0b)); + l2Token.approve(address(l2Bridge), 100 ether); + L2TokenBridge(l2Bridge).bridgeERC20To( + address(l2Token), + address(l1Token), + address(0xced), + 100 ether, + 1_000_000, + "" + ); + vm.stopPrank(); + + vm.expectRevert("CrossDomainMessenger: paused"); + l2Domain.relayToHost(true); + } + + function testUpgrade() public { + l2Domain.selectFork(); + address newL2Imp = address(new L2TokenBridgeV2Mock()); + l1Domain.selectFork(); + address newL1Imp = address(new L1TokenBridgeV2Mock()); + + vm.startPrank(PAUSE_PROXY); + l1Bridge.upgradeToAndCall(newL1Imp, abi.encodeCall(L1TokenBridgeV2Mock.reinitialize, ())); + vm.stopPrank(); + + assertEq(l1Bridge.getImplementation(), newL1Imp); + assertEq(l1Bridge.version(), "2"); + assertEq(l1Bridge.wards(PAUSE_PROXY), 1); // still a ward + + vm.startPrank(PAUSE_PROXY); + L1GovernanceRelay(l1GovRelay).relay({ + target: l2Spell, + targetData: abi.encodeCall(L2TokenBridgeSpell.upgradeToAndCall, ( + newL2Imp, + abi.encodeCall(L2TokenBridgeV2Mock.reinitialize, ()) + )), + minGasLimit: 100_000 + }); + vm.stopPrank(); + + l2Domain.relayFromHost(true); + + assertEq(l2Bridge.getImplementation(), newL2Imp); + assertEq(l2Bridge.version(), "2"); + assertEq(l2Bridge.wards(l2GovRelay), 1); // still a ward + } +} diff --git a/test/L1GovernanceRelay.t.sol b/test/L1GovernanceRelay.t.sol new file mode 100644 index 0000000..dce8115 --- /dev/null +++ b/test/L1GovernanceRelay.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import "dss-test/DssTest.sol"; + +import { L1GovernanceRelay } from "src/L1GovernanceRelay.sol"; +import { L2GovernanceRelay } from "src/L2GovernanceRelay.sol"; +import { MessengerMock } from "test/mocks/MessengerMock.sol"; + +contract L1GovernanceRelayTest is DssTest { + + L1GovernanceRelay relay; + address l2GovRelay = address(0x222); + address messenger; + + event SentMessage( + address indexed target, + address sender, + bytes message, + uint256 messageNonce, + uint256 gasLimit + ); + + function setUp() public { + messenger = address(new MessengerMock()); + relay = new L1GovernanceRelay(l2GovRelay, messenger); + } + + function testConstructor() public { + vm.expectEmit(true, true, true, true); + emit Rely(address(this)); + L1GovernanceRelay r = new L1GovernanceRelay(address(111), address(222)); + + assertEq(r.l2GovernanceRelay(), address(111)); + assertEq(address(r.messenger()), address(222)); + assertEq(r.wards(address(this)), 1); + } + + function testAuth() public { + checkAuth(address(relay), "L1GovernanceRelay"); + } + + function testAuthModifiers() public virtual { + relay.deny(address(this)); + + checkModifier(address(relay), string(abi.encodePacked("L1GovernanceRelay", "/not-authorized")), [ + relay.relay.selector + ]); + } + + function testRelay() public { + address target = address(0x333); + bytes memory targetData = "0xaabbccdd"; + uint32 minGasLimit = 1_234_567; + + vm.expectEmit(true, true, true, true); + emit SentMessage( + l2GovRelay, + address(relay), + abi.encodeCall(L2GovernanceRelay.relay, (target, targetData)), + 0, + minGasLimit + ); + relay.relay(target, targetData, minGasLimit); + } +} diff --git a/test/L1TokenBridge.t.sol b/test/L1TokenBridge.t.sol new file mode 100644 index 0000000..df130f7 --- /dev/null +++ b/test/L1TokenBridge.t.sol @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import "dss-test/DssTest.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { Upgrades, Options } from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import { L1TokenBridge } from "src/L1TokenBridge.sol"; +import { GemMock } from "test/mocks/GemMock.sol"; +import { MessengerMock } from "test/mocks/MessengerMock.sol"; +import { L1TokenBridgeV2Mock } from "test/mocks/L1TokenBridgeV2Mock.sol"; + +contract L1TokenBridgeTest is DssTest { + + event TokenSet(address indexed l1Address, address indexed l2Address); + event Closed(); + event ERC20BridgeInitiated( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + event ERC20BridgeFinalized( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + event SentMessage( + address indexed target, + address sender, + bytes message, + uint256 messageNonce, + uint256 gasLimit + ); + event UpgradedTo(string version); + + GemMock l1Token; + address l2Token = address(0x222); + L1TokenBridge bridge; + address escrow = address(0xeee); + address otherBridge = address(0xccc); + MessengerMock messenger; + bool validate; + + function setUp() public { + validate = vm.envOr("VALIDATE", false); + + messenger = new MessengerMock(); + messenger.setXDomainMessageSender(otherBridge); + + L1TokenBridge imp = new L1TokenBridge(otherBridge, address(messenger)); + assertEq(imp.otherBridge(), otherBridge); + assertEq(address(imp.messenger()), address(messenger)); + + vm.expectEmit(true, true, true, true); + emit Rely(address(this)); + bridge = L1TokenBridge(address(new ERC1967Proxy(address(imp), abi.encodeCall(L1TokenBridge.initialize, ())))); + assertEq(bridge.getImplementation(), address(imp)); + assertEq(bridge.wards(address(this)), 1); + assertEq(bridge.isOpen(), 1); + assertEq(bridge.otherBridge(), otherBridge); + assertEq(address(bridge.messenger()), address(messenger)); + + bridge.file("escrow", escrow); + l1Token = new GemMock(1_000_000 ether); + l1Token.transfer(address(0xe0a), 500_000 ether); + vm.prank(escrow); l1Token.approve(address(bridge), type(uint256).max); + bridge.registerToken(address(l1Token), l2Token); + } + + function testAuth() public { + checkAuth(address(bridge), "L1TokenBridge"); + } + + function testAuthModifiers() public virtual { + bridge.deny(address(this)); + + checkModifier(address(bridge), string(abi.encodePacked("L1TokenBridge", "/not-authorized")), [ + bridge.close.selector, + bridge.registerToken.selector, + bridge.upgradeToAndCall.selector + ]); + } + + function testFileAddress() public { + checkFileAddress(address(bridge), "L1TokenBridge", ["escrow"]); + } + + function testTokenRegistration() public { + assertEq(bridge.l1ToL2Token(address(11)), address(0)); + + vm.expectEmit(true, true, true, true); + emit TokenSet(address(11), address(22)); + bridge.registerToken(address(11), address(22)); + + assertEq(bridge.l1ToL2Token(address(11)), address(22)); + } + + function testClose() public { + assertEq(bridge.isOpen(), 1); + + l1Token.approve(address(bridge), type(uint256).max); + bridge.bridgeERC20To(address(l1Token), l2Token, address(0xb0b), 100 ether, 1_000_000, ""); + + vm.prank(address(messenger)); bridge.finalizeBridgeERC20(address(l1Token), l2Token, address(this), address(this), 1 ether, ""); + + vm.expectEmit(true, true, true, true); + emit Closed(); + bridge.close(); + + assertEq(bridge.isOpen(), 0); + vm.expectRevert("L1TokenBridge/closed"); + bridge.bridgeERC20To(address(l1Token), l2Token, address(0xb0b), 100 ether, 1_000_000, ""); + + // finalizing a transfer should still be possible + vm.prank(address(messenger)); bridge.finalizeBridgeERC20(address(l1Token), l2Token, address(this), address(this), 1 ether, ""); + } + + function testBridgeERC20() public { + vm.expectRevert("L1TokenBridge/sender-not-eoa"); + bridge.bridgeERC20(address(l1Token), l2Token, 100 ether, 1_000_000, ""); + + vm.expectRevert("L1TokenBridge/invalid-token"); + vm.prank(address(0xe0a)); bridge.bridgeERC20(address(l1Token), address(0xbad), 100 ether, 1_000_000, ""); + + vm.expectRevert("L1TokenBridge/invalid-token"); + vm.prank(address(0xe0a)); bridge.bridgeERC20(address(0xbad), address(0), 100 ether, 1_000_000, ""); + + uint256 eoaBefore = l1Token.balanceOf(address(0xe0a)); + vm.prank(address(0xe0a)); l1Token.approve(address(bridge), type(uint256).max); + + vm.expectEmit(true, true, true, true); + emit SentMessage( + otherBridge, + address(bridge), + abi.encodeCall(L1TokenBridge.finalizeBridgeERC20, (l2Token, address(l1Token), address(0xe0a), address(0xe0a), 100 ether, "abc")), + 0, + 1_000_000 + ); + vm.expectEmit(true, true, true, true); + emit ERC20BridgeInitiated(address(l1Token), l2Token, address(0xe0a), address(0xe0a), 100 ether, "abc"); + vm.prank(address(0xe0a)); bridge.bridgeERC20(address(l1Token), l2Token, 100 ether, 1_000_000, "abc"); + + assertEq(l1Token.balanceOf(address(0xe0a)), eoaBefore - 100 ether); + assertEq(l1Token.balanceOf(escrow), 100 ether); + + uint256 thisBefore = l1Token.balanceOf(address(this)); + l1Token.approve(address(bridge), type(uint256).max); + + vm.expectEmit(true, true, true, true); + emit SentMessage( + otherBridge, + address(bridge), + abi.encodeCall(L1TokenBridge.finalizeBridgeERC20, (l2Token, address(l1Token), address(this), address(0xb0b), 100 ether, "def")), + 0, + 1_000_000 + ); + vm.expectEmit(true, true, true, true); + emit ERC20BridgeInitiated(address(l1Token), l2Token, address(this), address(0xb0b), 100 ether, "def"); + bridge.bridgeERC20To(address(l1Token), l2Token, address(0xb0b), 100 ether, 1_000_000, "def"); + + assertEq(l1Token.balanceOf(address(this)), thisBefore - 100 ether); + assertEq(l1Token.balanceOf(escrow), 200 ether); + } + + function testFinalizeBridgeERC20() public { + vm.expectRevert("L1TokenBridge/not-from-other-bridge"); + bridge.finalizeBridgeERC20(address(l1Token), l2Token, address(0xb0b), address(0xced), 100 ether, "abc"); + + messenger.setXDomainMessageSender(address(0)); + + vm.expectRevert("L1TokenBridge/not-from-other-bridge"); + vm.prank(address(messenger)); bridge.finalizeBridgeERC20(address(l1Token), l2Token, address(0xb0b), address(0xced), 100 ether, "abc"); + + messenger.setXDomainMessageSender(otherBridge); + deal(address(l1Token), escrow, 100 ether, true); + + vm.expectEmit(true, true, true, true); + emit ERC20BridgeFinalized(address(l1Token), l2Token, address(0xb0b), address(0xced), 100 ether, "abc"); + vm.prank(address(messenger)); bridge.finalizeBridgeERC20(address(l1Token), l2Token, address(0xb0b), address(0xced), 100 ether, "abc"); + + assertEq(l1Token.balanceOf(escrow), 0); + assertEq(l1Token.balanceOf(address(0xced)), 100 ether); + } + + function testDeployWithUpgradesLib() public { + Options memory opts; + if (!validate) { + opts.unsafeSkipAllChecks = true; + } else { + opts.unsafeAllow = 'state-variable-immutable,constructor'; + } + opts.constructorData = abi.encode(otherBridge, address(messenger)); + + vm.expectEmit(true, true, true, true); + emit Rely(address(this)); + address proxy = Upgrades.deployUUPSProxy( + "out/L1TokenBridge.sol/L1TokenBridge.json", + abi.encodeCall(L1TokenBridge.initialize, ()), + opts + ); + assertEq(L1TokenBridge(proxy).version(), "1"); + assertEq(L1TokenBridge(proxy).wards(address(this)), 1); + } + + function testUpgrade() public { + address newImpl = address(new L1TokenBridgeV2Mock()); + vm.expectEmit(true, true, true, true); + emit UpgradedTo("2"); + bridge.upgradeToAndCall(newImpl, abi.encodeCall(L1TokenBridgeV2Mock.reinitialize, ())); + + assertEq(bridge.getImplementation(), newImpl); + assertEq(bridge.version(), "2"); + assertEq(bridge.wards(address(this)), 1); // still a ward + } + + function testUpgradeWithUpgradesLib() public { + address implementation1 = bridge.getImplementation(); + + Options memory opts; + if (!validate) { + opts.unsafeSkipAllChecks = true; + } else { + opts.referenceContract = "out/L1TokenBridge.sol/L1TokenBridge.json"; + opts.unsafeAllow = 'constructor'; + } + + vm.expectEmit(true, true, true, true); + emit UpgradedTo("2"); + Upgrades.upgradeProxy( + address(bridge), + "out/L1TokenBridgeV2Mock.sol/L1TokenBridgeV2Mock.json", + abi.encodeCall(L1TokenBridgeV2Mock.reinitialize, ()), + opts + ); + + address implementation2 = bridge.getImplementation(); + assertTrue(implementation1 != implementation2); + assertEq(bridge.version(), "2"); + assertEq(bridge.wards(address(this)), 1); // still a ward + } + + function testInitializeAgain() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + bridge.initialize(); + } + + function testInitializeDirectly() public { + address implementation = bridge.getImplementation(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + L1TokenBridge(implementation).initialize(); + } +} diff --git a/test/L2GovernanceRelay.t.sol b/test/L2GovernanceRelay.t.sol new file mode 100644 index 0000000..132d39f --- /dev/null +++ b/test/L2GovernanceRelay.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import "dss-test/DssTest.sol"; + +import { L2GovernanceRelay } from "src/L2GovernanceRelay.sol"; +import { MessengerMock } from "test/mocks/MessengerMock.sol"; + +contract L2SpellMock { + function exec() external {} + function revt() pure external { revert("L2SpellMock/revt"); } +} + +contract L2GovernanceRelayTest is DssTest { + + L2GovernanceRelay relay; + address l1GovRelay = address(0x111); + MessengerMock messenger; + address spell; + + function setUp() public { + messenger = new MessengerMock(); + relay = new L2GovernanceRelay(l1GovRelay, address(messenger)); + spell = address(new L2SpellMock()); + } + + function testConstructor() public { + L2GovernanceRelay r = new L2GovernanceRelay(address(111), address(222)); + + assertEq(r.l1GovernanceRelay(), address(111)); + assertEq(address(r.messenger()), address(222)); + } + + function testRelay() public { + messenger.setXDomainMessageSender(l1GovRelay); + + vm.expectRevert("L2GovernanceRelay/not-from-l1-gov-relay"); + relay.relay(spell, abi.encodeCall(L2SpellMock.exec, ())); // revert due to wrong msg.sender + + messenger.setXDomainMessageSender(address(0)); + + vm.expectRevert("L2GovernanceRelay/not-from-l1-gov-relay"); + vm.prank(address(messenger)); relay.relay(spell, abi.encodeCall(L2SpellMock.exec, ())); // revert due to wrong xDomainMessageSender + + messenger.setXDomainMessageSender(l1GovRelay); + + vm.expectRevert("L2GovernanceRelay/delegatecall-error"); + vm.prank(address(messenger)); relay.relay(spell, abi.encodeWithSignature("bad()")); + + vm.expectRevert("L2SpellMock/revt"); + vm.prank(address(messenger)); relay.relay(spell, abi.encodeCall(L2SpellMock.revt, ())); + + vm.prank(address(messenger)); relay.relay(spell, abi.encodeCall(L2SpellMock.exec, ())); + } +} diff --git a/test/L2TokenBridge.t.sol b/test/L2TokenBridge.t.sol new file mode 100644 index 0000000..f71a57c --- /dev/null +++ b/test/L2TokenBridge.t.sol @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import "dss-test/DssTest.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { Upgrades, Options } from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import { L2TokenBridge } from "src/L2TokenBridge.sol"; +import { GemMock } from "test/mocks/GemMock.sol"; +import { MessengerMock } from "test/mocks/MessengerMock.sol"; +import { L2TokenBridgeV2Mock } from "test/mocks/L2TokenBridgeV2Mock.sol"; + +contract L2TokenBridgeTest is DssTest { + + event TokenSet(address indexed l1Address, address indexed l2Address); + event MaxWithdrawSet(address indexed l2Token, uint256 maxWithdraw); + event Closed(); + event ERC20BridgeInitiated( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + event ERC20BridgeFinalized( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + event SentMessage( + address indexed target, + address sender, + bytes message, + uint256 messageNonce, + uint256 gasLimit + ); + event UpgradedTo(string version); + + GemMock l2Token; + address l1Token = address(0x111); + L2TokenBridge bridge; + address otherBridge = address(0xccc); + address l2Router = address(0xbbb); + MessengerMock messenger; + bool validate; + + function setUp() public { + validate = vm.envOr("VALIDATE", false); + + messenger = new MessengerMock(); + messenger.setXDomainMessageSender(otherBridge); + + L2TokenBridge imp = new L2TokenBridge(otherBridge, address(messenger)); + assertEq(imp.otherBridge(), otherBridge); + assertEq(address(imp.messenger()), address(messenger)); + + vm.expectEmit(true, true, true, true); + emit Rely(address(this)); + bridge = L2TokenBridge(address(new ERC1967Proxy(address(imp), abi.encodeCall(L2TokenBridge.initialize, ())))); + assertEq(bridge.getImplementation(), address(imp)); + assertEq(bridge.wards(address(this)), 1); + assertEq(bridge.isOpen(), 1); + assertEq(bridge.otherBridge(), otherBridge); + assertEq(address(bridge.messenger()), address(messenger)); + + l2Token = new GemMock(1_000_000 ether); + l2Token.transfer(address(0xe0a), 500_000 ether); + l2Token.rely(address(bridge)); + bridge.registerToken(l1Token, address(l2Token)); + bridge.setMaxWithdraw(address(l2Token), 1_000_000 ether); + } + + function testAuth() public { + checkAuth(address(bridge), "L2TokenBridge"); + } + + function testAuthModifiers() public virtual { + bridge.deny(address(this)); + + checkModifier(address(bridge), string(abi.encodePacked("L2TokenBridge", "/not-authorized")), [ + bridge.close.selector, + bridge.registerToken.selector, + bridge.setMaxWithdraw.selector, + bridge.upgradeToAndCall.selector + ]); + } + + function testTokenRegistration() public { + assertEq(bridge.l1ToL2Token(address(11)), address(0)); + + vm.expectEmit(true, true, true, true); + emit TokenSet(address(11), address(22)); + bridge.registerToken(address(11), address(22)); + + assertEq(bridge.l1ToL2Token(address(11)), address(22)); + } + + function testSetmaxWithdraw() public { + assertEq(bridge.maxWithdraws(address(22)), 0); + + vm.expectEmit(true, true, true, true); + emit MaxWithdrawSet(address(22), 123); + bridge.setMaxWithdraw(address(22), 123); + + assertEq(bridge.maxWithdraws(address(22)), 123); + } + + function testClose() public { + assertEq(bridge.isOpen(), 1); + + l2Token.approve(address(bridge), type(uint256).max); + bridge.bridgeERC20To(address(l2Token), l1Token, address(0xb0b), 100 ether, 1_000_000, ""); + + vm.prank(address(messenger)); bridge.finalizeBridgeERC20(address(l2Token), l1Token, address(this), address(this), 1 ether, ""); + + vm.expectEmit(true, true, true, true); + emit Closed(); + bridge.close(); + + assertEq(bridge.isOpen(), 0); + vm.expectRevert("L2TokenBridge/closed"); + bridge.bridgeERC20To(address(l2Token), l1Token, address(0xb0b), 100 ether, 1_000_000, ""); + + // finalizing a transfer should still be possible + vm.prank(address(messenger)); bridge.finalizeBridgeERC20(address(l2Token), l1Token, address(this), address(this), 1 ether, ""); + } + + function testBridgeERC20() public { + vm.expectRevert("L2TokenBridge/sender-not-eoa"); + bridge.bridgeERC20(address(l2Token), l1Token, 100 ether, 1_000_000, ""); + + vm.expectRevert("L2TokenBridge/invalid-token"); + vm.prank(address(0xe0a)); bridge.bridgeERC20(address(l1Token), address(0xbad), 100 ether, 1_000_000, ""); + + vm.expectRevert("L2TokenBridge/invalid-token"); + vm.prank(address(0xe0a)); bridge.bridgeERC20(address(0), address(0xbad), 100 ether, 1_000_000, ""); + + vm.expectRevert("L2TokenBridge/amount-too-large"); + vm.prank(address(0xe0a)); bridge.bridgeERC20(address(l2Token), l1Token, 1_000_000 ether + 1, 1_000_000, ""); + + uint256 supplyBefore = l2Token.totalSupply(); + uint256 eoaBefore = l2Token.balanceOf(address(0xe0a)); + vm.prank(address(0xe0a)); l2Token.approve(address(bridge), type(uint256).max); + + vm.expectEmit(true, true, true, true); + emit SentMessage( + otherBridge, + address(bridge), + abi.encodeCall(L2TokenBridge.finalizeBridgeERC20, (l1Token, address(l2Token), address(0xe0a), address(0xe0a), 100 ether, "abc")), + 0, + 1_000_000 + ); + vm.expectEmit(true, true, true, true); + emit ERC20BridgeInitiated(address(l2Token), l1Token, address(0xe0a), address(0xe0a), 100 ether, "abc"); + vm.prank(address(0xe0a)); bridge.bridgeERC20(address(l2Token), l1Token, 100 ether, 1_000_000, "abc"); + + assertEq(l2Token.totalSupply(), supplyBefore - 100 ether); + assertEq(l2Token.balanceOf(address(0xe0a)), eoaBefore - 100 ether); + + uint256 thisBefore = l2Token.balanceOf(address(this)); + l2Token.approve(address(bridge), type(uint256).max); + + vm.expectEmit(true, true, true, true); + emit SentMessage( + otherBridge, + address(bridge), + abi.encodeCall(L2TokenBridge.finalizeBridgeERC20, (l1Token, address(l2Token), address(this), address(0xb0b), 100 ether, "def")), + 0, + 1_000_000 + ); + vm.expectEmit(true, true, true, true); + emit ERC20BridgeInitiated(address(l2Token), l1Token, address(this), address(0xb0b), 100 ether, "def"); + bridge.bridgeERC20To(address(l2Token), l1Token, address(0xb0b), 100 ether, 1_000_000, "def"); + + assertEq(l2Token.totalSupply(), supplyBefore - 200 ether); + assertEq(l2Token.balanceOf(address(this)), thisBefore - 100 ether); + } + + function testFinalizeBridgeERC20() public { + vm.expectRevert("L2TokenBridge/not-from-other-bridge"); + bridge.finalizeBridgeERC20(address(l2Token), l1Token, address(0xb0b), address(0xced), 100 ether, "abc"); + + messenger.setXDomainMessageSender(address(0)); + + vm.expectRevert("L2TokenBridge/not-from-other-bridge"); + vm.prank(address(messenger)); bridge.finalizeBridgeERC20(address(l2Token), l1Token, address(0xb0b), address(0xced), 100 ether, "abc"); + + messenger.setXDomainMessageSender(otherBridge); + uint256 balanceBefore = l2Token.balanceOf(address(0xced)); + uint256 supplyBefore = l2Token.totalSupply(); + + vm.expectEmit(true, true, true, true); + emit ERC20BridgeFinalized(address(l2Token), l1Token, address(0xb0b), address(0xced), 100 ether, "abc"); + vm.prank(address(messenger)); bridge.finalizeBridgeERC20(address(l2Token), l1Token, address(0xb0b), address(0xced), 100 ether, "abc"); + + assertEq(l2Token.balanceOf(address(0xced)), balanceBefore + 100 ether); + assertEq(l2Token.totalSupply(), supplyBefore + 100 ether); + } + + function testDeployWithUpgradesLib() public { + Options memory opts; + if (!validate) { + opts.unsafeSkipAllChecks = true; + } else { + opts.unsafeAllow = 'state-variable-immutable,constructor'; + } + opts.constructorData = abi.encode(otherBridge, address(messenger)); + + vm.expectEmit(true, true, true, true); + emit Rely(address(this)); + address proxy = Upgrades.deployUUPSProxy( + "out/L2TokenBridge.sol/L2TokenBridge.json", + abi.encodeCall(L2TokenBridge.initialize, ()), + opts + ); + assertEq(L2TokenBridge(proxy).version(), "1"); + assertEq(L2TokenBridge(proxy).wards(address(this)), 1); + } + + function testUpgrade() public { + address newImpl = address(new L2TokenBridgeV2Mock()); + vm.expectEmit(true, true, true, true); + emit UpgradedTo("2"); + bridge.upgradeToAndCall(newImpl, abi.encodeCall(L2TokenBridgeV2Mock.reinitialize, ())); + + assertEq(bridge.getImplementation(), newImpl); + assertEq(bridge.version(), "2"); + assertEq(bridge.wards(address(this)), 1); // still a ward + } + + function testUpgradeWithUpgradesLib() public { + address implementation1 = bridge.getImplementation(); + + Options memory opts; + if (!validate) { + opts.unsafeSkipAllChecks = true; + } else { + opts.referenceContract = "out/L2TokenBridge.sol/L2TokenBridge.json"; + opts.unsafeAllow = 'constructor'; + } + + vm.expectEmit(true, true, true, true); + emit UpgradedTo("2"); + Upgrades.upgradeProxy( + address(bridge), + "out/L2TokenBridgeV2Mock.sol/L2TokenBridgeV2Mock.json", + abi.encodeCall(L2TokenBridgeV2Mock.reinitialize, ()), + opts + ); + + address implementation2 = bridge.getImplementation(); + assertTrue(implementation1 != implementation2); + assertEq(bridge.version(), "2"); + assertEq(bridge.wards(address(this)), 1); // still a ward + } + + function testInitializeAgain() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + bridge.initialize(); + } + + function testInitializeDirectly() public { + address implementation = bridge.getImplementation(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + L2TokenBridge(implementation).initialize(); + } +} diff --git a/test/mocks/GemMock.sol b/test/mocks/GemMock.sol new file mode 100644 index 0000000..f5d2ed0 --- /dev/null +++ b/test/mocks/GemMock.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +contract GemMock { + mapping (address => uint256) public wards; + mapping (address => uint256) public balanceOf; + mapping (address => mapping (address => uint256)) public allowance; + + uint256 public totalSupply; + + constructor(uint256 initialSupply) { + wards[msg.sender] = 1; + + mint(msg.sender, initialSupply); + } + + modifier auth() { + require(wards[msg.sender] == 1, "Gem/not-authorized"); + _; + } + + function rely(address usr) external auth { wards[usr] = 1; } + function deny(address usr) external auth { wards[usr] = 0; } + + function approve(address spender, uint256 value) external returns (bool) { + allowance[msg.sender][spender] = value; + return true; + } + + function transfer(address to, uint256 value) external returns (bool) { + uint256 balance = balanceOf[msg.sender]; + require(balance >= value, "Gem/insufficient-balance"); + + unchecked { + balanceOf[msg.sender] = balance - value; + balanceOf[to] += value; + } + return true; + } + + function transferFrom(address from, address to, uint256 value) external returns (bool) { + uint256 balance = balanceOf[from]; + require(balance >= value, "Gem/insufficient-balance"); + + if (from != msg.sender) { + uint256 allowed = allowance[from][msg.sender]; + if (allowed != type(uint256).max) { + require(allowed >= value, "Gem/insufficient-allowance"); + + unchecked { + allowance[from][msg.sender] = allowed - value; + } + } + } + + unchecked { + balanceOf[from] = balance - value; + balanceOf[to] += value; + } + return true; + } + + function mint(address to, uint256 value) public auth { + unchecked { + balanceOf[to] = balanceOf[to] + value; + } + totalSupply = totalSupply + value; + } + + function burn(address from, uint256 value) external { + uint256 balance = balanceOf[from]; + require(balance >= value, "Gem/insufficient-balance"); + + if (from != msg.sender) { + uint256 allowed = allowance[from][msg.sender]; + if (allowed != type(uint256).max) { + require(allowed >= value, "Gem/insufficient-allowance"); + + unchecked { + allowance[from][msg.sender] = allowed - value; + } + } + } + + unchecked { + balanceOf[from] = balance - value; + totalSupply = totalSupply - value; + } + } +} diff --git a/test/mocks/L1TokenBridgeV2Mock.sol b/test/mocks/L1TokenBridgeV2Mock.sol new file mode 100644 index 0000000..6eca2fe --- /dev/null +++ b/test/mocks/L1TokenBridgeV2Mock.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import { UUPSUpgradeable, ERC1967Utils } from "src/L1TokenBridge.sol"; + +contract L1TokenBridgeV2Mock is UUPSUpgradeable { + mapping(address => uint256) public wards; + mapping(address => address) public l1ToL2Token; + uint256 public isOpen; + address public escrow; + + string public constant version = "2"; + + event UpgradedTo(string version); + + modifier auth { + require(wards[msg.sender] == 1, "L1TokenBridge/not-authorized"); + _; + } + + constructor() { + _disableInitializers(); // Avoid initializing in the context of the implementation + } + + function reinitialize() reinitializer(2) external { + emit UpgradedTo(version); + } + + function _authorizeUpgrade(address newImplementation) internal override auth {} + + function getImplementation() external view returns (address) { + return ERC1967Utils.getImplementation(); + } +} diff --git a/test/mocks/L2TokenBridgeV2Mock.sol b/test/mocks/L2TokenBridgeV2Mock.sol new file mode 100644 index 0000000..9bd8e81 --- /dev/null +++ b/test/mocks/L2TokenBridgeV2Mock.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2024 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.21; + +import { UUPSUpgradeable, ERC1967Utils } from "src/L2TokenBridge.sol"; + +contract L2TokenBridgeV2Mock is UUPSUpgradeable { + mapping(address => uint256) public wards; + mapping(address => address) public l1ToL2Token; + mapping(address => uint256) public maxWithdraws; + uint256 public isOpen; + + string public constant version = "2"; + + event UpgradedTo(string version); + + modifier auth { + require(wards[msg.sender] == 1, "L2TokenBridge/not-authorized"); + _; + } + + constructor() { + _disableInitializers(); // Avoid initializing in the context of the implementation + } + + function reinitialize() reinitializer(2) external { + emit UpgradedTo(version); + } + + function _authorizeUpgrade(address newImplementation) internal override auth {} + + function getImplementation() external view returns (address) { + return ERC1967Utils.getImplementation(); + } +} diff --git a/test/mocks/MessengerMock.sol b/test/mocks/MessengerMock.sol new file mode 100644 index 0000000..9a11ef0 --- /dev/null +++ b/test/mocks/MessengerMock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.21; + +contract MessengerMock { + address public xDomainMessageSender; + address public lastTarget; + bytes32 public lastMessageHash; + uint32 public lastMinGasLimit; + + event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit); + + function setXDomainMessageSender(address xDomainMessageSender_) external { + xDomainMessageSender = xDomainMessageSender_; + } + + function sendMessage(address target, bytes calldata message, uint32 minGasLimit) external payable { + lastTarget = target; + lastMessageHash = keccak256(message); + lastMinGasLimit = minGasLimit; + emit SentMessage(target, msg.sender, message, 0, minGasLimit); + } +}