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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