diff --git a/.env.sample b/.env.sample index 957e611b..2fd22f8e 100644 --- a/.env.sample +++ b/.env.sample @@ -10,4 +10,6 @@ ROOT_GAS_SERVICE_ADDRESS= CHILD_GAS_SERVICE_ADDRESS= ROOT_CHAIN_NAME= CHILD_CHAIN_NAME= -ROOT_IMX_ADDRESS= \ No newline at end of file +ROOT_IMX_ADDRESS= +ROOT_WETH_ADDRESS= +ENVIRONMENT= diff --git a/.gitignore b/.gitignore index bd8726e5..3ab99ec0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Compiler files cache/ out/ +.idea/ # Ignores development broadcast logs !/broadcast diff --git a/README.md b/README.md index 8acd6cc0..5b0bd87d 100644 --- a/README.md +++ b/README.md @@ -138,10 +138,11 @@ ROOT_GATEWAY_ADDRESS="0x013459EC3E8Aeced878C5C4bFfe126A366cd19E9" CHILD_GATEWAY_ADDRESS="0xc7B788E88BAaB770A6d4936cdcCcd5250E1bbAd8" ROOT_GAS_SERVICE_ADDRESS="0x28f8B50E1Be6152da35e923602a2641491E71Ed8" CHILD_GAS_SERVICE_ADDRESS="0xC573c722e21eD7fadD38A8f189818433e01Ae466" - +ENVIRONMENT="local" ``` (Note that `{ROOT,CHILD}_PRIVATE_KEY` can be any of the standard localhost private keys that get funded) (Note that `ROOT_IMX_ADDRESS` is not currently used in this local environment. Therefore, any non-zero address is fine.) +(Note that `ENVIRONMENT` if the environment is not set to "local" then ROOT_WETH_ADDRESS will need to be manually set in the .env file since it is expected WETH to be already deployed on testnet or mainnet) 3. In a separate terminal window, deploy the smart contracts ```shell diff --git a/deploy.sh b/deploy.sh index 2db86279..8a106b18 100755 --- a/deploy.sh +++ b/deploy.sh @@ -13,6 +13,7 @@ function get_deployed_contract() { } function main() { + forge script script/DeployRootContracts.s.sol:DeployRootContracts --broadcast forge script script/DeployChildContracts.s.sol:DeployChildContracts --broadcast @@ -23,6 +24,11 @@ function main() { root_erc20_bridge=$( get_deployed_contract "$root_filename" "RootERC20Bridge" ) root_bridge_adaptor=$( get_deployed_contract "$root_filename" "RootAxelarBridgeAdaptor" ) root_chain_child_token_template=$( get_deployed_contract "$root_filename" "ChildERC20" ) + + if [ "$ENVIRONMENT" = "local" ]; then + root_weth_contract=$( get_deployed_contract "$root_filename" "WETH" ) + export ROOT_WETH_ADDRESS=$root_weth_contract + fi child_erc20_bridge=$( get_deployed_contract "$child_filename" "ChildERC20Bridge" ) child_bridge_adaptor=$( get_deployed_contract "$child_filename" "ChildAxelarBridgeAdaptor" ) @@ -31,6 +37,7 @@ function main() { export ROOT_ERC20_BRIDGE=$root_erc20_bridge export ROOT_BRIDGE_ADAPTOR=$root_bridge_adaptor export ROOTCHAIN_CHILD_TOKEN_TEMPLATE=$root_chain_child_token_template + export CHILD_BRIDGE_ADAPTOR=$child_bridge_adaptor export CHILD_ERC20_BRIDGE=$child_erc20_bridge export CHILDCHAIN_CHILD_TOKEN_TEMPLATE=$child_chain_child_token_template diff --git a/script/DeployRootContracts.s.sol b/script/DeployRootContracts.s.sol index 5f56d595..0c3d4102 100644 --- a/script/DeployRootContracts.s.sol +++ b/script/DeployRootContracts.s.sol @@ -9,6 +9,7 @@ import {RootAxelarBridgeAdaptor} from "../src/root/RootAxelarBridgeAdaptor.sol"; import {ChildERC20Bridge} from "../src/child/ChildERC20Bridge.sol"; import {ChildAxelarBridgeAdaptor} from "../src/child/ChildAxelarBridgeAdaptor.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {WETH} from "../src/test/root/WETH.sol"; // TODO update private key usage to be more secure: https://book.getfoundry.sh/reference/forge/forge-script#wallet-options---raw @@ -19,6 +20,7 @@ contract DeployRootContracts is Script { address rootGateway = vm.envAddress("ROOT_GATEWAY_ADDRESS"); address rootGasService = vm.envAddress("ROOT_GAS_SERVICE_ADDRESS"); string memory childChainName = vm.envString("CHILD_CHAIN_NAME"); + string memory deployEnvironment = vm.envString("ENVIRONMENT"); /** * DEPLOY ROOT CHAIN CONTRACTS @@ -39,11 +41,15 @@ contract DeployRootContracts is Script { rootGasService // axelar gas service ); + if (Strings.equal(deployEnvironment, "local")) { + new WETH(); + } + + vm.stopBroadcast(); + console2.log("====ROOT ADDRESSES===="); console2.log("Root ERC20 Bridge: %s", address(rootERC20Bridge)); console2.log("Root Axelar Bridge Adaptor: %s", address(rootBridgeAdaptor)); console2.log("ROOT CHAIN childTokenTemplate: %s", address(rootChainChildTokenTemplate)); - - vm.stopBroadcast(); } } diff --git a/script/InitializeRootContracts.s.sol b/script/InitializeRootContracts.s.sol index b77572be..2a8d43e1 100644 --- a/script/InitializeRootContracts.s.sol +++ b/script/InitializeRootContracts.s.sol @@ -15,7 +15,7 @@ import {Utils} from "./Utils.sol"; contract InitializeRootContracts is Script { function run() public { - RootERC20Bridge rootERC20Bridge = RootERC20Bridge(vm.envAddress("ROOT_ERC20_BRIDGE")); + RootERC20Bridge rootERC20Bridge = RootERC20Bridge(payable(vm.envAddress("ROOT_ERC20_BRIDGE"))); RootAxelarBridgeAdaptor rootBridgeAdaptor = RootAxelarBridgeAdaptor(vm.envAddress("ROOT_BRIDGE_ADAPTOR")); address rootChainChildTokenTemplate = vm.envAddress("ROOTCHAIN_CHILD_TOKEN_TEMPLATE"); address childBridgeAdaptor = vm.envAddress("CHILD_BRIDGE_ADAPTOR"); @@ -23,6 +23,7 @@ contract InitializeRootContracts is Script { string memory rootRpcUrl = vm.envString("ROOT_RPC_URL"); uint256 rootPrivateKey = vm.envUint("ROOT_PRIVATE_KEY"); address rootIMXToken = vm.envAddress("ROOT_IMX_ADDRESS"); + address rootWETHToken = vm.envAddress("ROOT_WETH_ADDRESS"); string[] memory checksumInputs = Utils.getChecksumInputs(childBridgeAdaptor); bytes memory checksumOutput = vm.ffi(checksumInputs); @@ -38,7 +39,8 @@ contract InitializeRootContracts is Script { childERC20Bridge, childBridgeAdaptorChecksum, rootChainChildTokenTemplate, - rootIMXToken + rootIMXToken, + rootWETHToken ); rootBridgeAdaptor.setChildBridgeAdaptor(); diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index d3f858ba..75fa06c4 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -46,6 +46,13 @@ interface IRootERC20BridgeEvents { uint256 amount ); event IMXDeposit(address indexed rootToken, address depositor, address indexed receiver, uint256 amount); + event WETHDeposit( + address indexed rootToken, + address indexed childToken, + address depositor, + address indexed receiver, + uint256 amount + ); event NativeEthDeposit( address indexed rootToken, address indexed childToken, @@ -70,6 +77,8 @@ interface IRootERC20BridgeErrors { error CantMapIMX(); /// @notice Error when attempting to map ETH. error CantMapETH(); + /// @notice Error when attempting to map wETH. + error CantMapWETH(); /// @notice Error when token balance invariant check fails. error BalanceInvariantCheckFailed(uint256 actualBalance, uint256 expectedBalance); /// @notice Error when the given child chain bridge adaptor is invalid. diff --git a/src/interfaces/root/IWETH.sol b/src/interfaces/root/IWETH.sol new file mode 100644 index 00000000..4cc378ab --- /dev/null +++ b/src/interfaces/root/IWETH.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @dev Interface of Wrapped ETH. + */ +interface IWETH is IERC20 { + /** + * @dev Emitted when `value` native ETH are deposited from `account`. + */ + event Deposit(address indexed account, uint256 value); + + /** + * @dev Emitted when `value` wETH tokens are withdrawn to `account`. + */ + event Withdrawal(address indexed account, uint256 value); + + /** + * @notice Deposit native ETH in the function call and mint the equal amount of wrapped ETH to msg.sender. + */ + function deposit() external payable; + + /** + * @notice Withdraw given amount of native ETH to msg.sender and burn the equal amount of wrapped ETH. + * @param value The amount to withdraw. + */ + function withdraw(uint256 value) external; +} diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 4fbf8146..23ae2835 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -12,6 +12,7 @@ import {IRootERC20Bridge, IERC20Metadata} from "../interfaces/root/IRootERC20Bri import {IRootERC20BridgeEvents, IRootERC20BridgeErrors} from "../interfaces/root/IRootERC20Bridge.sol"; import {IRootERC20BridgeAdaptor} from "../interfaces/root/IRootERC20BridgeAdaptor.sol"; import {IChildERC20} from "../interfaces/child/IChildERC20.sol"; +import {IWETH} from "../interfaces/root/IWETH.sol"; /** * @notice RootERC20Bridge is a bridge that allows ERC20 tokens to be transferred from the root chain to the child chain. @@ -21,6 +22,7 @@ import {IChildERC20} from "../interfaces/child/IChildERC20.sol"; * @dev Because of this pattern, any checks or logic that is agnostic to the messaging protocol should be done in RootERC20Bridge. * @dev Any checks or logic that is specific to the underlying messaging protocol should be done in the bridge adaptor. */ + contract RootERC20Bridge is Ownable2Step, Initializable, @@ -48,6 +50,8 @@ contract RootERC20Bridge is address public rootIMXToken; /// @dev The address of the ETH ERC20 token on L2. address public childETHToken; + /// @dev The address of the wETH ERC20 token on L1. + address public rootWETHToken; /** * @notice Initilization function for RootERC20Bridge. @@ -56,6 +60,7 @@ contract RootERC20Bridge is * @param newChildBridgeAdaptor Address of child bridge adaptor to communicate with (As a checksummed string). * @param newChildTokenTemplate Address of child token template to clone. * @param newRootIMXToken Address of ERC20 IMX on the root chain. + * @param newRootWETHToken Address of ERC20 WETH on the root chain. * @dev Can only be called once. */ function initialize( @@ -63,11 +68,12 @@ contract RootERC20Bridge is address newChildERC20Bridge, string memory newChildBridgeAdaptor, address newChildTokenTemplate, - address newRootIMXToken + address newRootIMXToken, + address newRootWETHToken ) public initializer { if ( newRootBridgeAdaptor == address(0) || newChildERC20Bridge == address(0) - || newChildTokenTemplate == address(0) || newRootIMXToken == address(0) + || newChildTokenTemplate == address(0) || newRootIMXToken == address(0) || newRootWETHToken == address(0) ) { revert ZeroAddress(); } @@ -77,7 +83,7 @@ contract RootERC20Bridge is childERC20Bridge = newChildERC20Bridge; childTokenTemplate = newChildTokenTemplate; rootIMXToken = newRootIMXToken; - + rootWETHToken = newRootWETHToken; childETHToken = Clones.predictDeterministicAddress( childTokenTemplate, keccak256(abi.encodePacked(NATIVE_ETH)), childERC20Bridge ); @@ -85,6 +91,18 @@ contract RootERC20Bridge is childBridgeAdaptor = newChildBridgeAdaptor; } + function updateRootBridgeAdaptor(address newRootBridgeAdaptor) external onlyOwner { + if (newRootBridgeAdaptor == address(0)) { + revert ZeroAddress(); + } + rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor); + } + + /** + * @dev method to receive the ETH back from the WETH contract when it is unwrapped + */ + receive() external payable {} + /** * @inheritdoc IRootERC20Bridge * @dev TODO when this becomes part of the deposit flow on a token's first bridge, this logic will need to be mostly moved into an internal function. @@ -97,15 +115,27 @@ contract RootERC20Bridge is } function depositETH(uint256 amount) external payable { - //override removed? _depositETH(msg.sender, amount); } function depositToETH(address receiver, uint256 amount) external payable { - //override removed? _depositETH(receiver, amount); } + /** + * @inheritdoc IRootERC20Bridge + */ + function deposit(IERC20Metadata rootToken, uint256 amount) external payable override { + _depositToken(rootToken, msg.sender, amount); + } + + /** + * @inheritdoc IRootERC20Bridge + */ + function depositTo(IERC20Metadata rootToken, address receiver, uint256 amount) external payable override { + _depositToken(rootToken, receiver, amount); + } + function _depositETH(address receiver, uint256 amount) private { if (msg.value < amount) { revert InsufficientValue(); @@ -121,18 +151,27 @@ contract RootERC20Bridge is } } - /** - * @inheritdoc IRootERC20Bridge - */ - function deposit(IERC20Metadata rootToken, uint256 amount) external payable override { - _depositERC20(rootToken, msg.sender, amount); + function _depositToken(IERC20Metadata rootToken, address receiver, uint256 amount) private { + if (address(rootToken) == rootWETHToken) { + _depositWrappedETH(receiver, amount); + } else { + _depositERC20(rootToken, receiver, amount); + } } - /** - * @inheritdoc IRootERC20Bridge - */ - function depositTo(IERC20Metadata rootToken, address receiver, uint256 amount) external payable override { - _depositERC20(rootToken, receiver, amount); + function _depositWrappedETH(address receiver, uint256 amount) private { + uint256 expectedBalance = address(this).balance + amount; + + IERC20Metadata erc20WETH = IERC20Metadata(rootWETHToken); + + erc20WETH.safeTransferFrom(msg.sender, address(this), amount); + IWETH(rootWETHToken).withdraw(amount); + + // invariant check to ensure that the root native balance has increased by the amount deposited + if (address(this).balance != expectedBalance) { + revert BalanceInvariantCheckFailed(address(this).balance, expectedBalance); + } + _deposit(IERC20Metadata(rootWETHToken), receiver, amount); } function _depositERC20(IERC20Metadata rootToken, address receiver, uint256 amount) private { @@ -157,6 +196,10 @@ contract RootERC20Bridge is revert CantMapETH(); } + if (address(rootToken) == rootWETHToken) { + revert CantMapWETH(); + } + if (rootTokenToChildToken[address(rootToken)] != address(0)) { revert AlreadyMapped(); } @@ -181,52 +224,50 @@ contract RootERC20Bridge is if (receiver == address(0) || address(rootToken) == address(0)) { revert ZeroAddress(); } - if (amount == 0) { revert ZeroAmount(); } - address childToken; - uint256 feeAmount; + // ETH, WETH and IMX do not need to be mapped since it should have been mapped on initialization + // ETH also cannot be transferred since it was received in the payable function call + // WETH is also not transferred here since it was earlier unwrapped to ETH - // The native token does not need to be mapped since it should have been mapped on initialization - // The native token also cannot be transferred since it was received in the payable function call // TODO We can call _mapToken here, but ordering in the GMP is not guaranteed. - // Therefore, we need to decide how to handle this and it may be a UI decision to wait until map token message is executed on child chain. - // Discuss this, and add this decision to the design doc. - if (address(rootToken) != NATIVE_ETH) { + // Therefore, we need to decide how to handle this and it may be a UI decision to wait until map token message is executed on child chain. + // Discuss this, and add this decision to the design doc. + + address childToken; + uint256 feeAmount = msg.value; + address payloadToken = address(rootToken); + + if (address(rootToken) == NATIVE_ETH) { + feeAmount = msg.value - amount; + } else if (address(rootToken) == rootWETHToken) { + payloadToken = NATIVE_ETH; + } else { if (address(rootToken) != rootIMXToken) { childToken = rootTokenToChildToken[address(rootToken)]; if (childToken == address(0)) { revert NotMapped(); } } - // ERC20 must be transferred explicitly rootToken.safeTransferFrom(msg.sender, address(this), amount); - feeAmount = msg.value; - } else { - feeAmount = msg.value - amount; } // Deposit sig, root token address, depositor, receiver, amount - bytes memory payload = abi.encode(DEPOSIT_SIG, rootToken, msg.sender, receiver, amount); - // TODO investigate using delegatecall to keep the axelar message sender as the bridge contract, since adaptor can change. + bytes memory payload = abi.encode(DEPOSIT_SIG, payloadToken, msg.sender, receiver, amount); + // TODO investigate using delegatecall to keep the axelar message sender as the bridge contract, since adaptor can change. rootBridgeAdaptor.sendMessage{value: feeAmount}(payload, msg.sender); if (address(rootToken) == NATIVE_ETH) { emit NativeEthDeposit(address(rootToken), childETHToken, msg.sender, receiver, amount); + } else if (address(rootToken) == rootWETHToken) { + emit WETHDeposit(address(rootToken), childETHToken, msg.sender, receiver, amount); } else if (address(rootToken) == rootIMXToken) { emit IMXDeposit(address(rootToken), msg.sender, receiver, amount); } else { emit ERC20Deposit(address(rootToken), childToken, msg.sender, receiver, amount); } } - - function updateRootBridgeAdaptor(address newRootBridgeAdaptor) external onlyOwner { - if (newRootBridgeAdaptor == address(0)) { - revert ZeroAddress(); - } - rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor); - } } diff --git a/src/test/root/WETH.sol b/src/test/root/WETH.sol new file mode 100644 index 00000000..e073e12d --- /dev/null +++ b/src/test/root/WETH.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.21; + +import {IWETH} from "../../interfaces/root/IWETH.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @notice WETH is a wrapped ETH contract that allows users to wrap their native ETH. + * @dev This contract is adapted from the official Wrapped ETH contract. + */ +contract WETH is IWETH { + string public name = "Wrapped ETH"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + /** + * @notice Fallback function on recieving native ETH. + */ + receive() external payable { + deposit(); + } + + /** + * @notice Deposit native ETH in the function call and mint the equal amount of wrapped ETH to msg.sender. + */ + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + /** + * @notice Withdraw given amount of native ETH to msg.sender and burn the equal amount of wrapped ETH. + * @param wad The amount to withdraw. + */ + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad, "Wrapped ETH: Insufficient balance"); + balanceOf[msg.sender] -= wad; + + Address.sendValue(payable(msg.sender), wad); + emit Withdrawal(msg.sender, wad); + } + + /** + * @notice Obtain the current total supply of wrapped ETH. + * @return uint The amount of supplied wrapped ETH. + */ + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + /** + * @notice Approve given spender the ability to spend a given amount of msg.sender's tokens. + * @param guy Approved spender. + * @param wad Amount of allowance. + * @return bool Returns true if function call is successful. + */ + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + /** + * @notice Transfer given amount of tokens from msg.sender to given destination. + * @param dst Destination of this transfer. + * @param wad Amount of this transfer. + * @return bool Returns true if function call is successful. + */ + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + /** + * @notice Transfer given amount of tokens from given source to given destination. + * @param src Source of this transfer. + * @param dst Destination of this transfer. + * @param wad Amount of this transfer. + * @return bool Returns true if function call is successful. + */ + function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + require(balanceOf[src] >= wad, "Wrapped ETH: Insufficient balance"); + + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + require(allowance[src][msg.sender] >= wad, "Wrapped ETH: Insufficient allowance"); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} diff --git a/test/integration/root/RootERC20Bridge.t.sol b/test/integration/root/RootERC20Bridge.t.sol index bab1f5c9..374ad9ff 100644 --- a/test/integration/root/RootERC20Bridge.t.sol +++ b/test/integration/root/RootERC20Bridge.t.sol @@ -10,6 +10,7 @@ import {MockAxelarGasService} from "../../../src/test/root/MockAxelarGasService. import {RootERC20Bridge, IRootERC20BridgeEvents, IERC20Metadata} from "../../../src/root/RootERC20Bridge.sol"; import {RootAxelarBridgeAdaptor, IRootAxelarBridgeAdaptorEvents} from "../../../src/root/RootAxelarBridgeAdaptor.sol"; import {Utils} from "../../utils.t.sol"; +import {WETH} from "../../../src/test/root/WETH.sol"; contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAxelarBridgeAdaptorEvents, Utils { address constant CHILD_BRIDGE = address(3); @@ -18,6 +19,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); address constant IMX_TOKEN_ADDRESS = address(0xccc); address constant NATIVE_ETH = address(0xeee); + address constant WRAPPED_ETH = address(0xddd); uint256 constant mapTokenFee = 300; uint256 constant depositFee = 200; @@ -30,8 +32,10 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx MockAxelarGasService public axelarGasService; function setUp() public { + deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); + (imxToken, token, rootBridge, axelarAdaptor, mockAxelarGateway, axelarGasService) = - integrationSetup(CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR, CHILD_CHAIN_NAME, IMX_TOKEN_ADDRESS); + integrationSetup(CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR, CHILD_CHAIN_NAME, IMX_TOKEN_ADDRESS, WRAPPED_ETH); } /** @@ -103,7 +107,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); (, bytes memory predictedPayload) = - setupDeposit(ERC20PresetMinterPauser(NATIVE_ETH), rootBridge, mapTokenFee, depositFee, tokenAmount, false); + setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, tokenAmount, false); console2.logBytes(predictedPayload); @@ -160,9 +164,8 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx uint256 tokenAmount = 300; string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); - (, bytes memory predictedPayload) = setupDeposit( - ERC20PresetMinterPauser(IMX_TOKEN_ADDRESS), rootBridge, mapTokenFee, depositFee, tokenAmount, false - ); + (, bytes memory predictedPayload) = + setupDeposit(IMX_TOKEN_ADDRESS, rootBridge, mapTokenFee, depositFee, tokenAmount, false); vm.expectEmit(address(axelarAdaptor)); emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); @@ -214,12 +217,70 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); } + // TODO split into multiple tests + function test_depositWETH() public { + uint256 tokenAmount = 300; + string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); + (, bytes memory predictedPayload) = + setupDeposit(WRAPPED_ETH, rootBridge, mapTokenFee, depositFee, tokenAmount, false); + + vm.expectEmit(address(axelarAdaptor)); + emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + vm.expectEmit(address(rootBridge)); + emit WETHDeposit(address(WRAPPED_ETH), rootBridge.childETHToken(), address(this), address(this), tokenAmount); + vm.expectCall( + address(axelarAdaptor), + depositFee, + abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + vm.expectCall( + address(axelarGasService), + depositFee, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + CHILD_CHAIN_NAME, + childBridgeAdaptorString, + predictedPayload, + address(this) + ) + ); + + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload + ) + ); + + uint256 thisPreBal = IERC20Metadata(WRAPPED_ETH).balanceOf(address(this)); + uint256 bridgePreBal = address(rootBridge).balance; + + uint256 thisNativePreBal = address(this).balance; + uint256 gasServiceNativePreBal = address(axelarGasService).balance; + + rootBridge.deposit{value: depositFee}(IERC20Metadata(WRAPPED_ETH), tokenAmount); + + // Check that tokens are transferred + assertEq( + thisPreBal - tokenAmount, + IERC20Metadata(WRAPPED_ETH).balanceOf(address(this)), + "Tokens not transferred from user" + ); + assertEq(bridgePreBal + tokenAmount, address(rootBridge).balance, "ETH not transferred to Bridge"); + // Check that native asset transferred to gas service + assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH for fee not paid from user"); + assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); + } + // TODO split into multiple tests function test_depositToken() public { uint256 tokenAmount = 300; string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); (address childToken, bytes memory predictedPayload) = - setupDeposit(token, rootBridge, mapTokenFee, depositFee, tokenAmount, true); + setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, true); vm.expectEmit(address(axelarAdaptor)); emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); @@ -275,7 +336,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx address recipient = address(9876); string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); (address childToken, bytes memory predictedPayload) = - setupDepositTo(token, rootBridge, mapTokenFee, depositFee, tokenAmount, recipient, true); + setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, recipient, true); vm.expectEmit(address(axelarAdaptor)); emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 91e23384..901a8f0b 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -15,6 +15,7 @@ import {MockAxelarGateway} from "../../../src/test/root/MockAxelarGateway.sol"; import {MockAxelarGasService} from "../../../src/test/root/MockAxelarGasService.sol"; import {MockAdaptor} from "../../../src/test/root/MockAdaptor.sol"; import {Utils} from "../../utils.t.sol"; +import {WETH} from "../../../src/test/root/WETH.sol"; contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20BridgeErrors, Utils { address constant CHILD_BRIDGE = address(3); @@ -23,6 +24,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid string constant CHILD_CHAIN_NAME = "test"; address constant IMX_TOKEN = address(0xccc); address constant NATIVE_ETH = address(0xeee); + address constant WRAPPED_ETH = address(0xddd); uint256 constant mapTokenFee = 300; uint256 constant depositFee = 200; @@ -36,6 +38,8 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid token = new ERC20PresetMinterPauser("Test", "TST"); deployCodeTo("ERC20PresetMinterPauser.sol", abi.encode("ImmutableX", "IMX"), IMX_TOKEN); + deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); + rootBridge = new RootERC20Bridge(); mockAxelarGateway = new MockAxelarGateway(); axelarGasService = new MockAxelarGasService(); @@ -44,7 +48,12 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid // The specific ERC20 token template does not matter for these unit tests rootBridge.initialize( - address(mockAxelarAdaptor), CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR_STRING, address(token), IMX_TOKEN + address(mockAxelarAdaptor), + CHILD_BRIDGE, + CHILD_BRIDGE_ADAPTOR_STRING, + address(token), + IMX_TOKEN, + WRAPPED_ETH ); } @@ -61,50 +70,55 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIfInitializeTwice() public { vm.expectRevert("Initializable: contract is already initialized"); rootBridge.initialize( - address(mockAxelarAdaptor), CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR_STRING, address(token), IMX_TOKEN + address(mockAxelarAdaptor), + CHILD_BRIDGE, + CHILD_BRIDGE_ADAPTOR_STRING, + address(token), + IMX_TOKEN, + WRAPPED_ETH ); } function test_RevertIf_InitializeWithAZeroAddressRootAdapter() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(0), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1)); + bridge.initialize(address(0), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(1)); } function test_RevertIf_InitializeWithAZeroAddressChildBridge() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(0), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1)); + bridge.initialize(address(1), address(0), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(1)); } function test_RevertIf_InitializeWithEmptyChildAdapter() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(InvalidChildERC20BridgeAdaptor.selector); - bridge.initialize(address(1), address(1), "", address(1), address(1)); + bridge.initialize(address(1), address(1), "", address(1), address(1), address(1)); } function test_RevertIf_InitializeWithAZeroAddressTokenTemplate() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(0), address(1)); + bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(0), address(1), address(1)); } function test_RevertIf_InitializeWithAZeroAddressIMXToken() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(0), address(1)); + bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(0), address(1)); } - function test_RevertIf_InitializeWithAZeroAddressETHToken() public { + function test_RevertIf_InitializeWithAZeroAddressWETHToken() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(0)); + bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(0)); } function test_RevertIf_InitializeWithAZeroAddressAll() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(0), address(0), "", address(0), address(0)); + bridge.initialize(address(0), address(0), "", address(0), address(0), address(0)); } /** @@ -209,8 +223,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_depositETHCallsSendMessage() public { uint256 amount = 1000; - (, bytes memory predictedPayload) = - setupDeposit(ERC20PresetMinterPauser(NATIVE_ETH), rootBridge, mapTokenFee, depositFee, amount, false); + (, bytes memory predictedPayload) = setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, false); vm.expectCall( address(mockAxelarAdaptor), @@ -223,7 +236,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_depositETHEmitsNativeEthDepositEvent() public { uint256 amount = 1000; - setupDeposit(ERC20PresetMinterPauser(NATIVE_ETH), rootBridge, mapTokenFee, depositFee, amount, false); + setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, false); vm.expectEmit(); emit NativeEthDeposit(NATIVE_ETH, rootBridge.childETHToken(), address(this), address(this), amount); @@ -232,7 +245,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIf_depositETHInsufficientValue() public { uint256 amount = 1000; - setupDeposit(ERC20PresetMinterPauser(NATIVE_ETH), rootBridge, mapTokenFee, depositFee, amount, false); + setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, false); vm.expectRevert(InsufficientValue.selector); rootBridge.depositETH{value: (amount / 2) + depositFee}(amount); @@ -245,9 +258,8 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_depositToETHCallsSendMessage() public { uint256 amount = 1000; address receiver = address(12345); - (, bytes memory predictedPayload) = setupDepositTo( - ERC20PresetMinterPauser(NATIVE_ETH), rootBridge, mapTokenFee, depositFee, amount, receiver, false - ); + (, bytes memory predictedPayload) = + setupDepositTo(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, receiver, false); vm.expectCall( address(mockAxelarAdaptor), depositFee, @@ -260,9 +272,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_depositToETHEmitsNativeEthDepositEvent() public { uint256 amount = 1000; address receiver = address(12345); - setupDepositTo( - ERC20PresetMinterPauser(NATIVE_ETH), rootBridge, mapTokenFee, depositFee, amount, receiver, false - ); + setupDepositTo(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, receiver, false); vm.expectEmit(); emit NativeEthDeposit(NATIVE_ETH, rootBridge.childETHToken(), address(this), receiver, amount); @@ -272,9 +282,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIf_depositToETHInsufficientValue() public { uint256 amount = 1000; address receiver = address(12345); - setupDepositTo( - ERC20PresetMinterPauser(NATIVE_ETH), rootBridge, mapTokenFee, depositFee, amount, receiver, false - ); + setupDepositTo(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, receiver, false); vm.expectRevert(InsufficientValue.selector); rootBridge.depositToETH{value: (amount / 2) + depositFee}(receiver, amount); @@ -286,7 +294,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIf_depositETHAmountIsZero() public { uint256 amount = 0; - setupDeposit(ERC20PresetMinterPauser(NATIVE_ETH), rootBridge, mapTokenFee, depositFee, amount, false); + setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, false); vm.expectRevert(ZeroAmount.selector); rootBridge.depositETH{value: amount + depositFee}(amount); @@ -296,7 +304,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid uint256 amount = 0; address receiver = address(12345); - setupDeposit(ERC20PresetMinterPauser(NATIVE_ETH), rootBridge, mapTokenFee, depositFee, amount, false); + setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, false); vm.expectRevert(ZeroAmount.selector); rootBridge.depositToETH{value: amount + depositFee}(receiver, amount); @@ -304,7 +312,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIf_depositAmountIsZero() public { uint256 amount = 0; - setupDeposit(ERC20PresetMinterPauser(NATIVE_ETH), rootBridge, mapTokenFee, depositFee, amount, false); + setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, false); vm.expectRevert(ZeroAmount.selector); rootBridge.deposit{value: depositFee}(token, amount); @@ -313,19 +321,95 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIf_depositToAmountIsZero() public { uint256 amount = 0; address receiver = address(12345); - setupDeposit(ERC20PresetMinterPauser(NATIVE_ETH), rootBridge, mapTokenFee, depositFee, amount, false); + setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, false); vm.expectRevert(ZeroAmount.selector); rootBridge.depositTo{value: depositFee}(token, receiver, amount); } + /** + * DEPOSIT WETH + */ + + function test_depositWETHCallsSendMessage() public { + uint256 amount = 100; + (, bytes memory predictedPayload) = + setupDeposit(WRAPPED_ETH, rootBridge, mapTokenFee, depositFee, amount, false); + + vm.expectCall( + address(mockAxelarAdaptor), + abi.encodeWithSelector(mockAxelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + rootBridge.deposit{value: depositFee}(IERC20Metadata(WRAPPED_ETH), amount); + } + + function test_depositWETHEmitsNativeDepositEvent() public { + uint256 amount = 100; + setupDeposit(WRAPPED_ETH, rootBridge, mapTokenFee, depositFee, amount, false); + + vm.expectEmit(); + emit WETHDeposit(WRAPPED_ETH, rootBridge.childETHToken(), address(this), address(this), amount); + rootBridge.deposit{value: depositFee}(IERC20Metadata(WRAPPED_ETH), amount); + } + + function test_depositToWETHEmitsWETHDepositEvent() public { + uint256 amount = 1000; + address receiver = address(12345); + setupDepositTo(WRAPPED_ETH, rootBridge, mapTokenFee, depositFee, amount, receiver, false); + + vm.expectEmit(); + emit WETHDeposit(WRAPPED_ETH, rootBridge.childETHToken(), address(this), receiver, amount); + rootBridge.depositTo{value: depositFee}(IERC20Metadata(WRAPPED_ETH), receiver, amount); + } + + function test_depositWETHTransfersTokens() public { + uint256 amount = 100; + + setupDeposit(WRAPPED_ETH, rootBridge, mapTokenFee, depositFee, amount, false); + + uint256 thisPreBal = IERC20Metadata(WRAPPED_ETH).balanceOf(address(this)); + uint256 bridgePreBal = address(rootBridge).balance; + + rootBridge.deposit{value: depositFee}(IERC20Metadata(WRAPPED_ETH), amount); + + // Check that tokens are transferred + assertEq( + thisPreBal - amount, + IERC20Metadata(WRAPPED_ETH).balanceOf(address(this)), + "Tokens not transferred from user" + ); + assertEq(bridgePreBal + amount, address(rootBridge).balance, "ETH not transferred to Bridge"); + } + + function test_depositToWETHTransfersTokens() public { + uint256 amount = 100; + address receiver = address(12345); + + setupDepositTo(WRAPPED_ETH, rootBridge, mapTokenFee, depositFee, amount, receiver, false); + + uint256 thisPreBal = IERC20Metadata(WRAPPED_ETH).balanceOf(address(this)); + uint256 bridgePreBal = address(rootBridge).balance; + + rootBridge.depositTo{value: depositFee}(IERC20Metadata(WRAPPED_ETH), receiver, amount); + + // Check that tokens are transferred + assertEq( + thisPreBal - amount, + IERC20Metadata(WRAPPED_ETH).balanceOf(address(this)), + "Tokens not transferred from user" + ); + assertEq(bridgePreBal + amount, address(rootBridge).balance, "ETH not transferred to Bridge"); + } + /** * DEPOSIT TOKEN */ function test_depositCallsSendMessage() public { uint256 amount = 100; - (, bytes memory predictedPayload) = setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); + (, bytes memory predictedPayload) = + setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, amount, true); vm.expectCall( address(mockAxelarAdaptor), @@ -338,7 +422,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_depositEmitsERC20DepositEvent() public { uint256 amount = 100; - (address childToken,) = setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); + (address childToken,) = setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, amount, true); vm.expectEmit(); emit ERC20Deposit(address(token), childToken, address(this), address(this), amount); @@ -348,7 +432,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_depositIMXEmitsIMXDepositEvent() public { uint256 amount = 100; - setupDeposit(ERC20PresetMinterPauser(IMX_TOKEN), rootBridge, mapTokenFee, depositFee, amount, false); + setupDeposit(IMX_TOKEN, rootBridge, mapTokenFee, depositFee, amount, false); vm.expectEmit(); emit IMXDeposit(IMX_TOKEN, address(this), address(this), amount); @@ -358,7 +442,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_depositTransfersTokens() public { uint256 amount = 100; - setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); + setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, amount, true); uint256 thisPreBal = token.balanceOf(address(this)); uint256 bridgePreBal = token.balanceOf(address(rootBridge)); @@ -372,7 +456,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_depositTransfersNativeAsset() public { uint256 amount = 100; - setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); + setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, amount, true); uint256 thisNativePreBal = address(this).balance; uint256 adaptorNativePreBal = address(mockAxelarAdaptor).balance; @@ -387,7 +471,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIf_depositCalledWithZeroAddress() public { uint256 amount = 100; - setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); + setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, amount, true); // Will fail when it tries to call balanceOf vm.expectRevert(); @@ -396,7 +480,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIf_depositCalledWithUnmappedToken() public { uint256 amount = 100; - setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); + setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, amount, true); ERC20PresetMinterPauser newToken = new ERC20PresetMinterPauser("Test", "TST"); @@ -407,7 +491,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid // We want to ensure that messages don't get sent when they are not supposed to function test_RevertIf_depositCalledWhenTokenApprovalNotProvided() public { uint256 amount = 100; - setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); + setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, amount, true); vm.expectRevert(); rootBridge.deposit{value: depositFee}(token, amount * 2); @@ -422,7 +506,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address receiver = address(12345); (, bytes memory predictedPayload) = - setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); + setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, amount, receiver, true); vm.expectCall( address(mockAxelarAdaptor), @@ -437,7 +521,8 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid uint256 amount = 100; address receiver = address(12345); - (address childToken,) = setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); + (address childToken,) = + setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, amount, receiver, true); vm.expectEmit(); emit ERC20Deposit(address(token), childToken, address(this), receiver, amount); @@ -449,7 +534,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address receiver = address(12345); - setupDepositTo(ERC20PresetMinterPauser(IMX_TOKEN), rootBridge, mapTokenFee, depositFee, amount, receiver, false); + setupDepositTo(IMX_TOKEN, rootBridge, mapTokenFee, depositFee, amount, receiver, false); vm.expectEmit(); emit IMXDeposit(IMX_TOKEN, address(this), receiver, amount); @@ -460,7 +545,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid uint256 amount = 100; address receiver = address(12345); - setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); + setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, amount, receiver, true); uint256 thisPreBal = token.balanceOf(address(this)); uint256 bridgePreBal = token.balanceOf(address(rootBridge)); @@ -476,7 +561,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid uint256 amount = 100; address receiver = address(12345); - setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); + setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, amount, receiver, true); uint256 thisNativePreBal = address(this).balance; uint256 adaptorNativePreBal = address(mockAxelarAdaptor).balance; @@ -493,7 +578,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIf_depositToCalledWhenTokenApprovalNotProvided() public { uint256 amount = 100; address receiver = address(12345); - setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); + setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, amount, receiver, true); vm.expectRevert(); rootBridge.depositTo{value: depositFee}(token, receiver, amount * 2); @@ -503,7 +588,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid uint256 amount = 100; address receiver = address(12345); - setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); + setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, amount, receiver, true); // Will fail when it tries to call balanceOf vm.expectRevert(); @@ -514,7 +599,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid uint256 amount = 100; address receiver = address(12345); - setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); + setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, amount, receiver, true); ERC20PresetMinterPauser newToken = new ERC20PresetMinterPauser("Test", "TST"); diff --git a/test/utils.t.sol b/test/utils.t.sol index a0a1f2e3..9a53896a 100644 --- a/test/utils.t.sol +++ b/test/utils.t.sol @@ -8,6 +8,8 @@ import {MockAxelarGateway} from "../src/test/root/MockAxelarGateway.sol"; import {MockAxelarGasService} from "../src/test/root/MockAxelarGasService.sol"; import {RootERC20Bridge, IERC20Metadata} from "../src/root/RootERC20Bridge.sol"; import {ChildERC20Bridge} from "../src/child/ChildERC20Bridge.sol"; +import {WETH} from "../src/test/root/WETH.sol"; +import {IWETH} from "../src/interfaces/root/IWETH.sol"; import {IChildERC20, ChildERC20} from "../src/child/ChildERC20.sol"; import {RootAxelarBridgeAdaptor} from "../src/root/RootAxelarBridgeAdaptor.sol"; @@ -17,7 +19,8 @@ contract Utils is Test { address childBridge, address childBridgeAdaptor, string memory childBridgeName, - address imxTokenAddress + address imxTokenAddress, + address wethTokenAddress ) public returns ( @@ -36,6 +39,10 @@ contract Utils is Test { imxToken = ERC20PresetMinterPauser(imxTokenAddress); imxToken.mint(address(this), 1000000 ether); + // deployCodeTo("WETH9.sol", abi.encode("Wrapped ETH", "WETH"), wethTokenAddress); + // imxToken = ERC20PresetMinterPauser(imxTokenAddress); + // imxToken.mint(address(this), 1000000 ether); + rootBridge = new RootERC20Bridge(); mockAxelarGateway = new MockAxelarGateway(); axelarGasService = new MockAxelarGasService(); @@ -52,13 +59,14 @@ contract Utils is Test { childBridge, Strings.toHexString(childBridgeAdaptor), address(token), - imxTokenAddress + imxTokenAddress, + wethTokenAddress ); axelarAdaptor.setChildBridgeAdaptor(); } function setupDeposit( - ERC20PresetMinterPauser token, + address token, RootERC20Bridge rootBridge, uint256 mapTokenFee, uint256 depositFee, @@ -69,7 +77,7 @@ contract Utils is Test { } function setupDepositTo( - ERC20PresetMinterPauser token, + address token, RootERC20Bridge rootBridge, uint256 mapTokenFee, uint256 depositFee, @@ -81,7 +89,7 @@ contract Utils is Test { } function _setupDeposit( - ERC20PresetMinterPauser token, + address token, RootERC20Bridge rootBridge, uint256 mapTokenFee, uint256 depositFee, @@ -89,16 +97,23 @@ contract Utils is Test { address to, bool saveTokenMapping ) public returns (address childToken, bytes memory predictedPayload) { - predictedPayload = abi.encode(rootBridge.DEPOSIT_SIG(), address(token), address(this), to, tokenAmount); + predictedPayload = abi.encode(rootBridge.DEPOSIT_SIG(), token, address(this), to, tokenAmount); + if (saveTokenMapping) { - childToken = rootBridge.mapToken{value: mapTokenFee}(token); + childToken = rootBridge.mapToken{value: mapTokenFee}(ERC20PresetMinterPauser(token)); } - if (address(token) == address(0xeee)) { + if (token == address(0xeee)) { + vm.deal(to, tokenAmount + depositFee); + } else if (address(token) == address(0xddd)) { + // set the payload to expect native eth when depositing wrapped eth + predictedPayload = abi.encode(rootBridge.DEPOSIT_SIG(), address(0xeee), address(this), to, tokenAmount); vm.deal(to, tokenAmount + depositFee); + IWETH(token).deposit{value: tokenAmount}(); + IWETH(token).approve(address(rootBridge), tokenAmount); } else { - token.mint(address(this), tokenAmount); - token.approve(address(rootBridge), tokenAmount); + ERC20PresetMinterPauser(token).mint(address(this), tokenAmount); + ERC20PresetMinterPauser(token).approve(address(rootBridge), tokenAmount); } return (childToken, predictedPayload);