diff --git a/.env.sample b/.env.sample index 957e611b..f1854b16 100644 --- a/.env.sample +++ b/.env.sample @@ -10,4 +10,5 @@ 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= +CHILD_ETH_ADDRESS= \ No newline at end of file diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 00000000..9be10507 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,13 @@ +name: Slither Analysis + +on: [push] + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: crytic/slither-action@v0.3.0 + with: + fail-on: high + slither-args: --filter-paths "./lib|./test" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09880b1d..921bdba7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ -name: test +name: Build and Test -on: workflow_dispatch +on: [push] env: FOUNDRY_PROFILE: ci diff --git a/.gitignore b/.gitignore index bafbad61..bd8726e5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,9 @@ out/ # Ignores development broadcast logs !/broadcast /broadcast/*/31337/ +/broadcast/*/2501/ /broadcast/*/31338/ +/broadcast/*/2500/ /broadcast/**/dry-run/ # Docs diff --git a/README.md b/README.md index 13464423..7e175266 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ CHILD_GAS_SERVICE_ADDRESS= ROOT_CHAIN_NAME="ROOT" CHILD_CHAIN_NAME="CHILD" ROOT_IMX_ADDRESS= +CHILD_ETH_ADDRESS= ``` where `{ROOT,CHILD}_{GATEWAY,GAS_SERVICE}_ADDRESS` refers to the gateway and gas service addresses used by Axelar. diff --git a/script/DeployChildContracts.s.sol b/script/DeployChildContracts.s.sol index 1b57d44f..0ce202f9 100644 --- a/script/DeployChildContracts.s.sol +++ b/script/DeployChildContracts.s.sol @@ -6,6 +6,7 @@ import {Script, console2} from "forge-std/Script.sol"; import {ChildERC20Bridge} from "../src/child/ChildERC20Bridge.sol"; import {ChildAxelarBridgeAdaptor} from "../src/child/ChildAxelarBridgeAdaptor.sol"; import {ChildERC20} from "../src/child/ChildERC20.sol"; +import {WIMX} from "../src/child/WIMX.sol"; // TODO update private key usage to be more secure: https://book.getfoundry.sh/reference/forge/forge-script#wallet-options---raw @@ -30,11 +31,14 @@ contract DeployChildContracts is Script { address(childBridge) // child bridge ); + WIMX wrappedIMX = new WIMX(); + vm.stopBroadcast(); console2.log("====ADDRESSES===="); console2.log("Child ERC20 Bridge: %s", address(childBridge)); console2.log("Child Axelar Bridge Adaptor: %s", address(childBridgeAdaptor)); console2.log("childTokenTemplate: %s", address(childTokenTemplate)); + console2.log("Wrapped IMX: %s", address(wrappedIMX)); } } diff --git a/script/InitializeRootContracts.s.sol b/script/InitializeRootContracts.s.sol index 1ca146a9..79455394 100644 --- a/script/InitializeRootContracts.s.sol +++ b/script/InitializeRootContracts.s.sol @@ -27,6 +27,7 @@ contract InitializeRootContracts is Script { string[] memory checksumInputs = Utils.getChecksumInputs(childBridgeAdaptor); bytes memory checksumOutput = vm.ffi(checksumInputs); string memory childBridgeAdaptorChecksum = string(Utils.removeZeroByteValues(checksumOutput)); + address childETHToken = vm.envAddress("CHILD_ETH_ADDRESS"); /** * INITIALIZE ROOT CHAIN CONTRACTS */ @@ -34,7 +35,12 @@ contract InitializeRootContracts is Script { vm.startBroadcast(rootPrivateKey); rootERC20Bridge.initialize( - address(rootBridgeAdaptor), childERC20Bridge, childBridgeAdaptorChecksum, rootChainChildTokenTemplate, rootIMXToken + address(rootBridgeAdaptor), + childERC20Bridge, + childBridgeAdaptorChecksum, + rootChainChildTokenTemplate, + rootIMXToken, + childETHToken ); rootBridgeAdaptor.setChildBridgeAdaptor(); diff --git a/src/child/WIMX.sol b/src/child/WIMX.sol new file mode 100644 index 00000000..1e778eaa --- /dev/null +++ b/src/child/WIMX.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.21; + +import {IWIMX} from "../interfaces/child/IWIMX.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @notice WIMX is a wrapped IMX contract that allows users to wrap their native IMX. + * @dev This contract is adapted from the official Wrapped ETH contract. + */ +contract WIMX is IWIMX { + string public name = "Wrapped IMX"; + string public symbol = "WIMX"; + uint8 public decimals = 18; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + /** + * @notice Fallback function on recieving native IMX. + */ + receive() external payable { + deposit(); + } + + /** + * @notice Deposit native IMX in the function call and mint the equal amount of wrapped IMX to msg.sender. + */ + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + /** + * @notice Withdraw given amount of native IMX to msg.sender and burn the equal amount of wrapped IMX. + * @param wad The amount to withdraw. + */ + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad, "Wrapped IMX: 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 IMX. + * @return uint The amount of supplied wrapped IMX. + */ + 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 IMX: Insufficient balance"); + + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + require(allowance[src][msg.sender] >= wad, "Wrapped IMX: Insufficient allowance"); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} diff --git a/src/interfaces/child/IWIMX.sol b/src/interfaces/child/IWIMX.sol new file mode 100644 index 00000000..c6c427c6 --- /dev/null +++ b/src/interfaces/child/IWIMX.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 IMX. + */ +interface IWIMX is IERC20 { + /** + * @dev Emitted when `value` native IMX are deposited from `account`. + */ + event Deposit(address indexed account, uint256 value); + + /** + * @dev Emitted when `value` wIMX tokens are withdrawn to `account`. + */ + event Withdrawal(address indexed account, uint256 value); + + /** + * @notice Deposit native IMX in the function call and mint the equal amount of wrapped IMX to msg.sender. + */ + function deposit() external payable; + + /** + * @notice Withdraw given amount of native IMX to msg.sender and burn the equal amount of wrapped IMX. + * @param value The amount to withdraw. + */ + function withdraw(uint256 value) external; +} diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index 5e26151a..4181cc25 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -56,6 +56,10 @@ interface IRootERC20BridgeEvents { } interface IRootERC20BridgeErrors { + /// @notice Error when the amount requested is less than the value sent. + error InsufficientValue(); + /// @notice Error when there is no gas payment received. + error ZeroAmount(); /// @notice Error when a zero address is given when not valid. error ZeroAddress(); /// @notice Error when a token is already mapped. diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 606c02bd..fd099021 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -42,8 +42,10 @@ contract RootERC20Bridge is /// @dev The address of the token template that will be cloned to create tokens on the child chain. address public childTokenTemplate; mapping(address => address) public rootTokenToChildToken; - + /// @dev The address of the IMX ERC20 token on L1. address public rootIMXToken; + /// @dev The address of the ETH ERC20 token on L2. + address public childETHToken; /** * @notice Initilization function for RootERC20Bridge. @@ -51,21 +53,26 @@ contract RootERC20Bridge is * @param newChildERC20Bridge Address of child ERC20 bridge to communicate with. * @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 ECR20 IMX on the root chain. + * @param newRootIMXToken Address of ERC20 IMX on the root chain. + * @param newChildETHToken Address of ERC20 ETH on the child chain. * @dev Can only be called once. */ function initialize( address newRootBridgeAdaptor, address newChildERC20Bridge, string memory newChildBridgeAdaptor, - address newChildTokenTemplate, - address newRootIMXToken - ) public initializer { - if ( - newRootBridgeAdaptor == address(0) || newChildERC20Bridge == address(0) - || newChildTokenTemplate == address(0) - || newRootIMXToken == address(0) - ) { + address newChildTokenTemplate, + address newRootIMXToken, + address newChildETHToken) + public + initializer + { + if (newRootBridgeAdaptor == address(0) + || newChildERC20Bridge == address(0) + || newChildTokenTemplate == address(0) + || newRootIMXToken == address(0) + || newChildETHToken == address(0)) + { revert ZeroAddress(); } if (bytes(newChildBridgeAdaptor).length == 0) { @@ -74,6 +81,7 @@ contract RootERC20Bridge is childERC20Bridge = newChildERC20Bridge; childTokenTemplate = newChildTokenTemplate; rootIMXToken = newRootIMXToken; + childETHToken = newChildETHToken; rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor); childBridgeAdaptor = newChildBridgeAdaptor; } @@ -89,6 +97,29 @@ contract RootERC20Bridge is return _mapToken(rootToken); } + 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); + } + + function _depositETH(address receiver, uint256 amount) private { + if (msg.value < amount) { + revert InsufficientValue(); + } + + uint256 expectedBalance = address(this).balance - (msg.value - amount); + + _deposit(IERC20Metadata(NATIVE_TOKEN), receiver, 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); + } + } + /** * @inheritdoc IRootERC20Bridge */ @@ -145,33 +176,40 @@ contract RootERC20Bridge is revert ZeroAddress(); } + if (amount == 0) { + revert ZeroAmount(); + } + address childToken; + uint256 feeAmount; // 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. - // TODO NATIVE TOKEN BRIDGING NOT YET SUPPORTED - if (address(rootToken) != NATIVE_TOKEN) { + if (address(rootToken) != NATIVE_TOKEN) { 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. - rootBridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender); + + rootBridgeAdaptor.sendMessage{value: feeAmount}(payload, msg.sender); if (address(rootToken) == NATIVE_TOKEN) { - // not used yet - emit NativeDeposit(address(rootToken), childToken, msg.sender, receiver, amount); + emit NativeDeposit(address(rootToken), childETHToken, msg.sender, receiver, amount); } else if (address(rootToken) == rootIMXToken) { emit IMXDeposit(address(rootToken), msg.sender, receiver, amount); } else { diff --git a/test/integration/root/RootERC20Bridge.t.sol b/test/integration/root/RootERC20Bridge.t.sol index 7dadb1c8..78254172 100644 --- a/test/integration/root/RootERC20Bridge.t.sol +++ b/test/integration/root/RootERC20Bridge.t.sol @@ -16,7 +16,10 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx address constant CHILD_BRIDGE_ADAPTOR = address(4); string constant CHILD_CHAIN_NAME = "test"; bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); - address constant IMX_TOKEN_ADDRESS = address(9); + address constant IMX_TOKEN_ADDRESS = address(0xccc); + address constant CHILD_ETH_TOKEN = address(0xddd); + uint256 constant mapTokenFee = 300; + uint256 constant depositFee = 200; ERC20PresetMinterPauser public token; RootERC20Bridge public rootBridge; @@ -26,7 +29,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx function setUp() public { (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, CHILD_ETH_TOKEN); } /** @@ -36,7 +39,6 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx */ function test_mapToken() public { // TODO split this up into multiple tests. - uint256 mapTokenFee = 300; address childToken = Clones.predictDeterministicAddress(address(token), keccak256(abi.encodePacked(token)), CHILD_BRIDGE); @@ -96,10 +98,10 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx // TODO split into multiple tests function test_depositToken() public { uint256 tokenAmount = 300; - uint256 gasPrice = 100; string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); - (address childToken, bytes memory predictedPayload) = - setupDeposit(token, rootBridge, gasPrice, tokenAmount, true); + // (address childToken, bytes memory predictedPayload) = + // setupDeposit(token, rootBridge, gasPrice, tokenAmount, true); + (address childToken, bytes memory predictedPayload) = setupDeposit(token, rootBridge, mapTokenFee, depositFee, tokenAmount, true); vm.expectEmit(address(axelarAdaptor)); emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); @@ -108,13 +110,13 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx vm.expectCall( address(axelarAdaptor), - gasPrice, + depositFee, abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) ); vm.expectCall( address(axelarGasService), - gasPrice, + depositFee, abi.encodeWithSelector( axelarGasService.payNativeGasForContractCall.selector, address(axelarAdaptor), @@ -139,24 +141,23 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx uint256 thisNativePreBal = address(this).balance; uint256 gasServiceNativePreBal = address(axelarGasService).balance; - rootBridge.deposit{value: gasPrice}(token, tokenAmount); + rootBridge.deposit{value: depositFee}(token, tokenAmount); // Check that tokens are transferred assertEq(thisPreBal - tokenAmount, token.balanceOf(address(this)), "Tokens not transferred from user"); assertEq(bridgePreBal + tokenAmount, token.balanceOf(address(rootBridge)), "Tokens not transferred to bridge"); // Check that native asset transferred to gas service - assertEq(thisNativePreBal - gasPrice, address(this).balance, "ETH not paid from user"); - assertEq(gasServiceNativePreBal + gasPrice, address(axelarGasService).balance, "ETH not paid to adaptor"); + assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH not paid from user"); + assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); } // TODO split into multiple tests function test_depositTo() public { uint256 tokenAmount = 300; - uint256 gasPrice = 100; address recipient = address(9876); string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); (address childToken, bytes memory predictedPayload) = - setupDepositTo(token, rootBridge, gasPrice, tokenAmount, recipient, true); + setupDepositTo(token, rootBridge, mapTokenFee, depositFee, tokenAmount, recipient, true); vm.expectEmit(address(axelarAdaptor)); emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); @@ -165,13 +166,13 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx vm.expectCall( address(axelarAdaptor), - gasPrice, + depositFee, abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) ); vm.expectCall( address(axelarGasService), - gasPrice, + depositFee, abi.encodeWithSelector( axelarGasService.payNativeGasForContractCall.selector, address(axelarAdaptor), @@ -196,13 +197,13 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx uint256 thisNativePreBal = address(this).balance; uint256 gasServiceNativePreBal = address(axelarGasService).balance; - rootBridge.depositTo{value: gasPrice}(token, recipient, tokenAmount); + rootBridge.depositTo{value: depositFee}(token, recipient, tokenAmount); // Check that tokens are transferred assertEq(thisPreBal - tokenAmount, token.balanceOf(address(this)), "Tokens not transferred from user"); assertEq(bridgePreBal + tokenAmount, token.balanceOf(address(rootBridge)), "Tokens not transferred to bridge"); // Check that native asset transferred to gas service - assertEq(thisNativePreBal - gasPrice, address(this).balance, "ETH not paid from user"); - assertEq(gasServiceNativePreBal + gasPrice, address(axelarGasService).balance, "ETH not paid to adaptor"); + assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH not paid from user"); + assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); } } diff --git a/test/unit/child/WIMX.t.sol b/test/unit/child/WIMX.t.sol new file mode 100644 index 00000000..570740f7 --- /dev/null +++ b/test/unit/child/WIMX.t.sol @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.21; + +import {Test} from "forge-std/Test.sol"; +import {WIMX} from "../../../src/child/WIMX.sol"; + +contract WIMXTest is Test { + string constant DEFAULT_WIMX_NAME = "Wrapped IMX"; + string constant DEFAULT_WIMX_SYMBOL = "WIMX"; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + WIMX public wIMX; + + function setUp() public { + wIMX = new WIMX(); + } + + function test_InitialState() public { + assertEq(wIMX.name(), DEFAULT_WIMX_NAME, "Incorrect token name"); + assertEq(wIMX.symbol(), DEFAULT_WIMX_SYMBOL, "Incorrect token symbol"); + assertEq(wIMX.totalSupply(), 0, "Incorrect token supply"); + } + + function test_RevertIf_DepositWithInsufficientBalance() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + uint256 imxAmt = 1 ether; + vm.deal(user, imxAmt); + + // Before deposit, user should have 0 wIMX + assertEq(user.balance, imxAmt, "User should have 1 IMX"); + assertEq(wIMX.balanceOf(user), 0, "User should have 0 wIMX"); + + vm.prank(user); + // Deposit should revert because user has only 1 IMX + vm.expectRevert(); + wIMX.deposit{value: imxAmt + 1}(); + + // After deposit, user should have 0 wIMX + assertEq(user.balance, imxAmt, "User should have 1 IMX"); + assertEq(wIMX.balanceOf(user), 0, "User should have 0 wIMX"); + } + + function test_RevertIf_TransferWithInsufficientBalance() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + uint256 imxAmt = 1 ether; + vm.deal(user, imxAmt); + + // Before deposit, user should have 0 wIMX + assertEq(user.balance, imxAmt, "User should have 1 IMX"); + assertEq(wIMX.balanceOf(user), 0, "User should have 0 wIMX"); + + vm.prank(user); + // Deposit should revert because user has only 1 IMX + vm.expectRevert(); + (bool success,) = address(wIMX).call{value: imxAmt + 1}(""); + require(success); + + // After deposit, user should have 0 wIMX + assertEq(user.balance, imxAmt, "User should have 1 IMX"); + assertEq(wIMX.balanceOf(user), 0, "User should have 0 wIMX"); + } + + function test_SucceedIf_DepositWithSufficientBalance() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + uint256 imxAmt = 1 ether; + vm.deal(user, imxAmt); + + // Before deposit, user should have 0 wIMX + assertEq(user.balance, imxAmt, "User should have 1 IMX"); + assertEq(wIMX.balanceOf(user), 0, "User should have 0 wIMX"); + + uint256 depositAmt = 0.1 ether; + vm.prank(user); + vm.expectEmit(address(wIMX)); + emit Deposit(user, depositAmt); + wIMX.deposit{value: depositAmt}(); + + // After deposit, user should have 0.1 wIMX + assertEq(user.balance, imxAmt - depositAmt, "User should have 0.9 IMX"); + assertEq(wIMX.balanceOf(user), depositAmt, "User should have 0.1 wIMX"); + } + + function test_SucceedIf_TransferWithSufficientBalance() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + uint256 imxAmt = 1 ether; + vm.deal(user, imxAmt); + + // Before deposit, user should have 0 wIMX + assertEq(user.balance, imxAmt, "User should have 1 IMX"); + assertEq(wIMX.balanceOf(user), 0, "User should have 0 wIMX"); + + uint256 depositAmt = 0.1 ether; + vm.prank(user); + vm.expectEmit(address(wIMX)); + emit Deposit(user, depositAmt); + (bool success,) = address(wIMX).call{value: depositAmt}(""); + require(success); + + // After deposit, user should have 0.1 wIMX + assertEq(user.balance, imxAmt - depositAmt, "User should have 0.9 wIMX"); + assertEq(wIMX.balanceOf(user), depositAmt, "User should have 0.1 IMX"); + } + + function test_RevertIf_OverWithdraw() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + uint256 imxAmt = 1 ether; + vm.deal(user, imxAmt); + + // Deposit 0.1 IMX + uint256 depositAmt = 0.1 ether; + vm.startPrank(user); + wIMX.deposit{value: depositAmt}(); + + // Try withdraw 0.2 wIMX + uint256 withdrawlAmt = 0.2 ether; + vm.expectRevert(); + wIMX.withdraw(withdrawlAmt); + vm.stopPrank(); + + // User should still have 0.1 wIMX and 0.9 IMX + assertEq(user.balance, imxAmt - depositAmt, "User should have 0.9 IMX"); + assertEq(wIMX.balanceOf(user), depositAmt, "User should have 0.1 wIMX"); + } + + function test_SucceedIf_WithdrawWithinLimit() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + uint256 imxAmt = 1 ether; + vm.deal(user, imxAmt); + + // Deposit 0.1 IMX + uint256 depositAmt = 0.1 ether; + vm.startPrank(user); + wIMX.deposit{value: depositAmt}(); + + // Try withdraw 0.05 wIMX + uint256 withdrawlAmt = 0.05 ether; + vm.expectEmit(address(wIMX)); + emit Withdrawal(user, withdrawlAmt); + wIMX.withdraw(withdrawlAmt); + vm.stopPrank(); + + // User should have 0.05 wIMX and 0.95 IMX + assertEq(user.balance, imxAmt - depositAmt + withdrawlAmt, "User should have 0.95 IMX"); + assertEq(wIMX.balanceOf(user), depositAmt - withdrawlAmt, "User should have 0.05 wIMX"); + } + + function test_SupplyUpdated_OnDepositAndWithdraw() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + uint256 imxAmt = 1 ether; + vm.deal(user, imxAmt); + + assertEq(wIMX.totalSupply(), 0, "Token supply should be 0 at start"); + + // Deposit 0.1 IMX + uint256 depositAmt1 = 0.1 ether; + vm.startPrank(user); + wIMX.deposit{value: depositAmt1}(); + assertEq(wIMX.totalSupply(), depositAmt1, "Token supply should be 0.1 after deposit"); + + // Withdraw 0.05 IMX + uint256 withdrawlAmt1 = 0.05 ether; + wIMX.withdraw(withdrawlAmt1); + assertEq(wIMX.totalSupply(), depositAmt1 - withdrawlAmt1, "Token supply should be 0.05 after withdraw"); + + vm.stopPrank(); + + // Create another user and fund it with 1 IMX + address user2 = address(1235); + vm.deal(user2, imxAmt); + + // Deposit 0.5 IMX + uint256 depositAmt2 = 0.5 ether; + vm.startPrank(user2); + wIMX.deposit{value: depositAmt2}(); + assertEq( + wIMX.totalSupply(), + depositAmt1 - withdrawlAmt1 + depositAmt2, + "Token supply should be 0.55 after 2nd deposit" + ); + + // Withdraw 0.5 IMX + uint256 withdrawlAmt2 = 0.5 ether; + vm.startPrank(user2); + wIMX.withdraw(withdrawlAmt2); + assertEq( + wIMX.totalSupply(), + depositAmt1 - withdrawlAmt1 + depositAmt2 - withdrawlAmt2, + "Token supply should be 0.05 after 2nd withdraw" + ); + } + + function test_RevertIf_TransferAmountExceedingBalance() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + vm.deal(user, 1 ether); + // Create a recipient + address recipient = address(1235); + + // Deposit 0.1 IMX + uint256 depositAmt = 0.1 ether; + vm.startPrank(user); + wIMX.deposit{value: depositAmt}(); + + // Transfer 0.2 wIMX to recipient should revert + vm.expectRevert(); + wIMX.transfer(recipient, depositAmt + 1); + + vm.stopPrank(); + } + + function test_SucceedIf_TransferAmountWithinBalance() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + vm.deal(user, 1 ether); + // Create a recipient + address recipient = address(1235); + + assertEq(wIMX.balanceOf(user), 0, "User should have 0 wIMX"); + assertEq(wIMX.balanceOf(recipient), 0, "Recipient should have 0 wIMX"); + + // Deposit 0.1 IMX + uint256 depositAmt = 0.1 ether; + vm.startPrank(user); + wIMX.deposit{value: depositAmt}(); + + assertEq(wIMX.balanceOf(user), depositAmt, "User should have 0.1 wIMX"); + assertEq(wIMX.balanceOf(recipient), 0, "Recipient should have 0 wIMX"); + + // Transfer 0.05 wIMX to recipient should revert + uint256 transferredAmt = 0.05 ether; + vm.expectEmit(address(wIMX)); + emit Transfer(user, recipient, transferredAmt); + wIMX.transfer(recipient, transferredAmt); + + vm.stopPrank(); + + assertEq(wIMX.balanceOf(user), depositAmt - transferredAmt, "User should have 0.05 wIMX"); + assertEq(wIMX.balanceOf(recipient), transferredAmt, "Recipient should have 0.05 wIMX"); + } + + function test_RevertIf_TransferFromWithNoAllowance() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + vm.deal(user, 1 ether); + // Create a second user + address user2 = address(1235); + + // Deposit 0.1 IMX + vm.startPrank(user); + wIMX.deposit{value: 0.1 ether}(); + + vm.stopPrank(); + vm.startPrank(user2); + + // Second user tries to transfer from + vm.expectRevert(); + wIMX.transferFrom(user, user2, 0.1 ether); + + vm.stopPrank(); + } + + function test_RevertIf_TransferFromWithInsufficientAllowance() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + vm.deal(user, 1 ether); + // Create a second user + address user2 = address(1235); + + // Deposit 0.1 IMX + vm.startPrank(user); + wIMX.deposit{value: 0.1 ether}(); + + // Allow second user + uint256 approvedAmt = 0.05 ether; + vm.expectEmit(address(wIMX)); + emit Approval(user, user2, approvedAmt); + wIMX.approve(user2, approvedAmt); + + vm.stopPrank(); + vm.startPrank(user2); + + // Second user tries to transfer from + vm.expectRevert(); + wIMX.transferFrom(user, user2, approvedAmt + 1); + + vm.stopPrank(); + } + + function test_RevertIf_TransferFromWithInsufficientBalance() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + vm.deal(user, 1 ether); + // Create a second user + address user2 = address(1235); + + // Deposit 0.1 IMX + uint256 depositAmt = 0.1 ether; + vm.startPrank(user); + wIMX.deposit{value: depositAmt}(); + + // Allow second user + vm.expectEmit(address(wIMX)); + emit Approval(user, user2, depositAmt + 1); + wIMX.approve(user2, depositAmt + 1); + + vm.stopPrank(); + vm.startPrank(user2); + + // Second user tries to transfer from + vm.expectRevert(); + wIMX.transferFrom(user, user2, depositAmt + 1); + + vm.stopPrank(); + } + + function test_SucceedIf_TransferFromWithinAllowanceAndBalance() public { + // Create a user and fund it with 1 IMX + address user = address(1234); + vm.deal(user, 1 ether); + // Create a second user + address user2 = address(1235); + + // Deposit 0.1 IMX + uint256 depositAmt = 0.1 ether; + vm.startPrank(user); + wIMX.deposit{value: depositAmt}(); + + // Allow second user + vm.expectEmit(address(wIMX)); + emit Approval(user, user2, depositAmt); + wIMX.approve(user2, depositAmt); + + vm.stopPrank(); + vm.startPrank(user2); + + // Second user tries to transfer from + uint256 transferredAmt = 0.1 ether; + vm.expectEmit(address(wIMX)); + emit Transfer(user, user2, transferredAmt); + wIMX.transferFrom(user, user2, transferredAmt); + + vm.stopPrank(); + + assertEq(wIMX.balanceOf(user), depositAmt - transferredAmt, "User should have 0 wIMX"); + assertEq(wIMX.balanceOf(user2), transferredAmt, "Recipient should have 0.05 wIMX"); + } +} diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 3c4e5057..221b7f4d 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -22,6 +22,10 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid string CHILD_BRIDGE_ADAPTOR_STRING = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); string constant CHILD_CHAIN_NAME = "test"; address constant IMX_TOKEN = address(99); + address constant CHILD_ETH_TOKEN = address(0xddd); + address constant NATIVE_TOKEN = address(0xeee); + uint256 constant mapTokenFee = 300; + uint256 constant depositFee = 200; ERC20PresetMinterPauser public token; RootERC20Bridge public rootBridge; @@ -40,7 +44,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid mockAxelarAdaptor = new MockAdaptor(); // 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); + rootBridge.initialize(address(mockAxelarAdaptor), CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR_STRING, address(token), IMX_TOKEN, CHILD_ETH_TOKEN); } /** @@ -55,43 +59,49 @@ 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); + rootBridge.initialize(address(mockAxelarAdaptor), CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR_STRING, address(token), IMX_TOKEN, CHILD_ETH_TOKEN); } 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_InitializeWithAZeroAddressChildAdapter() public { + 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(1), address(0)); + bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(0), address(1)); + } + + function test_RevertIf_InitializeWithAZeroAddressETHToken() public { + RootERC20Bridge bridge = new RootERC20Bridge(); + vm.expectRevert(ZeroAddress.selector); + 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), Strings.toHexString(address(0)), address(0), address(0)); + bridge.initialize(address(0), address(0), "", address(0), address(0), address(0)); } /** @@ -99,7 +109,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid */ function test_mapToken_EmitsTokenMappedEvent() public { - uint256 mapTokenFee = 300; address childToken = Clones.predictDeterministicAddress(address(token), keccak256(abi.encodePacked(token)), CHILD_BRIDGE); @@ -110,7 +119,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_mapToken_CallsAdaptor() public { - uint256 mapTokenFee = 300; bytes memory payload = abi.encode(rootBridge.MAP_TOKEN_SIG(), token, token.name(), token.symbol(), token.decimals()); @@ -125,7 +133,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_mapToken_SetsTokenMapping() public { - uint256 mapTokenFee = 300; address childToken = Clones.predictDeterministicAddress(address(token), keccak256(abi.encodePacked(token)), CHILD_BRIDGE); @@ -134,14 +141,14 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid assertEq(rootBridge.rootTokenToChildToken(address(token)), childToken, "rootTokenToChildToken mapping not set"); } - function testFuzz_mapToken_UpdatesEthBalance(uint256 mapTokenFee) public { - vm.assume(mapTokenFee < address(this).balance); - vm.assume(mapTokenFee > 0); + function testFuzz_mapToken_UpdatesEthBalance(uint256 _mapTokenFee) public { + vm.assume(_mapTokenFee < address(this).balance); + vm.assume(_mapTokenFee > 0); uint256 thisPreBal = address(this).balance; uint256 rootBridgePreBal = address(rootBridge).balance; uint256 adaptorPreBal = address(mockAxelarAdaptor).balance; - rootBridge.mapToken{value: mapTokenFee}(token); + rootBridge.mapToken{value: _mapTokenFee}(token); /* * Because this is a unit test, the adaptor is mocked. This adaptor would typically @@ -149,8 +156,8 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid */ // User pays - assertEq(address(this).balance, thisPreBal - mapTokenFee, "ETH balance not decreased"); - assertEq(address(mockAxelarAdaptor).balance, adaptorPreBal + mapTokenFee, "ETH not paid to adaptor"); + assertEq(address(this).balance, thisPreBal - _mapTokenFee, "ETH balance not decreased"); + assertEq(address(mockAxelarAdaptor).balance, adaptorPreBal + _mapTokenFee, "ETH not paid to adaptor"); assertEq(address(rootBridge).balance, rootBridgePreBal, "ETH balance not increased"); } @@ -189,51 +196,160 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid rootBridge.updateRootBridgeAdaptor(address(0)); } + /** + * DEPOSIT ETH + */ + + function test_depositETHCallsSendMessage() public { + uint256 amount = 1000; + (, bytes memory predictedPayload) = setupDeposit(ERC20PresetMinterPauser(NATIVE_TOKEN), rootBridge, mapTokenFee, depositFee, amount, false); + + vm.expectCall( + address(mockAxelarAdaptor), + depositFee, + abi.encodeWithSelector(mockAxelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + rootBridge.depositETH{value: amount+depositFee}(amount); + } + + function test_depositETHEmitsNativeDepositEvent() public { + uint256 amount = 1000; + setupDeposit(ERC20PresetMinterPauser(NATIVE_TOKEN), rootBridge, mapTokenFee, depositFee, amount, false); + + vm.expectEmit(); + emit NativeDeposit(NATIVE_TOKEN, CHILD_ETH_TOKEN, address(this), address(this), amount); + rootBridge.depositETH{value: amount+depositFee}(amount); + } + + function test_RevertIf_depositETHInsufficientValue() public { + uint256 amount = 1000; + setupDeposit(ERC20PresetMinterPauser(NATIVE_TOKEN), rootBridge, mapTokenFee, depositFee, amount, false); + + vm.expectRevert(InsufficientValue.selector); + rootBridge.depositETH{value: (amount/2)+depositFee}(amount); + } + + /** + * DEPOSIT TO ETH + */ + + function test_depositToETHCallsSendMessage() public { + uint256 amount = 1000; + address receiver = address(12345); + (, bytes memory predictedPayload) = setupDepositTo(ERC20PresetMinterPauser(NATIVE_TOKEN), rootBridge, mapTokenFee, depositFee, amount, receiver, false); + vm.expectCall( + address(mockAxelarAdaptor), + depositFee, + abi.encodeWithSelector(mockAxelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + rootBridge.depositToETH{value: amount+depositFee}(receiver, amount); + } + + function test_depositToETHEmitsNativeDepositEvent() public { + uint256 amount = 1000; + address receiver = address(12345); + setupDepositTo(ERC20PresetMinterPauser(NATIVE_TOKEN), rootBridge, mapTokenFee, depositFee, amount, receiver, false); + + vm.expectEmit(); + emit NativeDeposit(NATIVE_TOKEN, CHILD_ETH_TOKEN, address(this), receiver, amount); + rootBridge.depositToETH{value: amount+depositFee}(receiver, amount); + } + + function test_RevertIf_depositToETHInsufficientValue() public { + uint256 amount = 1000; + address receiver = address(12345); + setupDepositTo(ERC20PresetMinterPauser(NATIVE_TOKEN), rootBridge, mapTokenFee, depositFee, amount, receiver, false); + + vm.expectRevert(InsufficientValue.selector); + rootBridge.depositToETH{value: (amount/2)+depositFee}(receiver, amount); + } + + /** + * ZERO AMOUNT + */ + + function test_RevertIf_depositETHAmountIsZero() public { + uint256 amount = 0; + setupDeposit(ERC20PresetMinterPauser(NATIVE_TOKEN), rootBridge, mapTokenFee, depositFee, amount, false); + + vm.expectRevert(ZeroAmount.selector); + rootBridge.depositETH{value: amount+depositFee}(amount); + } + + function test_RevertIf_depositToETHAmountIsZero() public { + uint256 amount = 0; + address receiver = address(12345); + + setupDeposit(ERC20PresetMinterPauser(NATIVE_TOKEN), rootBridge, mapTokenFee, depositFee, amount, false); + + vm.expectRevert(ZeroAmount.selector); + rootBridge.depositToETH{value: amount+depositFee}(receiver, amount); + } + + function test_RevertIf_depositAmountIsZero() public { + uint256 amount = 0; + setupDeposit(ERC20PresetMinterPauser(NATIVE_TOKEN), rootBridge, mapTokenFee, depositFee, amount, false); + + vm.expectRevert(ZeroAmount.selector); + rootBridge.deposit{value: depositFee}(token, amount); + } + + function test_RevertIf_depositToAmountIsZero() public { + uint256 amount = 0; + address receiver = address(12345); + setupDeposit(ERC20PresetMinterPauser(NATIVE_TOKEN), rootBridge, mapTokenFee, depositFee, amount, false); + + vm.expectRevert(ZeroAmount.selector); + rootBridge.depositTo{value: depositFee}(token, receiver, amount); + } + /** * DEPOSIT TOKEN */ function test_depositCallsSendMessage() public { uint256 amount = 100; - (, bytes memory predictedPayload) = setupDeposit(token, rootBridge, 0, amount, true); + (, bytes memory predictedPayload) = setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); vm.expectCall( address(mockAxelarAdaptor), - 0, + depositFee, abi.encodeWithSelector(mockAxelarAdaptor.sendMessage.selector, predictedPayload, address(this)) ); - rootBridge.deposit(token, amount); + rootBridge.deposit{value: depositFee}(token, amount); } function test_depositEmitsERC20DepositEvent() public { uint256 amount = 100; - (address childToken,) = setupDeposit(token, rootBridge, 0, amount, true); + (address childToken,) = setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); vm.expectEmit(); emit ERC20Deposit(address(token), childToken, address(this), address(this), amount); - rootBridge.deposit(token, amount); + rootBridge.deposit{value: depositFee}(token, amount); } function test_depositIMXEmitsIMXDepositEvent() public { uint256 amount = 100; - setupDeposit(ERC20PresetMinterPauser(IMX_TOKEN), rootBridge, 0, amount, false); + setupDeposit(ERC20PresetMinterPauser(IMX_TOKEN), rootBridge, mapTokenFee, depositFee, amount, false); vm.expectEmit(); emit IMXDeposit(IMX_TOKEN, address(this), address(this), amount); - rootBridge.deposit(IERC20Metadata(IMX_TOKEN), amount); + rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN), amount); } function test_depositTransfersTokens() public { uint256 amount = 100; - setupDeposit(token, rootBridge, 0, amount, true); + setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); uint256 thisPreBal = token.balanceOf(address(this)); uint256 bridgePreBal = token.balanceOf(address(rootBridge)); - rootBridge.deposit(token, amount); + rootBridge.deposit{value: depositFee}(token, amount); // Check that tokens are transferred assertEq(thisPreBal - amount, token.balanceOf(address(this)), "Tokens not transferred from user"); @@ -241,47 +357,46 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_depositTransfersNativeAsset() public { - uint256 gasPrice = 300; uint256 amount = 100; - setupDeposit(token, rootBridge, 0, amount, true); + setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); uint256 thisNativePreBal = address(this).balance; uint256 adaptorNativePreBal = address(mockAxelarAdaptor).balance; - rootBridge.deposit{value: gasPrice}(token, amount); + rootBridge.deposit{value: depositFee}(token, amount); // Check that native asset transferred to adaptor // In this case, because the adaptor is mocked, gas payment goes to the adaptor. - assertEq(thisNativePreBal - gasPrice, address(this).balance, "ETH not paid from user"); - assertEq(adaptorNativePreBal + gasPrice, address(mockAxelarAdaptor).balance, "ETH not paid to adaptor"); + assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH not paid from user"); + assertEq(adaptorNativePreBal + depositFee, address(mockAxelarAdaptor).balance, "ETH not paid to adaptor"); } function test_RevertIf_depositCalledWithZeroAddress() public { uint256 amount = 100; - setupDeposit(token, rootBridge, 0, amount, true); + setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); // Will fail when it tries to call balanceOf vm.expectRevert(); - rootBridge.deposit(IERC20Metadata(address(0)), 100); + rootBridge.deposit{value: depositFee}(IERC20Metadata(address(0)), amount); } function test_RevertIf_depositCalledWithUnmappedToken() public { uint256 amount = 100; - setupDeposit(token, rootBridge, 0, amount, true); + setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); ERC20PresetMinterPauser newToken = new ERC20PresetMinterPauser("Test", "TST"); vm.expectRevert(NotMapped.selector); - rootBridge.deposit(newToken, 100); + rootBridge.deposit{value: depositFee}(newToken, amount); } // 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, 0, amount, true); + setupDeposit(token, rootBridge, mapTokenFee, depositFee, amount, true); vm.expectRevert(); - rootBridge.deposit(token, amount * 2); + rootBridge.deposit{value: depositFee}(token, amount * 2); } /** @@ -292,26 +407,26 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid uint256 amount = 100; address receiver = address(12345); - (, bytes memory predictedPayload) = setupDepositTo(token, rootBridge, 0, amount, receiver, true); + (, bytes memory predictedPayload) = setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); vm.expectCall( address(mockAxelarAdaptor), - 0, + depositFee, abi.encodeWithSelector(mockAxelarAdaptor.sendMessage.selector, predictedPayload, address(this)) ); - rootBridge.depositTo(token, receiver, amount); + rootBridge.depositTo{value: depositFee}(token, receiver, amount); } function test_depositToEmitsERC20DepositEvent() public { uint256 amount = 100; address receiver = address(12345); - (address childToken,) = setupDepositTo(token, rootBridge, 0, amount, receiver, true); + (address childToken,) = setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); vm.expectEmit(); emit ERC20Deposit(address(token), childToken, address(this), receiver, amount); - rootBridge.depositTo(token, receiver, amount); + rootBridge.depositTo{value: depositFee}(token, receiver, amount); } function test_depositToIMXEmitsIMXDepositEvent() public { @@ -319,23 +434,23 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address receiver = address(12345); - setupDepositTo(ERC20PresetMinterPauser(IMX_TOKEN), rootBridge, 0, amount, receiver, false); + setupDepositTo(ERC20PresetMinterPauser(IMX_TOKEN), rootBridge, mapTokenFee, depositFee, amount, receiver, false); vm.expectEmit(); emit IMXDeposit(IMX_TOKEN, address(this), receiver, amount); - rootBridge.depositTo(IERC20Metadata(IMX_TOKEN), receiver, amount); + rootBridge.depositTo{value: depositFee}(IERC20Metadata(IMX_TOKEN), receiver, amount); } function test_depositToTransfersTokens() public { uint256 amount = 100; address receiver = address(12345); - setupDepositTo(token, rootBridge, 0, amount, receiver, true); + setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); uint256 thisPreBal = token.balanceOf(address(this)); uint256 bridgePreBal = token.balanceOf(address(rootBridge)); - rootBridge.depositTo(token, receiver, amount); + rootBridge.depositTo{value: depositFee}(token, receiver, amount); // Check that tokens are transferred assertEq(thisPreBal - amount, token.balanceOf(address(this)), "Tokens not transferred from user"); @@ -343,53 +458,52 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_depositToTransfersNativeAsset() public { - uint256 gasPrice = 300; uint256 amount = 100; address receiver = address(12345); - setupDepositTo(token, rootBridge, gasPrice, amount, receiver, true); + setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); uint256 thisNativePreBal = address(this).balance; uint256 adaptorNativePreBal = address(mockAxelarAdaptor).balance; - rootBridge.depositTo{value: gasPrice}(token, receiver, amount); + rootBridge.depositTo{value: depositFee}(token, receiver, amount); // Check that native asset transferred to adaptor // In this case, because the adaptor is mocked, gas payment goes to the adaptor. - assertEq(thisNativePreBal - gasPrice, address(this).balance, "ETH not paid from user"); - assertEq(adaptorNativePreBal + gasPrice, address(mockAxelarAdaptor).balance, "ETH not paid to adaptor"); + assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH not paid from user"); + assertEq(adaptorNativePreBal + depositFee, address(mockAxelarAdaptor).balance, "ETH not paid to adaptor"); } // We want to ensure that messages don't get sent when they are not supposed to function test_RevertIf_depositToCalledWhenTokenApprovalNotProvided() public { uint256 amount = 100; address receiver = address(12345); - setupDepositTo(token, rootBridge, 0, amount, receiver, true); + setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); vm.expectRevert(); - rootBridge.depositTo(token, receiver, amount * 2); + rootBridge.depositTo{value: depositFee}(token, receiver, amount * 2); } function test_RevertIf_depositToCalledWithZeroAddress() public { uint256 amount = 100; address receiver = address(12345); - setupDepositTo(token, rootBridge, 0, amount, receiver, true); + setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); // Will fail when it tries to call balanceOf vm.expectRevert(); - rootBridge.depositTo(IERC20Metadata(address(0)), receiver, 100); + rootBridge.depositTo{value: depositFee}(IERC20Metadata(address(0)), receiver, amount); } function test_RevertIf_depositToCalledWithUnmappedToken() public { uint256 amount = 100; address receiver = address(12345); - setupDepositTo(token, rootBridge, 0, amount, receiver, true); + setupDepositTo(token, rootBridge, mapTokenFee, depositFee, amount, receiver, true); ERC20PresetMinterPauser newToken = new ERC20PresetMinterPauser("Test", "TST"); vm.expectRevert(NotMapped.selector); - rootBridge.depositTo(newToken, receiver, 100); + rootBridge.depositTo{value: depositFee}(newToken, receiver, amount); } } diff --git a/test/utils.t.sol b/test/utils.t.sol index 5cf99a89..3dfb6db0 100644 --- a/test/utils.t.sol +++ b/test/utils.t.sol @@ -13,12 +13,13 @@ import {IChildERC20, ChildERC20} from "../src/child/ChildERC20.sol"; import {RootAxelarBridgeAdaptor} from "../src/root/RootAxelarBridgeAdaptor.sol"; contract Utils is Test { + function integrationSetup( address childBridge, address childBridgeAdaptor, string memory childBridgeName, - address imxTokenAddress - ) + address imxTokenAddress, + address ethTokenAddress) public returns ( ERC20PresetMinterPauser token, @@ -42,45 +43,53 @@ contract Utils is Test { address(axelarGasService) ); - rootBridge.initialize(address(axelarAdaptor), childBridge, Strings.toHexString(childBridgeAdaptor), address(token), imxTokenAddress); + rootBridge.initialize(address(axelarAdaptor), childBridge, Strings.toHexString(childBridgeAdaptor), address(token), imxTokenAddress, ethTokenAddress); axelarAdaptor.setChildBridgeAdaptor(); } function setupDeposit( ERC20PresetMinterPauser token, RootERC20Bridge rootBridge, - uint256 gasPrice, + uint256 mapTokenFee, + uint256 depositFee, uint256 tokenAmount, bool saveTokenMapping ) public returns (address childToken, bytes memory predictedPayload) { - return _setupDeposit(token, rootBridge, gasPrice, tokenAmount, address(this), saveTokenMapping); + return _setupDeposit(token, rootBridge, mapTokenFee, depositFee, tokenAmount, address(this), saveTokenMapping); } function setupDepositTo( ERC20PresetMinterPauser token, RootERC20Bridge rootBridge, - uint256 gasPrice, + uint256 mapTokenFee, + uint256 depositFee, uint256 tokenAmount, address to, bool saveTokenMapping ) public returns (address childToken, bytes memory predictedPayload) { - return _setupDeposit(token, rootBridge, gasPrice, tokenAmount, to, saveTokenMapping); + return _setupDeposit(token, rootBridge, mapTokenFee, depositFee, tokenAmount, to, saveTokenMapping); } function _setupDeposit( ERC20PresetMinterPauser token, RootERC20Bridge rootBridge, - uint256 gasPrice, + uint256 mapTokenFee, + uint256 depositFee, uint256 tokenAmount, address to, bool saveTokenMapping ) public returns (address childToken, bytes memory predictedPayload) { predictedPayload = abi.encode(rootBridge.DEPOSIT_SIG(), address(token), address(this), to, tokenAmount); if (saveTokenMapping) { - childToken = rootBridge.mapToken{value: gasPrice}(token); + childToken = rootBridge.mapToken{value: mapTokenFee}(token); + } + if (address(token) == address(0xeee)) { + vm.deal(to, tokenAmount+depositFee); + } else { + token.mint(address(this), tokenAmount); + token.approve(address(rootBridge), tokenAmount); } - token.mint(address(this), tokenAmount); - token.approve(address(rootBridge), tokenAmount); + return (childToken, predictedPayload); }