diff --git a/.env.wsteth.mnt_goerli b/.env.wsteth.mnt_goerli new file mode 100644 index 00000000..d3e1872a --- /dev/null +++ b/.env.wsteth.mnt_goerli @@ -0,0 +1,71 @@ +# Detailed info: https://github.com/lidofinance/lido-l2#Project-Configuration + +# ############################ +# RPCs +# ############################ + +RPC_ETH_GOERLI=https://eth-goerli.g.alchemy.com/v2/ +RPC_MNT_GOERLI=https://rpc.testnet.mantle.xyz + +# ############################ +# Etherscan +# ############################ + +ETHERSCAN_API_KEY_ETH= +ETHERSCAN_API_KEY_MNT= + +# ############################ +# Bridge/Gateway Deployment +# ############################ + +# Address of the token to deploy the bridge/gateway for wstETH +TOKEN=0x6320cd32aa674d2898a68ec82e869385fc5f7e2f + +# Name of the network environments used by deployment scripts. +# Might be one of: "mainnet", "goerli". +NETWORK=goerli + +# Private key of the deployer account used for deployment process +ETH_DEPLOYER_PRIVATE_KEY= +MNT_DEPLOYER_PRIVATE_KEY= + +L1_PROXY_ADMIN=0x4333218072D5d7008546737786663c38B4D561A4 +L1_BRIDGE_ADMIN=0x4333218072D5d7008546737786663c38B4D561A4 +L1_DEPOSITS_ENABLED=true +L1_WITHDRAWALS_ENABLED=true +L1_DEPOSITS_ENABLERS=["0x4333218072D5d7008546737786663c38B4D561A4"] +L1_DEPOSITS_DISABLERS="["0x4333218072D5d7008546737786663c38B4D561A4", "0x7fE7fa4EF7D134Dbf8B616Ba7B675F26286BC2cd","0xa6688D0DcAd346eCc275cda98c91086fEC3fE31C"]" +L1_WITHDRAWALS_ENABLERS=["0x4333218072D5d7008546737786663c38B4D561A4"] +L1_WITHDRAWALS_DISABLERS="["0x4333218072D5d7008546737786663c38B4D561A4", "0x7fE7fa4EF7D134Dbf8B616Ba7B675F26286BC2cd","0xa6688D0DcAd346eCc275cda98c91086fEC3fE31C"]" + +L2_PROXY_ADMIN=0xaF3dcfddBbBC59E7d2ec6f6e4273f7F1a3C7B6fe +L2_BRIDGE_ADMIN=0xaF3dcfddBbBC59E7d2ec6f6e4273f7F1a3C7B6fe +L2_DEPOSITS_ENABLED=true +L2_WITHDRAWALS_ENABLED=true +L2_DEPOSITS_ENABLERS=["0x55C39356C714Cde16F8a80302c1Ce9DfAC6f5a35"] +L2_DEPOSITS_DISABLERS="["0x55C39356C714Cde16F8a80302c1Ce9DfAC6f5a35", "0x7fE7fa4EF7D134Dbf8B616Ba7B675F26286BC2cd"]" +L2_WITHDRAWALS_ENABLERS=["0x55C39356C714Cde16F8a80302c1Ce9DfAC6f5a35"] +L2_WITHDRAWALS_DISABLERS="["0x55C39356C714Cde16F8a80302c1Ce9DfAC6f5a35", "0x7fE7fa4EF7D134Dbf8B616Ba7B675F26286BC2cd"]" + +# ############################ +# Integration & E2E Testing +# ############################ + +TESTING_MNT_NETWORK=goerli +TESTING_MNT_L1_TOKEN=0x6320cd32aa674d2898a68ec82e869385fc5f7e2f +TESTING_MNT_L2_TOKEN=0xe8964a99d5DE7cEE2743B20113a52C953b0916E9 +TESTING_MNT_L1_ERC20_TOKEN_BRIDGE=0xDdC89Bd27F9A1C47A5c20dF0783dE52f55513598 +TESTING_MNT_L2_ERC20_TOKEN_BRIDGE=0x423702bC3Fb92f59Be440354456f0481934bF1f5 + +# ############################ +# Integration Testing +# ############################ + +TESTING_USE_DEPLOYED_CONTRACTS=true +TESTING_L1_TOKENS_HOLDER= + +# ############################ +# E2E Testing +# ############################ + +TESTING_PRIVATE_KEY= diff --git a/.env.wsteth.mnt_mainnet b/.env.wsteth.mnt_mainnet new file mode 100644 index 00000000..6bf3ae55 --- /dev/null +++ b/.env.wsteth.mnt_mainnet @@ -0,0 +1,74 @@ +# Detailed info: https://github.com/lidofinance/lido-l2#Project-Configuration + +# ############################ +# RPCs +# ############################ + +RPC_ETH_MAINNET= +RPC_OPT_MAINNET= + +# ############################ +# Etherscan +# ############################ + +ETHERSCAN_API_KEY_ETH= +ETHERSCAN_API_KEY_OPT= + +# ############################ +# Bridge/Gateway Deployment +# ############################ + +# Address of the token to deploy the bridge/gateway for +TOKEN=0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 + +# Name of the network environments used by deployment scripts. +# Might be one of: "mainnet", "goerli". +NETWORK=mainnet + +# Run deployment in the forking network instead of public ones +FORKING=true + +# Private key of the deployer account used for deployment process +ETH_DEPLOYER_PRIVATE_KEY= +OPT_DEPLOYER_PRIVATE_KEY= + +L1_PROXY_ADMIN=0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c +L1_BRIDGE_ADMIN=0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c +L1_DEPOSITS_ENABLED=false +L1_WITHDRAWALS_ENABLED=true +L1_DEPOSITS_ENABLERS=["0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c","0x3cd9F71F80AB08ea5a7Dca348B5e94BC595f26A0"] +L1_DEPOSITS_DISABLERS=["0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c","0x73b047fe6337183A454c5217241D780a932777bD"] +L1_WITHDRAWALS_ENABLERS=["0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c"] +L1_WITHDRAWALS_DISABLERS=["0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c","0x73b047fe6337183A454c5217241D780a932777bD"] + +L2_PROXY_ADMIN=0xEfa0dB536d2c8089685630fafe88CF7805966FC3 +L2_BRIDGE_ADMIN=0xEfa0dB536d2c8089685630fafe88CF7805966FC3 +L2_DEPOSITS_ENABLED=true +L2_WITHDRAWALS_ENABLED=true +L2_DEPOSITS_ENABLERS=["0xEfa0dB536d2c8089685630fafe88CF7805966FC3"] +L2_DEPOSITS_DISABLERS=["0xEfa0dB536d2c8089685630fafe88CF7805966FC3","0x4Cf8fE0A4c2539F7EFDD2047d8A5D46F14613088"] +L2_WITHDRAWALS_ENABLERS=["0xEfa0dB536d2c8089685630fafe88CF7805966FC3"] +L2_WITHDRAWALS_DISABLERS=["0xEfa0dB536d2c8089685630fafe88CF7805966FC3","0x4Cf8fE0A4c2539F7EFDD2047d8A5D46F14613088"] + +# ############################ +# Integration Acceptance & E2E Testing +# ############################ + +TESTING_OPT_NETWORK=mainnet +TESTING_OPT_L1_TOKEN=0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 +TESTING_OPT_L2_TOKEN=0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb +TESTING_OPT_L1_ERC20_TOKEN_BRIDGE=0x76943C0D61395d8F2edF9060e1533529cAe05dE6 +TESTING_OPT_L2_ERC20_TOKEN_BRIDGE=0x8E01013243a96601a86eb3153F0d9Fa4fbFb6957 + +# ############################ +# Integration Testing +# ############################ + +TESTING_USE_DEPLOYED_CONTRACTS=true +TESTING_L1_TOKENS_HOLDER= + +# ############################ +# E2E Testing +# ############################ + +TESTING_PRIVATE_KEY= diff --git a/.solcover.js b/.solcover.js index 7554c821..4d668a53 100644 --- a/.solcover.js +++ b/.solcover.js @@ -1,3 +1,3 @@ module.exports = { - skipFiles: ["stubs", "optimism/stubs", "proxy/stubs", "arbitrum/stubs"], + skipFiles: ["stubs", "optimism/stubs", "proxy/stubs", "arbitrum/stubs", "mantle/stubs"], }; diff --git a/contracts/mantle/CrossDomainEnabled.sol b/contracts/mantle/CrossDomainEnabled.sol new file mode 100644 index 00000000..0fe0e5bb --- /dev/null +++ b/contracts/mantle/CrossDomainEnabled.sol @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ICrossDomainMessenger} from "./interfaces/ICrossDomainMessenger.sol"; + +/// @dev Helper contract for contracts performing cross-domain communications +contract CrossDomainEnabled { + /// @notice Messenger contract used to send and receive messages from the other domain + ICrossDomainMessenger public immutable messenger; + + /// @param messenger_ Address of the CrossDomainMessenger on the current layer + constructor(address messenger_) { + messenger = ICrossDomainMessenger(messenger_); + } + + /// @dev Sends a message to an account on another domain + /// @param crossDomainTarget_ Intended recipient on the destination domain + /// @param message_ Data to send to the target (usually calldata to a function with + /// `onlyFromCrossDomainAccount()`) + /// @param gasLimit_ gasLimit for the receipt of the message on the target domain. + function sendCrossDomainMessage( + address crossDomainTarget_, + uint32 gasLimit_, + bytes memory message_ + ) internal { + messenger.sendMessage(crossDomainTarget_, message_, gasLimit_); + } + + /// @dev Enforces that the modified function is only callable by a specific cross-domain account + /// @param sourceDomainAccount_ The only account on the originating domain which is + /// authenticated to call this function + modifier onlyFromCrossDomainAccount(address sourceDomainAccount_) { + if (msg.sender != address(messenger)) { + revert ErrorUnauthorizedMessenger(); + } + if (messenger.xDomainMessageSender() != sourceDomainAccount_) { + revert ErrorWrongCrossDomainSender(); + } + _; + } + + error ErrorUnauthorizedMessenger(); + error ErrorWrongCrossDomainSender(); +} diff --git a/contracts/mantle/L1ERC20TokenBridge.sol b/contracts/mantle/L1ERC20TokenBridge.sol new file mode 100644 index 00000000..a4438b88 --- /dev/null +++ b/contracts/mantle/L1ERC20TokenBridge.sol @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; +import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; + +import {BridgingManager} from "../BridgingManager.sol"; +import {BridgeableTokens} from "../BridgeableTokens.sol"; +import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; + +/// @author psirex +/// @notice The L1 ERC20 token bridge locks bridged tokens on the L1 side, sends deposit messages +/// on the L2 side, and finalizes token withdrawals from L2. Additionally, adds the methods for +/// bridging management: enabling and disabling withdrawals/deposits +contract L1ERC20TokenBridge is + IL1ERC20Bridge, + BridgingManager, + BridgeableTokens, + CrossDomainEnabled +{ + using SafeERC20 for IERC20; + + /// @inheritdoc IL1ERC20Bridge + address public immutable l2TokenBridge; + + /// @param messenger_ L1 messenger address being used for cross-chain communications + /// @param l2TokenBridge_ Address of the corresponding L2 bridge + /// @param l1Token_ Address of the bridged token in the L1 chain + /// @param l2Token_ Address of the token minted on the L2 chain when token bridged + constructor( + address messenger_, + address l2TokenBridge_, + address l1Token_, + address l2Token_ + ) CrossDomainEnabled(messenger_) BridgeableTokens(l1Token_, l2Token_) { + l2TokenBridge = l2TokenBridge_; + } + + /// @inheritdoc IL1ERC20Bridge + function depositERC20( + address l1Token_, + address l2Token_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) + external + whenDepositsEnabled + onlySupportedL1Token(l1Token_) + onlySupportedL2Token(l2Token_) + { + if (Address.isContract(msg.sender)) { + revert ErrorSenderNotEOA(); + } + _initiateERC20Deposit(msg.sender, msg.sender, amount_, l2Gas_, data_); + } + + /// @inheritdoc IL1ERC20Bridge + function depositERC20To( + address l1Token_, + address l2Token_, + address to_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) + external + whenDepositsEnabled + onlyNonZeroAccount(to_) + onlySupportedL1Token(l1Token_) + onlySupportedL2Token(l2Token_) + { + _initiateERC20Deposit(msg.sender, to_, amount_, l2Gas_, data_); + } + + /// @inheritdoc IL1ERC20Bridge + function finalizeERC20Withdrawal( + address l1Token_, + address l2Token_, + address from_, + address to_, + uint256 amount_, + bytes calldata data_ + ) + external + whenWithdrawalsEnabled + onlySupportedL1Token(l1Token_) + onlySupportedL2Token(l2Token_) + onlyFromCrossDomainAccount(l2TokenBridge) + { + IERC20(l1Token_).safeTransfer(to_, amount_); + + emit ERC20WithdrawalFinalized( + l1Token_, + l2Token_, + from_, + to_, + amount_, + data_ + ); + } + + /// @dev Performs the logic for deposits by informing the L2 token bridge contract + /// of the deposit and calling safeTransferFrom to lock the L1 funds. + /// @param from_ Account to pull the deposit from on L1 + /// @param to_ Account to give the deposit to on L2 + /// @param amount_ Amount of the ERC20 to deposit. + /// @param l2Gas_ Gas limit required to complete the deposit on L2. + /// @param data_ Optional data to forward to L2. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content. + function _initiateERC20Deposit( + address from_, + address to_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) internal { + IERC20(l1Token).safeTransferFrom(from_, address(this), amount_); + + bytes memory message = abi.encodeWithSelector( + IL2ERC20Bridge.finalizeDeposit.selector, + l1Token, + l2Token, + from_, + to_, + amount_, + data_ + ); + + sendCrossDomainMessage(l2TokenBridge, l2Gas_, message); + + emit ERC20DepositInitiated( + l1Token, + l2Token, + from_, + to_, + amount_, + data_ + ); + } + + error ErrorSenderNotEOA(); +} diff --git a/contracts/mantle/L2ERC20TokenBridge.sol b/contracts/mantle/L2ERC20TokenBridge.sol new file mode 100644 index 00000000..3a41f5bc --- /dev/null +++ b/contracts/mantle/L2ERC20TokenBridge.sol @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; +import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; +import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; + +import {BridgingManager} from "../BridgingManager.sol"; +import {BridgeableTokens} from "../BridgeableTokens.sol"; +import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; + +/// @author psirex +/// @notice The L2 token bridge works with the L1 token bridge to enable ERC20 token bridging +/// between L1 and L2. It acts as a minter for new tokens when it hears about +/// deposits into the L1 token bridge. It also acts as a burner of the tokens +/// intended for withdrawal, informing the L1 bridge to release L1 funds. Additionally, adds +/// the methods for bridging management: enabling and disabling withdrawals/deposits +contract L2ERC20TokenBridge is + IL2ERC20Bridge, + BridgingManager, + BridgeableTokens, + CrossDomainEnabled +{ + /// @inheritdoc IL2ERC20Bridge + address public immutable l1TokenBridge; + + /// @param messenger_ L2 messenger address being used for cross-chain communications + /// @param l1TokenBridge_ Address of the corresponding L1 bridge + /// @param l1Token_ Address of the bridged token in the L1 chain + /// @param l2Token_ Address of the token minted on the L2 chain when token bridged + constructor( + address messenger_, + address l1TokenBridge_, + address l1Token_, + address l2Token_ + ) CrossDomainEnabled(messenger_) BridgeableTokens(l1Token_, l2Token_) { + l1TokenBridge = l1TokenBridge_; + } + + /// @inheritdoc IL2ERC20Bridge + function withdraw( + address l2Token_, + uint256 amount_, + uint32 l1Gas_, + bytes calldata data_ + ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { + if (Address.isContract(msg.sender)) { + revert ErrorSenderNotEOA(); + } + + _initiateWithdrawal(msg.sender, msg.sender, amount_, l1Gas_, data_); + } + + /// @inheritdoc IL2ERC20Bridge + function withdrawTo( + address l2Token_, + address to_, + uint256 amount_, + uint32 l1Gas_, + bytes calldata data_ + ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { + _initiateWithdrawal(msg.sender, to_, amount_, l1Gas_, data_); + } + + /// @inheritdoc IL2ERC20Bridge + function finalizeDeposit( + address l1Token_, + address l2Token_, + address from_, + address to_, + uint256 amount_, + bytes calldata data_ + ) + external + whenDepositsEnabled + onlySupportedL1Token(l1Token_) + onlySupportedL2Token(l2Token_) + onlyFromCrossDomainAccount(l1TokenBridge) + { + IERC20Bridged(l2Token_).bridgeMint(to_, amount_); + emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); + } + + /// @notice Performs the logic for withdrawals by burning the token and informing + /// the L1 token Gateway of the withdrawal + /// @param from_ Account to pull the withdrawal from on L2 + /// @param to_ Account to give the withdrawal to on L1 + /// @param amount_ Amount of the token to withdraw + /// @param l1Gas_ Unused, but included for potential forward compatibility considerations + /// @param data_ Optional data to forward to L1. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content + function _initiateWithdrawal( + address from_, + address to_, + uint256 amount_, + uint32 l1Gas_, + bytes calldata data_ + ) internal { + IERC20Bridged(l2Token).bridgeBurn(from_, amount_); + + bytes memory message = abi.encodeWithSelector( + IL1ERC20Bridge.finalizeERC20Withdrawal.selector, + l1Token, + l2Token, + from_, + to_, + amount_, + data_ + ); + + sendCrossDomainMessage(l1TokenBridge, l1Gas_, message); + + emit WithdrawalInitiated(l1Token, l2Token, from_, to_, amount_, data_); + } + + error ErrorSenderNotEOA(); +} + diff --git a/contracts/mantle/interfaces/ICrossDomainMessenger.sol b/contracts/mantle/interfaces/ICrossDomainMessenger.sol new file mode 100644 index 00000000..4c6736ca --- /dev/null +++ b/contracts/mantle/interfaces/ICrossDomainMessenger.sol @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +interface ICrossDomainMessenger { + function xDomainMessageSender() external view returns (address); + + /// Sends a cross domain message to the target messenger. + /// @param _target Target contract address. + /// @param _message Message to send to the target. + /// @param _gasLimit Gas limit for the provided message. + function sendMessage( + address _target, + bytes calldata _message, + uint32 _gasLimit + ) external; +} diff --git a/contracts/mantle/interfaces/IL1ERC20Bridge.sol b/contracts/mantle/interfaces/IL1ERC20Bridge.sol new file mode 100644 index 00000000..9cb012bd --- /dev/null +++ b/contracts/mantle/interfaces/IL1ERC20Bridge.sol @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @notice The L1 Standard bridge locks bridged tokens on the L1 side, sends deposit messages +/// on the L2 side, and finalizes token withdrawals from L2. +interface IL1ERC20Bridge { + event ERC20DepositInitiated( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + event ERC20WithdrawalFinalized( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + /// @notice get the address of the corresponding L2 bridge contract. + /// @return Address of the corresponding L2 bridge contract. + function l2TokenBridge() external returns (address); + + /// @notice deposit an amount of the ERC20 to the caller's balance on L2. + /// @param l1Token_ Address of the L1 ERC20 we are depositing + /// @param l2Token_ Address of the L1 respective L2 ERC20 + /// @param amount_ Amount of the ERC20 to deposit + /// @param l2Gas_ Gas limit required to complete the deposit on L2. + /// @param data_ Optional data to forward to L2. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content. + function depositERC20( + address l1Token_, + address l2Token_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) external; + + /// @notice deposit an amount of ERC20 to a recipient's balance on L2. + /// @param l1Token_ Address of the L1 ERC20 we are depositing + /// @param l2Token_ Address of the L1 respective L2 ERC20 + /// @param to_ L2 address to credit the withdrawal to. + /// @param amount_ Amount of the ERC20 to deposit. + /// @param l2Gas_ Gas limit required to complete the deposit on L2. + /// @param data_ Optional data to forward to L2. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content. + function depositERC20To( + address l1Token_, + address l2Token_, + address to_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) external; + + /// @notice Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the + /// L1 ERC20 token. + /// @dev This call will fail if the initialized withdrawal from L2 has not been finalized. + /// @param l1Token_ Address of L1 token to finalizeWithdrawal for. + /// @param l2Token_ Address of L2 token where withdrawal was initiated. + /// @param from_ L2 address initiating the transfer. + /// @param to_ L1 address to credit the withdrawal to. + /// @param amount_ Amount of the ERC20 to deposit. + /// @param data_ Data provided by the sender on L2. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content. + function finalizeERC20Withdrawal( + address l1Token_, + address l2Token_, + address from_, + address to_, + uint256 amount_, + bytes calldata data_ + ) external; +} diff --git a/contracts/mantle/interfaces/IL2ERC20Bridge.sol b/contracts/mantle/interfaces/IL2ERC20Bridge.sol new file mode 100644 index 00000000..448dfb8b --- /dev/null +++ b/contracts/mantle/interfaces/IL2ERC20Bridge.sol @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @notice The L2 token bridge works with the L1 token bridge to enable ERC20 token bridging +/// between L1 and L2. It acts as a minter for new tokens when it hears about +/// deposits into the L1 token bridge. It also acts as a burner of the tokens +/// intended for withdrawal, informing the L1 bridge to release L1 funds. +interface IL2ERC20Bridge { + event WithdrawalInitiated( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + event DepositFinalized( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + event DepositFailed( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + /// @notice Returns the address of the corresponding L1 bridge contract + function l1TokenBridge() external returns (address); + + /// @notice Initiates a withdraw of some tokens to the caller's account on L1 + /// @param l2Token_ Address of L2 token where withdrawal was initiated. + /// @param amount_ Amount of the token to withdraw. + /// @param l1Gas_ Unused, but included for potential forward compatibility considerations. + /// @param data_ Optional data to forward to L1. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content. + function withdraw( + address l2Token_, + uint256 amount_, + uint32 l1Gas_, + bytes calldata data_ + ) external; + + /// @notice Initiates a withdraw of some token to a recipient's account on L1. + /// @param l2Token_ Address of L2 token where withdrawal is initiated. + /// @param to_ L1 adress to credit the withdrawal to. + /// @param amount_ Amount of the token to withdraw. + /// @param l1Gas_ Unused, but included for potential forward compatibility considerations. + /// @param data_ Optional data to forward to L1. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content. + function withdrawTo( + address l2Token_, + address to_, + uint256 amount_, + uint32 l1Gas_, + bytes calldata data_ + ) external; + + /// @notice Completes a deposit from L1 to L2, and credits funds to the recipient's balance of + /// this L2 token. This call will fail if it did not originate from a corresponding deposit + /// in L1StandardTokenBridge. + /// @param l1Token_ Address for the l1 token this is called with + /// @param l2Token_ Address for the l2 token this is called with + /// @param from_ Account to pull the deposit from on L2. + /// @param to_ Address to receive the withdrawal at + /// @param amount_ Amount of the token to withdraw + /// @param data_ Data provider by the sender on L1. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content. + function finalizeDeposit( + address l1Token_, + address l2Token_, + address from_, + address to_, + uint256 amount_, + bytes calldata data_ + ) external; +} diff --git a/contracts/mantle/stubs/CrossDomainMessengerStub.sol b/contracts/mantle/stubs/CrossDomainMessengerStub.sol new file mode 100644 index 00000000..f2d30805 --- /dev/null +++ b/contracts/mantle/stubs/CrossDomainMessengerStub.sol @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ICrossDomainMessenger} from "../interfaces/ICrossDomainMessenger.sol"; + +contract CrossDomainMessengerStub is ICrossDomainMessenger { + address public xDomainMessageSender; + uint256 public messageNonce; + + constructor() payable {} + + function setXDomainMessageSender(address value) external { + xDomainMessageSender = value; + } + + function sendMessage( + address _target, + bytes calldata _message, + uint32 _gasLimit + ) external { + messageNonce += 1; + emit SentMessage( + _target, + msg.sender, + _message, + messageNonce, + _gasLimit + ); + } + + function relayMessage( + address _target, + address, // sender + bytes memory _message, + uint256 // _messageNonce + ) public { + (bool success, ) = _target.call(_message); + require(success, "CALL_FAILED"); + } + + event SentMessage( + address indexed target, + address sender, + bytes message, + uint256 messageNonce, + uint256 gasLimit + ); +} diff --git a/contracts/token/ERC20BridgedPermit.sol b/contracts/token/ERC20BridgedPermit.sol new file mode 100644 index 00000000..ee45c3d1 --- /dev/null +++ b/contracts/token/ERC20BridgedPermit.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ERC20Bridged} from "./ERC20Bridged.sol"; +import {IERC2612} from "@openzeppelin/contracts/interfaces/draft-IERC2612.sol"; + +/// @author 0xMantle +/// @notice Extends the ERC20 functionality that allows the bridge to mint/burn tokens +contract ERC20BridgedPermit is ERC20Bridged, IERC2612 { + + bytes32 private immutable _cachedDomainSeparator; + uint256 private immutable _cachedChainId; + address private immutable _cachedThis; + + mapping(address => uint256) public override nonces; + + bytes32 public immutable PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + constructor(string memory name_, string memory symbol_, uint8 decimals_, address bridge_) + ERC20Bridged(name_, symbol_, decimals_, bridge_){ + + _cachedChainId = block.chainid; + _cachedDomainSeparator = _buildDomainSeparator(); + _cachedThis = address(this); + } + + function _domainSeparatorV4() internal view returns (bytes32) { + if (address(this) == _cachedThis && block.chainid == _cachedChainId) { + return _cachedDomainSeparator; + } else { + return _buildDomainSeparator(); + } + } + + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name())), + keccak256(bytes(version())), + block.chainid, + address(this) + ) + ); + } + + function DOMAIN_SEPARATOR() external view virtual returns (bytes32) { + return _domainSeparatorV4(); + } + + + function version() public pure virtual returns (string memory) { + return "1"; + } + + function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + public + virtual + override + { + if (deadline < block.timestamp) { + revert ErrorExpiredPermit(); + } + + bytes32 hashStruct = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, amount, nonces[owner]++, deadline)); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", _domainSeparatorV4(), hashStruct)); + + address signer = ecrecover(hash, v, r, s); + + if (signer == address(0) || signer != owner) { + revert ErrorInvalidSignature(); + } + + _approve(owner, spender, amount); + } + + /// @dev used to consume a nonce so that the user is able to invalidate a signature. Returns the current value and increments. + function useNonce() external virtual returns (uint256 current) { + current = nonces[msg.sender]; + nonces[msg.sender]++; + } + + error ErrorExpiredPermit(); + error ErrorInvalidSignature(); +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index e09d0e91..6a5954d8 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -83,6 +83,22 @@ const config: HardhatUserConfig = { opt_goerli_fork: { url: "http://localhost:9545", }, + // Mantle Public Chains + mnt_mainnet: { + url: env.string("RPC_MNT_MAINNET", ""), + }, + mnt_goerli: { + url: env.string("RPC_MNT_GOERLI", ""), + }, + + // Mantle Fork Chains + mnt_mainnet_fork: { + url: "http://localhost:9545", + }, + mnt_goerli_fork: { + url: "http://localhost:9545", + + }, }, gasReporter: { enabled: env.string("REPORT_GAS", "false") !== "false", @@ -95,13 +111,25 @@ const config: HardhatUserConfig = { arbitrumGoerli: env.string("ETHERSCAN_API_KEY_ARB", ""), arbitrumOne: env.string("ETHERSCAN_API_KEY_ARB", ""), optimisticEthereum: env.string("ETHERSCAN_API_KEY_OPT", ""), - optimisticGoerli: env.string("ETHERSCAN_API_KEY_OPT", ""), - }, + mnt_goerli: env.string("ETHERSCAN_API_KEY_MNT", ""), + mnt_mainnet: env.string("ETHERSCAN_API_KEY_MNT", ""), + }, + customChains: [ + { + network: "mnt_mainnet", + chainId: 5000, + urls: { + apiURL: "https://explorer.mantle.xyz/api", + browserURL: "https://explorer.mantle.xyz/api?module=contract&action=verify" + } + } + ] }, typechain: { externalArtifacts: [ "./interfaces/**/*.json", "./utils/optimism/artifacts/*.json", + "./utils/mantle/artifacts/*.json", "./utils/arbitrum/artifacts/*.json", ], }, diff --git a/package-lock.json b/package-lock.json index 88751d15..b50b6443 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@eth-optimism/sdk": "3.0.0", "@ethersproject/providers": "^5.6.8", "@lidofinance/evm-script-decoder": "^0.2.2", + "@mantleio/sdk": "^0.2.2", "@openzeppelin/contracts": "4.6.0", "chalk": "4.1.2" }, @@ -1494,6 +1495,49 @@ "keccak256": "^1.0.3" } }, + "node_modules/@mantleio/contracts": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@mantleio/contracts/-/contracts-0.2.1.tgz", + "integrity": "sha512-NqWUO8Vhu2OVA+pH+k761uWwssQmKsSBRA1vO9wrKUvlHsWN50DL/wrIDHZBksDwcjqOFGeIxWmt9QLwndhAjw==", + "dependencies": { + "@ethersproject/abstract-provider": "^5.6.1", + "@ethersproject/abstract-signer": "^5.6.2", + "@mantleio/core-utils": "0.1.0" + }, + "peerDependencies": { + "ethers": "^5" + } + }, + "node_modules/@mantleio/core-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mantleio/core-utils/-/core-utils-0.1.0.tgz", + "integrity": "sha512-6v/CuKe8W3UVYVn6RUR6pbrMbKDcdrUoj24wCuwM4JFbPHzpTqUSnSXDHrDs3LJpWLxsptynNdIGw90wzCKpdg==", + "dependencies": { + "@ethersproject/abstract-provider": "^5.6.1", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/providers": "^5.6.8", + "@ethersproject/transactions": "^5.6.2", + "@ethersproject/web": "^5.6.1", + "bufio": "^1.0.7", + "chai": "^4.3.4", + "ethers": "^5.6.8" + } + }, + "node_modules/@mantleio/sdk": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@mantleio/sdk/-/sdk-0.2.2.tgz", + "integrity": "sha512-mmMGDG09GFpcWu/qPhJ18cU1N7m+qI7d5leykNoITw9Gwn70oGor9oDoH4ZtFfRe8W0XjP2jo0nGafS7FvOWJg==", + "dependencies": { + "@mantleio/contracts": "0.2.1", + "@mantleio/core-utils": "0.1.0", + "lodash": "^4.17.21", + "merkletreejs": "^0.2.27", + "rlp": "^2.2.7" + }, + "peerDependencies": { + "ethers": "^5" + } + }, "node_modules/@metamask/eth-sig-util": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz", @@ -25852,6 +25896,43 @@ "keccak256": "^1.0.3" } }, + "@mantleio/contracts": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@mantleio/contracts/-/contracts-0.2.1.tgz", + "integrity": "sha512-NqWUO8Vhu2OVA+pH+k761uWwssQmKsSBRA1vO9wrKUvlHsWN50DL/wrIDHZBksDwcjqOFGeIxWmt9QLwndhAjw==", + "requires": { + "@ethersproject/abstract-provider": "^5.6.1", + "@ethersproject/abstract-signer": "^5.6.2", + "@mantleio/core-utils": "0.1.0" + } + }, + "@mantleio/core-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mantleio/core-utils/-/core-utils-0.1.0.tgz", + "integrity": "sha512-6v/CuKe8W3UVYVn6RUR6pbrMbKDcdrUoj24wCuwM4JFbPHzpTqUSnSXDHrDs3LJpWLxsptynNdIGw90wzCKpdg==", + "requires": { + "@ethersproject/abstract-provider": "^5.6.1", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/providers": "^5.6.8", + "@ethersproject/transactions": "^5.6.2", + "@ethersproject/web": "^5.6.1", + "bufio": "^1.0.7", + "chai": "^4.3.4", + "ethers": "^5.6.8" + } + }, + "@mantleio/sdk": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@mantleio/sdk/-/sdk-0.2.2.tgz", + "integrity": "sha512-mmMGDG09GFpcWu/qPhJ18cU1N7m+qI7d5leykNoITw9Gwn70oGor9oDoH4ZtFfRe8W0XjP2jo0nGafS7FvOWJg==", + "requires": { + "@mantleio/contracts": "0.2.1", + "@mantleio/core-utils": "0.1.0", + "lodash": "^4.17.21", + "merkletreejs": "^0.2.27", + "rlp": "^2.2.7" + } + }, "@metamask/eth-sig-util": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz", diff --git a/package.json b/package.json index 8acec291..1ff6d214 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "fork:arb:mainnet": "hardhat node:fork arb_mainnet 8546", "fork:opt:goerli": "hardhat node:fork opt_goerli 9545", "fork:opt:mainnet": "hardhat node:fork opt_mainnet 9545", + "fork:mnt:goerli": "hardhat node:fork mnt_goerli 9545", + "fork:mnt:mainnet": "hardhat node:fork mnt_mainnet 9545", "arbitrum:deploy": "ts-node --files ./scripts/arbitrum/deploy-gateway.ts", "arbitrum:finalize-message": "ts-node --files ./scripts/arbitrum/finalize-message.ts", "arbitrum:test:e2e": "hardhat test ./test/arbitrum/*.e2e.test.ts", @@ -31,7 +33,17 @@ "optimism:test:integration": "hardhat test ./test/optimism/*.integration.test.ts", "optimism:test:acceptance": "hardhat test ./test/optimism/*.acceptance.test.ts", "optimism:test:executor": "hardhat test ./test/bridge-executor/optimism.integration.test.ts", - "optimism:test:launch": "REVERT=false hardhat test ./test/optimism/{_launch.test.ts,bridging.integration.test.ts}" + "optimism:test:launch": "REVERT=false hardhat test ./test/optimism/{_launch.test.ts,bridging.integration.test.ts}", + "mantle:deploy": "ts-node --files ./scripts/mantle/deploy-bridge.ts", + "mantle:test:permit": "hardhat test ./test/token/ERC20BridgedPermit.unit.test.ts", + "mantle:finalize-message": "ts-node --files ./scripts/mantle/finalize-message.ts", + "mantle:test:e2e": "hardhat test ./test/mantle/*.e2e.test.ts", + "mantle:test:bridginge2e": "hardhat test ./test/mantle/bridging.e2e.test.ts", + "mantle:test:proxye2e": "hardhat test ./test/mantle/managing-proxy.e2e.test.ts", + "mantle:test:unit": "hardhat test ./test/mantle/*.unit.test.ts", + "mantle:test:integration": "hardhat test ./test/mantle/*.integration.test.ts", + "mantle:test:acceptance": "hardhat test ./test/mantle/*.acceptance.test.ts", + "mantle:test:executor": "hardhat test ./test/bridge-executor/mantle.integration.test.ts" }, "keywords": [], "author": "", @@ -73,6 +85,7 @@ "@eth-optimism/sdk": "3.0.0", "@ethersproject/providers": "^5.6.8", "@lidofinance/evm-script-decoder": "^0.2.2", + "@mantleio/sdk": "^0.2.2", "@openzeppelin/contracts": "4.6.0", "chalk": "4.1.2" } diff --git a/scripts/mantle/deploy-bridge.ts b/scripts/mantle/deploy-bridge.ts new file mode 100644 index 00000000..9379a863 --- /dev/null +++ b/scripts/mantle/deploy-bridge.ts @@ -0,0 +1,79 @@ +import env from "../../utils/env"; +import prompt from "../../utils/prompt"; +import network from "../../utils/network"; +import mantle from "../../utils/mantle"; +import deployment from "../../utils/deployment"; +import { BridgingManagement } from "../../utils/bridging-management"; + +async function main() { + const networkName = env.network(); + const ethMntNetwork = network.multichain(["eth", "mnt"], networkName); + + const [ethDeployer] = ethMntNetwork.getSigners(env.privateKey(), { + forking: env.forking(), + }); + const [, mntDeployer] = ethMntNetwork.getSigners( + env.string("MNT_DEPLOYER_PRIVATE_KEY"), + { + forking: env.forking(), + } + ); + + const deploymentConfig = deployment.loadMultiChainDeploymentConfig(); + + const [l1DeployScript, l2DeployScript] = await mantle + .deployment(networkName, { logger: console }) + .erc20TokenBridgeDeployScript( + deploymentConfig.token, + { + deployer: ethDeployer, + admins: { + proxy: deploymentConfig.l1.proxyAdmin, + bridge: ethDeployer.address, + }, + }, + { + deployer: mntDeployer, + admins: { + proxy: deploymentConfig.l2.proxyAdmin, + bridge: mntDeployer.address, + }, + } + ); + + await deployment.printMultiChainDeploymentConfig( + "Deploy Mantle Bridge", + ethDeployer, + mntDeployer, + deploymentConfig, + l1DeployScript, + l2DeployScript + ); + + await prompt.proceed(); + + await l1DeployScript.run(); + await l2DeployScript.run(); + + const l1ERC20TokenBridgeProxyDeployStepIndex = 1; + const l1BridgingManagement = new BridgingManagement( + l1DeployScript.getContractAddress(l1ERC20TokenBridgeProxyDeployStepIndex), + ethDeployer, + { logger: console } + ); + + const l2ERC20TokenBridgeProxyDeployStepIndex = 3; + const l2BridgingManagement = new BridgingManagement( + l2DeployScript.getContractAddress(l2ERC20TokenBridgeProxyDeployStepIndex), + mntDeployer, + { logger: console } + ); + + await l1BridgingManagement.setup(deploymentConfig.l1); + await l2BridgingManagement.setup(deploymentConfig.l2); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/mantle/finalize-message.ts b/scripts/mantle/finalize-message.ts new file mode 100644 index 00000000..81c05871 --- /dev/null +++ b/scripts/mantle/finalize-message.ts @@ -0,0 +1,34 @@ +import { CrossChainMessenger, MessageStatus } from "@mantleio/sdk"; +import env from "../../utils/env"; +import network from "../../utils/network"; + +async function main() { + const networkName = env.network(); + const [l1Signer, l2Signer] = network + .multichain(["eth", "mnt"], networkName) + .getSigners(env.privateKey(), { forking: false }); + + const txHash = env.string("TX_HASH"); + + const crossDomainMessenger = new CrossChainMessenger({ + l1ChainId: network.chainId("eth", networkName), + l2ChainId: network.chainId("mnt", networkName), + l1SignerOrProvider: l1Signer, + l2SignerOrProvider: l2Signer, + }); + + const status = await crossDomainMessenger.getMessageStatus(txHash); + + if (status !== MessageStatus.READY_FOR_RELAY) { + throw new Error(`Invalid tx status: ${status}`); + } + + console.log("Finalizing the L2 -> L1 message"); + await crossDomainMessenger.finalizeMessage(txHash); + console.log("Message successfully finalized!"); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/mantle/prove-message.ts b/scripts/mantle/prove-message.ts new file mode 100644 index 00000000..2c5c1bd2 --- /dev/null +++ b/scripts/mantle/prove-message.ts @@ -0,0 +1,36 @@ +import { CrossChainMessenger, MessageStatus } from "@mantleio/sdk"; +import env from "../../utils/env"; +import network from "../../utils/network"; + +async function main() { + const networkName = env.network(); + const [l1Signer, l2Signer] = network + .multichain(["eth", "mnt"], networkName) + .getSigners(env.privateKey(), { forking: false }); + + const txHash = env.string("TX_HASH"); + + const crossChainMessenger = new CrossChainMessenger({ + l1ChainId: network.chainId("eth", networkName), + l2ChainId: network.chainId("mnt", networkName), + l1SignerOrProvider: l1Signer, + l2SignerOrProvider: l2Signer, + }); + + const status = await crossChainMessenger.getMessageStatus(txHash); + + // if (status !== MessageStatus.READY_TO_PROVE) { + // throw new Error(`Invalid tx status: ${status}`); + // } + + // console.log("Prove the L2 -> L1 message"); + // const tx = await crossChainMessenger.proveMessage(txHash); + // console.log(`Waiting for the prove tx ${tx.hash}...`); + // await tx.wait(); + // console.log(`Message was proved successfully!`); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/mantle/update-ethereum-executor.ts b/scripts/mantle/update-ethereum-executor.ts new file mode 100644 index 00000000..d5455536 --- /dev/null +++ b/scripts/mantle/update-ethereum-executor.ts @@ -0,0 +1,149 @@ +import { assert } from "chai"; +import { ethers } from "hardhat"; +import { GovBridgeExecutor__factory } from "../../typechain"; +import env from "../../utils/env"; +import lido from "../../utils/lido"; +import network from "../../utils/network"; +import mantle from "../../utils/mantle"; +import prompt from "../../utils/prompt"; + +// Set address of the bridge executor to run the script +const GOV_BRIDGE_EXECUTOR = ""; + +async function main() { + const isForking = env.forking(); + const networkName = env.network(); + const ethMntNetwork = network.multichain(["eth", "mnt"], networkName); + + const [l1LDOHolder] = ethMntNetwork.getSigners( + env.string("TESTING_MNT_LDO_HOLDER_PRIVATE_KEY"), + { forking: isForking } + ); + const [, mntRunner] = ethMntNetwork.getSigners(env.privateKey(), { + forking: isForking, + }); + + const govBridgeExecutor = GovBridgeExecutor__factory.connect( + GOV_BRIDGE_EXECUTOR, + mntRunner + ); + + const newEthExecutorLidoDAO = lido(networkName, l1LDOHolder); + const oldEthExecutorLidoDAO = lido( + networkName === "mainnet" ? "mainnet_test" : networkName, + l1LDOHolder + ); + const prevEthGovExecutorAddress = + await govBridgeExecutor.getEthereumGovernanceExecutor(); + + assert.equal( + oldEthExecutorLidoDAO.agent.address.toLocaleLowerCase(), + prevEthGovExecutorAddress.toLowerCase(), + `${oldEthExecutorLidoDAO.agent.address} is not current ethereumGovernanceExecutor` + ); + + console.log(` · Is forking: ${isForking}`); + console.log(` · Network Name: ${networkName}`); + console.log( + ` · Prev Ethereum Governance Executor: ${prevEthGovExecutorAddress}` + ); + console.log( + ` · New Ethereum Governance Executor: ${newEthExecutorLidoDAO.agent.address}` + ); + console.log(` · LDO Holder: ${l1LDOHolder.address}`); + console.log(` · LDO Holder ETH balance: ${await l1LDOHolder.getBalance()}`); + console.log(` · L2 tx runner: ${mntRunner.address}`); + console.log(` · L2 tx runner ETH balance: ${await mntRunner.getBalance()}`); + + await prompt.proceed(); + + console.log(`Preparing the voting tx...`); + + const mntAddresses = mantle.addresses(networkName); + + // Prepare data for Governance Bridge Executor + const executorCalldata = await govBridgeExecutor.interface.encodeFunctionData( + "queue", + [ + [GOV_BRIDGE_EXECUTOR], + [0], + ["updateEthereumGovernanceExecutor(address)"], + [ + ethers.utils.defaultAbiCoder.encode( + ["address"], + [newEthExecutorLidoDAO.agent.address] + ), + ], + [false], + ] + ); + + const { callvalue, calldata } = await mantle + .messaging(networkName, { forking: isForking }) + .prepareL2Message({ + calldata: executorCalldata, + recipient: GOV_BRIDGE_EXECUTOR, + sender: oldEthExecutorLidoDAO.agent.address, + }); + + const createVoteTx = await oldEthExecutorLidoDAO.createVote( + l1LDOHolder, + "Update ethereumGovernanceExecutor on Mantle Governance Bridge Executor", + { + address: oldEthExecutorLidoDAO.agent.address, + signature: "execute(address,uint256,bytes)", + decodedCallData: [ + mntAddresses.L1CrossDomainMessenger, + callvalue, + calldata, + ], + } + ); + + console.log("Creating voting to update ethereumGovernanceExecutor..."); + await createVoteTx.wait(); + console.log(`Vote was created!`); + + const votesCount = await oldEthExecutorLidoDAO.voting.votesLength(); + const voteId = votesCount.sub(1); + + console.log(`New vote id ${voteId.toString()}`); + console.log(`Voting for and executing the vote...`); + + const voteAndExecuteTx = await oldEthExecutorLidoDAO.voteAndExecute( + l1LDOHolder, + voteId, + true + ); + const executeTxReceipt = await voteAndExecuteTx.wait(); + + console.log(`Vote ${voteId.toString()} was executed!`); + + console.log(`Waiting for L2 transaction...`); + await mantle + .messaging(networkName, { forking: isForking }) + .waitForL2Message(executeTxReceipt.transactionHash); + + console.log(`Message delivered to L2`); + + console.log("Executing the queued task..."); + // execute task on L2 + const tasksCount = await govBridgeExecutor.getActionsSetCount(); + const targetTaskId = tasksCount.toNumber() - 1; + + const tx = await govBridgeExecutor.execute(targetTaskId); + await tx.wait(); + console.log(`Task executed on L2!`); + + const ethereumGovernanceExecutor = + await govBridgeExecutor.getEthereumGovernanceExecutor(); + + console.log( + `New ethereum governance executor is: ${ethereumGovernanceExecutor}` + ); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/test/bridge-executor/mantle.integration.test.ts b/test/bridge-executor/mantle.integration.test.ts new file mode 100644 index 00000000..21400185 --- /dev/null +++ b/test/bridge-executor/mantle.integration.test.ts @@ -0,0 +1,303 @@ +import { assert } from "chai"; +import { + ERC20BridgedStub__factory, + L2ERC20TokenBridge__factory, + OssifiableProxy__factory, + MantleBridgeExecutor__factory, + ERC20BridgedPermit__factory, +} from "../../typechain"; +import { wei } from "../../utils/wei"; +import mantle from "../../utils/mantle"; +import testing, { scenario } from "../../utils/testing"; +import { BridgingManagerRole } from "../../utils/bridging-management"; + +import env from "../../utils/env"; +import network from "../../utils/network"; +import { getBridgeExecutorParams } from "../../utils/bridge-executor"; + +scenario("Mantle :: Bridge Executor integration test", ctxFactory) + .step("Activate L2 bridge", async (ctx) => { + const { l2ERC20TokenBridge, bridgeExecutor, l2CrossDomainMessenger } = + ctx.l2; + + assert.isFalse( + await l2ERC20TokenBridge.hasRole( + BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, + bridgeExecutor.address + ) + ); + assert.isFalse( + await l2ERC20TokenBridge.hasRole( + BridgingManagerRole.WITHDRAWALS_ENABLER_ROLE.hash, + bridgeExecutor.address + ) + ); + assert.isFalse(await l2ERC20TokenBridge.isDepositsEnabled()); + assert.isFalse(await l2ERC20TokenBridge.isWithdrawalsEnabled()); + + const actionsSetCountBefore = await bridgeExecutor.getActionsSetCount(); + + await l2CrossDomainMessenger.relayMessage( + 0, + ctx.l1.l1EthGovExecutorAddress, + bridgeExecutor.address, + 0, + 300_000, + bridgeExecutor.interface.encodeFunctionData("queue", [ + new Array(4).fill(l2ERC20TokenBridge.address), + new Array(4).fill(0), + [ + "grantRole(bytes32,address)", + "grantRole(bytes32,address)", + "enableDeposits()", + "enableWithdrawals()", + ], + [ + "0x" + + l2ERC20TokenBridge.interface + .encodeFunctionData("grantRole", [ + BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, + bridgeExecutor.address, + ]) + .substring(10), + "0x" + + l2ERC20TokenBridge.interface + .encodeFunctionData("grantRole", [ + BridgingManagerRole.WITHDRAWALS_ENABLER_ROLE.hash, + bridgeExecutor.address, + ]) + .substring(10), + "0x" + + l2ERC20TokenBridge.interface + .encodeFunctionData("enableDeposits") + .substring(10), + "0x" + + l2ERC20TokenBridge.interface + .encodeFunctionData("enableWithdrawals") + .substring(10), + ], + new Array(4).fill(false), + ]), + { gasLimit: 5_000_000 } + ); + + const actionsSetCountAfter = await bridgeExecutor.getActionsSetCount(); + + assert.equalBN(actionsSetCountBefore.add(1), actionsSetCountAfter); + + // execute the last added actions set + await bridgeExecutor.execute(actionsSetCountAfter.sub(1), { value: 0 }); + + assert.isTrue( + await l2ERC20TokenBridge.hasRole( + BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, + bridgeExecutor.address + ) + ); + assert.isTrue( + await l2ERC20TokenBridge.hasRole( + BridgingManagerRole.WITHDRAWALS_ENABLER_ROLE.hash, + bridgeExecutor.address + ) + ); + assert.isTrue(await l2ERC20TokenBridge.isDepositsEnabled()); + assert.isTrue(await l2ERC20TokenBridge.isWithdrawalsEnabled()); + }) + + .step("Change Proxy implementation", async (ctx) => { + const { + l2Token, + l2CrossDomainMessenger, + l2ERC20TokenBridgeProxy, + bridgeExecutor, + } = ctx.l2; + + const actionsSetCountBefore = await bridgeExecutor.getActionsSetCount(); + + const proxyImplBefore = + await l2ERC20TokenBridgeProxy.proxy__getImplementation(); + + await l2CrossDomainMessenger.relayMessage( + 0, + ctx.l1.l1EthGovExecutorAddress, + bridgeExecutor.address, + 0, + 300_000, + bridgeExecutor.interface.encodeFunctionData("queue", [ + [l2ERC20TokenBridgeProxy.address], + [0], + ["proxy__upgradeTo(address)"], + [ + "0x" + + l2ERC20TokenBridgeProxy.interface + .encodeFunctionData("proxy__upgradeTo", [l2Token.address]) + .substring(10), + ], + [false], + ]), + { gasLimit: 5_000_000 } + ); + const actionSetCount = await bridgeExecutor.getActionsSetCount(); + + assert.equalBN(actionsSetCountBefore.add(1), actionSetCount); + + await bridgeExecutor.execute(actionsSetCountBefore, { value: 0 }); + const proxyImplAfter = + await l2ERC20TokenBridgeProxy.proxy__getImplementation(); + + assert.notEqual(proxyImplBefore, proxyImplAfter); + assert.equal(proxyImplAfter, l2Token.address); + }) + + .step("Change proxy Admin", async (ctx) => { + const { + l2CrossDomainMessenger, + l2ERC20TokenBridgeProxy, + bridgeExecutor, + accounts: { sender }, + } = ctx.l2; + + const actionsSetCountBefore = await bridgeExecutor.getActionsSetCount(); + + const proxyAdminBefore = await l2ERC20TokenBridgeProxy.proxy__getAdmin(); + + await l2CrossDomainMessenger.relayMessage( + 0, + ctx.l1.l1EthGovExecutorAddress, + bridgeExecutor.address, + 0, + 300_000, + bridgeExecutor.interface.encodeFunctionData("queue", [ + [l2ERC20TokenBridgeProxy.address], + [0], + ["proxy__changeAdmin(address)"], + [ + "0x" + + l2ERC20TokenBridgeProxy.interface + .encodeFunctionData("proxy__changeAdmin", [sender.address]) + .substring(10), + ], + [false], + ]), + { gasLimit: 5_000_000 } + ); + const actionSetCount = await bridgeExecutor.getActionsSetCount(); + + assert.equalBN(actionsSetCountBefore.add(1), actionSetCount); + + await bridgeExecutor.execute(actionsSetCountBefore, { value: 0 }); + const proxyAdminAfter = await l2ERC20TokenBridgeProxy.proxy__getAdmin(); + + assert.notEqual(proxyAdminBefore, proxyAdminAfter); + assert.equal(proxyAdminAfter, sender.address); + }) + + .run(); + +async function ctxFactory() { + const networkName = env.network("TESTING_MNT_NETWORK", "mainnet"); + const [l1Provider, l2Provider] = network + .multichain(["eth", "mnt"], networkName) + .getProviders({ forking: true }); + + const testingOnDeployedContracts = testing.env.USE_DEPLOYED_CONTRACTS(false); + + const l1Deployer = testing.accounts.deployer(l1Provider); + const l2Deployer = testing.accounts.deployer(l2Provider); + + await mantle.testing(networkName).stubL1CrossChainMessengerContract(); + + const l1Token = await new ERC20BridgedStub__factory(l1Deployer).deploy( + "Test Token", + "TT" + ); + + const mntAddresses = mantle.addresses(networkName); + + const govBridgeExecutor = testingOnDeployedContracts + ? MantleBridgeExecutor__factory.connect( + testing.env.MNT_GOV_BRIDGE_EXECUTOR(), + l2Provider + ) + : await new MantleBridgeExecutor__factory(l2Deployer).deploy( + mntAddresses.L2CrossDomainMessenger, + l1Deployer.address, + ...getBridgeExecutorParams(), + l2Deployer.address + ); + + const l1EthGovExecutorAddress = + await govBridgeExecutor.getEthereumGovernanceExecutor(); + + const [, l2DeployScript] = await mantle + .deployment(networkName) + .erc20TokenBridgeDeployScript( + l1Token.address, + { + deployer: l1Deployer, + admins: { proxy: l1Deployer.address, bridge: l1Deployer.address }, + }, + { + deployer: l2Deployer, + admins: { + proxy: govBridgeExecutor.address, + bridge: govBridgeExecutor.address, + }, + } + ); + + await l2DeployScript.run(); + + const l2Token = ERC20BridgedPermit__factory.connect( + l2DeployScript.getContractAddress(1), + l2Deployer + ); + const l2ERC20TokenBridge = L2ERC20TokenBridge__factory.connect( + l2DeployScript.getContractAddress(3), + l2Deployer + ); + const l2ERC20TokenBridgeProxy = OssifiableProxy__factory.connect( + l2DeployScript.getContractAddress(3), + l2Deployer + ); + + const mntContracts = mantle.contracts(networkName, { forking: true }); + + const l1CrossDomainMessengerAliased = await testing.impersonate( + testing.accounts.applyL1ToL2Alias( + mntContracts.L1CrossDomainMessenger.address + ), + l2Provider + ); + + const l2CrossDomainMessenger = + await mntContracts.L2CrossDomainMessenger.connect( + l1CrossDomainMessengerAliased + ); + + await testing.setBalance( + await l2CrossDomainMessenger.signer.getAddress(), + wei.toBigNumber(wei`1 ether`), + l2Provider + ); + + return { + l1: { + accounts: { + admin: l1Deployer, + }, + l1EthGovExecutorAddress, + }, + l2: { + l2Token, + bridgeExecutor: govBridgeExecutor.connect(l2Deployer), + l2ERC20TokenBridge, + l2CrossDomainMessenger, + l2ERC20TokenBridgeProxy, + accounts: { + sender: testing.accounts.sender(l2Provider), + admin: l2Deployer, + }, + }, + }; +} diff --git a/test/mantle/L1ERC20TokenBridge.unit.test.ts b/test/mantle/L1ERC20TokenBridge.unit.test.ts new file mode 100644 index 00000000..97d32b5d --- /dev/null +++ b/test/mantle/L1ERC20TokenBridge.unit.test.ts @@ -0,0 +1,541 @@ +import { assert } from "chai"; +import hre, { ethers } from "hardhat"; +import { + ERC20BridgedStub__factory, + L1ERC20TokenBridge__factory, + L2ERC20TokenBridge__factory, + OssifiableProxy__factory, + EmptyContractStub__factory, +} from "../../typechain"; +import { CrossDomainMessengerStub__factory } from "../../typechain/factories/CrossDomainMessengerStub__factory"; +import testing, { unit } from "../../utils/testing"; +import { wei } from "../../utils/wei"; + +unit("Mantle :: L1ERC20TokenBridge", ctxFactory) + .test("l2TokenBridge()", async (ctx) => { + assert.equal( + await ctx.l1TokenBridge.l2TokenBridge(), + ctx.accounts.l2TokenBridgeEOA.address + ); + }) + + .test("depositERC20() :: deposits disabled", async (ctx) => { + await ctx.l1TokenBridge.disableDeposits(); + + assert.isFalse(await ctx.l1TokenBridge.isDepositsEnabled()); + + await assert.revertsWith( + ctx.l1TokenBridge.depositERC20( + ctx.stubs.l1Token.address, + ctx.stubs.l2Token.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorDepositsDisabled()" + ); + }) + + .test("depositsERC20() :: wrong l1Token address", async (ctx) => { + await assert.revertsWith( + ctx.l1TokenBridge.depositERC20( + ctx.accounts.stranger.address, + ctx.stubs.l2Token.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + }) + + .test("depositsERC20() :: wrong l2Token address", async (ctx) => { + await assert.revertsWith( + ctx.l1TokenBridge.depositERC20( + ctx.stubs.l1Token.address, + ctx.accounts.stranger.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); + }) + + .test("depositERC20() :: not from EOA", async (ctx) => { + await assert.revertsWith( + ctx.l1TokenBridge + .connect(ctx.accounts.emptyContractAsEOA) + .depositERC20( + ctx.stubs.l1Token.address, + ctx.stubs.l2Token.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorSenderNotEOA()" + ); + }) + + .test("depositERC20()", async (ctx) => { + const { + l1TokenBridge, + accounts: { deployer, l2TokenBridgeEOA }, + stubs: { l1Token, l2Token, l1Messenger }, + } = ctx; + + const l2Gas = wei`0.99 wei`; + const amount = wei`1 ether`; + const data = "0xdeadbeaf"; + + await l1Token.approve(l1TokenBridge.address, amount); + + const deployerBalanceBefore = await l1Token.balanceOf(deployer.address); + const bridgeBalanceBefore = await l1Token.balanceOf(l1TokenBridge.address); + + const tx = await l1TokenBridge.depositERC20( + l1Token.address, + l2Token.address, + amount, + l2Gas, + data + ); + + await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ + l1Token.address, + l2Token.address, + deployer.address, + deployer.address, + amount, + data, + ]); + + await assert.emits(l1Messenger, tx, "SentMessage", [ + l2TokenBridgeEOA.address, + l1TokenBridge.address, + L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( + "finalizeDeposit", + [ + l1Token.address, + l2Token.address, + deployer.address, + deployer.address, + amount, + data, + ] + ), + 1, // message nonce + l2Gas, + ]); + + assert.equalBN( + await l1Token.balanceOf(deployer.address), + deployerBalanceBefore.sub(amount) + ); + + assert.equalBN( + await l1Token.balanceOf(l1TokenBridge.address), + bridgeBalanceBefore.add(amount) + ); + }) + + .test("depositERC20To() :: deposits disabled", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1Token, l2Token }, + accounts: { recipient }, + } = ctx; + await l1TokenBridge.disableDeposits(); + + assert.isFalse(await l1TokenBridge.isDepositsEnabled()); + + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1Token.address, + l2Token.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorDepositsDisabled()" + ); + }) + + .test("depositsERC20To() :: wrong l1Token address", async (ctx) => { + const { + l1TokenBridge, + stubs: { l2Token }, + accounts: { recipient, stranger }, + } = ctx; + await l1TokenBridge.disableDeposits(); + + assert.isFalse(await l1TokenBridge.isDepositsEnabled()); + + await assert.revertsWith( + l1TokenBridge.depositERC20To( + stranger.address, + l2Token.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorDepositsDisabled()" + ); + }) + + .test("depositsERC20To() :: wrong l2Token address", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1Token }, + accounts: { recipient, stranger }, + } = ctx; + await l1TokenBridge.disableDeposits(); + + assert.isFalse(await l1TokenBridge.isDepositsEnabled()); + + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1Token.address, + stranger.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorDepositsDisabled()" + ); + }) + + .test("depositsERC20To() :: recipient is zero address", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1Token }, + accounts: { stranger }, + } = ctx; + + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1Token.address, + stranger.address, + ethers.constants.AddressZero, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorAccountIsZeroAddress()" + ); + }) + + .test("depositERC20To()", async (ctx) => { + const { + l1TokenBridge, + accounts: { deployer, l2TokenBridgeEOA, recipient }, + stubs: { l1Token, l2Token, l1Messenger }, + } = ctx; + + const l2Gas = wei`0.99 wei`; + const amount = wei`1 ether`; + const data = "0x"; + + await l1Token.approve(l1TokenBridge.address, amount); + + const deployerBalanceBefore = await l1Token.balanceOf(deployer.address); + const bridgeBalanceBefore = await l1Token.balanceOf(l1TokenBridge.address); + + const tx = await l1TokenBridge.depositERC20To( + l1Token.address, + l2Token.address, + recipient.address, + amount, + l2Gas, + data + ); + + await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ + l1Token.address, + l2Token.address, + deployer.address, + recipient.address, + amount, + data, + ]); + + await assert.emits(l1Messenger, tx, "SentMessage", [ + l2TokenBridgeEOA.address, + l1TokenBridge.address, + L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( + "finalizeDeposit", + [ + l1Token.address, + l2Token.address, + deployer.address, + recipient.address, + amount, + data, + ] + ), + 1, // message nonce + l2Gas, + ]); + + assert.equalBN( + await l1Token.balanceOf(deployer.address), + deployerBalanceBefore.sub(amount) + ); + + assert.equalBN( + await l1Token.balanceOf(l1TokenBridge.address), + bridgeBalanceBefore.add(amount) + ); + }) + + .test( + "finalizeERC20Withdrawal() :: withdrawals are disabled", + async (ctx) => { + const { + l1TokenBridge, + stubs: { l1Token, l2Token }, + accounts: { deployer, recipient, l2TokenBridgeEOA }, + } = ctx; + await l1TokenBridge.disableWithdrawals(); + + assert.isFalse(await l1TokenBridge.isWithdrawalsEnabled()); + + await assert.revertsWith( + l1TokenBridge + .connect(l2TokenBridgeEOA) + .finalizeERC20Withdrawal( + l1Token.address, + l2Token.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorWithdrawalsDisabled()" + ); + } + ) + + .test("finalizeERC20Withdrawal() :: wrong l1Token", async (ctx) => { + const { + l1TokenBridge, + stubs: { l2Token }, + accounts: { deployer, recipient, l2TokenBridgeEOA, stranger }, + } = ctx; + + await assert.revertsWith( + l1TokenBridge + .connect(l2TokenBridgeEOA) + .finalizeERC20Withdrawal( + stranger.address, + l2Token.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + }) + + .test("finalizeERC20Withdrawal() :: wrong l2Token", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1Token }, + accounts: { deployer, recipient, l2TokenBridgeEOA, stranger }, + } = ctx; + + await assert.revertsWith( + l1TokenBridge + .connect(l2TokenBridgeEOA) + .finalizeERC20Withdrawal( + l1Token.address, + stranger.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); + }) + + .test("finalizeERC20Withdrawal() :: unauthorized messenger", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1Token, l2Token }, + accounts: { deployer, recipient, stranger }, + } = ctx; + + await assert.revertsWith( + l1TokenBridge + .connect(stranger) + .finalizeERC20Withdrawal( + l1Token.address, + l2Token.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnauthorizedMessenger()" + ); + }) + + .test( + "finalizeERC20Withdrawal() :: wrong cross domain sender", + async (ctx) => { + const { + l1TokenBridge, + stubs: { l1Token, l2Token, l1Messenger }, + accounts: { deployer, recipient, stranger, l1MessengerStubAsEOA }, + } = ctx; + + await l1Messenger.setXDomainMessageSender(stranger.address); + + await assert.revertsWith( + l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1Token.address, + l2Token.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorWrongCrossDomainSender()" + ); + } + ) + + .test("finalizeERC20Withdrawal()", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1Token, l2Token, l1Messenger }, + accounts: { deployer, recipient, l1MessengerStubAsEOA, l2TokenBridgeEOA }, + } = ctx; + + await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); + + const bridgeBalanceBefore = await l1Token.balanceOf(l1TokenBridge.address); + + const amount = wei`1 ether`; + const data = "0xdeadbeaf"; + + const tx = await l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1Token.address, + l2Token.address, + deployer.address, + recipient.address, + amount, + data + ); + + await assert.emits(l1TokenBridge, tx, "ERC20WithdrawalFinalized", [ + l1Token.address, + l2Token.address, + deployer.address, + recipient.address, + amount, + data, + ]); + + assert.equalBN(await l1Token.balanceOf(recipient.address), amount); + assert.equalBN( + await l1Token.balanceOf(l1TokenBridge.address), + bridgeBalanceBefore.sub(amount) + ); + }) + + .run(); + +async function ctxFactory() { + const [deployer, l2TokenBridgeEOA, stranger, recipient] = + await hre.ethers.getSigners(); + + const l1MessengerStub = await new CrossDomainMessengerStub__factory( + deployer + ).deploy({ value: wei.toBigNumber(wei`1 ether`) }); + + const l1TokenStub = await new ERC20BridgedStub__factory(deployer).deploy( + "L1 Token", + "L1" + ); + + const l2TokenStub = await new ERC20BridgedStub__factory(deployer).deploy( + "L2 Token", + "L2" + ); + + const emptyContract = await new EmptyContractStub__factory(deployer).deploy({ + value: wei.toBigNumber(wei`1 ether`), + }); + const emptyContractAsEOA = await testing.impersonate(emptyContract.address); + + const l1MessengerStubAsEOA = await testing.impersonate( + l1MessengerStub.address + ); + + const l1TokenBridgeImpl = await new L1ERC20TokenBridge__factory( + deployer + ).deploy( + l1MessengerStub.address, + l2TokenBridgeEOA.address, + l1TokenStub.address, + l2TokenStub.address + ); + + const l1TokenBridgeProxy = await new OssifiableProxy__factory( + deployer + ).deploy( + l1TokenBridgeImpl.address, + deployer.address, + l1TokenBridgeImpl.interface.encodeFunctionData("initialize", [ + deployer.address, + ]) + ); + + const l1TokenBridge = L1ERC20TokenBridge__factory.connect( + l1TokenBridgeProxy.address, + deployer + ); + + await l1TokenStub.transfer(l1TokenBridge.address, wei`100 ether`); + + const roles = await Promise.all([ + l1TokenBridge.DEPOSITS_ENABLER_ROLE(), + l1TokenBridge.DEPOSITS_DISABLER_ROLE(), + l1TokenBridge.WITHDRAWALS_ENABLER_ROLE(), + l1TokenBridge.WITHDRAWALS_DISABLER_ROLE(), + ]); + + for (const role of roles) { + await l1TokenBridge.grantRole(role, deployer.address); + } + + await l1TokenBridge.enableDeposits(); + await l1TokenBridge.enableWithdrawals(); + + return { + accounts: { + deployer, + stranger, + l2TokenBridgeEOA, + emptyContractAsEOA, + recipient, + l1MessengerStubAsEOA, + }, + stubs: { + l1Token: l1TokenStub, + l2Token: l2TokenStub, + l1Messenger: l1MessengerStub, + }, + l1TokenBridge, + }; +} diff --git a/test/mantle/L2ERC20TokenBridge.unit.test.ts b/test/mantle/L2ERC20TokenBridge.unit.test.ts new file mode 100644 index 00000000..65f39a9e --- /dev/null +++ b/test/mantle/L2ERC20TokenBridge.unit.test.ts @@ -0,0 +1,469 @@ +import hre from "hardhat"; +import { + ERC20BridgedStub__factory, + L1ERC20TokenBridge__factory, + L2ERC20TokenBridge__factory, + OssifiableProxy__factory, + EmptyContractStub__factory, + CrossDomainMessengerStub__factory, +} from "../../typechain"; +import testing, { unit } from "../../utils/testing"; +import { wei } from "../../utils/wei"; +import { assert } from "chai"; + + +unit("Mantle:: L2ERC20TokenBridge", ctxFactory) + .test("l1TokenBridge()", async (ctx) => { + assert.equal( + await ctx.l2TokenBridge.l1TokenBridge(), + ctx.accounts.l1TokenBridgeEOA.address + ); + }) + + .test("withdraw() :: withdrawals disabled", async (ctx) => { + const { + l2TokenBridge, + stubs: { l2Token: l2TokenStub }, + } = ctx; + + await ctx.l2TokenBridge.disableWithdrawals(); + + assert.isFalse(await ctx.l2TokenBridge.isWithdrawalsEnabled()); + + await assert.revertsWith( + l2TokenBridge.withdraw( + l2TokenStub.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorWithdrawalsDisabled()" + ); + }) + + .test("withdraw() :: unsupported l2Token", async (ctx) => { + const { + l2TokenBridge, + accounts: { stranger }, + } = ctx; + await assert.revertsWith( + l2TokenBridge.withdraw(stranger.address, wei`1 ether`, wei`1 gwei`, "0x"), + "ErrorUnsupportedL2Token()" + ); + }) + + .test("withdraw() :: not from EOA", async (ctx) => { + await assert.revertsWith( + ctx.l2TokenBridge + .connect(ctx.accounts.emptyContractEOA) + .withdraw( + ctx.stubs.l2Token.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorSenderNotEOA()" + ); + }) + + .test("withdraw()", async (ctx) => { + const { + l2TokenBridge, + accounts: { deployer, l1TokenBridgeEOA }, + stubs: { + l2Messenger: l2MessengerStub, + l1Token: l1TokenStub, + l2Token: l2TokenStub, + }, + } = ctx; + + const deployerBalanceBefore = await l2TokenStub.balanceOf(deployer.address); + const totalSupplyBefore = await l2TokenStub.totalSupply(); + + const amount = wei`1 ether`; + const l1Gas = wei`1 wei`; + const data = "0xdeadbeaf"; + + const tx = await l2TokenBridge.withdraw( + l2TokenStub.address, + amount, + l1Gas, + data + ); + + await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ + l1TokenStub.address, + l2TokenStub.address, + deployer.address, + deployer.address, + amount, + data, + ]); + + await assert.emits(l2MessengerStub, tx, "SentMessage", [ + l1TokenBridgeEOA.address, + l2TokenBridge.address, + L1ERC20TokenBridge__factory.createInterface().encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenStub.address, + l2TokenStub.address, + deployer.address, + deployer.address, + amount, + data, + ] + ), + 1, // message nonce + l1Gas, + ]); + + assert.equalBN( + await l2TokenStub.balanceOf(deployer.address), + deployerBalanceBefore.sub(amount) + ); + + assert.equalBN( + await l2TokenStub.totalSupply(), + totalSupplyBefore.sub(amount) + ); + }) + + .test("withdrawTo() :: withdrawals disabled", async (ctx) => { + const { + l2TokenBridge, + stubs: { l2Token: l2TokenStub }, + accounts: { recipient }, + } = ctx; + + await ctx.l2TokenBridge.disableWithdrawals(); + + assert.isFalse(await ctx.l2TokenBridge.isWithdrawalsEnabled()); + + await assert.revertsWith( + l2TokenBridge.withdrawTo( + l2TokenStub.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorWithdrawalsDisabled()" + ); + }) + + .test("withdrawTo() :: unsupported l2Token", async (ctx) => { + const { + l2TokenBridge, + accounts: { stranger, recipient }, + } = ctx; + await assert.revertsWith( + l2TokenBridge.withdrawTo( + stranger.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); + }) + + .test("withdrawTo()", async (ctx) => { + const { + l2TokenBridge, + accounts: { deployer, recipient, l1TokenBridgeEOA }, + stubs: { + l2Messenger: l2MessengerStub, + l1Token: l1TokenStub, + l2Token: l2TokenStub, + }, + } = ctx; + + const deployerBalanceBefore = await l2TokenStub.balanceOf(deployer.address); + const totalSupplyBefore = await l2TokenStub.totalSupply(); + + const amount = wei`1 ether`; + const l1Gas = wei`1 wei`; + const data = "0xdeadbeaf"; + + const tx = await l2TokenBridge.withdrawTo( + l2TokenStub.address, + recipient.address, + amount, + l1Gas, + data + ); + + await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ + l1TokenStub.address, + l2TokenStub.address, + deployer.address, + recipient.address, + amount, + data, + ]); + + await assert.emits(l2MessengerStub, tx, "SentMessage", [ + l1TokenBridgeEOA.address, + l2TokenBridge.address, + L1ERC20TokenBridge__factory.createInterface().encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenStub.address, + l2TokenStub.address, + deployer.address, + recipient.address, + amount, + data, + ] + ), + 1, // message nonce + l1Gas, + ]); + + assert.equalBN( + await l2TokenStub.balanceOf(deployer.address), + deployerBalanceBefore.sub(amount) + ); + + assert.equalBN( + await l2TokenStub.totalSupply(), + totalSupplyBefore.sub(amount) + ); + }) + + .test("finalizeDeposit() :: deposits disabled", async (ctx) => { + const { + l2TokenBridge, + accounts: { l2MessengerStubEOA, deployer, recipient }, + stubs: { l1Token: l1TokenStub, l2Token: l2TokenStub }, + } = ctx; + + await l2TokenBridge.disableDeposits(); + + assert.isFalse(await l2TokenBridge.isDepositsEnabled()); + + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenStub.address, + l2TokenStub.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorDepositsDisabled()" + ); + }) + + .test("finalizeDeposit() :: unsupported l1Token", async (ctx) => { + const { + l2TokenBridge, + accounts: { l2MessengerStubEOA, deployer, recipient, stranger }, + stubs: { l2Token: l2TokenStub }, + } = ctx; + + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + stranger.address, + l2TokenStub.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + }) + + .test("finalizeDeposit() :: unsupported l2Token", async (ctx) => { + const { + l2TokenBridge, + accounts: { l2MessengerStubEOA, deployer, recipient, stranger }, + stubs: { l1Token: l1TokenStub }, + } = ctx; + + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenStub.address, + stranger.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); + }) + + .test("finalizeDeposit() :: unauthorized messenger", async (ctx) => { + const { + l2TokenBridge, + stubs: { l1Token, l2Token }, + accounts: { deployer, recipient, stranger }, + } = ctx; + + await assert.revertsWith( + l2TokenBridge + .connect(stranger) + .finalizeDeposit( + l1Token.address, + l2Token.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnauthorizedMessenger()" + ); + }) + + .test("finalizeDeposit() :: wrong cross domain sender", async (ctx) => { + const { + l2TokenBridge, + stubs: { l1Token, l2Token, l2Messenger }, + accounts: { deployer, recipient, stranger, l2MessengerStubEOA }, + } = ctx; + + await l2Messenger.setXDomainMessageSender(stranger.address); + + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1Token.address, + l2Token.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorWrongCrossDomainSender()" + ); + }) + + .test("finalizeDeposit()", async (ctx) => { + const { + l2TokenBridge, + stubs: { l1Token, l2Token, l2Messenger }, + accounts: { deployer, recipient, l2MessengerStubEOA, l1TokenBridgeEOA }, + } = ctx; + + await l2Messenger.setXDomainMessageSender(l1TokenBridgeEOA.address); + + const totalSupplyBefore = await l2Token.totalSupply(); + + const amount = wei`1 ether`; + const data = "0xdeadbeaf"; + + const tx = await l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1Token.address, + l2Token.address, + deployer.address, + recipient.address, + amount, + data + ); + + await assert.emits(l2TokenBridge, tx, "DepositFinalized", [ + l1Token.address, + l2Token.address, + deployer.address, + recipient.address, + amount, + data, + ]); + + assert.equalBN(await l2Token.balanceOf(recipient.address), amount); + assert.equalBN(await l2Token.totalSupply(), totalSupplyBefore.add(amount)); + }) + + .run(); + +async function ctxFactory() { + const [deployer, stranger, recipient, l1TokenBridgeEOA] = + await hre.ethers.getSigners(); + + const l2Messenger = await new CrossDomainMessengerStub__factory( + deployer + ).deploy({ value: wei.toBigNumber(wei`1 ether`) }); + + const l2MessengerStubEOA = await testing.impersonate(l2Messenger.address); + + const l1Token = await new ERC20BridgedStub__factory(deployer).deploy( + "L1 Token", + "L1" + ); + + const l2Token = await new ERC20BridgedStub__factory(deployer).deploy( + "L2 Token", + "L2" + ); + + const emptyContract = await new EmptyContractStub__factory(deployer).deploy({ + value: wei.toBigNumber(wei`1 ether`), + }); + const emptyContractEOA = await testing.impersonate(emptyContract.address); + + const l2TokenBridgeImpl = await new L2ERC20TokenBridge__factory( + deployer + ).deploy( + l2Messenger.address, + l1TokenBridgeEOA.address, + l1Token.address, + l2Token.address + ); + + const l2TokenBridgeProxy = await new OssifiableProxy__factory( + deployer + ).deploy( + l2TokenBridgeImpl.address, + deployer.address, + l2TokenBridgeImpl.interface.encodeFunctionData("initialize", [ + deployer.address, + ]) + ); + + const l2TokenBridge = L2ERC20TokenBridge__factory.connect( + l2TokenBridgeProxy.address, + deployer + ); + + await l2Token.transfer(l2TokenBridge.address, wei`100 ether`); + + const roles = await Promise.all([ + l2TokenBridge.DEPOSITS_ENABLER_ROLE(), + l2TokenBridge.DEPOSITS_DISABLER_ROLE(), + l2TokenBridge.WITHDRAWALS_ENABLER_ROLE(), + l2TokenBridge.WITHDRAWALS_DISABLER_ROLE(), + ]); + + for (const role of roles) { + await l2TokenBridge.grantRole(role, deployer.address); + } + + await l2TokenBridge.enableDeposits(); + await l2TokenBridge.enableWithdrawals(); + + return { + stubs: { l1Token, l2Token, l2Messenger: l2Messenger }, + accounts: { + deployer, + stranger, + recipient, + l2MessengerStubEOA, + emptyContractEOA, + l1TokenBridgeEOA, + }, + l2TokenBridge, + }; +} diff --git a/test/mantle/_launch.test.ts b/test/mantle/_launch.test.ts new file mode 100644 index 00000000..bb039662 --- /dev/null +++ b/test/mantle/_launch.test.ts @@ -0,0 +1,91 @@ +import { assert } from "chai"; + +import env from "../../utils/env"; +import { wei } from "../../utils/wei"; +import mantle from "../../utils/mantle"; +import testing, { scenario } from "../../utils/testing"; +import { BridgingManagerRole } from "../../utils/bridging-management"; +import { L1ERC20TokenBridge__factory } from "../../typechain"; + +const REVERT = env.bool("REVERT", true); + +scenario("Mantle :: Launch integration test", ctxFactory) + .after(async (ctx) => { + if (REVERT) { + await ctx.l1Provider.send("evm_revert", [ctx.snapshot.l1]); + await ctx.l2Provider.send("evm_revert", [ctx.snapshot.l2]); + } else { + console.warn( + "Revert is skipped! Forked node restart might be required for repeated launches!" + ); + } + }) + + .step("Enable deposits", async (ctx) => { + const { l1ERC20TokenBridge } = ctx; + assert.isFalse(await l1ERC20TokenBridge.isDepositsEnabled()); + + await l1ERC20TokenBridge.enableDeposits(); + assert.isTrue(await l1ERC20TokenBridge.isDepositsEnabled()); + }) + + .step("Renounce role", async (ctx) => { + const { l1ERC20TokenBridge, l1DevMultisig } = ctx; + assert.isTrue( + await l1ERC20TokenBridge.hasRole( + BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, + await l1DevMultisig.getAddress() + ) + ); + + await l1ERC20TokenBridge.renounceRole( + BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, + await l1DevMultisig.getAddress() + ); + assert.isFalse( + await l1ERC20TokenBridge.hasRole( + BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, + await l1DevMultisig.getAddress() + ) + ); + }) + + .run(); + +async function ctxFactory() { + const networkName = env.network("TESTING_MNT_NETWORK", "mainnet"); + + const { l1Provider, l2Provider, l1ERC20TokenBridge } = await mantle + .testing(networkName) + .getIntegrationTestSetup(); + + const hasDeployedContracts = testing.env.USE_DEPLOYED_CONTRACTS(false); + const l1DevMultisig = hasDeployedContracts + ? await testing.impersonate(testing.env.L1_DEV_MULTISIG(), l1Provider) + : testing.accounts.deployer(l1Provider); + + const l1Snapshot = await l1Provider.send("evm_snapshot", []); + const l2Snapshot = await l2Provider.send("evm_snapshot", []); + + await testing.setBalance( + await l1DevMultisig.getAddress(), + wei.toBigNumber(wei`1 ether`), + l1Provider + ); + + const l1ERC20TokenBridgeImpl = L1ERC20TokenBridge__factory.connect( + l1ERC20TokenBridge.address, + l1DevMultisig + ); + + return { + l1Provider, + l2Provider, + l1DevMultisig, + l1ERC20TokenBridge: l1ERC20TokenBridgeImpl, + snapshot: { + l1: l1Snapshot, + l2: l2Snapshot, + }, + }; +} diff --git a/test/mantle/bridging-to.e2e.test.ts b/test/mantle/bridging-to.e2e.test.ts new file mode 100644 index 00000000..1331f7a9 --- /dev/null +++ b/test/mantle/bridging-to.e2e.test.ts @@ -0,0 +1,161 @@ +import { + CrossChainMessenger, + ERC20BridgeAdapter, + MessageStatus, +} from "@mantleio/sdk"; +import { assert } from "chai"; +import { TransactionResponse } from "@ethersproject/providers"; + +import env from "../../utils/env"; +import { wei } from "../../utils/wei"; +import network from "../../utils/network"; +import mantle from "../../utils/mantle"; +import { ERC20Mintable } from "../../typechain"; +import { scenario } from "../../utils/testing"; + +let depositTokensTxResponse: TransactionResponse; +let withdrawTokensTxResponse: TransactionResponse; + +scenario("Mantle :: Bridging via depositTo/withdrawTo E2E test", ctxFactory) + .step( + "Validate tester has required amount of L1 token", + async ({ l1Token, l1Tester, depositAmount }) => { + const balanceBefore = await l1Token.balanceOf(l1Tester.address); + if (balanceBefore.lt(depositAmount)) { + try { + await (l1Token as ERC20Mintable).mint( + l1Tester.address, + depositAmount + ); + } catch {} + const balanceAfter = await l1Token.balanceOf(l1Tester.address); + assert.isTrue( + balanceAfter.gte(depositAmount), + "Tester has not enough L1 token" + ); + } + } + ) + + .step("Set allowance for L1ERC20TokenBridge to deposit", async (ctx) => { + const allowanceTxResponse = await ctx.crossChainMessenger.approveERC20( + ctx.l1Token.address, + ctx.l2Token.address, + ctx.depositAmount + ); + + await allowanceTxResponse.wait(); + + assert.equalBN( + await ctx.l1Token.allowance( + ctx.l1Tester.address, + ctx.l1ERC20TokenBridge.address + ), + ctx.depositAmount + ); + }) + + .step("Bridge tokens to L2 via depositERC20To()", async (ctx) => { + depositTokensTxResponse = await ctx.l1ERC20TokenBridge + .connect(ctx.l1Tester) + .depositERC20To( + ctx.l1Token.address, + ctx.l2Token.address, + ctx.l1Tester.address, + ctx.depositAmount, + 2_000_000, + "0x" + ); + + await depositTokensTxResponse.wait(); + }) + + .step("Waiting for status to change to RELAYED", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + depositTokensTxResponse.hash, + MessageStatus.RELAYED + ); + }) + + .step("Withdraw tokens from L2 via withdrawERC20To()", async (ctx) => { + withdrawTokensTxResponse = await ctx.l2ERC20TokenBridge + .connect(ctx.l2Tester) + .withdrawTo( + ctx.l2Token.address, + ctx.l1Tester.address, + ctx.withdrawalAmount, + 0, + "0x" + ); + await withdrawTokensTxResponse.wait(); + }) + + // .step("Waiting for status to change to READY_TO_PROVE", async (ctx) => { + // await ctx.crossChainMessenger.waitForMessageStatus( + // withdrawTokensTxResponse.hash, + // MessageStatus.READY_TO_PROVE + // ); + // }) + + // .step("Proving the L2 -> L1 message", async (ctx) => { + // const tx = await ctx.crossChainMessenger.proveMessage( + // withdrawTokensTxResponse.hash + // ); + // await tx.wait(); + // }) + + .step("Waiting for status to change to IN_CHALLENGE_PERIOD", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse.hash, + MessageStatus.IN_CHALLENGE_PERIOD + ); + }) + + .step("Waiting for status to change to READY_FOR_RELAY", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse.hash, + MessageStatus.READY_FOR_RELAY + ); + }) + + .step("Finalizing L2 -> L1 message", async (ctx) => { + await ctx.crossChainMessenger.finalizeMessage(withdrawTokensTxResponse); + }) + + .step("Waiting for status to change to RELAYED", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse, + MessageStatus.RELAYED + ); + }) + + .run(); + +async function ctxFactory() { + const networkName = env.network("TESTING_MNT_NETWORK", "goerli"); + const testingSetup = await mantle.testing(networkName).getE2ETestSetup(); + + return { + depositAmount: wei`0.0025 ether`, + withdrawalAmount: wei`0.0025 ether`, + l1Tester: testingSetup.l1Tester, + l2Tester: testingSetup.l2Tester, + l1Token: testingSetup.l1Token, + l2Token: testingSetup.l2Token, + l1ERC20TokenBridge: testingSetup.l1ERC20TokenBridge, + l2ERC20TokenBridge: testingSetup.l2ERC20TokenBridge, + crossChainMessenger: new CrossChainMessenger({ + l2ChainId: network.chainId("mnt", networkName), + l1ChainId: network.chainId("eth", networkName), + l1SignerOrProvider: testingSetup.l1Tester, + l2SignerOrProvider: testingSetup.l2Tester, + bridges: { + LidoBridge: { + Adapter: ERC20BridgeAdapter, + l1Bridge: testingSetup.l1ERC20TokenBridge.address, + l2Bridge: testingSetup.l2ERC20TokenBridge.address, + }, + }, + }), + }; +} diff --git a/test/mantle/bridging.e2e.test.ts b/test/mantle/bridging.e2e.test.ts new file mode 100644 index 00000000..c6d10ed4 --- /dev/null +++ b/test/mantle/bridging.e2e.test.ts @@ -0,0 +1,149 @@ +import { + CrossChainMessenger, + ERC20BridgeAdapter, + MessageStatus, +} from "@mantleio/sdk"; +import { assert } from "chai"; +import { TransactionResponse } from "@ethersproject/providers"; + +import env from "../../utils/env"; +import { wei } from "../../utils/wei"; +import network from "../../utils/network"; +import mantle from "../../utils/mantle"; +import { ERC20Mintable } from "../../typechain"; +import { scenario } from "../../utils/testing"; + +let depositTokensTxResponse: TransactionResponse; +let withdrawTokensTxResponse: TransactionResponse; + +scenario("Mantle :: Bridging via deposit/withdraw E2E test", ctxFactory) + .step( + "Validate tester has required amount of L1 token", + async ({ l1Token, l1Tester, depositAmount }) => { + const balanceBefore = await l1Token.balanceOf(l1Tester.address); + if (balanceBefore.lt(depositAmount)) { + try { + await (l1Token as ERC20Mintable).mint( + l1Tester.address, + depositAmount + ); + } catch {} + const balanceAfter = await l1Token.balanceOf(l1Tester.address); + assert.isTrue( + balanceAfter.gte(depositAmount), + "Tester has not enough L1 token" + ); + } + } + ) + + .step("Set allowance for L1ERC20TokenBridge to deposit", async (ctx) => { + const allowanceTxResponse = await ctx.crossChainMessenger.approveERC20( + ctx.l1Token.address, + ctx.l2Token.address, + ctx.depositAmount + ); + + await allowanceTxResponse.wait(); + + assert.equalBN( + await ctx.l1Token.allowance( + ctx.l1Tester.address, + ctx.l1ERC20TokenBridge.address + ), + ctx.depositAmount + ); + }) + + .step("Bridge tokens to L2 via depositERC20()", async (ctx) => { + depositTokensTxResponse = await ctx.crossChainMessenger.depositERC20( + ctx.l1Token.address, + ctx.l2Token.address, + ctx.depositAmount + ); + await depositTokensTxResponse.wait(); + }) + + .step("Waiting for status to change to RELAYED", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + depositTokensTxResponse.hash, + MessageStatus.RELAYED + ); + }) + + .step("Withdraw tokens from L2 via withdrawERC20()", async (ctx) => { + withdrawTokensTxResponse = await ctx.crossChainMessenger.withdrawERC20( + ctx.l1Token.address, + ctx.l2Token.address, + ctx.withdrawalAmount + ); + await withdrawTokensTxResponse.wait(); + }) + + // .step("Waiting for status to change to READY_TO_PROVE", async (ctx) => { + // await ctx.crossChainMessenger.waitForMessageStatus( + // withdrawTokensTxResponse.hash, + // MessageStatus.READY_TO_PROVE + // ); + // }) + + // .step("Proving the L2 -> L1 message", async (ctx) => { + // const tx = await ctx.crossChainMessenger.proveMessage( + // withdrawTokensTxResponse.hash + // ); + // await tx.wait(); + // }) + + .step("Waiting for status to change to IN_CHALLENGE_PERIOD", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse.hash, + MessageStatus.IN_CHALLENGE_PERIOD + ); + }) + + .step("Waiting for status to change to READY_FOR_RELAY", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse.hash, + MessageStatus.READY_FOR_RELAY + ); + }) + + .step("Finalizing L2 -> L1 message", async (ctx) => { + await ctx.crossChainMessenger.finalizeMessage(withdrawTokensTxResponse); + }) + + .step("Waiting for status to change to RELAYED", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse, + MessageStatus.RELAYED + ); + }) + + .run(); + +async function ctxFactory() { + const networkName = env.network("TESTING_MNT_NETWORK", "goerli"); + const testingSetup = await mantle.testing(networkName).getE2ETestSetup(); + + return { + depositAmount: wei`0.000025 ether`, + withdrawalAmount: wei`0.0000025 ether`, + l1Tester: testingSetup.l1Tester, + l1Token: testingSetup.l1Token, + l2Token: testingSetup.l2Token, + l1ERC20TokenBridge: testingSetup.l1ERC20TokenBridge, + crossChainMessenger: new CrossChainMessenger({ + l2ChainId: network.chainId("mnt", networkName), + l1ChainId: network.chainId("eth", networkName), + l1SignerOrProvider: testingSetup.l1Tester, + l2SignerOrProvider: testingSetup.l2Tester, + bridges: { + LidoBridge: { + Adapter: ERC20BridgeAdapter, + l1Bridge: testingSetup.l1ERC20TokenBridge.address, + l2Bridge: testingSetup.l2ERC20TokenBridge.address, + }, + }, + }), + }; +} diff --git a/test/mantle/bridging.integration.test.ts b/test/mantle/bridging.integration.test.ts new file mode 100644 index 00000000..91255ca9 --- /dev/null +++ b/test/mantle/bridging.integration.test.ts @@ -0,0 +1,613 @@ +import { assert } from "chai"; + +import env from "../../utils/env"; +import { wei } from "../../utils/wei"; +import mantle from "../../utils/mantle"; +import testing, { scenario } from "../../utils/testing"; + +scenario("Mantle :: Bridging integration test", ctxFactory) + .after(async (ctx) => { + await ctx.l1Provider.send("evm_revert", [ctx.snapshot.l1]); + await ctx.l2Provider.send("evm_revert", [ctx.snapshot.l2]); + }) + + .step("Activate bridging on L1", async (ctx) => { + const { l1ERC20TokenBridge } = ctx; + const { l1ERC20TokenBridgeAdmin } = ctx.accounts; + + const isDepositsEnabled = await l1ERC20TokenBridge.isDepositsEnabled(); + + if (!isDepositsEnabled) { + await l1ERC20TokenBridge + .connect(l1ERC20TokenBridgeAdmin) + .enableDeposits(); + } else { + console.log("L1 deposits already enabled"); + } + + const isWithdrawalsEnabled = + await l1ERC20TokenBridge.isWithdrawalsEnabled(); + + if (!isWithdrawalsEnabled) { + await l1ERC20TokenBridge + .connect(l1ERC20TokenBridgeAdmin) + .enableWithdrawals(); + } else { + console.log("L1 withdrawals already enabled"); + } + + assert.isTrue(await l1ERC20TokenBridge.isDepositsEnabled()); + assert.isTrue(await l1ERC20TokenBridge.isWithdrawalsEnabled()); + }) + + .step("Activate bridging on L2", async (ctx) => { + const { l2ERC20TokenBridge } = ctx; + const { l2ERC20TokenBridgeAdmin } = ctx.accounts; + + const isDepositsEnabled = await l2ERC20TokenBridge.isDepositsEnabled(); + + if (!isDepositsEnabled) { + await l2ERC20TokenBridge + .connect(l2ERC20TokenBridgeAdmin) + .enableDeposits(); + } else { + console.log("L2 deposits already enabled"); + } + + const isWithdrawalsEnabled = + await l2ERC20TokenBridge.isWithdrawalsEnabled(); + + if (!isWithdrawalsEnabled) { + await l2ERC20TokenBridge + .connect(l2ERC20TokenBridgeAdmin) + .enableWithdrawals(); + } else { + console.log("L2 withdrawals already enabled"); + } + + assert.isTrue(await l2ERC20TokenBridge.isDepositsEnabled()); + assert.isTrue(await l2ERC20TokenBridge.isWithdrawalsEnabled()); + }) + + .step("L1 -> L2 deposit via depositERC20() method", async (ctx) => { + const { + l1Token, + l1ERC20TokenBridge, + l2Token, + l1CrossDomainMessenger, + l2ERC20TokenBridge, + } = ctx; + const { accountA: tokenHolderA } = ctx.accounts; + const { depositAmount } = ctx.common; + + await l1Token + .connect(tokenHolderA.l1Signer) + .approve(l1ERC20TokenBridge.address, depositAmount); + + const tokenHolderABalanceBefore = await l1Token.balanceOf( + tokenHolderA.address + ); + const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + l1ERC20TokenBridge.address + ); + + const tx = await l1ERC20TokenBridge + .connect(tokenHolderA.l1Signer) + .depositERC20( + l1Token.address, + l2Token.address, + depositAmount, + 200_000, + "0x" + ); + + await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmount, + "0x", + ]); + + const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + "finalizeDeposit", + [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmount, + "0x", + ] + ); + + const messageNonce = await l1CrossDomainMessenger.messageNonce(); + + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + l2ERC20TokenBridge.address, + l1ERC20TokenBridge.address, + l2DepositCalldata, + messageNonce, + 200_000, + ]); + + assert.equalBN( + await l1Token.balanceOf(l1ERC20TokenBridge.address), + l1ERC20TokenBridgeBalanceBefore.add(depositAmount) + ); + + assert.equalBN( + await l1Token.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.sub(depositAmount) + ); + }) + + .step("Finalize deposit on L2", async (ctx) => { + const { + l1Token, + l2Token, + l1ERC20TokenBridge, + l2CrossDomainMessenger, + l2ERC20TokenBridge, + } = ctx; + const { depositAmount } = ctx.common; + const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = + ctx.accounts; + + const tokenHolderABalanceBefore = await l2Token.balanceOf( + tokenHolderA.address + ); + const l2TokenTotalSupplyBefore = await l2Token.totalSupply(); + + const tx = await l2CrossDomainMessenger + .connect(l1CrossDomainMessengerAliased) + .relayMessage( + 1, + l1ERC20TokenBridge.address, + l2ERC20TokenBridge.address, + 0, + 300_000, + l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmount, + "0x", + ]), + { gasLimit: 5_000_000 } + ); + + await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmount, + "0x", + ]); + assert.equalBN( + await l2Token.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.add(depositAmount) + ); + assert.equalBN( + await l2Token.totalSupply(), + l2TokenTotalSupplyBefore.add(depositAmount) + ); + }) + + .step("L2 -> L1 withdrawal via withdraw()", async (ctx) => { + const { accountA: tokenHolderA } = ctx.accounts; + const { withdrawalAmount } = ctx.common; + const { l1Token, l2Token, l2ERC20TokenBridge } = ctx; + + const tokenHolderABalanceBefore = await l2Token.balanceOf( + tokenHolderA.address + ); + const l2TotalSupplyBefore = await l2Token.totalSupply(); + + const tx = await l2ERC20TokenBridge + .connect(tokenHolderA.l2Signer) + .withdraw(l2Token.address, withdrawalAmount, 0, "0x"); + + await assert.emits(l2ERC20TokenBridge, tx, "WithdrawalInitiated", [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderA.address, + withdrawalAmount, + "0x", + ]); + assert.equalBN( + await l2Token.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.sub(withdrawalAmount) + ); + assert.equalBN( + await l2Token.totalSupply(), + l2TotalSupplyBefore.sub(withdrawalAmount) + ); + }) + + .step("Finalize withdrawal on L1", async (ctx) => { + const { + l1Token, + l1CrossDomainMessenger, + l1ERC20TokenBridge, + l2CrossDomainMessenger, + l2Token, + l2ERC20TokenBridge, + } = ctx; + const { accountA: tokenHolderA, l1Stranger } = ctx.accounts; + const { withdrawalAmount } = ctx.common; + + const tokenHolderABalanceBefore = await l1Token.balanceOf( + tokenHolderA.address + ); + const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + l1ERC20TokenBridge.address + ); + + await l1CrossDomainMessenger + .connect(l1Stranger) + .setXDomainMessageSender(l2ERC20TokenBridge.address); + + const tx = await l1CrossDomainMessenger + .connect(l1Stranger) + .relayMessage( + l1ERC20TokenBridge.address, + l2CrossDomainMessenger.address, + l1ERC20TokenBridge.interface.encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderA.address, + withdrawalAmount, + "0x", + ] + ), + 0 + ); + + await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderA.address, + withdrawalAmount, + "0x", + ]); + + assert.equalBN( + await l1Token.balanceOf(l1ERC20TokenBridge.address), + l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) + ); + + assert.equalBN( + await l1Token.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.add(withdrawalAmount) + ); + }) + + .step("L1 -> L2 deposit via depositERC20To()", async (ctx) => { + const { + l1Token, + l2Token, + l1ERC20TokenBridge, + l2ERC20TokenBridge, + l1CrossDomainMessenger, + } = ctx; + const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; + const { depositAmount } = ctx.common; + + assert.notEqual(tokenHolderA.address, tokenHolderB.address); + + await l1Token + .connect(tokenHolderA.l1Signer) + .approve(l1ERC20TokenBridge.address, depositAmount); + + const tokenHolderABalanceBefore = await l1Token.balanceOf( + tokenHolderA.address + ); + const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + l1ERC20TokenBridge.address + ); + + const tx = await l1ERC20TokenBridge + .connect(tokenHolderA.l1Signer) + .depositERC20To( + l1Token.address, + l2Token.address, + tokenHolderB.address, + depositAmount, + 200_000, + "0x" + ); + + await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmount, + "0x", + ]); + + const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + "finalizeDeposit", + [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmount, + "0x", + ] + ); + + const messageNonce = await l1CrossDomainMessenger.messageNonce(); + + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + l2ERC20TokenBridge.address, + l1ERC20TokenBridge.address, + l2DepositCalldata, + messageNonce, + 200_000, + ]); + + assert.equalBN( + await l1Token.balanceOf(l1ERC20TokenBridge.address), + l1ERC20TokenBridgeBalanceBefore.add(depositAmount) + ); + + assert.equalBN( + await l1Token.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.sub(depositAmount) + ); + }) + + .step("Finalize deposit on L2", async (ctx) => { + const { + l1Token, + l1ERC20TokenBridge, + l2Token, + l2CrossDomainMessenger, + l2ERC20TokenBridge, + } = ctx; + const { + accountA: tokenHolderA, + accountB: tokenHolderB, + l1CrossDomainMessengerAliased, + } = ctx.accounts; + const { depositAmount } = ctx.common; + + const l2TokenTotalSupplyBefore = await l2Token.totalSupply(); + const tokenHolderBBalanceBefore = await l2Token.balanceOf( + tokenHolderB.address + ); + + const tx = await l2CrossDomainMessenger + .connect(l1CrossDomainMessengerAliased) + .relayMessage( + 1, + l1ERC20TokenBridge.address, + l2ERC20TokenBridge.address, + 0, + 300_000, + l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmount, + "0x", + ]), + { gasLimit: 5_000_000 } + ); + + await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmount, + "0x", + ]); + + assert.equalBN( + await l2Token.totalSupply(), + l2TokenTotalSupplyBefore.add(depositAmount) + ); + assert.equalBN( + await l2Token.balanceOf(tokenHolderB.address), + tokenHolderBBalanceBefore.add(depositAmount) + ); + }) + + .step("L2 -> L1 withdrawal via withdrawTo()", async (ctx) => { + const { l1Token, l2Token, l2ERC20TokenBridge } = ctx; + const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; + const { withdrawalAmount } = ctx.common; + + const tokenHolderBBalanceBefore = await l2Token.balanceOf( + tokenHolderB.address + ); + const l2TotalSupplyBefore = await l2Token.totalSupply(); + + const tx = await l2ERC20TokenBridge + .connect(tokenHolderB.l2Signer) + .withdrawTo( + l2Token.address, + tokenHolderA.address, + withdrawalAmount, + 0, + "0x" + ); + + await assert.emits(l2ERC20TokenBridge, tx, "WithdrawalInitiated", [ + l1Token.address, + l2Token.address, + tokenHolderB.address, + tokenHolderA.address, + withdrawalAmount, + "0x", + ]); + + assert.equalBN( + await l2Token.balanceOf(tokenHolderB.address), + tokenHolderBBalanceBefore.sub(withdrawalAmount) + ); + + assert.equalBN( + await l2Token.totalSupply(), + l2TotalSupplyBefore.sub(withdrawalAmount) + ); + }) + + .step("Finalize withdrawal on L1", async (ctx) => { + const { + l1Token, + l1CrossDomainMessenger, + l1ERC20TokenBridge, + l2CrossDomainMessenger, + l2Token, + l2ERC20TokenBridge, + } = ctx; + const { + accountA: tokenHolderA, + accountB: tokenHolderB, + l1Stranger, + } = ctx.accounts; + const { withdrawalAmount } = ctx.common; + + const tokenHolderABalanceBefore = await l1Token.balanceOf( + tokenHolderA.address + ); + const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + l1ERC20TokenBridge.address + ); + + await l1CrossDomainMessenger + .connect(l1Stranger) + .setXDomainMessageSender(l2ERC20TokenBridge.address); + + const tx = await l1CrossDomainMessenger + .connect(l1Stranger) + .relayMessage( + l1ERC20TokenBridge.address, + l2CrossDomainMessenger.address, + l1ERC20TokenBridge.interface.encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1Token.address, + l2Token.address, + tokenHolderB.address, + tokenHolderA.address, + withdrawalAmount, + "0x", + ] + ), + 0 + ); + + await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ + l1Token.address, + l2Token.address, + tokenHolderB.address, + tokenHolderA.address, + withdrawalAmount, + "0x", + ]); + + assert.equalBN( + await l1Token.balanceOf(l1ERC20TokenBridge.address), + l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) + ); + + assert.equalBN( + await l1Token.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.add(withdrawalAmount) + ); + }) + + .run(); + +async function ctxFactory() { + const networkName = env.network("TESTING_MNT_NETWORK", "mainnet"); + + const { + l1Provider, + l2Provider, + l1ERC20TokenBridgeAdmin, + l2ERC20TokenBridgeAdmin, + ...contracts + } = await mantle.testing(networkName).getIntegrationTestSetup(); + + const l1Snapshot = await l1Provider.send("evm_snapshot", []); + const l2Snapshot = await l2Provider.send("evm_snapshot", []); + + await mantle.testing(networkName).stubL1CrossChainMessengerContract(); + + const accountA = testing.accounts.accountA(l1Provider, l2Provider); + const accountB = testing.accounts.accountB(l1Provider, l2Provider); + + const depositAmount = wei`0.15 ether`; + const withdrawalAmount = wei`0.05 ether`; + + await testing.setBalance( + await contracts.l1TokensHolder.getAddress(), + wei.toBigNumber(wei`1 ether`), + l1Provider + ); + + await testing.setBalance( + await l1ERC20TokenBridgeAdmin.getAddress(), + wei.toBigNumber(wei`1 ether`), + l1Provider + ); + + await testing.setBalance( + await l2ERC20TokenBridgeAdmin.getAddress(), + wei.toBigNumber(wei`1 ether`), + l2Provider + ); + + await contracts.l1Token + .connect(contracts.l1TokensHolder) + .transfer(accountA.l1Signer.address, wei.toBigNumber(depositAmount).mul(2)); + + const l1CrossDomainMessengerAliased = await testing.impersonate( + testing.accounts.applyL1ToL2Alias(contracts.l1CrossDomainMessenger.address), + l2Provider + ); + + await testing.setBalance( + await l1CrossDomainMessengerAliased.getAddress(), + wei.toBigNumber(wei`1 ether`), + l2Provider + ); + + return { + l1Provider, + l2Provider, + ...contracts, + accounts: { + accountA, + accountB, + l1Stranger: testing.accounts.stranger(l1Provider), + l1ERC20TokenBridgeAdmin, + l2ERC20TokenBridgeAdmin, + l1CrossDomainMessengerAliased, + }, + common: { + depositAmount, + withdrawalAmount, + }, + snapshot: { + l1: l1Snapshot, + l2: l2Snapshot, + }, + }; +} diff --git a/test/mantle/deployment.acceptance.test.ts b/test/mantle/deployment.acceptance.test.ts new file mode 100644 index 00000000..ecf43a2c --- /dev/null +++ b/test/mantle/deployment.acceptance.test.ts @@ -0,0 +1,301 @@ +import { assert } from "chai"; +import { + IERC20Metadata__factory, + OssifiableProxy__factory, +} from "../../typechain"; +import { BridgingManagerRole } from "../../utils/bridging-management"; +import deployment from "../../utils/deployment"; +import env from "../../utils/env"; +import mantle from "../../utils/mantle"; +import { getRoleHolders, scenario } from "../../utils/testing"; +import { wei } from "../../utils/wei"; + +scenario("Mantle Bridge :: deployment acceptance test", ctxFactory) + .step("L1 Bridge :: proxy admin", async (ctx) => { + assert.equal( + await ctx.l1ERC20TokenBridgeProxy.proxy__getAdmin(), + ctx.deployment.l1.proxyAdmin + ); + }) + .step("L1 Bridge :: bridge admin", async (ctx) => { + const currentAdmins = await getRoleHolders( + ctx.l1ERC20TokenBridge, + BridgingManagerRole.DEFAULT_ADMIN_ROLE.hash + ); + assert.equal(currentAdmins.size, 1); + assert.isTrue(currentAdmins.has(ctx.deployment.l1.bridgeAdmin)); + + await assert.isTrue( + await ctx.l1ERC20TokenBridge.hasRole( + BridgingManagerRole.DEFAULT_ADMIN_ROLE.hash, + ctx.deployment.l1.bridgeAdmin + ) + ); + }) + .step("L1 bridge :: L1 token", async (ctx) => { + assert.equal(await ctx.l1ERC20TokenBridge.l1Token(), ctx.deployment.token); + }) + .step("L1 bridge :: L2 token", async (ctx) => { + assert.equal( + await ctx.l1ERC20TokenBridge.l2Token(), + ctx.erc20Bridged.address + ); + }) + .step("L1 bridge :: L2 token bridge", async (ctx) => { + assert.equal( + await ctx.l1ERC20TokenBridge.l2TokenBridge(), + ctx.l2ERC20TokenBridge.address + ); + }) + .step("L1 Bridge :: is deposits enabled", async (ctx) => { + assert.equal( + await ctx.l1ERC20TokenBridge.isDepositsEnabled(), + ctx.deployment.l1.depositsEnabled + ); + }) + .step("L1 Bridge :: is withdrawals enabled", async (ctx) => { + assert.equal( + await ctx.l1ERC20TokenBridge.isWithdrawalsEnabled(), + ctx.deployment.l1.withdrawalsEnabled + ); + }) + .step("L1 Bridge :: deposits enablers", async (ctx) => { + const actualDepositsEnablers = await getRoleHolders( + ctx.l1ERC20TokenBridge, + BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash + ); + const expectedDepositsEnablers = ctx.deployment.l1.depositsEnablers || []; + + assert.equal(actualDepositsEnablers.size, expectedDepositsEnablers.length); + for (const expectedDepositsEnabler of expectedDepositsEnablers) { + assert.isTrue(actualDepositsEnablers.has(expectedDepositsEnabler)); + } + }) + .step("L1 Bridge :: deposits disablers", async (ctx) => { + const actualDepositsDisablers = await getRoleHolders( + ctx.l1ERC20TokenBridge, + BridgingManagerRole.DEPOSITS_DISABLER_ROLE.hash + ); + const expectedDepositsDisablers = ctx.deployment.l1.depositsDisablers || []; + assert.equal( + actualDepositsDisablers.size, + expectedDepositsDisablers.length + ); + for (const expectedDepositsDisabler of expectedDepositsDisablers) { + assert.isTrue(actualDepositsDisablers.has(expectedDepositsDisabler)); + } + }) + .step("L1 Bridge :: withdrawals enablers", async (ctx) => { + const actualWithdrawalsEnablers = await getRoleHolders( + ctx.l1ERC20TokenBridge, + BridgingManagerRole.WITHDRAWALS_ENABLER_ROLE.hash + ); + const expectedWithdrawalsEnablers = + ctx.deployment.l1.withdrawalsEnablers || []; + + assert.equal( + actualWithdrawalsEnablers.size, + expectedWithdrawalsEnablers.length + ); + for (const expectedWithdrawalsEnabler of expectedWithdrawalsEnablers) { + assert.isTrue(actualWithdrawalsEnablers.has(expectedWithdrawalsEnabler)); + } + }) + .step("L1 Bridge :: withdrawals disablers", async (ctx) => { + const actualWithdrawalsDisablers = await getRoleHolders( + ctx.l1ERC20TokenBridge, + BridgingManagerRole.WITHDRAWALS_DISABLER_ROLE.hash + ); + const expectedWithdrawalsDisablers = + ctx.deployment.l1.withdrawalsDisablers || []; + + assert.equal( + actualWithdrawalsDisablers.size, + expectedWithdrawalsDisablers.length + ); + for (const expectedWithdrawalsDisabler of expectedWithdrawalsDisablers) { + assert.isTrue( + actualWithdrawalsDisablers.has(expectedWithdrawalsDisabler) + ); + } + }) + + .step("L2 Bridge :: proxy admin", async (ctx) => { + assert.equal( + await ctx.l2ERC20TokenBridgeProxy.proxy__getAdmin(), + ctx.deployment.l2.proxyAdmin + ); + }) + .step("L2 Bridge :: bridge admin", async (ctx) => { + const currentAdmins = await getRoleHolders( + ctx.l2ERC20TokenBridge, + BridgingManagerRole.DEFAULT_ADMIN_ROLE.hash + ); + assert.equal(currentAdmins.size, 1); + assert.isTrue(currentAdmins.has(ctx.deployment.l2.bridgeAdmin)); + + await assert.isTrue( + await ctx.l2ERC20TokenBridge.hasRole( + BridgingManagerRole.DEFAULT_ADMIN_ROLE.hash, + ctx.deployment.l2.bridgeAdmin + ) + ); + }) + .step("L2 bridge :: L1 token", async (ctx) => { + assert.equal(await ctx.l2ERC20TokenBridge.l1Token(), ctx.deployment.token); + }) + .step("L2 bridge :: L2 token", async (ctx) => { + assert.equal( + await ctx.l2ERC20TokenBridge.l2Token(), + ctx.erc20Bridged.address + ); + }) + .step("L2 bridge :: L1 token bridge", async (ctx) => { + assert.equal( + await ctx.l2ERC20TokenBridge.l1TokenBridge(), + ctx.l1ERC20TokenBridge.address + ); + }) + .step("L2 Bridge :: is deposits enabled", async (ctx) => { + assert.equal( + await ctx.l2ERC20TokenBridge.isDepositsEnabled(), + ctx.deployment.l2.depositsEnabled + ); + }) + .step("L2 Bridge :: is withdrawals enabled", async (ctx) => { + assert.equal( + await ctx.l2ERC20TokenBridge.isWithdrawalsEnabled(), + ctx.deployment.l2.withdrawalsEnabled + ); + }) + .step("L2 Bridge :: deposits enablers", async (ctx) => { + const actualDepositsEnablers = await getRoleHolders( + ctx.l2ERC20TokenBridge, + BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash + ); + const expectedDepositsEnablers = ctx.deployment.l2.depositsEnablers || []; + + assert.equal(actualDepositsEnablers.size, expectedDepositsEnablers.length); + for (const expectedDepositsEnabler of expectedDepositsEnablers) { + assert.isTrue(actualDepositsEnablers.has(expectedDepositsEnabler)); + } + }) + .step("L2 Bridge :: deposits disablers", async (ctx) => { + const actualDepositsDisablers = await getRoleHolders( + ctx.l2ERC20TokenBridge, + BridgingManagerRole.DEPOSITS_DISABLER_ROLE.hash + ); + const expectedDepositsDisablers = ctx.deployment.l2.depositsDisablers || []; + + assert.equal( + actualDepositsDisablers.size, + expectedDepositsDisablers.length + ); + for (const expectedDepositsDisabler of expectedDepositsDisablers) { + assert.isTrue(actualDepositsDisablers.has(expectedDepositsDisabler)); + } + }) + .step("L2 Bridge :: withdrawals enablers", async (ctx) => { + const actualWithdrawalsEnablers = await getRoleHolders( + ctx.l2ERC20TokenBridge, + BridgingManagerRole.WITHDRAWALS_ENABLER_ROLE.hash + ); + const expectedWithdrawalsEnablers = + ctx.deployment.l2.withdrawalsEnablers || []; + + assert.equal( + actualWithdrawalsEnablers.size, + expectedWithdrawalsEnablers.length + ); + for (const expectedWithdrawalsEnabler of expectedWithdrawalsEnablers) { + assert.isTrue(actualWithdrawalsEnablers.has(expectedWithdrawalsEnabler)); + } + }) + .step("L2 Bridge :: withdrawals disablers", async (ctx) => { + const actualWithdrawalsDisablers = await getRoleHolders( + ctx.l2ERC20TokenBridge, + BridgingManagerRole.WITHDRAWALS_DISABLER_ROLE.hash + ); + const expectedWithdrawalsDisablers = + ctx.deployment.l2.withdrawalsDisablers || []; + + assert.equal( + actualWithdrawalsDisablers.size, + expectedWithdrawalsDisablers.length + ); + for (const expectedWithdrawalsDisabler of expectedWithdrawalsDisablers) { + assert.isTrue( + actualWithdrawalsDisablers.has(expectedWithdrawalsDisabler) + ); + } + }) + + .step("L2 Token :: proxy admin", async (ctx) => { + assert.equal( + await ctx.erc20BridgedProxy.proxy__getAdmin(), + ctx.deployment.l2.proxyAdmin + ); + }) + .step("L2 Token :: name", async (ctx) => { + assert.equal(await ctx.erc20Bridged.name(), ctx.l2TokenInfo.name); + }) + .step("L2 Token :: symbol", async (ctx) => { + assert.equal(await ctx.erc20Bridged.symbol(), ctx.l2TokenInfo.symbol); + }) + .step("L2 Token :: decimals", async (ctx) => { + assert.equal(await ctx.erc20Bridged.decimals(), ctx.l2TokenInfo.decimals); + }) + .step("L2 Token :: total supply", async (ctx) => { + assert.equalBN(await ctx.erc20Bridged.totalSupply(), wei`0`); + }) + .step("L2 token :: bridge", async (ctx) => { + assert.equalBN( + await ctx.erc20Bridged.bridge(), + ctx.l2ERC20TokenBridge.address + ); + }) + + .run(); + +async function ctxFactory() { + const networkName = env.network(); + const deploymentConfig = deployment.loadMultiChainDeploymentConfig(); + const testingSetup = await mantle + .testing(networkName) + .getAcceptanceTestSetup(); + + const l1Token = IERC20Metadata__factory.connect( + deploymentConfig.token, + testingSetup.l1Provider + ); + + const [name, symbol, decimals] = await Promise.all([ + l1Token.name(), + l1Token.symbol(), + l1Token.decimals(), + ]); + + return { + deployment: deploymentConfig, + l2TokenInfo: { + name, + symbol, + decimals, + }, + l1ERC20TokenBridge: testingSetup.l1ERC20TokenBridge, + l1ERC20TokenBridgeProxy: OssifiableProxy__factory.connect( + testingSetup.l1ERC20TokenBridge.address, + testingSetup.l1Provider + ), + l2ERC20TokenBridge: testingSetup.l2ERC20TokenBridge, + l2ERC20TokenBridgeProxy: OssifiableProxy__factory.connect( + testingSetup.l2ERC20TokenBridge.address, + testingSetup.l2Provider + ), + erc20Bridged: testingSetup.l2Token, + erc20BridgedProxy: OssifiableProxy__factory.connect( + testingSetup.l2Token.address, + testingSetup.l2Provider + ), + }; +} diff --git a/test/mantle/managing-deposits.e2e.test.ts b/test/mantle/managing-deposits.e2e.test.ts new file mode 100644 index 00000000..e981c3e9 --- /dev/null +++ b/test/mantle/managing-deposits.e2e.test.ts @@ -0,0 +1,170 @@ +import { assert } from "chai"; +import { TransactionResponse } from "@ethersproject/providers"; + +import { + L2ERC20TokenBridge__factory, + GovBridgeExecutor__factory, +} from "../../typechain"; +import { + E2E_TEST_CONTRACTS_MANTLE as E2E_TEST_CONTRACTS, + sleep, +} from "../../utils/testing/e2e"; +import env from "../../utils/env"; +import { wei } from "../../utils/wei"; +import network from "../../utils/network"; +import { scenario } from "../../utils/testing"; +import lido from "../../utils/lido"; +import mantle from "../../utils/mantle"; + +const DEPOSIT_ENABLER_ROLE = + "0x4b43b36766bde12c5e9cbbc37d15f8d1f769f08f54720ab370faeb4ce893753a"; +const DEPOSIT_DISABLER_ROLE = + "0x63f736f21cb2943826cd50b191eb054ebbea670e4e962d0527611f830cd399d6"; + +let l2DepositsInitialState = true; + +let messageTx: TransactionResponse; + +const scenarioTest = scenario( + "Mantle :: AAVE governance crosschain bridge: token bridge management", + ctxFactory +) + .step("LDO Holder has enought ETH", async ({ l1LDOHolder, gasAmount }) => { + assert.gte(await l1LDOHolder.getBalance(), gasAmount); + }) + + .step("Checking deposits status", async ({ l2ERC20TokenBridge }) => { + l2DepositsInitialState = await l2ERC20TokenBridge.isDepositsEnabled(); + }) + + .step(`Starting DAO vote`, async (ctx) => { + const grantRoleCalldata = + ctx.l2ERC20TokenBridge.interface.encodeFunctionData("grantRole", [ + l2DepositsInitialState ? DEPOSIT_DISABLER_ROLE : DEPOSIT_ENABLER_ROLE, + ctx.govBridgeExecutor.address, + ]); + const grantRoleData = "0x" + grantRoleCalldata.substring(10); + + const actionCalldata = l2DepositsInitialState + ? ctx.l2ERC20TokenBridge.interface.encodeFunctionData("disableDeposits") + : ctx.l2ERC20TokenBridge.interface.encodeFunctionData("enableDeposits"); + + const actionData = "0x" + actionCalldata.substring(10); + + const executorCalldata = + await ctx.govBridgeExecutor.interface.encodeFunctionData("queue", [ + [ctx.l2ERC20TokenBridge.address, ctx.l2ERC20TokenBridge.address], + [0, 0], + [ + "grantRole(bytes32,address)", + l2DepositsInitialState ? "disableDeposits()" : "enableDeposits()", + ], + [grantRoleData, actionData], + [false, false], + ]); + + const mntAddresses = mantle.addresses("goerli"); + + const { calldata, callvalue } = await ctx.messaging.prepareL2Message({ + sender: ctx.lidoAragonDAO.agent.address, + recipient: ctx.govBridgeExecutor.address, + calldata: executorCalldata, + }); + + const tx = await ctx.lidoAragonDAO.createVote( + ctx.l1LDOHolder, + "E2E Test Voting", + { + address: ctx.lidoAragonDAO.agent.address, + signature: "execute(address,uint256,bytes)", + decodedCallData: [ + mntAddresses.L1CrossDomainMessenger, + callvalue, + calldata, + ], + } + ); + + await tx.wait(); + }) + + .step("Enacting Vote", async ({ lidoAragonDAO, l1LDOHolder }) => { + const votesLength = await lidoAragonDAO.voting.votesLength(); + + messageTx = await lidoAragonDAO.voteAndExecute( + l1LDOHolder, + votesLength.toNumber() - 1 + ); + + await messageTx.wait(); + }) + + .step("Waiting for status to change to RELAYED", async ({ messaging }) => { + await messaging.waitForL2Message(messageTx.hash); + }) + + .step("Execute queued task", async ({ govBridgeExecutor, l2Tester }) => { + const tasksCount = await govBridgeExecutor.getActionsSetCount(); + + const targetTask = tasksCount.toNumber() - 1; + + const executionTime = ( + await govBridgeExecutor.getActionsSetById(targetTask) + ).executionTime.toNumber(); + let chainTime; + + do { + await sleep(5000); + const currentBlockNumber = await l2Tester.provider.getBlockNumber(); + const currentBlock = await l2Tester.provider.getBlock(currentBlockNumber); + chainTime = currentBlock.timestamp; + } while (chainTime <= executionTime); + + const tx = await govBridgeExecutor.execute(targetTask); + await tx.wait(); + }) + + .step("Checking deposits state", async ({ l2ERC20TokenBridge }) => { + assert.equal( + await l2ERC20TokenBridge.isDepositsEnabled(), + !l2DepositsInitialState + ); + }); + +// make first run to change state from enabled/disabled -> disabled/enabled +scenarioTest.run(); + +// make another run to return the state to the initial and test vice versa actions +scenarioTest.run(); + +async function ctxFactory() { + const ethMntNetwork = network.multichain(["eth", "mnt"], "goerli"); + + const [l1Provider] = ethMntNetwork.getProviders({ forking: false }); + const [l1Tester, l2Tester] = ethMntNetwork.getSigners( + env.string("TESTING_PRIVATE_KEY"), + { forking: false } + ); + + const [l1LDOHolder] = ethMntNetwork.getSigners( + env.string("TESTING_MNT_LDO_HOLDER_PRIVATE_KEY"), + { forking: false } + ); + + return { + lidoAragonDAO: lido("goerli", l1Provider), + messaging: mantle.messaging("goerli", { forking: false }), + gasAmount: wei`0.1 ether`, + l1Tester, + l2Tester, + l1LDOHolder, + l2ERC20TokenBridge: L2ERC20TokenBridge__factory.connect( + E2E_TEST_CONTRACTS.l2.l2ERC20TokenBridge, + l2Tester + ), + govBridgeExecutor: GovBridgeExecutor__factory.connect( + E2E_TEST_CONTRACTS.l2.govBridgeExecutor, + l2Tester + ), + }; +} diff --git a/test/mantle/managing-executor.e2e.test.ts b/test/mantle/managing-executor.e2e.test.ts new file mode 100644 index 00000000..05436b5a --- /dev/null +++ b/test/mantle/managing-executor.e2e.test.ts @@ -0,0 +1,146 @@ +import { assert } from "chai"; +import { TransactionResponse } from "@ethersproject/providers"; + +import { + L2ERC20TokenBridge__factory, + GovBridgeExecutor__factory, +} from "../../typechain"; +import { + E2E_TEST_CONTRACTS_MANTLE as E2E_TEST_CONTRACTS, + sleep, +} from "../../utils/testing/e2e"; +import env from "../../utils/env"; +import { wei } from "../../utils/wei"; +import network from "../../utils/network"; +import { scenario } from "../../utils/testing"; +import lido from "../../utils/lido"; +import mantle from "../../utils/mantle"; + +let messageTx: TransactionResponse; +let oldGuardian: string; +let newGuardian: string; + +scenario("Mantle :: AAVE governance crosschain bridge management", ctxFactory) + .step("LDO Holder has enought ETH", async ({ l1LDOHolder, gasAmount }) => { + assert.gte(await l1LDOHolder.getBalance(), gasAmount); + }) + + .step(`Starting DAO vote: Update guardian`, async (ctx) => { + oldGuardian = await ctx.govBridgeExecutor.getGuardian(); + newGuardian = + oldGuardian === "0x4e8CC9024Ea3FE886623025fF2aD0CA4bb3D1F42" + ? "0xD06491e4C8B3107B83dC134894C4c96ED8ddbfa2" + : "0x4e8CC9024Ea3FE886623025fF2aD0CA4bb3D1F42"; + + const updateGuardianCalldata = + ctx.govBridgeExecutor.interface.encodeFunctionData("updateGuardian", [ + newGuardian, + ]); + const updateGuardianData = "0x" + updateGuardianCalldata.substring(10); + + const executorCalldata = + await ctx.govBridgeExecutor.interface.encodeFunctionData("queue", [ + [ctx.govBridgeExecutor.address], + [0], + ["updateGuardian(address)"], + [updateGuardianData], + [false], + ]); + + const mntAddresses = mantle.addresses("goerli"); + + const { calldata, callvalue } = await ctx.messaging.prepareL2Message({ + sender: ctx.lidoAragonDAO.agent.address, + recipient: ctx.govBridgeExecutor.address, + calldata: executorCalldata, + }); + + const tx = await ctx.lidoAragonDAO.createVote( + ctx.l1LDOHolder, + "E2E Test Voting", + { + address: ctx.lidoAragonDAO.agent.address, + signature: "execute(address,uint256,bytes)", + decodedCallData: [ + mntAddresses.L1CrossDomainMessenger, + callvalue, + calldata, + ], + } + ); + + await tx.wait(); + }) + + .step("Enacting Vote", async ({ lidoAragonDAO, l1LDOHolder }) => { + const votesLength = await lidoAragonDAO.voting.votesLength(); + + messageTx = await lidoAragonDAO.voteAndExecute( + l1LDOHolder, + votesLength.toNumber() - 1 + ); + + await messageTx.wait(); + }) + + .step("Waiting for status to change to RELAYED", async ({ messaging }) => { + await messaging.waitForL2Message(messageTx.hash); + }) + + .step("Execute queued task", async ({ govBridgeExecutor, l2Tester }) => { + const tasksCount = await govBridgeExecutor.getActionsSetCount(); + + const targetTask = tasksCount.toNumber() - 1; + + const executionTime = ( + await govBridgeExecutor.getActionsSetById(targetTask) + ).executionTime.toNumber(); + let chainTime; + + do { + await sleep(5000); + const currentBlockNumber = await l2Tester.provider.getBlockNumber(); + const currentBlock = await l2Tester.provider.getBlock(currentBlockNumber); + chainTime = currentBlock.timestamp; + } while (chainTime <= executionTime); + + const tx = await govBridgeExecutor.execute(targetTask); + await tx.wait(); + }) + + .step("Checking guardian", async ({ govBridgeExecutor }) => { + assert.equal(await govBridgeExecutor.getGuardian(), newGuardian); + }) + + .run(); + +async function ctxFactory() { + const ethMntNetwork = network.multichain(["eth", "mnt"], "goerli"); + + const [l1Provider] = ethMntNetwork.getProviders({ forking: false }); + const [, l2Tester] = ethMntNetwork.getSigners( + env.string("TESTING_PRIVATE_KEY"), + { forking: false } + ); + + const [l1LDOHolder] = ethMntNetwork.getSigners( + env.string("TESTING_MNT_LDO_HOLDER_PRIVATE_KEY"), + { forking: false } + ); + + return { + lidoAragonDAO: lido("goerli", l1Provider), + messaging: mantle.messaging("goerli", { forking: false }), + gasAmount: wei`0.1 ether`, + l2Tester, + l1LDOHolder, + l2ERC20TokenBridge: L2ERC20TokenBridge__factory.connect( + E2E_TEST_CONTRACTS.l2.l2ERC20TokenBridge, + l2Tester + ), + govBridgeExecutor: GovBridgeExecutor__factory.connect( + E2E_TEST_CONTRACTS.l2.govBridgeExecutor, + l2Tester + ), + }; +} diff --git a/test/mantle/managing-proxy.e2e.test.ts b/test/mantle/managing-proxy.e2e.test.ts new file mode 100644 index 00000000..504f3c00 --- /dev/null +++ b/test/mantle/managing-proxy.e2e.test.ts @@ -0,0 +1,221 @@ +import { assert } from "chai"; +import { TransactionResponse } from "@ethersproject/providers"; + +import { + ERC20BridgedPermit__factory, + GovBridgeExecutor__factory, + OssifiableProxy__factory, + L2ERC20TokenBridge__factory, +} from "../../typechain"; +import { E2E_TEST_CONTRACTS_MANTLE as E2E_TEST_CONTRACTS } from "../../utils/testing/e2e"; +import env from "../../utils/env"; +import { wei } from "../../utils/wei"; +import network from "../../utils/network"; +import { scenario } from "../../utils/testing"; +import lido from "../../utils/lido"; +import mantle from "../../utils/mantle"; + +let ossifyMessageResponse: TransactionResponse; +let upgradeMessageResponse: TransactionResponse; + +scenario( + "Mantle :: AAVE governance crosschain bridge: proxy management", + ctxFactory +) + .step("Check OssifiableProxy deployed correct", async (ctx) => { + const { proxyToOssify } = ctx; + const admin = await proxyToOssify.proxy__getAdmin(); + + assert.equal(admin, E2E_TEST_CONTRACTS.l2.govBridgeExecutor); + }) + + .step("Proxy upgrade: send crosschain message", async (ctx) => { + const implBefore = await await ctx.proxyToOssify.proxy__getImplementation(); + + assert.equal(implBefore, ctx.l2ERC20TokenBridge.address); + const executorCalldata = + await ctx.govBridgeExecutor.interface.encodeFunctionData("queue", [ + [ctx.proxyToOssify.address], + [0], + ["proxy__upgradeTo(address)"], + [ + "0x" + + ctx.proxyToOssify.interface + .encodeFunctionData("proxy__upgradeTo", [ctx.l2Token.address]) + .substring(10), + ], + [false], + ]); + + const mntAddresses = mantle.addresses("goerli"); + + const { calldata, callvalue } = await ctx.messaging.prepareL2Message({ + sender: ctx.lidoAragonDAO.agent.address, + recipient: ctx.govBridgeExecutor.address, + calldata: executorCalldata, + }); + + const tx = await ctx.lidoAragonDAO.createVote( + ctx.l1LDOHolder, + "E2E Test Voting", + { + address: ctx.lidoAragonDAO.agent.address, + signature: "execute(address,uint256,bytes)", + decodedCallData: [ + mntAddresses.L1CrossDomainMessenger, + callvalue, + calldata, + ], + } + ); + + await tx.wait(); + }) + + .step( + "Proxy upgrade: Enacting Voting", + async ({ lidoAragonDAO, l1LDOHolder }) => { + const votesLength = await lidoAragonDAO.voting.votesLength(); + + upgradeMessageResponse = await lidoAragonDAO.voteAndExecute( + l1LDOHolder, + votesLength.toNumber() - 1 + ); + + await upgradeMessageResponse.wait(); + } + ) + + .step("Proxy upgrade: wait for relay", async ({ messaging }) => { + await messaging.waitForL2Message(upgradeMessageResponse.hash); + }) + + .step( + "Proxy upgrade: execute", + async ({ proxyToOssify, govBridgeExecutor, l2Token }) => { + const taskId = + (await govBridgeExecutor.getActionsSetCount()).toNumber() - 1; + + const executeTx = await govBridgeExecutor.execute(taskId); + await executeTx.wait(); + const implAfter = await await proxyToOssify.proxy__getImplementation(); + + assert(implAfter, l2Token.address); + } + ) + + .step("Proxy ossify: send crosschain message", async (ctx) => { + const isOssifiedBefore = await ctx.proxyToOssify.proxy__getIsOssified(); + + assert.isFalse(isOssifiedBefore); + + const executorCalldata = + await ctx.govBridgeExecutor.interface.encodeFunctionData("queue", [ + [ctx.proxyToOssify.address], + [0], + ["proxy__ossify()"], + ["0x00"], + [false], + ]); + + const mntAddresses = mantle.addresses("goerli"); + + const { calldata, callvalue } = await ctx.messaging.prepareL2Message({ + sender: ctx.lidoAragonDAO.agent.address, + recipient: ctx.govBridgeExecutor.address, + calldata: executorCalldata, + }); + + const tx = await ctx.lidoAragonDAO.createVote( + ctx.l1LDOHolder, + "E2E Test Voting", + { + address: ctx.lidoAragonDAO.agent.address, + signature: "execute(address,uint256,bytes)", + decodedCallData: [ + mntAddresses.L1CrossDomainMessenger, + callvalue, + calldata, + ], + } + ); + + await tx.wait(); + }) + + .step( + "Proxy ossify: Enacting Voting", + async ({ lidoAragonDAO, l1LDOHolder }) => { + const votesLength = await lidoAragonDAO.voting.votesLength(); + + ossifyMessageResponse = await lidoAragonDAO.voteAndExecute( + l1LDOHolder, + votesLength.toNumber() - 1 + ); + + await ossifyMessageResponse.wait(); + } + ) + + .step("Proxy ossify: wait for relay", async ({ messaging }) => { + await messaging.waitForL2Message(ossifyMessageResponse.hash); + }) + + .step( + "Proxy ossify: execute", + async ({ govBridgeExecutor, proxyToOssify }) => { + const taskId = + (await govBridgeExecutor.getActionsSetCount()).toNumber() - 1; + const executeTx = await govBridgeExecutor.execute(taskId, { + gasLimit: 2000000, + }); + await executeTx.wait(); + + const isOssifiedAfter = await proxyToOssify.proxy__getIsOssified(); + + assert.isTrue(isOssifiedAfter); + } + ) + + .run(); + +async function ctxFactory() { + const ethMntNetwork = network.multichain(["eth", "mnt"], "goerli"); + + const [l1Provider] = ethMntNetwork.getProviders({ forking: false }); + const [l1Tester, l2Tester] = ethMntNetwork.getSigners( + env.string("TESTING_PRIVATE_KEY"), + { forking: false } + ); + + const [l1LDOHolder] = ethMntNetwork.getSigners( + env.string("TESTING_MNT_LDO_HOLDER_PRIVATE_KEY"), + { forking: false } + ); + + return { + lidoAragonDAO: lido("goerli", l1Provider), + messaging: mantle.messaging("goerli", { forking: false }), + gasAmount: wei`0.1 ether`, + l1Tester, + l2Tester, + l1LDOHolder, + l2Token: ERC20BridgedPermit__factory.connect( + E2E_TEST_CONTRACTS.l2.l2Token, + l2Tester + ), + l2ERC20TokenBridge: L2ERC20TokenBridge__factory.connect( + E2E_TEST_CONTRACTS.l2.l2ERC20TokenBridge, + l2Tester + ), + govBridgeExecutor: GovBridgeExecutor__factory.connect( + E2E_TEST_CONTRACTS.l2.govBridgeExecutor, + l2Tester + ), + proxyToOssify: await new OssifiableProxy__factory(l2Tester).deploy( + E2E_TEST_CONTRACTS.l2.l2ERC20TokenBridge, + E2E_TEST_CONTRACTS.l2.govBridgeExecutor, + "0x" + ), + }; +} diff --git a/test/token/ERC20BridgedPermit.unit.test.ts b/test/token/ERC20BridgedPermit.unit.test.ts new file mode 100644 index 00000000..68a7a2d7 --- /dev/null +++ b/test/token/ERC20BridgedPermit.unit.test.ts @@ -0,0 +1,129 @@ +import { assert } from "chai"; +import hre from "hardhat"; +import { + ERC20BridgedPermit__factory, + OssifiableProxy__factory, +} from "../../typechain"; +import { unit } from "../../utils/testing"; +import { wei } from "../../utils/wei"; +import { getPermitSignature } from "../../utils/testing/permit"; +import { constants } from 'ethers' + +unit("ERC20BridgedPermit", ctxFactory) + .test("bridge()", async (ctx) => { + assert.equal(await ctx.erc20BridgedPermit.bridge(), ctx.accounts.owner.address); + }) + + .test("totalSupply()", async (ctx) => { + assert.equalBN(await ctx.erc20BridgedPermit.totalSupply(), ctx.constants.premint); + }) + + .test("initialize() :: name already set", async (ctx) => { + const { deployer, owner } = ctx.accounts; + + // deploy new implementation + const erc20BridgedPermitImpl = await new ERC20BridgedPermit__factory(deployer).deploy( + "Name", + "", + 9, + owner.address + ); + await assert.revertsWith( + erc20BridgedPermitImpl.initialize("New Name", ""), + "ErrorNameAlreadySet()" + ); + }) + + .test("permit() :: valid signature", async (ctx) => { + const { erc20BridgedPermit } = ctx; + const { holder, spender } = ctx.accounts; + const value = 123 + + const { v, r, s } = await getPermitSignature(holder, erc20BridgedPermit, spender.address, value) + + assert.equalBN(await erc20BridgedPermit.allowance(holder.address, spender.address), 0) + await erc20BridgedPermit.permit( + holder.address, + spender.address, + value, + constants.MaxUint256, + v, + r, + s + ) + + assert.equalBN(await erc20BridgedPermit.allowance(holder.address, spender.address), value) + }) + + .test("useNonce() :: can invalidate valid signature", async (ctx) => { + const { erc20BridgedPermit } = ctx; + const { holder, spender } = ctx.accounts; + const value = 123 + + const { v, r, s } = await getPermitSignature(holder, erc20BridgedPermit, spender.address, value) + + assert.equalBN(await erc20BridgedPermit.allowance(holder.address, spender.address), 0) + + await erc20BridgedPermit.useNonce() + + await assert.revertsWith( + erc20BridgedPermit.permit( + holder.address, + spender.address, + value, + constants.MaxUint256, + v, + r, + s + ), + "ErrorInvalidSignature()" + ); + + assert.equalBN(await erc20BridgedPermit.allowance(holder.address, spender.address), 0) + }) + + .run(); + + async function ctxFactory() { + const name = "ERC20 Test Token"; + const symbol = "ERC20"; + const decimals = 18; + const premint = wei`100 ether`; + const [deployer, owner, recipient, spender, holder, stranger] = + await hre.ethers.getSigners(); + const l2TokenImpl = await new ERC20BridgedPermit__factory(deployer).deploy( + name, + symbol, + decimals, + owner.address + ); + + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [hre.ethers.constants.AddressZero], + }); + + const zero = await hre.ethers.getSigner(hre.ethers.constants.AddressZero); + + const l2TokensProxy = await new OssifiableProxy__factory(deployer).deploy( + l2TokenImpl.address, + deployer.address, + ERC20BridgedPermit__factory.createInterface().encodeFunctionData("initialize", [ + name, + symbol, + ]) + ); + + const erc20BridgedPermitProxied = ERC20BridgedPermit__factory.connect( + l2TokensProxy.address, + holder + ); + ERC20BridgedPermit__factory + await erc20BridgedPermitProxied.connect(owner).bridgeMint(holder.address, premint); + + return { + accounts: { deployer, owner, recipient, spender, holder, zero, stranger }, + constants: { name, symbol, decimals, premint }, + erc20BridgedPermit: erc20BridgedPermitProxied, + }; + } \ No newline at end of file diff --git a/utils/deployment/DeployScript.ts b/utils/deployment/DeployScript.ts index d2617b4c..3b33dd3f 100644 --- a/utils/deployment/DeployScript.ts +++ b/utils/deployment/DeployScript.ts @@ -169,6 +169,8 @@ export class DeployScript { 5: "eth_goerli", 10: "opt_mainnet", 420: "opt_goerli", + 5000: "mnt_mainnet", + 5001: "mnt_goerli", 31337: "hardhat", 42161: "arb_mainnet", 421613: "arb_goerli", diff --git a/utils/lido.ts b/utils/lido.ts index 949f8f4a..9c68619e 100644 --- a/utils/lido.ts +++ b/utils/lido.ts @@ -16,9 +16,9 @@ const ARAGON_MAINNET = { }; const ARAGON_GOERLI = { - agent: "0x4333218072D5d7008546737786663c38B4D561A4", - voting: "0xbc0B67b4553f4CF52a913DE9A6eD0057E2E758Db", - tokenManager: "0xDfe76d11b365f5e0023343A367f0b311701B3bc1", + agent: "0x45B1F6E7ABFf8A8bf516554634Abf37D73C79fBC", + voting: "0xfDdA522eF6626e155d47Be0aeF74c204CfB3d2c4", + tokenManager: "0xF3BfaD8a6960ad130e02c9d14262788dea2C3Cd5", }; const ARAGON_CONTRACTS_BY_NAME = { diff --git a/utils/mantle/addresses.ts b/utils/mantle/addresses.ts new file mode 100644 index 00000000..5c51c14b --- /dev/null +++ b/utils/mantle/addresses.ts @@ -0,0 +1,28 @@ +import { NetworkName } from "../network"; +import { MntContractAddresses, CommonOptions } from "./types"; + +const MantleMainnetAddresses: MntContractAddresses = { + L1CrossDomainMessenger: "0x676A795fe6E43C17c668de16730c3F690FEB7120", + L2CrossDomainMessenger: "0x4200000000000000000000000000000000000007", + CanonicalTransactionChain: "0x291dc3819b863e19b0a9b9809F8025d2EB4aaE93", +}; + +const MantleGoerliAddresses: MntContractAddresses = { + L1CrossDomainMessenger: "0x7Bfe603647d5380ED3909F6f87580D0Af1B228B4", + L2CrossDomainMessenger: "0x4200000000000000000000000000000000000007", + CanonicalTransactionChain: "0x258e80D5371fD7fFdDFE29E60b366f9FC44844c8", +}; + +export default function addresses( + networkName: NetworkName, + options: CommonOptions = {} +) { + switch (networkName) { + case "mainnet": + return { ...MantleMainnetAddresses, ...options.customAddresses }; + case "goerli": + return { ...MantleGoerliAddresses, ...options.customAddresses }; + default: + throw new Error(`Network "${networkName}" is not supported`); + } +} diff --git a/utils/mantle/artifacts/MantleBridgeExecutor.json b/utils/mantle/artifacts/MantleBridgeExecutor.json new file mode 100644 index 00000000..122ea45b --- /dev/null +++ b/utils/mantle/artifacts/MantleBridgeExecutor.json @@ -0,0 +1,714 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "MantleBridgeExecutor", + "sourceName": "contracts/bridges/MantleBridgeExecutor.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "bvmL2CrossDomainMessenger", + "type": "address" + }, + { + "internalType": "address", + "name": "ethereumGovernanceExecutor", + "type": "address" + }, + { + "internalType": "uint256", + "name": "delay", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gracePeriod", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minimumDelay", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maximumDelay", + "type": "uint256" + }, + { + "internalType": "address", + "name": "guardian", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "DelayLongerThanMax", + "type": "error" + }, + { + "inputs": [], + "name": "DelayShorterThanMin", + "type": "error" + }, + { + "inputs": [], + "name": "DuplicateAction", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyTargets", + "type": "error" + }, + { + "inputs": [], + "name": "FailedActionExecution", + "type": "error" + }, + { + "inputs": [], + "name": "GracePeriodTooShort", + "type": "error" + }, + { + "inputs": [], + "name": "InconsistentParamsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidActionsSetId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitParams", + "type": "error" + }, + { + "inputs": [], + "name": "MaximumDelayTooShort", + "type": "error" + }, + { + "inputs": [], + "name": "MinimumDelayTooLong", + "type": "error" + }, + { + "inputs": [], + "name": "NotGuardian", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyCallableByThis", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyQueuedActions", + "type": "error" + }, + { + "inputs": [], + "name": "TimelockNotFinished", + "type": "error" + }, + { + "inputs": [], + "name": "UnauthorizedEthereumExecutor", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "ActionsSetCanceled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "initiatorExecution", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes[]", + "name": "returnedData", + "type": "bytes[]" + } + ], + "name": "ActionsSetExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "targets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "string[]", + "name": "signatures", + "type": "string[]" + }, + { + "indexed": false, + "internalType": "bytes[]", + "name": "calldatas", + "type": "bytes[]" + }, + { + "indexed": false, + "internalType": "bool[]", + "name": "withDelegatecalls", + "type": "bool[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "executionTime", + "type": "uint256" + } + ], + "name": "ActionsSetQueued", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldDelay", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newDelay", + "type": "uint256" + } + ], + "name": "DelayUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldEthereumGovernanceExecutor", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newEthereumGovernanceExecutor", + "type": "address" + } + ], + "name": "EthereumGovernanceExecutorUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldGracePeriod", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newGracePeriod", + "type": "uint256" + } + ], + "name": "GracePeriodUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldGuardian", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newGuardian", + "type": "address" + } + ], + "name": "GuardianUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldMaximumDelay", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newMaximumDelay", + "type": "uint256" + } + ], + "name": "MaximumDelayUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldMinimumDelay", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newMinimumDelay", + "type": "uint256" + } + ], + "name": "MinimumDelayUpdate", + "type": "event" + }, + { + "inputs": [], + "name": "BVM_L2_CROSS_DOMAIN_MESSENGER", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "actionsSetId", + "type": "uint256" + } + ], + "name": "cancel", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "actionsSetId", + "type": "uint256" + } + ], + "name": "execute", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "executeDelegateCall", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "actionsSetId", + "type": "uint256" + } + ], + "name": "getActionsSetById", + "outputs": [ + { + "components": [ + { + "internalType": "address[]", + "name": "targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "internalType": "string[]", + "name": "signatures", + "type": "string[]" + }, + { + "internalType": "bytes[]", + "name": "calldatas", + "type": "bytes[]" + }, + { + "internalType": "bool[]", + "name": "withDelegatecalls", + "type": "bool[]" + }, + { + "internalType": "uint256", + "name": "executionTime", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "executed", + "type": "bool" + }, + { + "internalType": "bool", + "name": "canceled", + "type": "bool" + } + ], + "internalType": "struct IExecutorBase.ActionsSet", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getActionsSetCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "actionsSetId", + "type": "uint256" + } + ], + "name": "getCurrentState", + "outputs": [ + { + "internalType": "enum IExecutorBase.ActionsSetState", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getDelay", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getEthereumGovernanceExecutor", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getGracePeriod", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getGuardian", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMaximumDelay", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMinimumDelay", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "actionHash", + "type": "bytes32" + } + ], + "name": "isActionQueued", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "internalType": "string[]", + "name": "signatures", + "type": "string[]" + }, + { + "internalType": "bytes[]", + "name": "calldatas", + "type": "bytes[]" + }, + { + "internalType": "bool[]", + "name": "withDelegatecalls", + "type": "bool[]" + } + ], + "name": "queue", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "receiveFunds", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "delay", + "type": "uint256" + } + ], + "name": "updateDelay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "ethereumGovernanceExecutor", + "type": "address" + } + ], + "name": "updateEthereumGovernanceExecutor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "gracePeriod", + "type": "uint256" + } + ], + "name": "updateGracePeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "guardian", + "type": "address" + } + ], + "name": "updateGuardian", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "maximumDelay", + "type": "uint256" + } + ], + "name": "updateMaximumDelay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "minimumDelay", + "type": "uint256" + } + ], + "name": "updateMinimumDelay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60a06040523480156200001157600080fd5b50604051620027ec380380620027ec833981016040819052620000349162000280565b8585858585858484848484610258841080620000505750818310155b806200005b57508285105b806200006657508185115b156200008557604051630a5addd160e21b815260040160405180910390fd5b6200009085620000f6565b6200009b8462000137565b620000a68362000178565b620000b182620001b9565b620000bc81620001fa565b5050600880546001600160a01b0319166001600160a01b039a8b16179055505050509a90931660805250620002f198505050505050505050565b60005460408051918252602082018390527f43de56b886294fc29767e51a88b5c67fd24aefebc5ddf813b1d9b91b1df38444910160405180910390a1600055565b60015460408051918252602082018390527f9953f3a71052edcd4ae7a0f97302839d1dda32ab93c1039207c91c866b094f72910160405180910390a1600155565b60025460408051918252602082018390527fc534cdcbe9b52100810d787afd57e4174322776fcb58872ea706f23e9319fa8d910160405180910390a1600255565b60035460408051918252602082018390527faf46013422363beb5e6f00ab923cffe3574670494c864de2828b9d7c201fdde5910160405180910390a1600355565b600454604080516001600160a01b03928316815291831660208301527f85bd8788d3c4a160f0f6254229589f137d5633a870dcb46f99ffe07b4da1894b910160405180910390a1600480546001600160a01b0319166001600160a01b0392909216919091179055565b80516001600160a01b03811681146200027b57600080fd5b919050565b600080600080600080600060e0888a0312156200029c57600080fd5b620002a78862000263565b9650620002b76020890162000263565b955060408801519450606088015193506080880151925060a08801519150620002e360c0890162000263565b905092959891949750929550565b6080516124d16200031b6000396000818161019101528181610c300152610c7901526124d16000f3fe6080604052600436106101345760003560e01c8063b3c82e92116100ab578063d9a4cbdf1161006f578063d9a4cbdf14610361578063dbd1838814610381578063e471b02614610396578063e68a5c3d146103b6578063fc525395146103d6578063fe0d94c1146103f657600080fd5b8063b3c82e92146102cb578063b68df16d146102f8578063c3a7688614610319578063cebc9a8214610337578063d89aac391461034c57600080fd5b80635748c130116100fd5780635748c130146101eb5780635ab98d5a1461021857806364d62353146102385780638533f33714610258578063a75b87d21461026d578063b1fc87961461028b57600080fd5b80625c33e11461013957806303c276211461013b578063083a73a21461015f5780631aef4ead1461017f57806340e58ee5146101cb575b600080fd5b005b34801561014757600080fd5b506002545b6040519081526020015b60405180910390f35b34801561016b57600080fd5b5061013961017a366004611a8d565b610409565b34801561018b57600080fd5b506101b37f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610156565b3480156101d757600080fd5b506101396101e6366004611a8d565b610462565b3480156101f757600080fd5b5061020b610206366004611a8d565b61070d565b6040516101569190611abc565b34801561022457600080fd5b50610139610233366004611a8d565b6107a3565b34801561024457600080fd5b50610139610253366004611a8d565b6107ef565b34801561026457600080fd5b5060055461014c565b34801561027957600080fd5b506004546001600160a01b03166101b3565b34801561029757600080fd5b506102bb6102a6366004611a8d565b60009081526007602052604090205460ff1690565b6040519015158152602001610156565b3480156102d757600080fd5b506102eb6102e6366004611a8d565b610821565b6040516101569190611c3b565b61030b610306366004611d1c565b610b94565b604051610156929190611da1565b34801561032557600080fd5b506008546001600160a01b03166101b3565b34801561034357600080fd5b5060005461014c565b34801561035857600080fd5b5060035461014c565b34801561036d57600080fd5b5061013961037c3660046120fb565b610c25565b34801561038d57600080fd5b5060015461014c565b3480156103a257600080fd5b506101396103b1366004611a8d565b610d26565b3480156103c257600080fd5b506101396103d13660046121cd565b610d71565b3480156103e257600080fd5b506101396103f13660046121cd565b610dfa565b610139610404366004611a8d565b610e23565b33301461042957604051631dbf5f2360e01b815260040160405180910390fd5b600254811161044b5760405163cb2f2b2360e01b815260040160405180910390fd5b6104548161114b565b61045f60005461118c565b50565b6004546001600160a01b0316331461048d576040516377b6878160e11b815260040160405180910390fd5b60006104988261070d565b60038111156104a9576104a9611aa6565b146104c75760405163050ac78b60e11b815260040160405180910390fd5b60008181526006602081905260408220908101805461ff001916610100179055805490915b818110156106dc576106d483600001828154811061050c5761050c6121f1565b6000918252602090912001546001850180546001600160a01b03909216918490811061053a5761053a6121f1565b906000526020600020015485600201848154811061055a5761055a6121f1565b90600052602060002001805461056f90612207565b80601f016020809104026020016040519081016040528092919081815260200182805461059b90612207565b80156105e85780601f106105bd576101008083540402835291602001916105e8565b820191906000526020600020905b8154815290600101906020018083116105cb57829003601f168201915b5050505050866003018581548110610602576106026121f1565b90600052602060002001805461061790612207565b80601f016020809104026020016040519081016040528092919081815260200182805461064390612207565b80156106905780601f1061066557610100808354040283529160200191610690565b820191906000526020600020905b81548152906001019060200180831161067357829003601f168201915b505050505087600501548860040187815481106106af576106af6121f1565b90600052602060002090602091828204019190069054906101000a900460ff166111d2565b6001016104ec565b5060405183907f0743c673685efcbf3db8591d9e1d98336bc844fe4f4599e6f7efb6d71c02563490600090a2505050565b600081600554116107315760405163e5bc0e7b60e01b815260040160405180910390fd5b600082815260066020819052604090912090810154610100900460ff161561075c5750600292915050565b600681015460ff16156107725750600192915050565b6001548160050154610784919061223c565b4211156107945750600392915050565b50600092915050565b50919050565b3330146107c357604051631dbf5f2360e01b815260040160405180910390fd5b6102588110156107e6576040516301f6f9e560e71b815260040160405180910390fd5b61045f81611224565b33301461080f57604051631dbf5f2360e01b815260040160405180910390fd5b6108188161118c565b61045f81611265565b61086d6040518061010001604052806060815260200160608152602001606081526020016060815260200160608152602001600081526020016000151581526020016000151581525090565b6000828152600660209081526040918290208251815461012093810282018401909452610100810184815290939192849284918401828280156108d957602002820191906000526020600020905b81546001600160a01b031681526001909101906020018083116108bb575b505050505081526020016001820180548060200260200160405190810160405280929190818152602001828054801561093157602002820191906000526020600020905b81548152602001906001019080831161091d575b5050505050815260200160028201805480602002602001604051908101604052809291908181526020016000905b82821015610a0b57838290600052602060002001805461097e90612207565b80601f01602080910402602001604051908101604052809291908181526020018280546109aa90612207565b80156109f75780601f106109cc576101008083540402835291602001916109f7565b820191906000526020600020905b8154815290600101906020018083116109da57829003601f168201915b50505050508152602001906001019061095f565b50505050815260200160038201805480602002602001604051908101604052809291908181526020016000905b82821015610ae4578382906000526020600020018054610a5790612207565b80601f0160208091040260200160405190810160405280929190818152602001828054610a8390612207565b8015610ad05780601f10610aa557610100808354040283529160200191610ad0565b820191906000526020600020905b815481529060010190602001808311610ab357829003601f168201915b505050505081526020019060010190610a38565b50505050815260200160048201805480602002602001604051908101604052809291908181526020018280548015610b5b57602002820191906000526020600020906000905b825461010083900a900460ff161515815260206001928301818104948501949093039092029101808411610b2a5790505b50505091835250506005820154602082015260069091015460ff8082161515604084015261010090910416151560609091015292915050565b60006060333014610bb857604051631dbf5f2360e01b815260040160405180910390fd5b60006060866001600160a01b03168686604051610bd6929190612262565b600060405180830381855af49150503d8060008114610c11576040519150601f19603f3d011682016040523d82523d6000602084013e610c16565b606091505b50909890975095505050505050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016141580610cf4575060085460408051636e296e4560e01b815290516001600160a01b03928316927f00000000000000000000000000000000000000000000000000000000000000001691636e296e459160048083019260209291908290030181865afa158015610cc4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610ce89190612272565b6001600160a01b031614155b15610d12576040516359e8359960e01b815260040160405180910390fd5b610d1f85858585856112a6565b5050505050565b333014610d4657604051631dbf5f2360e01b815260040160405180910390fd5b6003548110610d68576040516301b1029b60e61b815260040160405180910390fd5b61045481611513565b333014610d9157604051631dbf5f2360e01b815260040160405180910390fd5b600854604080516001600160a01b03928316815291831660208301527f01dd0ca50426f21c1b37ce7f3e95eef45b4a55bc5da80a7b67b2c65a7463caf0910160405180910390a1600880546001600160a01b0319166001600160a01b0392909216919091179055565b333014610e1a57604051631dbf5f2360e01b815260040160405180910390fd5b61045f81611554565b6000610e2e8261070d565b6003811115610e3f57610e3f611aa6565b14610e5d5760405163050ac78b60e11b815260040160405180910390fd5b60008181526006602052604090206005810154421015610e9057604051635192dd5560e01b815260040160405180910390fd5b60068101805460ff19166001179055805460008167ffffffffffffffff811115610ebc57610ebc611dc4565b604051908082528060200260200182016040528015610eef57816020015b6060815260200190600190039081610eda5790505b50905060005b82811015611102576110dd846000018281548110610f1557610f156121f1565b6000918252602090912001546001860180546001600160a01b039092169184908110610f4357610f436121f1565b9060005260206000200154866002018481548110610f6357610f636121f1565b906000526020600020018054610f7890612207565b80601f0160208091040260200160405190810160405280929190818152602001828054610fa490612207565b8015610ff15780601f10610fc657610100808354040283529160200191610ff1565b820191906000526020600020905b815481529060010190602001808311610fd457829003601f168201915b505050505087600301858154811061100b5761100b6121f1565b90600052602060002001805461102090612207565b80601f016020809104026020016040519081016040528092919081815260200182805461104c90612207565b80156110995780601f1061106e57610100808354040283529160200191611099565b820191906000526020600020905b81548152906001019060200180831161107c57829003601f168201915b505050505088600501548960040187815481106110b8576110b86121f1565b90600052602060002090602091828204019190069054906101000a900460ff166115bd565b8282815181106110ef576110ef6121f1565b6020908102919091010152600101610ef5565b50336001600160a01b0316847ff5efc4bb09a12b6c9561a7e7ab02938a72a4351316b473d574fdaaa89c43eb9a8360405161113d919061228f565b60405180910390a350505050565b60035460408051918252602082018390527faf46013422363beb5e6f00ab923cffe3574670494c864de2828b9d7c201fdde5910160405180910390a1600355565b6002548110156111af576040516361759e6560e01b815260040160405180910390fd5b60035481111561045f576040516386dac63560e01b815260040160405180910390fd5b60008686868686866040516020016111ef969594939291906122a2565b60408051601f198184030181529181528151602092830120600090815260079092529020805460ff1916905550505050505050565b60015460408051918252602082018390527f9953f3a71052edcd4ae7a0f97302839d1dda32ab93c1039207c91c866b094f72910160405180910390a1600155565b60005460408051918252602082018390527f43de56b886294fc29767e51a88b5c67fd24aefebc5ddf813b1d9b91b1df38444910160405180910390a1600055565b84516112c557604051636a8e3e9360e11b815260040160405180910390fd5b84518451811415806112d8575083518114155b806112e4575082518114155b806112f0575081518114155b1561130e57604051630d10f63b60e01b815260040160405180910390fd5b6005546000805461131f904261223c565b600580546001019055905060005b83811015611440576000898281518110611349576113496121f1565b6020026020010151898381518110611363576113636121f1565b602002602001015189848151811061137d5761137d6121f1565b6020026020010151898581518110611397576113976121f1565b6020026020010151868a87815181106113b2576113b26121f1565b60200260200101516040516020016113cf969594939291906122a2565b6040516020818303038152906040528051906020012090506114008160009081526007602052604090205460ff1690565b1561141e57604051633b2f04e360e21b815260040160405180910390fd5b6000908152600760205260409020805460ff191660019081179091550161132d565b506000828152600660209081526040909120895190916114649183918c01906117a3565b50875161147a90600183019060208b0190611808565b50865161149090600283019060208a0190611843565b5085516114a6906003830190602089019061189c565b5084516114bc90600483019060208801906118f5565b50818160050181905550827f0325966a4aa089b42f4766ec96f599405102bb309e065f24874aff59082dbc8b8a8a8a8a8a88604051611500969594939291906122f6565b60405180910390a2505050505050505050565b60025460408051918252602082018390527fc534cdcbe9b52100810d787afd57e4174322776fcb58872ea706f23e9319fa8d910160405180910390a1600255565b600454604080516001600160a01b03928316815291831660208301527f85bd8788d3c4a160f0f6254229589f137d5633a870dcb46f99ffe07b4da1894b910160405180910390a1600480546001600160a01b0319166001600160a01b0392909216919091179055565b6060854710156115e057604051631e9acf1760e31b815260040160405180910390fd5b60008787878787876040516020016115fd969594939291906122a2565b60408051601f198184030181529181528151602092830120600081815260079093529120805460ff19169055865190915060609061163c575084611668565b86805190602001208660405160200161165692919061239d565b60405160208183030381529060405290505b6000606085156116ea5760405163b68df16d60e01b8152309063b68df16d908c90611699908f9088906004016123ce565b60006040518083038185885af11580156116b7573d6000803e3d6000fd5b50505050506040513d6000823e601f3d908101601f191682016040526116e091908101906123f2565b909250905061174c565b8a6001600160a01b03168a84604051611703919061247f565b60006040518083038185875af1925050503d8060008114611740576040519150601f19603f3d011682016040523d82523d6000602084013e611745565b606091505b5090925090505b6117568282611765565b9b9a5050505050505050505050565b6060821561177457508061179d565b8151156117845781518083602001fd5b6040516332f63ed360e21b815260040160405180910390fd5b92915050565b8280548282559060005260206000209081019282156117f8579160200282015b828111156117f857825182546001600160a01b0319166001600160a01b039091161782556020909201916001909101906117c3565b50611804929150611991565b5090565b8280548282559060005260206000209081019282156117f8579160200282015b828111156117f8578251825591602001919060010190611828565b828054828255906000526020600020908101928215611890579160200282015b8281111561189057825180516118809184916020909101906119a6565b5091602001919060010190611863565b50611804929150611a19565b8280548282559060005260206000209081019282156118e9579160200282015b828111156118e957825180516118d99184916020909101906119a6565b50916020019190600101906118bc565b50611804929150611a36565b82805482825590600052602060002090601f016020900481019282156117f85791602002820160005b8382111561195b57835183826101000a81548160ff021916908315150217905550926020019260010160208160000104928301926001030261191e565b80156119885782816101000a81549060ff021916905560010160208160000104928301926001030261195b565b50506118049291505b5b808211156118045760008155600101611992565b8280546119b290612207565b90600052602060002090601f0160209004810192826119d457600085556117f8565b82601f106119ed57805160ff19168380011785556117f8565b828001600101855582156117f857918201828111156117f8578251825591602001919060010190611828565b80821115611804576000611a2d8282611a53565b50600101611a19565b80821115611804576000611a4a8282611a53565b50600101611a36565b508054611a5f90612207565b6000825580601f10611a6f575050565b601f01602090049060005260206000209081019061045f9190611991565b600060208284031215611a9f57600080fd5b5035919050565b634e487b7160e01b600052602160045260246000fd5b6020810160048310611ade57634e487b7160e01b600052602160045260246000fd5b91905290565b600081518084526020808501945080840160005b83811015611b1d5781516001600160a01b031687529582019590820190600101611af8565b509495945050505050565b600081518084526020808501945080840160005b83811015611b1d57815187529582019590820190600101611b3c565b60005b83811015611b73578181015183820152602001611b5b565b83811115611b82576000848401525b50505050565b60008151808452611ba0816020860160208601611b58565b601f01601f19169290920160200192915050565b600081518084526020808501808196508360051b8101915082860160005b85811015611bfc578284038952611bea848351611b88565b98850198935090840190600101611bd2565b5091979650505050505050565b600081518084526020808501945080840160005b83811015611b1d578151151587529582019590820190600101611c1d565b6020815260008251610100806020850152611c5a610120850183611ae4565b91506020850151601f1980868503016040870152611c788483611b28565b93506040870151915080868503016060870152611c958483611bb4565b93506060870151915080868503016080870152611cb28483611bb4565b935060808701519150808685030160a087015250611cd08382611c09565b92505060a085015160c085015260c0850151611cf060e086018215159052565b5060e0850151801515858301525090949350505050565b6001600160a01b038116811461045f57600080fd5b600080600060408486031215611d3157600080fd5b8335611d3c81611d07565b9250602084013567ffffffffffffffff80821115611d5957600080fd5b818601915086601f830112611d6d57600080fd5b813581811115611d7c57600080fd5b876020828501011115611d8e57600080fd5b6020830194508093505050509250925092565b8215158152604060208201526000611dbc6040830184611b88565b949350505050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff81118282101715611e0357611e03611dc4565b604052919050565b600067ffffffffffffffff821115611e2557611e25611dc4565b5060051b60200190565b600082601f830112611e4057600080fd5b81356020611e55611e5083611e0b565b611dda565b82815260059290921b84018101918181019086841115611e7457600080fd5b8286015b84811015611e98578035611e8b81611d07565b8352918301918301611e78565b509695505050505050565b600082601f830112611eb457600080fd5b81356020611ec4611e5083611e0b565b82815260059290921b84018101918181019086841115611ee357600080fd5b8286015b84811015611e985780358352918301918301611ee7565b600067ffffffffffffffff821115611f1857611f18611dc4565b50601f01601f191660200190565b6000611f34611e5084611efe565b9050828152838383011115611f4857600080fd5b828260208301376000602084830101529392505050565b600082601f830112611f7057600080fd5b81356020611f80611e5083611e0b565b82815260059290921b84018101918181019086841115611f9f57600080fd5b8286015b84811015611e9857803567ffffffffffffffff811115611fc35760008081fd5b8701603f81018913611fd55760008081fd5b611fe6898683013560408401611f26565b845250918301918301611fa3565b600082601f83011261200557600080fd5b81356020612015611e5083611e0b565b82815260059290921b8401810191818101908684111561203457600080fd5b8286015b84811015611e9857803567ffffffffffffffff8111156120585760008081fd5b8701603f8101891361206a5760008081fd5b61207b898683013560408401611f26565b845250918301918301612038565b801515811461045f57600080fd5b600082601f8301126120a857600080fd5b813560206120b8611e5083611e0b565b82815260059290921b840181019181810190868411156120d757600080fd5b8286015b84811015611e985780356120ee81612089565b83529183019183016120db565b600080600080600060a0868803121561211357600080fd5b853567ffffffffffffffff8082111561212b57600080fd5b61213789838a01611e2f565b9650602088013591508082111561214d57600080fd5b61215989838a01611ea3565b9550604088013591508082111561216f57600080fd5b61217b89838a01611f5f565b9450606088013591508082111561219157600080fd5b61219d89838a01611ff4565b935060808801359150808211156121b357600080fd5b506121c088828901612097565b9150509295509295909350565b6000602082840312156121df57600080fd5b81356121ea81611d07565b9392505050565b634e487b7160e01b600052603260045260246000fd5b600181811c9082168061221b57607f821691505b6020821081141561079d57634e487b7160e01b600052602260045260246000fd5b6000821982111561225d57634e487b7160e01b600052601160045260246000fd5b500190565b8183823760009101908152919050565b60006020828403121561228457600080fd5b81516121ea81611d07565b6020815260006121ea6020830184611bb4565b60018060a01b038716815285602082015260c0604082015260006122c960c0830187611b88565b82810360608401526122db8187611b88565b6080840195909552505090151560a090910152949350505050565b60c0808252875190820181905260009060209060e0840190828b01845b828110156123385781516001600160a01b031684529284019290840190600101612313565b5050508381038285015261234c818a611b28565b91505082810360408401526123618188611bb4565b905082810360608401526123758187611bb4565b905082810360808401526123898186611c09565b9150508260a0830152979650505050505050565b6001600160e01b03198316815281516000906123c0816004850160208701611b58565b919091016004019392505050565b6001600160a01b0383168152604060208201819052600090611dbc90830184611b88565b6000806040838503121561240557600080fd5b825161241081612089565b602084015190925067ffffffffffffffff81111561242d57600080fd5b8301601f8101851361243e57600080fd5b805161244c611e5082611efe565b81815286602083850101111561246157600080fd5b612472826020830160208601611b58565b8093505050509250929050565b60008251612491818460208701611b58565b919091019291505056fea26469706673582212203276c029a6aeac4d52334c5ef5421d1312356da0bcbda7eb48bd003f777ab76564736f6c634300080a0033", + "deployedBytecode": "0x6080604052600436106101345760003560e01c8063b3c82e92116100ab578063d9a4cbdf1161006f578063d9a4cbdf14610361578063dbd1838814610381578063e471b02614610396578063e68a5c3d146103b6578063fc525395146103d6578063fe0d94c1146103f657600080fd5b8063b3c82e92146102cb578063b68df16d146102f8578063c3a7688614610319578063cebc9a8214610337578063d89aac391461034c57600080fd5b80635748c130116100fd5780635748c130146101eb5780635ab98d5a1461021857806364d62353146102385780638533f33714610258578063a75b87d21461026d578063b1fc87961461028b57600080fd5b80625c33e11461013957806303c276211461013b578063083a73a21461015f5780631aef4ead1461017f57806340e58ee5146101cb575b600080fd5b005b34801561014757600080fd5b506002545b6040519081526020015b60405180910390f35b34801561016b57600080fd5b5061013961017a366004611a8d565b610409565b34801561018b57600080fd5b506101b37f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610156565b3480156101d757600080fd5b506101396101e6366004611a8d565b610462565b3480156101f757600080fd5b5061020b610206366004611a8d565b61070d565b6040516101569190611abc565b34801561022457600080fd5b50610139610233366004611a8d565b6107a3565b34801561024457600080fd5b50610139610253366004611a8d565b6107ef565b34801561026457600080fd5b5060055461014c565b34801561027957600080fd5b506004546001600160a01b03166101b3565b34801561029757600080fd5b506102bb6102a6366004611a8d565b60009081526007602052604090205460ff1690565b6040519015158152602001610156565b3480156102d757600080fd5b506102eb6102e6366004611a8d565b610821565b6040516101569190611c3b565b61030b610306366004611d1c565b610b94565b604051610156929190611da1565b34801561032557600080fd5b506008546001600160a01b03166101b3565b34801561034357600080fd5b5060005461014c565b34801561035857600080fd5b5060035461014c565b34801561036d57600080fd5b5061013961037c3660046120fb565b610c25565b34801561038d57600080fd5b5060015461014c565b3480156103a257600080fd5b506101396103b1366004611a8d565b610d26565b3480156103c257600080fd5b506101396103d13660046121cd565b610d71565b3480156103e257600080fd5b506101396103f13660046121cd565b610dfa565b610139610404366004611a8d565b610e23565b33301461042957604051631dbf5f2360e01b815260040160405180910390fd5b600254811161044b5760405163cb2f2b2360e01b815260040160405180910390fd5b6104548161114b565b61045f60005461118c565b50565b6004546001600160a01b0316331461048d576040516377b6878160e11b815260040160405180910390fd5b60006104988261070d565b60038111156104a9576104a9611aa6565b146104c75760405163050ac78b60e11b815260040160405180910390fd5b60008181526006602081905260408220908101805461ff001916610100179055805490915b818110156106dc576106d483600001828154811061050c5761050c6121f1565b6000918252602090912001546001850180546001600160a01b03909216918490811061053a5761053a6121f1565b906000526020600020015485600201848154811061055a5761055a6121f1565b90600052602060002001805461056f90612207565b80601f016020809104026020016040519081016040528092919081815260200182805461059b90612207565b80156105e85780601f106105bd576101008083540402835291602001916105e8565b820191906000526020600020905b8154815290600101906020018083116105cb57829003601f168201915b5050505050866003018581548110610602576106026121f1565b90600052602060002001805461061790612207565b80601f016020809104026020016040519081016040528092919081815260200182805461064390612207565b80156106905780601f1061066557610100808354040283529160200191610690565b820191906000526020600020905b81548152906001019060200180831161067357829003601f168201915b505050505087600501548860040187815481106106af576106af6121f1565b90600052602060002090602091828204019190069054906101000a900460ff166111d2565b6001016104ec565b5060405183907f0743c673685efcbf3db8591d9e1d98336bc844fe4f4599e6f7efb6d71c02563490600090a2505050565b600081600554116107315760405163e5bc0e7b60e01b815260040160405180910390fd5b600082815260066020819052604090912090810154610100900460ff161561075c5750600292915050565b600681015460ff16156107725750600192915050565b6001548160050154610784919061223c565b4211156107945750600392915050565b50600092915050565b50919050565b3330146107c357604051631dbf5f2360e01b815260040160405180910390fd5b6102588110156107e6576040516301f6f9e560e71b815260040160405180910390fd5b61045f81611224565b33301461080f57604051631dbf5f2360e01b815260040160405180910390fd5b6108188161118c565b61045f81611265565b61086d6040518061010001604052806060815260200160608152602001606081526020016060815260200160608152602001600081526020016000151581526020016000151581525090565b6000828152600660209081526040918290208251815461012093810282018401909452610100810184815290939192849284918401828280156108d957602002820191906000526020600020905b81546001600160a01b031681526001909101906020018083116108bb575b505050505081526020016001820180548060200260200160405190810160405280929190818152602001828054801561093157602002820191906000526020600020905b81548152602001906001019080831161091d575b5050505050815260200160028201805480602002602001604051908101604052809291908181526020016000905b82821015610a0b57838290600052602060002001805461097e90612207565b80601f01602080910402602001604051908101604052809291908181526020018280546109aa90612207565b80156109f75780601f106109cc576101008083540402835291602001916109f7565b820191906000526020600020905b8154815290600101906020018083116109da57829003601f168201915b50505050508152602001906001019061095f565b50505050815260200160038201805480602002602001604051908101604052809291908181526020016000905b82821015610ae4578382906000526020600020018054610a5790612207565b80601f0160208091040260200160405190810160405280929190818152602001828054610a8390612207565b8015610ad05780601f10610aa557610100808354040283529160200191610ad0565b820191906000526020600020905b815481529060010190602001808311610ab357829003601f168201915b505050505081526020019060010190610a38565b50505050815260200160048201805480602002602001604051908101604052809291908181526020018280548015610b5b57602002820191906000526020600020906000905b825461010083900a900460ff161515815260206001928301818104948501949093039092029101808411610b2a5790505b50505091835250506005820154602082015260069091015460ff8082161515604084015261010090910416151560609091015292915050565b60006060333014610bb857604051631dbf5f2360e01b815260040160405180910390fd5b60006060866001600160a01b03168686604051610bd6929190612262565b600060405180830381855af49150503d8060008114610c11576040519150601f19603f3d011682016040523d82523d6000602084013e610c16565b606091505b50909890975095505050505050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016141580610cf4575060085460408051636e296e4560e01b815290516001600160a01b03928316927f00000000000000000000000000000000000000000000000000000000000000001691636e296e459160048083019260209291908290030181865afa158015610cc4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610ce89190612272565b6001600160a01b031614155b15610d12576040516359e8359960e01b815260040160405180910390fd5b610d1f85858585856112a6565b5050505050565b333014610d4657604051631dbf5f2360e01b815260040160405180910390fd5b6003548110610d68576040516301b1029b60e61b815260040160405180910390fd5b61045481611513565b333014610d9157604051631dbf5f2360e01b815260040160405180910390fd5b600854604080516001600160a01b03928316815291831660208301527f01dd0ca50426f21c1b37ce7f3e95eef45b4a55bc5da80a7b67b2c65a7463caf0910160405180910390a1600880546001600160a01b0319166001600160a01b0392909216919091179055565b333014610e1a57604051631dbf5f2360e01b815260040160405180910390fd5b61045f81611554565b6000610e2e8261070d565b6003811115610e3f57610e3f611aa6565b14610e5d5760405163050ac78b60e11b815260040160405180910390fd5b60008181526006602052604090206005810154421015610e9057604051635192dd5560e01b815260040160405180910390fd5b60068101805460ff19166001179055805460008167ffffffffffffffff811115610ebc57610ebc611dc4565b604051908082528060200260200182016040528015610eef57816020015b6060815260200190600190039081610eda5790505b50905060005b82811015611102576110dd846000018281548110610f1557610f156121f1565b6000918252602090912001546001860180546001600160a01b039092169184908110610f4357610f436121f1565b9060005260206000200154866002018481548110610f6357610f636121f1565b906000526020600020018054610f7890612207565b80601f0160208091040260200160405190810160405280929190818152602001828054610fa490612207565b8015610ff15780601f10610fc657610100808354040283529160200191610ff1565b820191906000526020600020905b815481529060010190602001808311610fd457829003601f168201915b505050505087600301858154811061100b5761100b6121f1565b90600052602060002001805461102090612207565b80601f016020809104026020016040519081016040528092919081815260200182805461104c90612207565b80156110995780601f1061106e57610100808354040283529160200191611099565b820191906000526020600020905b81548152906001019060200180831161107c57829003601f168201915b505050505088600501548960040187815481106110b8576110b86121f1565b90600052602060002090602091828204019190069054906101000a900460ff166115bd565b8282815181106110ef576110ef6121f1565b6020908102919091010152600101610ef5565b50336001600160a01b0316847ff5efc4bb09a12b6c9561a7e7ab02938a72a4351316b473d574fdaaa89c43eb9a8360405161113d919061228f565b60405180910390a350505050565b60035460408051918252602082018390527faf46013422363beb5e6f00ab923cffe3574670494c864de2828b9d7c201fdde5910160405180910390a1600355565b6002548110156111af576040516361759e6560e01b815260040160405180910390fd5b60035481111561045f576040516386dac63560e01b815260040160405180910390fd5b60008686868686866040516020016111ef969594939291906122a2565b60408051601f198184030181529181528151602092830120600090815260079092529020805460ff1916905550505050505050565b60015460408051918252602082018390527f9953f3a71052edcd4ae7a0f97302839d1dda32ab93c1039207c91c866b094f72910160405180910390a1600155565b60005460408051918252602082018390527f43de56b886294fc29767e51a88b5c67fd24aefebc5ddf813b1d9b91b1df38444910160405180910390a1600055565b84516112c557604051636a8e3e9360e11b815260040160405180910390fd5b84518451811415806112d8575083518114155b806112e4575082518114155b806112f0575081518114155b1561130e57604051630d10f63b60e01b815260040160405180910390fd5b6005546000805461131f904261223c565b600580546001019055905060005b83811015611440576000898281518110611349576113496121f1565b6020026020010151898381518110611363576113636121f1565b602002602001015189848151811061137d5761137d6121f1565b6020026020010151898581518110611397576113976121f1565b6020026020010151868a87815181106113b2576113b26121f1565b60200260200101516040516020016113cf969594939291906122a2565b6040516020818303038152906040528051906020012090506114008160009081526007602052604090205460ff1690565b1561141e57604051633b2f04e360e21b815260040160405180910390fd5b6000908152600760205260409020805460ff191660019081179091550161132d565b506000828152600660209081526040909120895190916114649183918c01906117a3565b50875161147a90600183019060208b0190611808565b50865161149090600283019060208a0190611843565b5085516114a6906003830190602089019061189c565b5084516114bc90600483019060208801906118f5565b50818160050181905550827f0325966a4aa089b42f4766ec96f599405102bb309e065f24874aff59082dbc8b8a8a8a8a8a88604051611500969594939291906122f6565b60405180910390a2505050505050505050565b60025460408051918252602082018390527fc534cdcbe9b52100810d787afd57e4174322776fcb58872ea706f23e9319fa8d910160405180910390a1600255565b600454604080516001600160a01b03928316815291831660208301527f85bd8788d3c4a160f0f6254229589f137d5633a870dcb46f99ffe07b4da1894b910160405180910390a1600480546001600160a01b0319166001600160a01b0392909216919091179055565b6060854710156115e057604051631e9acf1760e31b815260040160405180910390fd5b60008787878787876040516020016115fd969594939291906122a2565b60408051601f198184030181529181528151602092830120600081815260079093529120805460ff19169055865190915060609061163c575084611668565b86805190602001208660405160200161165692919061239d565b60405160208183030381529060405290505b6000606085156116ea5760405163b68df16d60e01b8152309063b68df16d908c90611699908f9088906004016123ce565b60006040518083038185885af11580156116b7573d6000803e3d6000fd5b50505050506040513d6000823e601f3d908101601f191682016040526116e091908101906123f2565b909250905061174c565b8a6001600160a01b03168a84604051611703919061247f565b60006040518083038185875af1925050503d8060008114611740576040519150601f19603f3d011682016040523d82523d6000602084013e611745565b606091505b5090925090505b6117568282611765565b9b9a5050505050505050505050565b6060821561177457508061179d565b8151156117845781518083602001fd5b6040516332f63ed360e21b815260040160405180910390fd5b92915050565b8280548282559060005260206000209081019282156117f8579160200282015b828111156117f857825182546001600160a01b0319166001600160a01b039091161782556020909201916001909101906117c3565b50611804929150611991565b5090565b8280548282559060005260206000209081019282156117f8579160200282015b828111156117f8578251825591602001919060010190611828565b828054828255906000526020600020908101928215611890579160200282015b8281111561189057825180516118809184916020909101906119a6565b5091602001919060010190611863565b50611804929150611a19565b8280548282559060005260206000209081019282156118e9579160200282015b828111156118e957825180516118d99184916020909101906119a6565b50916020019190600101906118bc565b50611804929150611a36565b82805482825590600052602060002090601f016020900481019282156117f85791602002820160005b8382111561195b57835183826101000a81548160ff021916908315150217905550926020019260010160208160000104928301926001030261191e565b80156119885782816101000a81549060ff021916905560010160208160000104928301926001030261195b565b50506118049291505b5b808211156118045760008155600101611992565b8280546119b290612207565b90600052602060002090601f0160209004810192826119d457600085556117f8565b82601f106119ed57805160ff19168380011785556117f8565b828001600101855582156117f857918201828111156117f8578251825591602001919060010190611828565b80821115611804576000611a2d8282611a53565b50600101611a19565b80821115611804576000611a4a8282611a53565b50600101611a36565b508054611a5f90612207565b6000825580601f10611a6f575050565b601f01602090049060005260206000209081019061045f9190611991565b600060208284031215611a9f57600080fd5b5035919050565b634e487b7160e01b600052602160045260246000fd5b6020810160048310611ade57634e487b7160e01b600052602160045260246000fd5b91905290565b600081518084526020808501945080840160005b83811015611b1d5781516001600160a01b031687529582019590820190600101611af8565b509495945050505050565b600081518084526020808501945080840160005b83811015611b1d57815187529582019590820190600101611b3c565b60005b83811015611b73578181015183820152602001611b5b565b83811115611b82576000848401525b50505050565b60008151808452611ba0816020860160208601611b58565b601f01601f19169290920160200192915050565b600081518084526020808501808196508360051b8101915082860160005b85811015611bfc578284038952611bea848351611b88565b98850198935090840190600101611bd2565b5091979650505050505050565b600081518084526020808501945080840160005b83811015611b1d578151151587529582019590820190600101611c1d565b6020815260008251610100806020850152611c5a610120850183611ae4565b91506020850151601f1980868503016040870152611c788483611b28565b93506040870151915080868503016060870152611c958483611bb4565b93506060870151915080868503016080870152611cb28483611bb4565b935060808701519150808685030160a087015250611cd08382611c09565b92505060a085015160c085015260c0850151611cf060e086018215159052565b5060e0850151801515858301525090949350505050565b6001600160a01b038116811461045f57600080fd5b600080600060408486031215611d3157600080fd5b8335611d3c81611d07565b9250602084013567ffffffffffffffff80821115611d5957600080fd5b818601915086601f830112611d6d57600080fd5b813581811115611d7c57600080fd5b876020828501011115611d8e57600080fd5b6020830194508093505050509250925092565b8215158152604060208201526000611dbc6040830184611b88565b949350505050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff81118282101715611e0357611e03611dc4565b604052919050565b600067ffffffffffffffff821115611e2557611e25611dc4565b5060051b60200190565b600082601f830112611e4057600080fd5b81356020611e55611e5083611e0b565b611dda565b82815260059290921b84018101918181019086841115611e7457600080fd5b8286015b84811015611e98578035611e8b81611d07565b8352918301918301611e78565b509695505050505050565b600082601f830112611eb457600080fd5b81356020611ec4611e5083611e0b565b82815260059290921b84018101918181019086841115611ee357600080fd5b8286015b84811015611e985780358352918301918301611ee7565b600067ffffffffffffffff821115611f1857611f18611dc4565b50601f01601f191660200190565b6000611f34611e5084611efe565b9050828152838383011115611f4857600080fd5b828260208301376000602084830101529392505050565b600082601f830112611f7057600080fd5b81356020611f80611e5083611e0b565b82815260059290921b84018101918181019086841115611f9f57600080fd5b8286015b84811015611e9857803567ffffffffffffffff811115611fc35760008081fd5b8701603f81018913611fd55760008081fd5b611fe6898683013560408401611f26565b845250918301918301611fa3565b600082601f83011261200557600080fd5b81356020612015611e5083611e0b565b82815260059290921b8401810191818101908684111561203457600080fd5b8286015b84811015611e9857803567ffffffffffffffff8111156120585760008081fd5b8701603f8101891361206a5760008081fd5b61207b898683013560408401611f26565b845250918301918301612038565b801515811461045f57600080fd5b600082601f8301126120a857600080fd5b813560206120b8611e5083611e0b565b82815260059290921b840181019181810190868411156120d757600080fd5b8286015b84811015611e985780356120ee81612089565b83529183019183016120db565b600080600080600060a0868803121561211357600080fd5b853567ffffffffffffffff8082111561212b57600080fd5b61213789838a01611e2f565b9650602088013591508082111561214d57600080fd5b61215989838a01611ea3565b9550604088013591508082111561216f57600080fd5b61217b89838a01611f5f565b9450606088013591508082111561219157600080fd5b61219d89838a01611ff4565b935060808801359150808211156121b357600080fd5b506121c088828901612097565b9150509295509295909350565b6000602082840312156121df57600080fd5b81356121ea81611d07565b9392505050565b634e487b7160e01b600052603260045260246000fd5b600181811c9082168061221b57607f821691505b6020821081141561079d57634e487b7160e01b600052602260045260246000fd5b6000821982111561225d57634e487b7160e01b600052601160045260246000fd5b500190565b8183823760009101908152919050565b60006020828403121561228457600080fd5b81516121ea81611d07565b6020815260006121ea6020830184611bb4565b60018060a01b038716815285602082015260c0604082015260006122c960c0830187611b88565b82810360608401526122db8187611b88565b6080840195909552505090151560a090910152949350505050565b60c0808252875190820181905260009060209060e0840190828b01845b828110156123385781516001600160a01b031684529284019290840190600101612313565b5050508381038285015261234c818a611b28565b91505082810360408401526123618188611bb4565b905082810360608401526123758187611bb4565b905082810360808401526123898186611c09565b9150508260a0830152979650505050505050565b6001600160e01b03198316815281516000906123c0816004850160208701611b58565b919091016004019392505050565b6001600160a01b0383168152604060208201819052600090611dbc90830184611b88565b6000806040838503121561240557600080fd5b825161241081612089565b602084015190925067ffffffffffffffff81111561242d57600080fd5b8301601f8101851361243e57600080fd5b805161244c611e5082611efe565b81815286602083850101111561246157600080fd5b612472826020830160208601611b58565b8093505050509250929050565b60008251612491818460208701611b58565b919091019291505056fea26469706673582212203276c029a6aeac4d52334c5ef5421d1312356da0bcbda7eb48bd003f777ab76564736f6c634300080a0033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/utils/mantle/contracts.ts b/utils/mantle/contracts.ts new file mode 100644 index 00000000..c2f58889 --- /dev/null +++ b/utils/mantle/contracts.ts @@ -0,0 +1,43 @@ +import { + CanonicalTransactionChain__factory, + CrossDomainMessengerStub__factory, + L1CrossDomainMessenger__factory, + L2CrossDomainMessenger__factory, +} from "../../typechain"; +import addresses from "./addresses"; +import { CommonOptions } from "./types"; +import network, { NetworkName } from "../network"; + +interface ContractsOptions extends CommonOptions { + forking: boolean; +} + +export default function contracts( + networkName: NetworkName, + options: ContractsOptions +) { + const [l1Provider, l2Provider] = network + .multichain(["eth", "mnt"], networkName) + .getProviders(options); + + const mntAddresses = addresses(networkName, options); + + return { + L1CrossDomainMessenger: L1CrossDomainMessenger__factory.connect( + mntAddresses.L1CrossDomainMessenger, + l1Provider + ), + L1CrossDomainMessengerStub: CrossDomainMessengerStub__factory.connect( + mntAddresses.L1CrossDomainMessenger, + l1Provider + ), + L2CrossDomainMessenger: L2CrossDomainMessenger__factory.connect( + mntAddresses.L2CrossDomainMessenger, + l2Provider + ), + CanonicalTransactionChain: CanonicalTransactionChain__factory.connect( + mntAddresses.CanonicalTransactionChain, + l1Provider + ), + }; +} diff --git a/utils/mantle/deployment.ts b/utils/mantle/deployment.ts new file mode 100644 index 00000000..4082077b --- /dev/null +++ b/utils/mantle/deployment.ts @@ -0,0 +1,153 @@ +import { assert } from "chai"; +import { Overrides, Wallet } from "ethers"; +import { + ERC20BridgedPermit__factory, + IERC20Metadata__factory, + L1ERC20TokenBridge__factory, + L2ERC20TokenBridge__factory, + OssifiableProxy__factory, +} from "../../typechain"; + +import addresses from "./addresses"; +import { CommonOptions } from "./types"; +import network, { NetworkName } from "../network"; +import { DeployScript, Logger } from "../deployment/DeployScript"; + +interface MntL1DeployScriptParams { + deployer: Wallet; + admins: { proxy: string; bridge: string }; +} + +interface MntL2DeployScriptParams extends MntL1DeployScriptParams { + l2Token?: { name?: string; symbol?: string }; +} + +interface MntDeploymentOptions extends CommonOptions { + logger?: Logger; + overrides?: Overrides; +} + +export default function deployment( + networkName: NetworkName, + options: MntDeploymentOptions = {} +) { + const mntAddresses = addresses(networkName, options); + return { + async erc20TokenBridgeDeployScript( + l1Token: string, + l1Params: MntL1DeployScriptParams, + l2Params: MntL2DeployScriptParams + ) { + const [ + expectedL1TokenBridgeImplAddress, + expectedL1TokenBridgeProxyAddress, + ] = await network.predictAddresses(l1Params.deployer, 2); + + const [ + expectedL2TokenImplAddress, + expectedL2TokenProxyAddress, + expectedL2TokenBridgeImplAddress, + expectedL2TokenBridgeProxyAddress, + ] = await network.predictAddresses(l2Params.deployer, 4); + + const l1DeployScript = new DeployScript( + l1Params.deployer, + options?.logger + ) + .addStep({ + factory: L1ERC20TokenBridge__factory, + args: [ + mntAddresses.L1CrossDomainMessenger, + expectedL2TokenBridgeProxyAddress, + l1Token, + expectedL2TokenProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL1TokenBridgeImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL1TokenBridgeImplAddress, + l1Params.admins.proxy, + L1ERC20TokenBridge__factory.createInterface().encodeFunctionData( + "initialize", + [l1Params.admins.bridge] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL1TokenBridgeProxyAddress), + }); + + const l1TokenInfo = IERC20Metadata__factory.connect( + l1Token, + l1Params.deployer + ); + + const [decimals, l2TokenName, l2TokenSymbol] = await Promise.all([ + l1TokenInfo.decimals(), + l2Params.l2Token?.name ?? l1TokenInfo.name(), + l2Params.l2Token?.symbol ?? l1TokenInfo.symbol(), + ]); + + const l2DeployScript = new DeployScript( + l2Params.deployer, + options?.logger + ) + .addStep({ + factory: ERC20BridgedPermit__factory, + args: [ + l2TokenName, + l2TokenSymbol, + decimals, + expectedL2TokenBridgeProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenImplAddress, + l2Params.admins.proxy, + ERC20BridgedPermit__factory.createInterface().encodeFunctionData( + "initialize", + [l2TokenName, l2TokenSymbol] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenProxyAddress), + }) + .addStep({ + factory: L2ERC20TokenBridge__factory, + args: [ + mntAddresses.L2CrossDomainMessenger, + expectedL1TokenBridgeProxyAddress, + l1Token, + expectedL2TokenProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenBridgeImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenBridgeImplAddress, + l2Params.admins.proxy, + L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( + "initialize", + [l2Params.admins.bridge] + ), + options?.overrides, + ], + }); + + return [l1DeployScript, l2DeployScript]; + }, + }; +} diff --git a/utils/mantle/index.ts b/utils/mantle/index.ts new file mode 100644 index 00000000..0ca0e9a2 --- /dev/null +++ b/utils/mantle/index.ts @@ -0,0 +1,13 @@ +import addresses from "./addresses"; +import contracts from "./contracts"; +import deployment from "./deployment"; +import testing from "./testing"; +import messaging from "./messaging"; + +export default { + testing, + addresses, + contracts, + messaging, + deployment, +}; diff --git a/utils/mantle/messaging.ts b/utils/mantle/messaging.ts new file mode 100644 index 00000000..7bf22a59 --- /dev/null +++ b/utils/mantle/messaging.ts @@ -0,0 +1,48 @@ +import contracts from "./contracts"; +import network, { NetworkName } from "../network"; +import { CommonOptions } from "./types"; +import { CrossChainMessenger, MessageStatus } from "@mantleio/sdk"; + +interface ContractsOptions extends CommonOptions { + forking: boolean; +} + +interface MessageData { + sender: string; + recipient: string; + calldata: string; + gasLimit?: number; +} + +export default function messaging( + networkName: NetworkName, + options: ContractsOptions +) { + const [ethProvider, mntProvider] = network + .multichain(["eth", "mnt"], networkName) + .getProviders(options); + + const mntContracts = contracts(networkName, options); + const crossChainMessenger = new CrossChainMessenger({ + l2ChainId: network.chainId("mnt", networkName), + l1SignerOrProvider: ethProvider, + l2SignerOrProvider: mntProvider, + l1ChainId: network.chainId("eth", networkName), + }); + return { + prepareL2Message(msg: MessageData) { + const calldata = + mntContracts.L1CrossDomainMessenger.interface.encodeFunctionData( + "sendMessage", + [msg.recipient, msg.calldata, msg.gasLimit || 1_000_000] + ); + return { calldata, callvalue: 0 }; + }, + async waitForL2Message(txHash: string) { + await crossChainMessenger.waitForMessageStatus( + txHash, + MessageStatus.RELAYED + ); + }, + }; +} diff --git a/utils/mantle/testing.ts b/utils/mantle/testing.ts new file mode 100644 index 00000000..05fc884a --- /dev/null +++ b/utils/mantle/testing.ts @@ -0,0 +1,300 @@ +import { Signer } from "ethers"; +import { JsonRpcProvider } from "@ethersproject/providers"; + +import { + IERC20, + ERC20Bridged, + IERC20__factory, + L1ERC20TokenBridge, + L2ERC20TokenBridge, + ERC20BridgedPermit__factory, + ERC20BridgedStub__factory, + L1ERC20TokenBridge__factory, + L2ERC20TokenBridge__factory, + CrossDomainMessengerStub__factory, +} from "../../typechain"; +import addresses from "./addresses"; +import contracts from "./contracts"; +import deployment from "./deployment"; +import testingUtils from "../testing"; +import { BridgingManagement } from "../bridging-management"; +import network, { NetworkName, SignerOrProvider } from "../network"; + +export default function testing(networkName: NetworkName) { + const mntAddresses = addresses(networkName); + const ethMntNetworks = network.multichain(["eth", "mnt"], networkName); + + return { + async getAcceptanceTestSetup() { + const [ethProvider, mntProvider] = ethMntNetworks.getProviders({ + forking: true, + }); + + const bridgeContracts = await loadDeployedBridges( + ethProvider, + mntProvider + ); + + await printLoadedTestConfig(networkName, bridgeContracts); + + return { + l1Provider: ethProvider, + l2Provider: mntProvider, + ...bridgeContracts, + }; + }, + async getIntegrationTestSetup() { + const hasDeployedContracts = + testingUtils.env.USE_DEPLOYED_CONTRACTS(false); + + const [ethProvider, mntProvider] = ethMntNetworks.getProviders({ + forking: true, + }); + + const bridgeContracts = hasDeployedContracts + ? await loadDeployedBridges(ethProvider, mntProvider) + : await deployTestBridge(networkName, ethProvider, mntProvider); + + const [l1ERC20TokenBridgeAdminAddress] = + await BridgingManagement.getAdmins(bridgeContracts.l1ERC20TokenBridge); + + const [l2ERC20TokenBridgeAdminAddress] = + await BridgingManagement.getAdmins(bridgeContracts.l2ERC20TokenBridge); + + const l1TokensHolder = hasDeployedContracts + ? await testingUtils.impersonate( + testingUtils.env.L1_TOKENS_HOLDER(), + ethProvider + ) + : testingUtils.accounts.deployer(ethProvider); + + if (hasDeployedContracts) { + await printLoadedTestConfig( + networkName, + bridgeContracts, + l1TokensHolder + ); + } + + // if the L1 bridge admin is a contract, remove it's code to + // make it behave as EOA + await ethProvider.send("hardhat_setCode", [ + l1ERC20TokenBridgeAdminAddress, + "0x", + ]); + + // same for the L2 bridge admin + await mntProvider.send("hardhat_setCode", [ + l2ERC20TokenBridgeAdminAddress, + "0x", + ]); + + const mntContracts = contracts(networkName, { forking: true }); + + return { + l1Provider: ethProvider, + l2Provider: mntProvider, + l1TokensHolder, + ...bridgeContracts, + l1CrossDomainMessenger: mntContracts.L1CrossDomainMessengerStub, + l2CrossDomainMessenger: mntContracts.L2CrossDomainMessenger, + l1ERC20TokenBridgeAdmin: await testingUtils.impersonate( + l1ERC20TokenBridgeAdminAddress, + ethProvider + ), + l2ERC20TokenBridgeAdmin: await testingUtils.impersonate( + l2ERC20TokenBridgeAdminAddress, + mntProvider + ), + canonicalTransactionChain: mntContracts.CanonicalTransactionChain, + }; + }, + async getE2ETestSetup() { + const testerPrivateKey = testingUtils.env.TESTING_PRIVATE_KEY(); + const [ethProvider, mntProvider] = ethMntNetworks.getProviders({ + forking: false, + }); + const [l1Tester, l2Tester] = ethMntNetworks.getSigners(testerPrivateKey, { + forking: false, + }); + + const bridgeContracts = await loadDeployedBridges(l1Tester, l2Tester); + + await printLoadedTestConfig(networkName, bridgeContracts, l1Tester); + + return { + l1Tester, + l2Tester, + l1Provider: ethProvider, + l2Provider: mntProvider, + ...bridgeContracts, + }; + }, + async stubL1CrossChainMessengerContract() { + const [ethProvider] = ethMntNetworks.getProviders({ forking: true }); + const deployer = testingUtils.accounts.deployer(ethProvider); + const stub = await new CrossDomainMessengerStub__factory( + deployer + ).deploy(); + const stubBytecode = await ethProvider.send("eth_getCode", [ + stub.address, + ]); + + await ethProvider.send("hardhat_setCode", [ + mntAddresses.L1CrossDomainMessenger, + stubBytecode, + ]); + }, + }; +} + +async function loadDeployedBridges( + l1SignerOrProvider: SignerOrProvider, + l2SignerOrProvider: SignerOrProvider +) { + return { + l1Token: IERC20__factory.connect( + testingUtils.env.MNT_L1_TOKEN(), + l1SignerOrProvider + ), + ...connectBridgeContracts( + { + l2Token: testingUtils.env.MNT_L2_TOKEN(), + l1ERC20TokenBridge: testingUtils.env.MNT_L1_ERC20_TOKEN_BRIDGE(), + l2ERC20TokenBridge: testingUtils.env.MNT_L2_ERC20_TOKEN_BRIDGE(), + }, + l1SignerOrProvider, + l2SignerOrProvider + ), + }; +} + +async function deployTestBridge( + networkName: NetworkName, + ethProvider: JsonRpcProvider, + mntProvider: JsonRpcProvider +) { + const ethDeployer = testingUtils.accounts.deployer(ethProvider); + const mntDeployer = testingUtils.accounts.deployer(mntProvider); + + const l1Token = await new ERC20BridgedStub__factory(ethDeployer).deploy( + "Test Token", + "TT" + ); + + const [ethDeployScript, mntDeployScript] = await deployment( + networkName + ).erc20TokenBridgeDeployScript( + l1Token.address, + { + deployer: ethDeployer, + admins: { proxy: ethDeployer.address, bridge: ethDeployer.address }, + }, + { + deployer: mntDeployer, + admins: { proxy: mntDeployer.address, bridge: mntDeployer.address }, + } + ); + + await ethDeployScript.run(); + await mntDeployScript.run(); + + const l1ERC20TokenBridgeProxyDeployStepIndex = 1; + const l1BridgingManagement = new BridgingManagement( + ethDeployScript.getContractAddress(l1ERC20TokenBridgeProxyDeployStepIndex), + ethDeployer + ); + + const l2ERC20TokenBridgeProxyDeployStepIndex = 3; + const l2BridgingManagement = new BridgingManagement( + mntDeployScript.getContractAddress(l2ERC20TokenBridgeProxyDeployStepIndex), + mntDeployer + ); + + await l1BridgingManagement.setup({ + bridgeAdmin: ethDeployer.address, + depositsEnabled: true, + withdrawalsEnabled: true, + }); + + await l2BridgingManagement.setup({ + bridgeAdmin: mntDeployer.address, + depositsEnabled: true, + withdrawalsEnabled: true, + }); + + return { + l1Token: l1Token.connect(ethProvider), + ...connectBridgeContracts( + { + l2Token: mntDeployScript.getContractAddress(1), + l1ERC20TokenBridge: ethDeployScript.getContractAddress(1), + l2ERC20TokenBridge: mntDeployScript.getContractAddress(3), + }, + ethProvider, + mntProvider + ), + }; +} + +function connectBridgeContracts( + addresses: { + l2Token: string; + l1ERC20TokenBridge: string; + l2ERC20TokenBridge: string; + }, + ethSignerOrProvider: SignerOrProvider, + mntSignerOrProvider: SignerOrProvider +) { + const l1ERC20TokenBridge = L1ERC20TokenBridge__factory.connect( + addresses.l1ERC20TokenBridge, + ethSignerOrProvider + ); + const l2ERC20TokenBridge = L2ERC20TokenBridge__factory.connect( + addresses.l2ERC20TokenBridge, + mntSignerOrProvider + ); + const l2Token = ERC20BridgedPermit__factory.connect( + addresses.l2Token, + mntSignerOrProvider + ); + return { + l2Token, + l1ERC20TokenBridge, + l2ERC20TokenBridge, + }; +} + +async function printLoadedTestConfig( + networkName: NetworkName, + bridgeContracts: { + l1Token: IERC20; + l2Token: ERC20Bridged; + l1ERC20TokenBridge: L1ERC20TokenBridge; + l2ERC20TokenBridge: L2ERC20TokenBridge; + }, + l1TokensHolder?: Signer +) { + console.log("Using the deployed contracts for testing"); + console.log( + "In case of unexpected fails, please, make sure that you are forking correct Ethereum/Mantle networks" + ); + console.log(` · Network Name: ${networkName}`); + console.log(` · L1 Token: ${bridgeContracts.l1Token.address}`); + console.log(` · L2 Token: ${bridgeContracts.l2Token.address}`); + if (l1TokensHolder) { + const l1TokensHolderAddress = await l1TokensHolder.getAddress(); + console.log(` · L1 Tokens Holder: ${l1TokensHolderAddress}`); + const holderBalance = await bridgeContracts.l1Token.balanceOf( + l1TokensHolderAddress + ); + console.log(` · L1 Tokens Holder Balance: ${holderBalance.toString()}`); + } + console.log( + ` · L1 ERC20 Token Bridge: ${bridgeContracts.l1ERC20TokenBridge.address}` + ); + console.log( + ` · L2 ERC20 Token Bridge: ${bridgeContracts.l2ERC20TokenBridge.address}` + ); + console.log(); +} diff --git a/utils/mantle/types.ts b/utils/mantle/types.ts new file mode 100644 index 00000000..3981af5c --- /dev/null +++ b/utils/mantle/types.ts @@ -0,0 +1,10 @@ +export type MntContractNames = + | "L1CrossDomainMessenger" + | "L2CrossDomainMessenger" + | "CanonicalTransactionChain"; + +export type MntContractAddresses = Record; +export type CustomMntContractAddresses = Partial; +export interface CommonOptions { + customAddresses?: CustomMntContractAddresses; +} diff --git a/utils/network.ts b/utils/network.ts index 5fd29d0d..4a84a115 100644 --- a/utils/network.ts +++ b/utils/network.ts @@ -6,7 +6,7 @@ import { HardhatRuntimeEnvironment, HttpNetworkConfig } from "hardhat/types"; import env from "./env"; -type ChainNameShort = "arb" | "opt" | "eth"; +type ChainNameShort = "arb" | "opt" | "eth" | "mnt"; export type NetworkName = "goerli" | "mainnet"; export type SignerOrProvider = Signer | Provider; @@ -23,6 +23,10 @@ const HARDHAT_NETWORK_NAMES = { goerli: "opt_goerli", mainnet: "opt_mainnet", }, + mnt: { + goerli: "mnt_goerli", + mainnet: "mnt_mainnet", + }, }; const HARDHAT_NETWORK_NAMES_FORK = { @@ -38,6 +42,10 @@ const HARDHAT_NETWORK_NAMES_FORK = { goerli: "opt_goerli_fork", mainnet: "opt_mainnet_fork", }, + mnt: { + goerli: "mnt_goerli_fork", + mainnet: "mnt_mainnet_fork", + }, }; export function getConfig(networkName: string, hre: HardhatRuntimeEnvironment) { @@ -125,6 +133,10 @@ function getChainId(protocol: ChainNameShort, networkName: NetworkName) { mainnet: 10, goerli: 420, }, + mnt: { + mainnet: 5000, + goerli: 5001, + }, arb: { mainnet: 42161, goerli: 421613, @@ -150,6 +162,9 @@ function getBlockExplorerBaseUrlByChainId(chainId: number) { 420: "https://blockscout.com/optimism/goerli", // forked node 31337: "https://etherscan.io", + // mantle + 5000: "https://explorer.mantle.xyz/", + 5001: "https://explorer.testnet.mantle.xyz/" }; return baseUrlByChainId[chainId]; } diff --git a/utils/testing/e2e.ts b/utils/testing/e2e.ts index 010596e7..c025f845 100644 --- a/utils/testing/e2e.ts +++ b/utils/testing/e2e.ts @@ -21,6 +21,24 @@ export const E2E_TEST_CONTRACTS_OPTIMISM = { }, }; +export const E2E_TEST_CONTRACTS_MANTLE = { + l1: { + l1Token: "0x6320cD32aA674d2898A68ec82e869385Fc5f7E2f", + l1LDOToken: "0xcAdf242b97BFdD1Cb4Fd282E5FcADF965883065f", + l1ERC20TokenBridge: "0x67fa6217C48BAd4777BF2C742c728a7200CE971E", + aragonVoting: "0xfDdA522eF6626e155d47Be0aeF74c204CfB3d2c4", + tokenManager: "0xF3BfaD8a6960ad130e02c9d14262788dea2C3Cd5", + agent: "0x45B1F6E7ABFf8A8bf516554634Abf37D73C79fBC", + l1CrossDomainMessenger: "0x7Bfe603647d5380ED3909F6f87580D0Af1B228B4", + }, + l2: { + l2Token: "0xf53f81Ef9F9291Ce714d5691edb13b40C31F8781", + l2ERC20TokenBridge: "0x081299187587cBA30Bc29f4Ac4a4c6987C575f5f", + govBridgeExecutor: "0x970Dcbd7eA1fd378462Ef0C82B0BE7f2083DD7fE", + }, +}; + + export const E2E_TEST_CONTRACTS_ARBITRUM = { l1: { l1Token: "0x7AEE39c46f20135114e85A03C02aB4FE73fB8127", @@ -69,6 +87,35 @@ export const createOptimismVoting = async ( await newVotingTx.wait(); }; +export const createMantleVoting = async ( + ctx: any, + executorCalldata: string +) => { + const messageCalldata = + await ctx.l1CrossDomainMessenger.interface.encodeFunctionData( + "sendMessage", + [ctx.govBridgeExecutor.address, executorCalldata, 1000000] + ); + const messageEvmScript = encodeEVMScript( + ctx.l1CrossDomainMessenger.address, + messageCalldata + ); + + const agentCalldata = ctx.agent.interface.encodeFunctionData("forward", [ + messageEvmScript, + ]); + const agentEvmScript = encodeEVMScript(ctx.agent.address, agentCalldata); + + const newVoteCalldata = + "0xd5db2c80" + + abiCoder.encode(["bytes", "string"], [agentEvmScript, ""]).substring(2); + const votingEvmScript = encodeEVMScript(ctx.voting.address, newVoteCalldata); + + const newVotingTx = await ctx.tokenMnanager.forward(votingEvmScript); + + await newVotingTx.wait(); +}; + export const encodeEVMScript = ( target: AddressLike, messageCalldata: string diff --git a/utils/testing/env.ts b/utils/testing/env.ts index faf8acd6..edfc9212 100644 --- a/utils/testing/env.ts +++ b/utils/testing/env.ts @@ -42,6 +42,22 @@ export default { OPT_GOV_BRIDGE_EXECUTOR() { return env.address("TESTING_OPT_GOV_BRIDGE_EXECUTOR"); }, + + MNT_L1_TOKEN() { + return env.address("TESTING_MNT_L1_TOKEN"); + }, + MNT_L2_TOKEN() { + return env.address("TESTING_MNT_L2_TOKEN"); + }, + MNT_L1_ERC20_TOKEN_BRIDGE() { + return env.address("TESTING_MNT_L1_ERC20_TOKEN_BRIDGE"); + }, + MNT_L2_ERC20_TOKEN_BRIDGE() { + return env.address("TESTING_MNT_L2_ERC20_TOKEN_BRIDGE"); + }, + MNT_GOV_BRIDGE_EXECUTOR() { + return env.address("TESTING_MNT_GOV_BRIDGE_EXECUTOR"); + }, L1_DEV_MULTISIG() { return env.address("L1_DEV_MULTISIG"); }, diff --git a/utils/testing/permit.ts b/utils/testing/permit.ts new file mode 100644 index 00000000..d443449b --- /dev/null +++ b/utils/testing/permit.ts @@ -0,0 +1,64 @@ +// from https://github.com/Uniswap/v3-periphery/blob/main/test/shared/permit.ts + +import { BigNumberish, constants, Signature, Wallet } from 'ethers' +import { splitSignature } from 'ethers/lib/utils' +import { ERC20BridgedPermit } from '../../typechain' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; + +export async function getPermitSignature( + wallet: SignerWithAddress, + token: ERC20BridgedPermit, + spender: string, + value: BigNumberish = constants.MaxUint256, + deadline = constants.MaxUint256, + permitConfig?: { nonce?: BigNumberish; name?: string; chainId?: number; version?: string } +): Promise { + const [nonce, name, version, chainId] = await Promise.all([ + permitConfig?.nonce ?? token.nonces(wallet.address), + permitConfig?.name ?? token.name(), + permitConfig?.version ?? '1', + permitConfig?.chainId ?? wallet.getChainId(), + ]) + + return splitSignature( + await wallet._signTypedData( + { + name, + version, + chainId, + verifyingContract: token.address, + }, + { + Permit: [ + { + name: 'owner', + type: 'address', + }, + { + name: 'spender', + type: 'address', + }, + { + name: 'value', + type: 'uint256', + }, + { + name: 'nonce', + type: 'uint256', + }, + { + name: 'deadline', + type: 'uint256', + }, + ], + }, + { + owner: wallet.address, + spender, + value, + nonce, + deadline, + } + ) + ) +} \ No newline at end of file diff --git a/utils/testing/scenario.ts b/utils/testing/scenario.ts index a11ba74f..e7fd8c62 100644 --- a/utils/testing/scenario.ts +++ b/utils/testing/scenario.ts @@ -34,11 +34,11 @@ class ScenarioTest { for (let i = 0; i < repeat; i++) { describe(this.title, function () { // @ts-ignore - let ctx: T = {}; + let ctx = {}; before(async () => { ctx = Object.assign(ctx, await self.ctxFactory()); if (beforeFn) { - await beforeFn(ctx); + await beforeFn(ctx as T); } }); @@ -52,7 +52,7 @@ class ScenarioTest { this.skip(); } try { - await step.test(ctx); + await step.test(ctx as T); } catch (error) { skipOtherTests = true; throw error; @@ -61,7 +61,7 @@ class ScenarioTest { } if (afterFn !== undefined) { - after(async () => afterFn(ctx)); + after(async () => afterFn(ctx as T)); } }); } diff --git a/utils/testing/signatures.ts b/utils/testing/signatures.ts new file mode 100644 index 00000000..4bba3a53 --- /dev/null +++ b/utils/testing/signatures.ts @@ -0,0 +1,62 @@ +// from https://github.com/alcueca/ERC20Permit/blob/master/utils/signatures.ts + +import { keccak256, defaultAbiCoder, toUtf8Bytes, solidityPack } from 'ethers/lib/utils' +import { BigNumberish } from 'ethers' +import { ecsign } from 'ethereumjs-util' + +export const sign = (digest: any, privateKey: any) => { + return ecsign(Buffer.from(digest.slice(2), 'hex'), privateKey) +} + +export const PERMIT_TYPEHASH = keccak256( + toUtf8Bytes('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)') +) + +// Returns the EIP712 hash which should be signed by the user +// in order to make a call to `permit` +export function getPermitDigest( + name: string, + address: string, + chainId: number, + approve: { + owner: string + spender: string + value: BigNumberish + }, + nonce: BigNumberish, + deadline: BigNumberish +) { + const DOMAIN_SEPARATOR = getDomainSeparator(name, address, chainId) + return keccak256( + solidityPack( + ['bytes1', 'bytes1', 'bytes32', 'bytes32'], + [ + '0x19', + '0x01', + DOMAIN_SEPARATOR, + keccak256( + defaultAbiCoder.encode( + ['bytes32', 'address', 'address', 'uint256', 'uint256', 'uint256'], + [PERMIT_TYPEHASH, approve.owner, approve.spender, approve.value, nonce, deadline] + ) + ), + ] + ) + ) +} + +// Gets the EIP712 domain separator +export function getDomainSeparator(name: string, contractAddress: string, chainId: number) { + return keccak256( + defaultAbiCoder.encode( + ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], + [ + keccak256(toUtf8Bytes('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')), + keccak256(toUtf8Bytes(name)), + keccak256(toUtf8Bytes('1')), + chainId, + contractAddress, + ] + ) + ) +} \ No newline at end of file diff --git a/utils/testing/unit.ts b/utils/testing/unit.ts index 246a847e..9c84ed48 100644 --- a/utils/testing/unit.ts +++ b/utils/testing/unit.ts @@ -34,7 +34,7 @@ class UnitTest { describe(title, function () { // @ts-ignore - let ctx: T = {}; + let ctx = {}; let evmSnapshotId: string; @@ -46,7 +46,7 @@ class UnitTest { // create mocha tests for (const { title, test } of tests) { - it(title, () => test(ctx)); + it(title, () => test(ctx as T)); } afterEach(async () => {