From 9f209dbb320b6a468bf7d63a45bc37d1ac678096 Mon Sep 17 00:00:00 2001 From: telome <> Date: Tue, 9 Jul 2024 18:02:18 +0300 Subject: [PATCH 01/40] Add bridging functions --- src/L1TokenBridge.sol | 204 ++++++++++++++++++++++++++++++++++++++++++ src/L2TokenBridge.sol | 202 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 src/L1TokenBridge.sol create mode 100644 src/L2TokenBridge.sol diff --git a/src/L1TokenBridge.sol b/src/L1TokenBridge.sol new file mode 100644 index 0000000..8ad8fc3 --- /dev/null +++ b/src/L1TokenBridge.sol @@ -0,0 +1,204 @@ +// 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 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 { + // --- storage variables --- + + mapping(address => uint256) public wards; + mapping(address => address) public l1ToL2Token; + uint256 public isOpen = 1; + + // --- immutables --- + + address public immutable otherBridge; + address public immutable escrow; + CrossDomainMessengerLike public immutable messenger; + + // --- events --- + + event Rely(address indexed usr); + event Deny(address indexed usr); + 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 _escrow, + address _messenger + ) { + otherBridge = _otherBridge; + escrow = _escrow; + messenger = CrossDomainMessengerLike(_messenger); + + 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); + } + + function close() external auth { + isOpen = 0; + emit Closed(); + } + + function registerToken(address l1Token, address l2Token) external auth { + l1ToL2Token[l1Token] = l2Token; + emit TokenSet(l1Token, l2Token); + } + + // -- bridging -- + + /// @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"); + bridgeERC20To(_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 + ) public { + require(isOpen == 1, "L1TokenBridge/closed"); // do not allow initiating new xchain messages if bridge is closed + require(l1ToL2Token[_localToken] == _remoteToken, "L1TokenBridge/invalid-token"); + + TokenLike(_localToken).transferFrom(msg.sender, escrow, _amount); + + emit ERC20BridgeInitiated(_localToken, _remoteToken, msg.sender, _to, _amount, _extraData); + + messenger.sendMessage({ + _target: address(otherBridge), + _message: abi.encodeWithSelector( + this.finalizeBridgeERC20.selector, + // 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 + }); + } + + /// @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/L2TokenBridge.sol b/src/L2TokenBridge.sol new file mode 100644 index 0000000..f70008b --- /dev/null +++ b/src/L2TokenBridge.sol @@ -0,0 +1,202 @@ +// 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 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 { + // --- storage variables --- + + mapping(address => uint256) public wards; + mapping(address => address) public l1ToL2Token; + uint256 public isOpen = 1; + + // --- immutables --- + + address public immutable otherBridge; + CrossDomainMessengerLike public immutable messenger; + + // --- events --- + + event Rely(address indexed usr); + event Deny(address indexed usr); + 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, "L2TokenBridge/not-authorized"); + _; + } + + modifier onlyOtherBridge() { + require( + msg.sender == address(messenger) && messenger.xDomainMessageSender() == otherBridge, + "L2TokenBridge/not-from-other-bridge" + ); + _; + } + + // --- constructor --- + + constructor( + address _otherBridge, + address _messenger + ) { + otherBridge = _otherBridge; + messenger = CrossDomainMessengerLike(_messenger); + + 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); + } + + function close() external auth { + isOpen = 0; + emit Closed(); + } + + function registerToken(address l1Token, address l2Token) external auth { + l1ToL2Token[l1Token] = l2Token; + emit TokenSet(l1Token, l2Token); + } + + // -- bridging -- + + /// @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"); + bridgeERC20To(_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 + ) public { + require(isOpen == 1, "L2TokenBridge/closed"); // do not allow initiating new xchain messages if bridge is closed + require(l1ToL2Token[_remoteToken] == _localToken, "L2TokenBridge/invalid-token"); + + TokenLike(_localToken).burn(msg.sender, _amount); // TODO: should l2Tokens allow authed burn? + + emit ERC20BridgeInitiated(_localToken, _remoteToken, msg.sender, _to, _amount, _extraData); + + messenger.sendMessage({ + _target: address(otherBridge), + _message: abi.encodeWithSelector( + this.finalizeBridgeERC20.selector, + // 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 + }); + } + + /// @notice Finalizes an ERC20 bridge on L2. Can only be triggered by the L2TokenBridge. + /// @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); + } +} From cfbdcc019ddde4ad63f19126daa24b427f6c3090 Mon Sep 17 00:00:00 2001 From: telome <> Date: Mon, 15 Jul 2024 13:31:42 +0300 Subject: [PATCH 02/40] Block remoteToken == 0 --- src/L1TokenBridge.sol | 2 +- src/L2TokenBridge.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/L1TokenBridge.sol b/src/L1TokenBridge.sol index 8ad8fc3..4f5d5c3 100644 --- a/src/L1TokenBridge.sol +++ b/src/L1TokenBridge.sol @@ -153,7 +153,7 @@ contract L1TokenBridge { bytes calldata _extraData ) public { require(isOpen == 1, "L1TokenBridge/closed"); // do not allow initiating new xchain messages if bridge is closed - require(l1ToL2Token[_localToken] == _remoteToken, "L1TokenBridge/invalid-token"); + require(_remoteToken != address(0) && l1ToL2Token[_localToken] == _remoteToken, "L1TokenBridge/invalid-token"); TokenLike(_localToken).transferFrom(msg.sender, escrow, _amount); diff --git a/src/L2TokenBridge.sol b/src/L2TokenBridge.sol index f70008b..526bde2 100644 --- a/src/L2TokenBridge.sol +++ b/src/L2TokenBridge.sol @@ -151,7 +151,7 @@ contract L2TokenBridge { bytes calldata _extraData ) public { require(isOpen == 1, "L2TokenBridge/closed"); // do not allow initiating new xchain messages if bridge is closed - require(l1ToL2Token[_remoteToken] == _localToken, "L2TokenBridge/invalid-token"); + require(_remoteToken != address(0) && l1ToL2Token[_remoteToken] == _localToken, "L2TokenBridge/invalid-token"); TokenLike(_localToken).burn(msg.sender, _amount); // TODO: should l2Tokens allow authed burn? From 130be8169c4a383a077b9fc7a446effcf7ac5d0f Mon Sep 17 00:00:00 2001 From: telome <> Date: Thu, 18 Jul 2024 20:52:24 +0300 Subject: [PATCH 03/40] Add init lib --- deploy/L1TokenBridgeInstance.sol | 23 ++++ deploy/L2TokenBridgeInstance.sol | 23 ++++ deploy/L2TokenBridgeSpell.sol | 79 +++++++++++++ deploy/TokenBridgeDeploy.sol | 57 +++++++++ deploy/TokenBridgeInit.sol | 106 +++++++++++++++++ foundry.toml | 8 +- script/input/1/config.json | 13 +++ script/input/11155111/config.json | 9 ++ src/Escrow.sol | 67 +++++++++++ src/L1GovernanceRelay.sol | 84 ++++++++++++++ src/L2GovernanceRelay.sol | 63 ++++++++++ test/Integration.t.sol | 186 ++++++++++++++++++++++++++++++ test/mocks/GemMock.sol | 106 +++++++++++++++++ 13 files changed, 822 insertions(+), 2 deletions(-) create mode 100644 deploy/L1TokenBridgeInstance.sol create mode 100644 deploy/L2TokenBridgeInstance.sol create mode 100644 deploy/L2TokenBridgeSpell.sol create mode 100644 deploy/TokenBridgeDeploy.sol create mode 100644 deploy/TokenBridgeInit.sol create mode 100644 script/input/1/config.json create mode 100644 script/input/11155111/config.json create mode 100644 src/Escrow.sol create mode 100644 src/L1GovernanceRelay.sol create mode 100644 src/L2GovernanceRelay.sol create mode 100644 test/Integration.t.sol create mode 100644 test/mocks/GemMock.sol diff --git a/deploy/L1TokenBridgeInstance.sol b/deploy/L1TokenBridgeInstance.sol new file mode 100644 index 0000000..1ed06a9 --- /dev/null +++ b/deploy/L1TokenBridgeInstance.sol @@ -0,0 +1,23 @@ +// 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; +} diff --git a/deploy/L2TokenBridgeInstance.sol b/deploy/L2TokenBridgeInstance.sol new file mode 100644 index 0000000..a8d965b --- /dev/null +++ b/deploy/L2TokenBridgeInstance.sol @@ -0,0 +1,23 @@ +// 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 spell; +} diff --git a/deploy/L2TokenBridgeSpell.sol b/deploy/L2TokenBridgeSpell.sol new file mode 100644 index 0000000..6871e1d --- /dev/null +++ b/deploy/L2TokenBridgeSpell.sol @@ -0,0 +1,79 @@ +// 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 rely(address) external; + function deny(address) external; + function close() external; + function registerToken(address, address) 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 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 init( + address l2GovRelay_, + address l2Bridge_, + address l1GovRelay, + address l1Bridge, + address l2Messenger, + address[] calldata l1Tokens, + address[] calldata l2Tokens + ) external { + L2GovRelayLike l2GovRelay = L2GovRelayLike(l2GovRelay_); + + // sanity checks + require(address(l2Bridge) == l2Bridge_, "L2TokenBridgeSpell/l2-gateway-mismatch"); + 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); + } +} diff --git a/deploy/TokenBridgeDeploy.sol b/deploy/TokenBridgeDeploy.sol new file mode 100644 index 0000000..acc6988 --- /dev/null +++ b/deploy/TokenBridgeDeploy.sol @@ -0,0 +1,57 @@ +// 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 { 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 deployL1Bridge( + 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.bridge = address(new L1TokenBridge(l2Bridge, l1BridgeInstance.escrow, l1Messenger)); + ScriptTools.switchOwner(l1BridgeInstance.govRelay, deployer, owner); + ScriptTools.switchOwner(l1BridgeInstance.escrow, deployer, owner); + ScriptTools.switchOwner(l1BridgeInstance.bridge, deployer, owner); + } + + function deployL2Bridge( + address deployer, + address l1GovRelay, + address l1Bridge, + address l2Messenger + ) internal returns (L2TokenBridgeInstance memory l2BridgeInstance) { + l2BridgeInstance.govRelay = address(new L2GovernanceRelay(l1GovRelay, l2Messenger)); + l2BridgeInstance.bridge = address(new L2TokenBridge(l1Bridge, l2Messenger)); + 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..664d73c --- /dev/null +++ b/deploy/TokenBridgeInit.sol @@ -0,0 +1,106 @@ +// 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 escrow() external view returns (address); + function messenger() external view returns (address); + function registerToken(address l1Token, address l2Token) 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; + uint32 minGasLimit; + bytes32 govRelayCLKey; + bytes32 escrowCLKey; + bytes32 l1BridgeCLKey; +} + +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(l1Bridge.isOpen() == 1, "TokenBridgeInit/not-open"); + require(l1Bridge.otherBridge() == l2BridgeInstance.bridge, "TokenBridgeInit/other-bridge-mismatch"); + require(l1Bridge.escrow() == address(escrow), "TokenBridgeInit/escrow-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"); + + 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(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, + address(l1GovRelay), + address(l1Bridge), + cfg.l2Messenger, + cfg.l1Tokens, + cfg.l2Tokens + )), + minGasLimit: cfg.minGasLimit + }); + + dss.chainlog.setAddress(cfg.govRelayCLKey, address(l1GovRelay)); + dss.chainlog.setAddress(cfg.escrowCLKey, address(escrow)); + dss.chainlog.setAddress(cfg.l1BridgeCLKey, address(l1Bridge)); + } +} diff --git a/foundry.toml b/foundry.toml index 25b918f..63de96e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,9 @@ src = "src" out = "out" libs = ["lib"] - -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +solc = "0.8.21" +fs_permissions = [ + { access = "read", path = "./script/input/"}, + { access = "read", path = "./out/"}, + { access = "read-write", path = "./script/output/"} +] diff --git a/script/input/1/config.json b/script/input/1/config.json new file mode 100644 index 0000000..c41272f --- /dev/null +++ b/script/input/1/config.json @@ -0,0 +1,13 @@ +{ + "domains": { + "mainnet": { + "chainlog": "0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F", + "tokens": [] + }, + "base": { + "l1Messenger": "0x866E82a600A1414e583f7F13623F1aC5d58b0Afa", + "l2Messenger": "0x4200000000000000000000000000000000000007", + "tokens": [] + } + } +} diff --git a/script/input/11155111/config.json b/script/input/11155111/config.json new file mode 100644 index 0000000..d4201b7 --- /dev/null +++ b/script/input/11155111/config.json @@ -0,0 +1,9 @@ +{ + "domains": { + "sepolia": {}, + "base_sepolia": { + "l1Messenger": "0xC34855F4De64F1840e5686e64278da901e261f20", + "l2Messenger": "0x4200000000000000000000000000000000000007" + } + } +} diff --git a/src/Escrow.sol b/src/Escrow.sol new file mode 100644 index 0000000..e933be4 --- /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 { + emit Approve(token, spender, value); + GemLike(token).approve(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/L2GovernanceRelay.sol b/src/L2GovernanceRelay.sol new file mode 100644 index 0000000..4912e1a --- /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) { + // Next 5 lines from https://ethereum.stackexchange.com/a/83577 + if (result.length < 68) revert("L2GovernanceRelay/delegatecall-error"); + assembly { result := add(result, 0x04) } + revert(abi.decode(result, (string))); + } + } +} diff --git a/test/Integration.t.sol b/test/Integration.t.sol new file mode 100644 index 0000000..98c6f51 --- /dev/null +++ b/test/Integration.t.sol @@ -0,0 +1,186 @@ +// 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 { GemMock } from "test/mocks/GemMock.sol"; + +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 L2_MESSENGER; + + function setUp() public {} // TODO: setUp seems to cause a foundry bug whereby l2Domain is not actually persistent and incorrectly resets its log index + + function _setUp() internal { + vm.setEnv("FOUNDRY_ROOT_CHAINID", "1"); // used by ScriptTools to determine config path + string memory config = ScriptTools.loadConfig("config"); + + l1Domain = new Domain(config, getChain("mainnet")); + l1Domain.selectFork(); + l1Domain.loadDssFromChainlog(); + dss = l1Domain.dss(); + PAUSE_PROXY = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); + vm.label(address(PAUSE_PROXY), "PAUSE_PROXY"); + + l2Domain = new OptimismDomain(config, getChain("base"), l1Domain); + 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)) + 3); // foundry increments a global nonce across domains + address l1Bridge_ = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 5); // foundry increments a global nonce across domains + l2Domain.selectFork(); + L2TokenBridgeInstance memory l2BridgeInstance = TokenBridgeDeploy.deployL2Bridge({ + deployer: address(this), + l1GovRelay: l1GovRelay_, + l1Bridge: l1Bridge_, + l2Messenger: L2_MESSENGER + }); + l2GovRelay = l2BridgeInstance.govRelay; + l2Bridge = L2TokenBridge(l2BridgeInstance.bridge); + assertEq(address(L2TokenBridgeSpell(l2BridgeInstance.spell).l2Bridge()), address(l2Bridge)); + + l1Domain.selectFork(); + L1TokenBridgeInstance memory l1BridgeInstance = TokenBridgeDeploy.deployL1Bridge({ + deployer: address(this), + owner: PAUSE_PROXY, + l2GovRelay: l2BridgeInstance.govRelay, + l2Bridge: address(l2Bridge), + l1Messenger: L1_MESSENGER + }); + l1GovRelay = l1BridgeInstance.govRelay; + escrow = l1BridgeInstance.escrow; + l1Bridge = L1TokenBridge(l1BridgeInstance.bridge); + assertEq(l1GovRelay, l1GovRelay_); + assertEq(address(l1Bridge), l1Bridge_); + + 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); + BridgesConfig memory cfg = BridgesConfig({ + l1Messenger: L1_MESSENGER, + l2Messenger: L2_MESSENGER, + l1Tokens: l1Tokens, + l2Tokens: l2Tokens, + minGasLimit: 1_000_000, + govRelayCLKey: "BASE_GOV_RELAY", + escrowCLKey: "BASE_ESCROW", + l1BridgeCLKey: "BASE_TOKEN_BRIDGE" + }); + + 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_); + + l2Domain.relayFromHost(true); + + // test L2 side of initBridges + assertEq(l2Bridge.l1ToL2Token(address(l1Token)), address(l2Token)); + assertEq(l2Token.wards(address(l2Bridge)), 1); + } + + function testDeposit() public { + _setUp(); + + 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); + } +} 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; + } + } +} From ca1fc25f93e2ad59562ca54a2dd08a535b97dd58 Mon Sep 17 00:00:00 2001 From: telome <> Date: Fri, 19 Jul 2024 13:45:48 +0300 Subject: [PATCH 04/40] Mitigate domain persistence issue in test --- src/L2GovernanceRelay.sol | 2 +- test/Integration.t.sol | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/L2GovernanceRelay.sol b/src/L2GovernanceRelay.sol index 4912e1a..5d29567 100644 --- a/src/L2GovernanceRelay.sol +++ b/src/L2GovernanceRelay.sol @@ -54,7 +54,7 @@ contract L2GovernanceRelay { function relay(address target, bytes calldata targetData) external onlyL1GovRelay { (bool success, bytes memory result) = target.delegatecall(targetData); if (!success) { - // Next 5 lines from https://ethereum.stackexchange.com/a/83577 + // Next 3 lines are based on https://ethereum.stackexchange.com/a/83577 if (result.length < 68) revert("L2GovernanceRelay/delegatecall-error"); assembly { result := add(result, 0x04) } revert(abi.decode(result, (string))); diff --git a/test/Integration.t.sol b/test/Integration.t.sol index 98c6f51..eac6035 100644 --- a/test/Integration.t.sol +++ b/test/Integration.t.sol @@ -50,27 +50,28 @@ contract IntegrationTest is DssTest { L2TokenBridge l2Bridge; address L2_MESSENGER; - function setUp() public {} // TODO: setUp seems to cause a foundry bug whereby l2Domain is not actually persistent and incorrectly resets its log index - - function _setUp() internal { + 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"); + vm.label(address(PAUSE_PROXY), "PAUSE_PROXY"); - l2Domain = new OptimismDomain(config, getChain("base"), l1Domain); 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)) + 3); // foundry increments a global nonce across domains - address l1Bridge_ = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 5); // foundry increments a global nonce across domains + address l1Bridge_ = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 5); l2Domain.selectFork(); L2TokenBridgeInstance memory l2BridgeInstance = TokenBridgeDeploy.deployL2Bridge({ deployer: address(this), @@ -140,8 +141,6 @@ contract IntegrationTest is DssTest { } function testDeposit() public { - _setUp(); - l1Domain.selectFork(); l1Token.approve(address(l1Bridge), 100 ether); uint256 escrowBefore = l1Token.balanceOf(escrow); From f5c545bce2e7bada9df11c23d89a8b0191ecdd54 Mon Sep 17 00:00:00 2001 From: telome <> Date: Fri, 19 Jul 2024 19:37:01 +0300 Subject: [PATCH 05/40] Add bridge unit tests --- src/Escrow.sol | 2 +- test/L1TokenBridge.t.sol | 162 ++++++++++++++++++++++++++++++++++ test/L2TokenBridge.t.sol | 164 +++++++++++++++++++++++++++++++++++ test/mocks/MessengerMock.sol | 24 +++++ 4 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 test/L1TokenBridge.t.sol create mode 100644 test/L2TokenBridge.t.sol create mode 100644 test/mocks/MessengerMock.sol diff --git a/src/Escrow.sol b/src/Escrow.sol index e933be4..7edc697 100644 --- a/src/Escrow.sol +++ b/src/Escrow.sol @@ -61,7 +61,7 @@ contract Escrow { // --- approve --- function approve(address token, address spender, uint256 value) external auth { - emit Approve(token, spender, value); GemLike(token).approve(spender, value); + emit Approve(token, spender, value); } } diff --git a/test/L1TokenBridge.t.sol b/test/L1TokenBridge.t.sol new file mode 100644 index 0000000..3b415d3 --- /dev/null +++ b/test/L1TokenBridge.t.sol @@ -0,0 +1,162 @@ +// 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 { L1TokenBridge } from "src/L1TokenBridge.sol"; +import { GemMock } from "test/mocks/GemMock.sol"; +import { MessengerMock } from "test/mocks/MessengerMock.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 + ); + + GemMock l1Token; + address l2Token = address(0x222); + L1TokenBridge bridge; + address escrow = address(0xeee); + address otherBridge = address(0xccc); + MessengerMock messenger; + + function setUp() public { + messenger = new MessengerMock(otherBridge); + bridge = new L1TokenBridge(otherBridge, escrow, address(messenger)); + 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 testConstructor() public { + vm.expectEmit(true, true, true, true); + emit Rely(address(this)); + L1TokenBridge b = new L1TokenBridge(address(111), address(222), address(333)); + + assertEq(b.isOpen(), 1); + assertEq(b.otherBridge(), address(111)); + assertEq(b.escrow(), address(222)); + assertEq(address(b.messenger()), address(333)); + assertEq(b.wards(address(this)), 1); + } + + 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 + ]); + } + + 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(this)); + vm.prank(address(0xe0a)); l1Token.approve(address(bridge), type(uint256).max); + + 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 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"); + + 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); + } +} diff --git a/test/L2TokenBridge.t.sol b/test/L2TokenBridge.t.sol new file mode 100644 index 0000000..9f1ba33 --- /dev/null +++ b/test/L2TokenBridge.t.sol @@ -0,0 +1,164 @@ +// 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 { L2TokenBridge } from "src/L2TokenBridge.sol"; +import { GemMock } from "test/mocks/GemMock.sol"; +import { MessengerMock } from "test/mocks/MessengerMock.sol"; + +contract L2TokenBridgeTest 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 + ); + + GemMock l2Token; + address l1Token = address(0x111); + L2TokenBridge bridge; + address otherBridge = address(0xccc); + address l2Router = address(0xbbb); + MessengerMock messenger; + + function setUp() public { + messenger = new MessengerMock(otherBridge); + bridge = new L2TokenBridge(otherBridge, 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)); + } + + function testConstructor() public { + vm.expectEmit(true, true, true, true); + emit Rely(address(this)); + L2TokenBridge b = new L2TokenBridge(address(111), address(222)); + + assertEq(b.isOpen(), 1); + assertEq(b.otherBridge(), address(111)); + assertEq(address(b.messenger()), address(222)); + assertEq(b.wards(address(this)), 1); + } + + 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 + ]); + } + + 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); + + 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(0xbad), address(0), 100 ether, 1_000_000, ""); + + uint256 supplyBefore = l2Token.totalSupply(); + uint256 eoaBefore = l2Token.balanceOf(address(this)); + vm.prank(address(0xe0a)); l2Token.approve(address(bridge), type(uint256).max); + + 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 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"); + + 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); + } +} diff --git a/test/mocks/MessengerMock.sol b/test/mocks/MessengerMock.sol new file mode 100644 index 0000000..a61ac24 --- /dev/null +++ b/test/mocks/MessengerMock.sol @@ -0,0 +1,24 @@ +// 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 MessengerMock { + address public xDomainMessageSender; + constructor(address xDomainMessageSender_) { xDomainMessageSender = xDomainMessageSender_; } + function sendMessage(address _target, bytes calldata _message, uint32 _minGasLimit) external payable {} +} From 99d86697f9667cab209dd9a8ca1ffe46dc75d45a Mon Sep 17 00:00:00 2001 From: telome <> Date: Mon, 22 Jul 2024 12:05:42 +0300 Subject: [PATCH 06/40] Use internal for common bridgeERC20 logic --- src/L1TokenBridge.sol | 60 +++++++++++++++++++++++++------------------ src/L2TokenBridge.sol | 60 +++++++++++++++++++++++++------------------ 2 files changed, 70 insertions(+), 50 deletions(-) diff --git a/src/L1TokenBridge.sol b/src/L1TokenBridge.sol index 4f5d5c3..4debc8c 100644 --- a/src/L1TokenBridge.sol +++ b/src/L1TokenBridge.sol @@ -116,6 +116,38 @@ contract L1TokenBridge { // -- 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. @@ -132,7 +164,7 @@ contract L1TokenBridge { bytes calldata _extraData ) external { require(msg.sender.code.length == 0, "L1TokenBridge/sender-not-eoa"); - bridgeERC20To(_localToken, _remoteToken, msg.sender, _amount, _minGasLimit, _extraData); + _initiateBridgeERC20(_localToken, _remoteToken, msg.sender, _amount, _minGasLimit, _extraData); } /// @notice Sends ERC20 tokens to a receiver's address on L2. @@ -151,30 +183,8 @@ contract L1TokenBridge { uint256 _amount, uint32 _minGasLimit, bytes calldata _extraData - ) public { - 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); - - emit ERC20BridgeInitiated(_localToken, _remoteToken, msg.sender, _to, _amount, _extraData); - - messenger.sendMessage({ - _target: address(otherBridge), - _message: abi.encodeWithSelector( - this.finalizeBridgeERC20.selector, - // 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 - }); + ) external { + _initiateBridgeERC20(_localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); } /// @notice Finalizes an ERC20 bridge on L1. Can only be triggered by the L2TokenBridge. diff --git a/src/L2TokenBridge.sol b/src/L2TokenBridge.sol index 526bde2..5510a1e 100644 --- a/src/L2TokenBridge.sol +++ b/src/L2TokenBridge.sol @@ -114,6 +114,38 @@ contract L2TokenBridge { // -- 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(_remoteToken != address(0) && l1ToL2Token[_remoteToken] == _localToken, "L2TokenBridge/invalid-token"); + + TokenLike(_localToken).burn(msg.sender, _amount); // TODO: should l2Tokens allow authed burn? + + 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. @@ -130,7 +162,7 @@ contract L2TokenBridge { bytes calldata _extraData ) external { require(msg.sender.code.length == 0, "L2TokenBridge/sender-not-eoa"); - bridgeERC20To(_localToken, _remoteToken, msg.sender, _amount, _minGasLimit, _extraData); + _initiateBridgeERC20(_localToken, _remoteToken, msg.sender, _amount, _minGasLimit, _extraData); } /// @notice Sends ERC20 tokens to a receiver's address on L1. @@ -149,30 +181,8 @@ contract L2TokenBridge { uint256 _amount, uint32 _minGasLimit, bytes calldata _extraData - ) public { - require(isOpen == 1, "L2TokenBridge/closed"); // do not allow initiating new xchain messages if bridge is closed - require(_remoteToken != address(0) && l1ToL2Token[_remoteToken] == _localToken, "L2TokenBridge/invalid-token"); - - TokenLike(_localToken).burn(msg.sender, _amount); // TODO: should l2Tokens allow authed burn? - - emit ERC20BridgeInitiated(_localToken, _remoteToken, msg.sender, _to, _amount, _extraData); - - messenger.sendMessage({ - _target: address(otherBridge), - _message: abi.encodeWithSelector( - this.finalizeBridgeERC20.selector, - // 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 - }); + ) external { + _initiateBridgeERC20(_localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); } /// @notice Finalizes an ERC20 bridge on L2. Can only be triggered by the L2TokenBridge. From 544254c4d7f75dd2afe77a85c0a3f4acf16f0e27 Mon Sep 17 00:00:00 2001 From: telome <> Date: Mon, 22 Jul 2024 12:12:09 +0300 Subject: [PATCH 07/40] Bubble up undecoded revert --- src/L2GovernanceRelay.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/L2GovernanceRelay.sol b/src/L2GovernanceRelay.sol index 5d29567..4e392b5 100644 --- a/src/L2GovernanceRelay.sol +++ b/src/L2GovernanceRelay.sol @@ -54,10 +54,10 @@ contract L2GovernanceRelay { function relay(address target, bytes calldata targetData) external onlyL1GovRelay { (bool success, bytes memory result) = target.delegatecall(targetData); if (!success) { - // Next 3 lines are based on https://ethereum.stackexchange.com/a/83577 - if (result.length < 68) revert("L2GovernanceRelay/delegatecall-error"); - assembly { result := add(result, 0x04) } - revert(abi.decode(result, (string))); + if (result.length == 0) revert("L2GovernanceRelay/delegatecall-error"); + assembly ("memory-safe") { + revert(add(32, result), mload(result)) + } } } } From 8a366da2258a685d1193e2a7b8c1617744300220 Mon Sep 17 00:00:00 2001 From: telome <> Date: Mon, 22 Jul 2024 17:48:22 +0300 Subject: [PATCH 08/40] Add gov relay unit tests --- test/L1GovernanceRelay.t.sol | 82 ++++++++++++++++++++++++++++++++++++ test/L1TokenBridge.t.sol | 32 +++++++++++++- test/L2GovernanceRelay.t.sol | 70 ++++++++++++++++++++++++++++++ test/L2TokenBridge.t.sol | 33 ++++++++++++++- test/mocks/MessengerMock.sol | 12 +++++- 5 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 test/L1GovernanceRelay.t.sol create mode 100644 test/L2GovernanceRelay.t.sol 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 index 3b415d3..8f811d5 100644 --- a/test/L1TokenBridge.t.sol +++ b/test/L1TokenBridge.t.sol @@ -43,6 +43,13 @@ contract L1TokenBridgeTest is DssTest { uint256 amount, bytes extraData ); + event SentMessage( + address indexed target, + address sender, + bytes message, + uint256 messageNonce, + uint256 gasLimit + ); GemMock l1Token; address l2Token = address(0x222); @@ -52,7 +59,8 @@ contract L1TokenBridgeTest is DssTest { MessengerMock messenger; function setUp() public { - messenger = new MessengerMock(otherBridge); + messenger = new MessengerMock(); + messenger.setXDomainMessageSender(otherBridge); bridge = new L1TokenBridge(otherBridge, escrow, address(messenger)); l1Token = new GemMock(1_000_000 ether); l1Token.transfer(address(0xe0a), 500_000 ether); @@ -128,6 +136,14 @@ contract L1TokenBridgeTest is DssTest { uint256 eoaBefore = l1Token.balanceOf(address(this)); 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"); @@ -138,6 +154,14 @@ contract L1TokenBridgeTest is DssTest { 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"); @@ -150,6 +174,12 @@ contract L1TokenBridgeTest is DssTest { 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); diff --git a/test/L2GovernanceRelay.t.sol b/test/L2GovernanceRelay.t.sol new file mode 100644 index 0000000..5baa1e1 --- /dev/null +++ b/test/L2GovernanceRelay.t.sol @@ -0,0 +1,70 @@ +// 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(); + messenger.setXDomainMessageSender(l1GovRelay); + 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 { + vm.expectRevert("L2GovernanceRelay/not-from-l1-gov-relay"); + relay.relay(spell, abi.encodeCall(L2SpellMock.exec, ())); + + messenger.setXDomainMessageSender(address(0)); + + vm.expectRevert("L2GovernanceRelay/not-from-l1-gov-relay"); + vm.prank(address(messenger)); relay.relay(spell, abi.encodeCall(L2SpellMock.exec, ())); + + 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 index 9f1ba33..62efe0c 100644 --- a/test/L2TokenBridge.t.sol +++ b/test/L2TokenBridge.t.sol @@ -43,6 +43,13 @@ contract L2TokenBridgeTest is DssTest { uint256 amount, bytes extraData ); + event SentMessage( + address indexed target, + address sender, + bytes message, + uint256 messageNonce, + uint256 gasLimit + ); GemMock l2Token; address l1Token = address(0x111); @@ -52,7 +59,8 @@ contract L2TokenBridgeTest is DssTest { MessengerMock messenger; function setUp() public { - messenger = new MessengerMock(otherBridge); + messenger = new MessengerMock(); + messenger.setXDomainMessageSender(otherBridge); bridge = new L2TokenBridge(otherBridge, address(messenger)); l2Token = new GemMock(1_000_000 ether); l2Token.transfer(address(0xe0a), 500_000 ether); @@ -100,7 +108,6 @@ contract L2TokenBridgeTest is DssTest { 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); @@ -129,6 +136,14 @@ contract L2TokenBridgeTest is DssTest { uint256 eoaBefore = l2Token.balanceOf(address(this)); 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"); @@ -139,6 +154,14 @@ contract L2TokenBridgeTest is DssTest { 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"); @@ -150,7 +173,13 @@ contract L2TokenBridgeTest is DssTest { 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(); diff --git a/test/mocks/MessengerMock.sol b/test/mocks/MessengerMock.sol index a61ac24..e744690 100644 --- a/test/mocks/MessengerMock.sol +++ b/test/mocks/MessengerMock.sol @@ -19,6 +19,14 @@ pragma solidity ^0.8.21; contract MessengerMock { address public xDomainMessageSender; - constructor(address xDomainMessageSender_) { xDomainMessageSender = xDomainMessageSender_; } - function sendMessage(address _target, bytes calldata _message, uint32 _minGasLimit) external payable {} + + 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 { + emit SentMessage(_target, msg.sender, _message, 0, _minGasLimit); + } } From 2c39c493172e2b5ba898779058ea83a86f47d97e Mon Sep 17 00:00:00 2001 From: telome <> Date: Mon, 22 Jul 2024 17:57:03 +0300 Subject: [PATCH 09/40] Add escrow unit tests --- test/Escrow.t.sol | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 test/Escrow.t.sol 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); + } +} From f071a37bae7c63c582fdfc6b0df63f93df586520 Mon Sep 17 00:00:00 2001 From: telome <> Date: Tue, 23 Jul 2024 17:57:57 +0300 Subject: [PATCH 10/40] Test paused withdraw --- lib/dss-test | 2 +- test/Integration.t.sol | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/lib/dss-test b/lib/dss-test index 41066f6..e130242 160000 --- a/lib/dss-test +++ b/lib/dss-test @@ -1 +1 @@ -Subproject commit 41066f6d18202c61208d8cf09b38532a6f5b0d0a +Subproject commit e130242a00a4c1e7936d7ef761454c545f880cde diff --git a/test/Integration.t.sol b/test/Integration.t.sol index eac6035..3adac32 100644 --- a/test/Integration.t.sol +++ b/test/Integration.t.sol @@ -30,6 +30,17 @@ import { L1TokenBridge } from "src/L1TokenBridge.sol"; import { L2TokenBridge } from "src/L2TokenBridge.sol"; import { GemMock } from "test/mocks/GemMock.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; @@ -178,8 +189,36 @@ contract IntegrationTest is DssTest { 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); + } } From 43bcbb06609d2f02b88359c9a183bbf06b10d161 Mon Sep 17 00:00:00 2001 From: telome <> Date: Wed, 24 Jul 2024 15:25:39 +0300 Subject: [PATCH 11/40] Add Deploy.s.sol --- .env.example | 12 ++ README.md | 31 ++++ deploy/TokenBridgeDeploy.sol | 4 +- deploy/mocks/ChainLog.sol | 157 +++++++++++++++++++ foundry.toml | 6 + script/Deploy.s.sol | 162 ++++++++++++++++++++ script/output/1/deployed-latest.json | 1 + script/output/11155111/deployed-latest.json | 20 +++ test/Integration.t.sol | 4 +- 9 files changed, 393 insertions(+), 4 deletions(-) create mode 100644 .env.example create mode 100644 deploy/mocks/ChainLog.sol create mode 100644 script/Deploy.s.sol create mode 100644 script/output/1/deployed-latest.json create mode 100644 script/output/11155111/deployed-latest.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cc1b8bb --- /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 ETH_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/README.md b/README.md index e69de29..dd33d20 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,31 @@ +## 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 + +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 +``` diff --git a/deploy/TokenBridgeDeploy.sol b/deploy/TokenBridgeDeploy.sol index acc6988..ee3b6a1 100644 --- a/deploy/TokenBridgeDeploy.sol +++ b/deploy/TokenBridgeDeploy.sol @@ -28,7 +28,7 @@ import { L1TokenBridge } from "src/L1TokenBridge.sol"; import { L2TokenBridge } from "src/L2TokenBridge.sol"; library TokenBridgeDeploy { - function deployL1Bridge( + function deployL1( address deployer, address owner, address l2GovRelay, @@ -43,7 +43,7 @@ library TokenBridgeDeploy { ScriptTools.switchOwner(l1BridgeInstance.bridge, deployer, owner); } - function deployL2Bridge( + function deployL2( address deployer, address l1GovRelay, address l1Bridge, diff --git a/deploy/mocks/ChainLog.sol b/deploy/mocks/ChainLog.sol new file mode 100644 index 0000000..c399619 --- /dev/null +++ b/deploy/mocks/ChainLog.sol @@ -0,0 +1,157 @@ +/** + *Submitted for verification at Etherscan.io on 2020-10-09 +*/ + +// 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 63de96e..dda1b1a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,3 +8,9 @@ fs_permissions = [ { access = "read", path = "./out/"}, { access = "read-write", path = "./script/output/"} ] + +[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/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..021eff5 --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,162 @@ +// 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"; + +// TODO: Add to dss-test/ScriptTools.sol +library ScriptToolsExtended { + VmSafe private constant vm = VmSafe(address(uint160(uint256(keccak256("hevm cheat code"))))); + function exportContracts(string memory name, string memory label, address[] memory addr) internal { + name = vm.envOr("FOUNDRY_EXPORTS_NAME", name); + string memory json = vm.serializeAddress(ScriptTools.EXPORT_JSON_KEY, label, addr); + ScriptTools._doExport(name, json); + } +} + +// TODO: Add to dss-test/domains/Domain.sol +library DomainExtended { + using stdJson for string; + function hasConfigKey(Domain domain, string memory key) internal view returns (bool) { + bytes memory raw = domain.config().parseRaw(string.concat(".domains.", domain.details().chainAlias, ".", key)); + return raw.length > 0; + } + function readConfigAddresses(Domain domain, string memory key) internal view returns (address[] memory) { + return domain.config().readAddressArray(string.concat(".domains.", domain.details().chainAlias, ".", key)); + } +} + +contract Deploy is Script { + using DomainExtended for Domain; + + 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) + 1); + + // 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", "l2Bridge", l2Bridge); + ScriptTools.exportContract("deployed", "l2BridgeSpell", l2BridgeInstance.spell); + ScriptToolsExtended.exportContracts("deployed", "l1Tokens", l1Tokens); + ScriptToolsExtended.exportContracts("deployed", "l2Tokens", l2Tokens); + } +} 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..f483d68 --- /dev/null +++ b/script/output/11155111/deployed-latest.json @@ -0,0 +1,20 @@ +{ + "chainlog": "0xb236F4788B22b4Aac8eA5c9D19Ae005Acd2d4600", + "escrow": "0x0B158315399e471A3a86CEd62EecCA1595A19758", + "l1Bridge": "0xB4911bcADAeC5433Ab0e5881FB7C270Ed472eB23", + "l1GovRelay": "0x69DC88AD4E06E35C408DFA607c61618a98e0Bd95", + "l1Messenger": "0xC34855F4De64F1840e5686e64278da901e261f20", + "l1Tokens": [ + "0x0F39Ef97815b3e2a662DB6F8aA27aB0E37a99A84", + "0x82A905b58b326e5b4bA0D3cBc4c6a6F559b3C766" + ], + "l2Bridge": "0xD5C97e3A28a4725fb1A37E2b7386287205abe03e", + "l2BridgeSpell": "0xD433160F49136d137305DbfeC2EF824eA9CB4B09", + "l2GovRelay": "0xc63E1400c0B7B32Eb6416B7768a8449A6Ff9aee1", + "l2Messenger": "0x4200000000000000000000000000000000000007", + "l2Tokens": [ + "0x6198E812d02184B054165A035eECEdc2882D1573", + "0x5b7C7f76ac36d01853d37378e98c15E89d586B1a" + ], + "owner": "0x8aD7ce270a5c53541d8A7be460fC42F31D5D51EB" +} \ No newline at end of file diff --git a/test/Integration.t.sol b/test/Integration.t.sol index 3adac32..45d74b5 100644 --- a/test/Integration.t.sol +++ b/test/Integration.t.sol @@ -84,7 +84,7 @@ contract IntegrationTest is DssTest { address l1GovRelay_ = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 3); // foundry increments a global nonce across domains address l1Bridge_ = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 5); l2Domain.selectFork(); - L2TokenBridgeInstance memory l2BridgeInstance = TokenBridgeDeploy.deployL2Bridge({ + L2TokenBridgeInstance memory l2BridgeInstance = TokenBridgeDeploy.deployL2({ deployer: address(this), l1GovRelay: l1GovRelay_, l1Bridge: l1Bridge_, @@ -95,7 +95,7 @@ contract IntegrationTest is DssTest { assertEq(address(L2TokenBridgeSpell(l2BridgeInstance.spell).l2Bridge()), address(l2Bridge)); l1Domain.selectFork(); - L1TokenBridgeInstance memory l1BridgeInstance = TokenBridgeDeploy.deployL1Bridge({ + L1TokenBridgeInstance memory l1BridgeInstance = TokenBridgeDeploy.deployL1({ deployer: address(this), owner: PAUSE_PROXY, l2GovRelay: l2BridgeInstance.govRelay, From f0ffd356edbcdcfdb82c0e350b3690de3871990c Mon Sep 17 00:00:00 2001 From: telome <> Date: Wed, 24 Jul 2024 18:18:45 +0300 Subject: [PATCH 12/40] Add testnet init script --- README.md | 10 ++- deploy/TokenBridgeInit.sol | 2 +- script/Init.s.sol | 74 +++++++++++++++++++++ script/input/1/config.json | 5 +- script/input/11155111/config.json | 5 +- script/output/11155111/deployed-latest.json | 2 +- 6 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 script/Init.s.sol diff --git a/README.md b/README.md index dd33d20..3531468 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,18 @@ L2=base_sepolia ### Deploy the bridge -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. +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 +``` diff --git a/deploy/TokenBridgeInit.sol b/deploy/TokenBridgeInit.sol index 664d73c..87ae05a 100644 --- a/deploy/TokenBridgeInit.sol +++ b/deploy/TokenBridgeInit.sol @@ -57,7 +57,7 @@ struct BridgesConfig { library TokenBridgeInit { function initBridges( - DssInstance memory dss, + DssInstance memory dss, L1TokenBridgeInstance memory l1BridgeInstance, L2TokenBridgeInstance memory l2BridgeInstance, BridgesConfig memory cfg diff --git a/script/Init.s.sol b/script/Init.s.sol new file mode 100644 index 0000000..fa078a3 --- /dev/null +++ b/script/Init.s.sol @@ -0,0 +1,74 @@ +// 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.minGasLimit = 100_000; + cfg.govRelayCLKey = l2Domain.readConfigBytes32FromString("govRelayCLKey"); + cfg.escrowCLKey = l2Domain.readConfigBytes32FromString("escrowCLKey"); + cfg.l1BridgeCLKey = l2Domain.readConfigBytes32FromString("l1BridgeCLKey"); + + L1TokenBridgeInstance memory l1BridgeInstance = L1TokenBridgeInstance({ + govRelay: deps.readAddress(".l1GovRelay"), + escrow: deps.readAddress(".escrow"), + bridge: deps.readAddress(".l1Bridge") + }); + L2TokenBridgeInstance memory l2BridgeInstance = L2TokenBridgeInstance({ + govRelay: deps.readAddress(".l2GovRelay"), + spell: deps.readAddress(".l2BridgeSpell"), + bridge: deps.readAddress(".l2Bridge") + }); + + vm.startBroadcast(l1PrivKey); + TokenBridgeInit.initBridges(dss, l1BridgeInstance, l2BridgeInstance, cfg); + vm.stopBroadcast(); + } +} diff --git a/script/input/1/config.json b/script/input/1/config.json index c41272f..ffec488 100644 --- a/script/input/1/config.json +++ b/script/input/1/config.json @@ -7,7 +7,10 @@ "base": { "l1Messenger": "0x866E82a600A1414e583f7F13623F1aC5d58b0Afa", "l2Messenger": "0x4200000000000000000000000000000000000007", - "tokens": [] + "tokens": [], + "govRelayCLKey": "BASE_GOV_RELAY", + "escrowCLKey": "BASE_ESCROW", + "l1BridgeCLKey": "BASE_TOKEN_BRIDGE" } } } diff --git a/script/input/11155111/config.json b/script/input/11155111/config.json index d4201b7..c61e2c7 100644 --- a/script/input/11155111/config.json +++ b/script/input/11155111/config.json @@ -3,7 +3,10 @@ "sepolia": {}, "base_sepolia": { "l1Messenger": "0xC34855F4De64F1840e5686e64278da901e261f20", - "l2Messenger": "0x4200000000000000000000000000000000000007" + "l2Messenger": "0x4200000000000000000000000000000000000007", + "govRelayCLKey": "BASE_GOV_RELAY", + "escrowCLKey": "BASE_ESCROW", + "l1BridgeCLKey": "BASE_TOKEN_BRIDGE" } } } diff --git a/script/output/11155111/deployed-latest.json b/script/output/11155111/deployed-latest.json index f483d68..50f4265 100644 --- a/script/output/11155111/deployed-latest.json +++ b/script/output/11155111/deployed-latest.json @@ -17,4 +17,4 @@ "0x5b7C7f76ac36d01853d37378e98c15E89d586B1a" ], "owner": "0x8aD7ce270a5c53541d8A7be460fC42F31D5D51EB" -} \ No newline at end of file +} From 12dc476f765ad0c90b530be5c3501a4813d2442a Mon Sep 17 00:00:00 2001 From: telome <> Date: Wed, 24 Jul 2024 18:54:26 +0300 Subject: [PATCH 13/40] Add deposit/withdraw scripts --- README.md | 16 +++++++++ script/Deposit.s.sol | 73 +++++++++++++++++++++++++++++++++++++++++ script/Withdraw.s.sol | 76 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 script/Deposit.s.sol create mode 100644 script/Withdraw.s.sol diff --git a/README.md b/README.md index 3531468..82be0a6 100644 --- a/README.md +++ b/README.md @@ -37,3 +37,19 @@ On mainnet, the bridge should be initialized via the spell process. Importantly, ``` 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: + +``` +forge script script/Deposit.s.sol:Deposit --slow --multi --broadcast +``` + +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/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/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/ + } +} From 3c464fd7ca419c3292451e046960652dedfd1979 Mon Sep 17 00:00:00 2001 From: telome <> Date: Mon, 5 Aug 2024 18:59:25 +0200 Subject: [PATCH 14/40] Complete README --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 82be0a6..c82b8c9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,45 @@ +# 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 + +### 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. + +### 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. + +## Upgrades + +### Upgrade to a new bridge (and deprecate this bridge) + +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. + ## Deployment ### Declare env variables From b78c00629726697b955deba15312f9f9077473d5 Mon Sep 17 00:00:00 2001 From: telome <> Date: Tue, 6 Aug 2024 14:24:16 +0200 Subject: [PATCH 15/40] Add minGasLimit bound check --- deploy/TokenBridgeInit.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/TokenBridgeInit.sol b/deploy/TokenBridgeInit.sol index 87ae05a..73e9911 100644 --- a/deploy/TokenBridgeInit.sol +++ b/deploy/TokenBridgeInit.sol @@ -74,6 +74,7 @@ library TokenBridgeInit { 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.minGasLimit <= 1_000_000_000, "TokenBridgeInit/min-gas-limit-out-of-bounds"); for (uint256 i; i < cfg.l1Tokens.length; ++i) { (address l1Token, address l2Token) = (cfg.l1Tokens[i], cfg.l2Tokens[i]); From e98b020fa41b2fe647adbb4c937769b75064a5ce Mon Sep 17 00:00:00 2001 From: telome <> Date: Tue, 20 Aug 2024 12:55:53 +0200 Subject: [PATCH 16/40] Update dss-test --- lib/dss-test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dss-test b/lib/dss-test index e130242..a99c543 160000 --- a/lib/dss-test +++ b/lib/dss-test @@ -1 +1 @@ -Subproject commit e130242a00a4c1e7936d7ef761454c545f880cde +Subproject commit a99c543178d058484dc030c3d6f9bd3c6a79a022 From 2ec7b5606d4c182ef7c7aecb809e7a232be8be2f Mon Sep 17 00:00:00 2001 From: telome <130504305+telome@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:37:05 +0200 Subject: [PATCH 17/40] Update deploy/mocks/ChainLog.sol Co-authored-by: sunbreak1211 <129470872+sunbreak1211@users.noreply.github.com> --- deploy/mocks/ChainLog.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/deploy/mocks/ChainLog.sol b/deploy/mocks/ChainLog.sol index c399619..0b48909 100644 --- a/deploy/mocks/ChainLog.sol +++ b/deploy/mocks/ChainLog.sol @@ -1,7 +1,3 @@ -/** - *Submitted for verification at Etherscan.io on 2020-10-09 -*/ - // SPDX-License-Identifier: AGPL-3.0-or-later /// ChainLog.sol - An on-chain governance-managed contract registry From a2b29e27b4938c20e12599dead068c3b3d226896 Mon Sep 17 00:00:00 2001 From: telome <> Date: Tue, 20 Aug 2024 15:25:02 +0200 Subject: [PATCH 18/40] Rearrange L2GovRelay test --- test/L2GovernanceRelay.t.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/L2GovernanceRelay.t.sol b/test/L2GovernanceRelay.t.sol index 5baa1e1..132d39f 100644 --- a/test/L2GovernanceRelay.t.sol +++ b/test/L2GovernanceRelay.t.sol @@ -36,7 +36,6 @@ contract L2GovernanceRelayTest is DssTest { function setUp() public { messenger = new MessengerMock(); - messenger.setXDomainMessageSender(l1GovRelay); relay = new L2GovernanceRelay(l1GovRelay, address(messenger)); spell = address(new L2SpellMock()); } @@ -49,13 +48,15 @@ contract L2GovernanceRelayTest is DssTest { } function testRelay() public { + messenger.setXDomainMessageSender(l1GovRelay); + vm.expectRevert("L2GovernanceRelay/not-from-l1-gov-relay"); - relay.relay(spell, abi.encodeCall(L2SpellMock.exec, ())); + 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, ())); + vm.prank(address(messenger)); relay.relay(spell, abi.encodeCall(L2SpellMock.exec, ())); // revert due to wrong xDomainMessageSender messenger.setXDomainMessageSender(l1GovRelay); From 9d82c7c25b3c856872108c5fa95aa307d8c0904f Mon Sep 17 00:00:00 2001 From: telome <130504305+telome@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:05:22 +0200 Subject: [PATCH 19/40] Update test/L1TokenBridge.t.sol Co-authored-by: sunbreak1211 <129470872+sunbreak1211@users.noreply.github.com> --- test/L1TokenBridge.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/L1TokenBridge.t.sol b/test/L1TokenBridge.t.sol index 8f811d5..9ebf688 100644 --- a/test/L1TokenBridge.t.sol +++ b/test/L1TokenBridge.t.sol @@ -133,7 +133,7 @@ contract L1TokenBridgeTest is DssTest { 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(this)); + uint256 eoaBefore = l1Token.balanceOf(address(0xe0a)); vm.prank(address(0xe0a)); l1Token.approve(address(bridge), type(uint256).max); vm.expectEmit(true, true, true, true); From a9afb22be4ae38d0ade2946fe14a0bb2863727eb Mon Sep 17 00:00:00 2001 From: telome <130504305+telome@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:52:33 +0200 Subject: [PATCH 20/40] Update test/Integration.t.sol Co-authored-by: sunbreak1211 <129470872+sunbreak1211@users.noreply.github.com> --- test/Integration.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Integration.t.sol b/test/Integration.t.sol index 45d74b5..05bd0ed 100644 --- a/test/Integration.t.sol +++ b/test/Integration.t.sol @@ -98,7 +98,7 @@ contract IntegrationTest is DssTest { L1TokenBridgeInstance memory l1BridgeInstance = TokenBridgeDeploy.deployL1({ deployer: address(this), owner: PAUSE_PROXY, - l2GovRelay: l2BridgeInstance.govRelay, + l2GovRelay: l2GovRelay l2Bridge: address(l2Bridge), l1Messenger: L1_MESSENGER }); From 0f71468c43bb6b80eda2feb1d5b294def51ef0d3 Mon Sep 17 00:00:00 2001 From: telome <> Date: Tue, 20 Aug 2024 17:53:40 +0200 Subject: [PATCH 21/40] Add , --- test/Integration.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Integration.t.sol b/test/Integration.t.sol index 05bd0ed..c381c21 100644 --- a/test/Integration.t.sol +++ b/test/Integration.t.sol @@ -98,7 +98,7 @@ contract IntegrationTest is DssTest { L1TokenBridgeInstance memory l1BridgeInstance = TokenBridgeDeploy.deployL1({ deployer: address(this), owner: PAUSE_PROXY, - l2GovRelay: l2GovRelay + l2GovRelay: l2GovRelay, l2Bridge: address(l2Bridge), l1Messenger: L1_MESSENGER }); From 1e289a396b93bdfe86420b9cf1c3e1b11597194c Mon Sep 17 00:00:00 2001 From: telome <130504305+telome@users.noreply.github.com> Date: Tue, 20 Aug 2024 18:29:27 +0200 Subject: [PATCH 22/40] Update test/L2TokenBridge.t.sol Co-authored-by: sunbreak1211 <129470872+sunbreak1211@users.noreply.github.com> --- test/L2TokenBridge.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/L2TokenBridge.t.sol b/test/L2TokenBridge.t.sol index 62efe0c..2c17f9f 100644 --- a/test/L2TokenBridge.t.sol +++ b/test/L2TokenBridge.t.sol @@ -133,7 +133,7 @@ contract L2TokenBridgeTest is DssTest { vm.prank(address(0xe0a)); bridge.bridgeERC20(address(0xbad), address(0), 100 ether, 1_000_000, ""); uint256 supplyBefore = l2Token.totalSupply(); - uint256 eoaBefore = l2Token.balanceOf(address(this)); + uint256 eoaBefore = l2Token.balanceOf(address(0xe0a)); vm.prank(address(0xe0a)); l2Token.approve(address(bridge), type(uint256).max); vm.expectEmit(true, true, true, true); From 2c9c30c33d700102d7d4865622da0ba221beefc1 Mon Sep 17 00:00:00 2001 From: telome <130504305+telome@users.noreply.github.com> Date: Tue, 20 Aug 2024 18:35:53 +0200 Subject: [PATCH 23/40] Update README.md Co-authored-by: sunbreak1211 <129470872+sunbreak1211@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c82b8c9..204a8e7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The OP Token Bridge is a [custom bridge](https://docs.optimism.io/builders/app-d - `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 +- `L2GovernanceRelay.sol` - L2 side of the governance relay. ### External dependencies From b7e225239ce5901dfea4854c36dc7a078fd9f7a6 Mon Sep 17 00:00:00 2001 From: telome <130504305+telome@users.noreply.github.com> Date: Tue, 20 Aug 2024 18:48:15 +0200 Subject: [PATCH 24/40] Update README.md Co-authored-by: sunbreak1211 <129470872+sunbreak1211@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 204a8e7..23e7ee0 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The OP Token Bridge is a [custom bridge](https://docs.optimism.io/builders/app-d ### 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. +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 From ca7cfe1d4a6968d7b1695de9280af064feeeaa03 Mon Sep 17 00:00:00 2001 From: telome <130504305+telome@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:57:34 +0200 Subject: [PATCH 25/40] Update README.md Co-authored-by: sunbreak1211 <129470872+sunbreak1211@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23e7ee0..13b7314 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ To deposit a given amount of a supported token to the L2, Alice calls `bridgeERC ### 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. +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 From 603dafa86bec33754c80664e67e37a5f50e5ce24 Mon Sep 17 00:00:00 2001 From: telome <> Date: Wed, 21 Aug 2024 08:28:51 +0200 Subject: [PATCH 26/40] Fix exportContracts issue --- script/Deploy.s.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 021eff5..bf5280d 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -30,7 +30,7 @@ library ScriptToolsExtended { VmSafe private constant vm = VmSafe(address(uint160(uint256(keccak256("hevm cheat code"))))); function exportContracts(string memory name, string memory label, address[] memory addr) internal { name = vm.envOr("FOUNDRY_EXPORTS_NAME", name); - string memory json = vm.serializeAddress(ScriptTools.EXPORT_JSON_KEY, label, addr); + string memory json = vm.serializeAddress(string(abi.encodePacked(ScriptTools.EXPORT_JSON_KEY, "_", name)), label, addr); ScriptTools._doExport(name, json); } } From 687d9bda39a2b4d67adcd6526798ca3256a78836 Mon Sep 17 00:00:00 2001 From: telome <> Date: Wed, 21 Aug 2024 13:03:43 +0200 Subject: [PATCH 27/40] Use new dss-test functions --- lib/dss-test | 2 +- script/Deploy.s.sol | 28 ++-------------------------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/lib/dss-test b/lib/dss-test index a99c543..f2a2b2b 160000 --- a/lib/dss-test +++ b/lib/dss-test @@ -1 +1 @@ -Subproject commit a99c543178d058484dc030c3d6f9bd3c6a79a022 +Subproject commit f2a2b2bbea71921103c5b7cf3cb1d241b957bec7 diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index bf5280d..69f4480 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -25,31 +25,7 @@ import { TokenBridgeDeploy, L1TokenBridgeInstance, L2TokenBridgeInstance } from import { ChainLog } from "deploy/mocks/ChainLog.sol"; import { GemMock } from "test/mocks/GemMock.sol"; -// TODO: Add to dss-test/ScriptTools.sol -library ScriptToolsExtended { - VmSafe private constant vm = VmSafe(address(uint160(uint256(keccak256("hevm cheat code"))))); - function exportContracts(string memory name, string memory label, address[] memory addr) internal { - name = vm.envOr("FOUNDRY_EXPORTS_NAME", name); - string memory json = vm.serializeAddress(string(abi.encodePacked(ScriptTools.EXPORT_JSON_KEY, "_", name)), label, addr); - ScriptTools._doExport(name, json); - } -} - -// TODO: Add to dss-test/domains/Domain.sol -library DomainExtended { - using stdJson for string; - function hasConfigKey(Domain domain, string memory key) internal view returns (bool) { - bytes memory raw = domain.config().parseRaw(string.concat(".domains.", domain.details().chainAlias, ".", key)); - return raw.length > 0; - } - function readConfigAddresses(Domain domain, string memory key) internal view returns (address[] memory) { - return domain.config().readAddressArray(string.concat(".domains.", domain.details().chainAlias, ".", key)); - } -} - contract Deploy is Script { - using DomainExtended for Domain; - address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; uint256 l1PrivKey = vm.envUint("L1_PRIVATE_KEY"); @@ -156,7 +132,7 @@ contract Deploy is Script { ScriptTools.exportContract("deployed", "l1Bridge", l1Bridge); ScriptTools.exportContract("deployed", "l2Bridge", l2Bridge); ScriptTools.exportContract("deployed", "l2BridgeSpell", l2BridgeInstance.spell); - ScriptToolsExtended.exportContracts("deployed", "l1Tokens", l1Tokens); - ScriptToolsExtended.exportContracts("deployed", "l2Tokens", l2Tokens); + ScriptTools.exportContracts("deployed", "l1Tokens", l1Tokens); + ScriptTools.exportContracts("deployed", "l2Tokens", l2Tokens); } } From 22bcee78de91c8c1e5e1858961e276e142ea0cb6 Mon Sep 17 00:00:00 2001 From: telome <> Date: Wed, 21 Aug 2024 16:06:43 +0200 Subject: [PATCH 28/40] Use gas estimate multiplier for Deposit.s.sol --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 13b7314..d1123a0 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,10 @@ 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: +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 +forge script script/Deposit.s.sol:Deposit --slow --multi --broadcast --gas-estimate-multiplier 120 ``` To subsequently perform a test withdrawal, use the following command: From 4b6cd2a362eec506bfcf295c19769a933d9a49fd Mon Sep 17 00:00:00 2001 From: telome <> Date: Fri, 23 Aug 2024 13:03:39 +0200 Subject: [PATCH 29/40] Update CI --- .env.example | 2 +- .github/workflows/test.yml | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index cc1b8bb..c376036 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ export FOUNDRY_SCRIPT_DEPS=deployed export FOUNDRY_EXPORTS_OVERWRITE_LATEST=true export L1="sepolia" export L2="base_sepolia" -export ETH_RPC_URL= +export MAINNET_RPC_URL= export BASE_RPC_URL= export SEPOLIA_RPC_URL= export BASE_SEPOLIA_RPC_URL= 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 }} From bae891c15067738eae18baa4866f7972289b7841 Mon Sep 17 00:00:00 2001 From: telome <130504305+telome@users.noreply.github.com> Date: Mon, 2 Sep 2024 19:38:55 +0200 Subject: [PATCH 30/40] Address Cantina audit findings (#2) Co-authored-by: telome <> --- deploy/L2TokenBridgeSpell.sol | 2 +- src/L2TokenBridge.sol | 4 ++-- test/L2TokenBridge.t.sol | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/L2TokenBridgeSpell.sol b/deploy/L2TokenBridgeSpell.sol index 6871e1d..cb494d0 100644 --- a/deploy/L2TokenBridgeSpell.sol +++ b/deploy/L2TokenBridgeSpell.sol @@ -67,7 +67,7 @@ contract L2TokenBridgeSpell { L2GovRelayLike l2GovRelay = L2GovRelayLike(l2GovRelay_); // sanity checks - require(address(l2Bridge) == l2Bridge_, "L2TokenBridgeSpell/l2-gateway-mismatch"); + require(address(l2Bridge) == l2Bridge_, "L2TokenBridgeSpell/l2-bridge-mismatch"); require(l2Bridge.isOpen() == 1, "L2TokenBridgeSpell/not-open"); require(l2Bridge.otherBridge() == l1Bridge, "L2TokenBridgeSpell/other-bridge-mismatch"); require(l2Bridge.messenger() == l2Messenger, "L2TokenBridgeSpell/l2-bridge-messenger-mismatch"); diff --git a/src/L2TokenBridge.sol b/src/L2TokenBridge.sol index 5510a1e..666ec56 100644 --- a/src/L2TokenBridge.sol +++ b/src/L2TokenBridge.sol @@ -123,7 +123,7 @@ contract L2TokenBridge { bytes memory _extraData ) internal { require(isOpen == 1, "L2TokenBridge/closed"); // do not allow initiating new xchain messages if bridge is closed - require(_remoteToken != address(0) && l1ToL2Token[_remoteToken] == _localToken, "L2TokenBridge/invalid-token"); + require(_localToken != address(0) && l1ToL2Token[_remoteToken] == _localToken, "L2TokenBridge/invalid-token"); TokenLike(_localToken).burn(msg.sender, _amount); // TODO: should l2Tokens allow authed burn? @@ -185,7 +185,7 @@ contract L2TokenBridge { _initiateBridgeERC20(_localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); } - /// @notice Finalizes an ERC20 bridge on L2. Can only be triggered by the L2TokenBridge. + /// @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. diff --git a/test/L2TokenBridge.t.sol b/test/L2TokenBridge.t.sol index 2c17f9f..e17d528 100644 --- a/test/L2TokenBridge.t.sol +++ b/test/L2TokenBridge.t.sol @@ -130,7 +130,7 @@ contract L2TokenBridgeTest is DssTest { 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(0xbad), address(0), 100 ether, 1_000_000, ""); + vm.prank(address(0xe0a)); bridge.bridgeERC20(address(0), address(0xbad), 100 ether, 1_000_000, ""); uint256 supplyBefore = l2Token.totalSupply(); uint256 eoaBefore = l2Token.balanceOf(address(0xe0a)); From 6226a761d6813dc66d209f69ee182a1e6d951ad1 Mon Sep 17 00:00:00 2001 From: telome <130504305+telome@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:10:33 +0200 Subject: [PATCH 31/40] Add Cantina Audit Report (#3) * Add Cantina Report * Rename Cantina report --------- Co-authored-by: telome <> --- ...-report-review-makerdao-op-token-bridge.pdf | Bin 0 -> 504531 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 audit/20240909-cantina-report-review-makerdao-op-token-bridge.pdf 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 0000000000000000000000000000000000000000..28206c4a31f7814d06ce6889466be5d92451e41b GIT binary patch literal 504531 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