diff --git a/script/DeployChildContracts.s.sol b/script/DeployChildContracts.s.sol index 9bf20df8..45a0cae0 100644 --- a/script/DeployChildContracts.s.sol +++ b/script/DeployChildContracts.s.sol @@ -14,7 +14,7 @@ contract DeployChildContracts is Script { function run() public { uint256 deployerPrivateKey = vm.envUint("CHILD_PRIVATE_KEY"); address childGateway = vm.envAddress("CHILD_GATEWAY_ADDRESS"); - address childGasService = vm.envAddress("CHILD_GAS_SERVICE_ADDRESS"); // Not yet used. + //address childGasService = vm.envAddress("CHILD_GAS_SERVICE_ADDRESS"); // Not yet used. string memory childRpcUrl = vm.envString("CHILD_RPC_URL"); vm.createSelectFork(childRpcUrl); diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index c52c3a93..9b191539 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -15,6 +15,7 @@ import { } from "../interfaces/child/IChildERC20Bridge.sol"; import {IChildERC20BridgeAdaptor} from "../interfaces/child/IChildERC20BridgeAdaptor.sol"; import {IChildERC20} from "../interfaces/child/IChildERC20.sol"; +import {console2} from "forge-std/Test.sol"; /** * @notice RootERC20Bridge is a bridge that allows ERC20 tokens to be transferred from the root chain to the child chain. @@ -38,7 +39,7 @@ contract ChildERC20Bridge is bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); - address public constant NATIVE_TOKEN = address(0xeee); + address public constant NATIVE_ETH = address(0xeee); IChildERC20BridgeAdaptor public bridgeAdaptor; /// @dev The address that will be sending messages to, and receiving messages from, the child chain. @@ -60,7 +61,7 @@ contract ChildERC20Bridge is * @param newChildTokenTemplate Address of child token template to clone. * @param newRootChain A stringified representation of the chain that this bridge is connected to. Used for validation. * @param newRootIMXToken Address of ECR20 IMX on the root chain. - * @param newChildETHToken Address of ERC20 ETH on the child chain. + * @param newRootETHToken Address used to denote ETH on the root chain. * @dev Can only be called once. */ function initialize( @@ -69,13 +70,13 @@ contract ChildERC20Bridge is address newChildTokenTemplate, string memory newRootChain, address newRootIMXToken, - address newChildETHToken) + address newRootETHToken) public initializer { if (newBridgeAdaptor == address(0) || newChildTokenTemplate == address(0) || newRootIMXToken == address(0) - || newChildETHToken == address(0)) { + || newRootETHToken == address(0)) { revert ZeroAddress(); } @@ -92,8 +93,11 @@ contract ChildERC20Bridge is bridgeAdaptor = IChildERC20BridgeAdaptor(newBridgeAdaptor); rootChain = newRootChain; rootIMXToken = newRootIMXToken; - childETHToken = newChildETHToken; + IChildERC20 clonedETHToken = + IChildERC20(Clones.cloneDeterministic(childTokenTemplate, keccak256(abi.encodePacked(newRootETHToken)))); + clonedETHToken.initialize(newRootETHToken, "Ethereum", "ETH", 18); + childETHToken = address(clonedETHToken); } /** @@ -163,10 +167,15 @@ contract ChildERC20Bridge is address childToken; if (address(rootToken) != rootIMXToken) { - childToken = rootTokenToChildToken[address(rootToken)]; - if (childToken == address(0)) { - revert NotMapped(); + if (address(rootToken) == NATIVE_ETH) { + childToken = childETHToken; + } else { + childToken = rootTokenToChildToken[address(rootToken)]; + if (childToken == address(0)) { + revert NotMapped(); + } } + if (address(childToken).code.length == 0) { revert EmptyTokenContract(); } @@ -174,7 +183,12 @@ contract ChildERC20Bridge is if (!IChildERC20(childToken).mint(receiver, amount)) { revert MintFailed(); } - emit ERC20Deposit(address(rootToken), childToken, sender, receiver, amount); + + if (address(rootToken) == NATIVE_ETH) { + emit NativeEthDeposit(address(rootToken), childToken, sender, receiver, amount); + } else { + emit ERC20Deposit(address(rootToken), childToken, sender, receiver, amount); + } } else { Address.sendValue(payable(receiver), amount); emit IMXDeposit(address(rootToken), sender, receiver, amount); diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index e20e1f92..302c37e5 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -38,7 +38,7 @@ interface IChildERC20BridgeEvents { address indexed receiver, uint256 amount ); - event NativeDeposit( + event NativeEthDeposit( address indexed rootToken, address indexed childToken, address depositor, diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index 0752b2cb..08ee6634 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -51,7 +51,7 @@ interface IRootERC20BridgeEvents { address indexed receiver, uint256 amount ); - event NativeDeposit( + event NativeEthDeposit( address indexed rootToken, address indexed childToken, address depositor, diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 1fb146b9..75a0b8f1 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -34,7 +34,7 @@ contract RootERC20Bridge is bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); - address public constant NATIVE_TOKEN = address(0xeee); + address public constant NATIVE_ETH = address(0xeee); IRootERC20BridgeAdaptor public rootBridgeAdaptor; /// @dev Used to verify source address in messages sent from child chain. @@ -111,7 +111,7 @@ contract RootERC20Bridge is uint256 expectedBalance = address(this).balance - (msg.value - amount); - _deposit(IERC20Metadata(NATIVE_TOKEN), receiver, amount); + _deposit(IERC20Metadata(NATIVE_ETH), receiver, amount); // invariant check to ensure that the root native balance has increased by the amount deposited if (address(this).balance != expectedBalance) { @@ -187,7 +187,7 @@ contract RootERC20Bridge is // 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_TOKEN) { + if (address(rootToken) != NATIVE_ETH) { if (address(rootToken) != rootIMXToken) { childToken = rootTokenToChildToken[address(rootToken)]; if (childToken == address(0)) { @@ -207,8 +207,8 @@ contract RootERC20Bridge is rootBridgeAdaptor.sendMessage{value: feeAmount}(payload, msg.sender); - if (address(rootToken) == NATIVE_TOKEN) { - emit NativeDeposit(address(rootToken), childETHToken, msg.sender, receiver, amount); + if (address(rootToken) == NATIVE_ETH) { + emit NativeEthDeposit(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/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index b2d11942..0218b9a7 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import { @@ -10,6 +11,7 @@ import { IERC20Metadata, IChildERC20BridgeErrors } from "../../../src/child/ChildERC20Bridge.sol"; +import {IChildERC20} from "../../../src/interfaces/child/IChildERC20.sol"; import {ChildERC20} from "../../../src/child/ChildERC20.sol"; import {MockAdaptor} from "../../../src/test/root/MockAdaptor.sol"; import {Utils} from "../../utils.t.sol"; @@ -18,12 +20,11 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B address constant ROOT_BRIDGE = address(3); string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); string constant ROOT_CHAIN_NAME = "test"; - - address constant IMX_TOKEN = address(0xccc); - address constant CHILD_ETH_TOKEN = address(0xddd); - address constant NATIVE_TOKEN = address(0xeee); + address constant ROOT_IMX_TOKEN = address(0xccc); + address constant NATIVE_ETH = address(0xeee); ChildERC20 public childTokenTemplate; ChildERC20 public rootToken; + address public childETHToken; ChildERC20Bridge public childBridge; function setUp() public { @@ -32,10 +33,9 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B childTokenTemplate = new ChildERC20(); childTokenTemplate.initialize(address(123), "Test", "TST", 18); - + childBridge = new ChildERC20Bridge(); - - childBridge.initialize(address(this), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, IMX_TOKEN, CHILD_ETH_TOKEN); + childBridge.initialize(address(this), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN, NATIVE_ETH); } function test_Initialize() public { @@ -43,11 +43,14 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B assertEq(childBridge.rootERC20BridgeAdaptor(), ROOT_BRIDGE_ADAPTOR, "rootERC20BridgeAdaptor not set"); assertEq(childBridge.childTokenTemplate(), address(childTokenTemplate), "childTokenTemplate not set"); assertEq(childBridge.rootChain(), ROOT_CHAIN_NAME, "rootChain not set"); + assertEq(childBridge.rootIMXToken(), ROOT_IMX_TOKEN, "rootIMXToken not set"); + assertNotEq(childBridge.childETHToken(), address(0), "childETHToken not set"); + assertNotEq(address(childBridge.childETHToken()).code.length, 0, "childETHToken contract empty"); } function test_RevertIfInitializeTwice() public { vm.expectRevert("Initializable: contract is already initialized"); - childBridge.initialize(address(this), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, IMX_TOKEN, CHILD_ETH_TOKEN); + childBridge.initialize(address(this), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN, NATIVE_ETH); } function test_RevertIf_InitializeWithAZeroAddressAdapter() public { @@ -68,7 +71,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B bridge.initialize(address(1), ROOT_BRIDGE_ADAPTOR, address(1), ROOT_CHAIN_NAME, address(0), address(1)); } - function test_RevertIf_InitializeWithAZeroAddressEthChildToken() public { + function test_RevertIf_InitializeWithAZeroAddressEthRootToken() public { ChildERC20Bridge bridge = new ChildERC20Bridge(); vm.expectRevert(ZeroAddress.selector); bridge.initialize(address(1), ROOT_BRIDGE_ADAPTOR, address(1), ROOT_CHAIN_NAME, address(1), address(0)); @@ -83,13 +86,13 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B function test_RevertIf_InitializeWithAnEmptyBridgeAdaptorString() public { ChildERC20Bridge bridge = new ChildERC20Bridge(); vm.expectRevert(InvalidRootERC20BridgeAdaptor.selector); - bridge.initialize(address(this), "", address(childTokenTemplate), ROOT_CHAIN_NAME, IMX_TOKEN, CHILD_ETH_TOKEN); + bridge.initialize(address(this), "", address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN, NATIVE_ETH); } function test_RevertIf_InitializeWithAnEmptyChainNameString() public { ChildERC20Bridge bridge = new ChildERC20Bridge(); vm.expectRevert(InvalidRootChain.selector); - bridge.initialize(address(this), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), "", IMX_TOKEN, CHILD_ETH_TOKEN); + bridge.initialize(address(this), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), "", ROOT_IMX_TOKEN, NATIVE_ETH); } function test_onMessageReceive_EmitsTokenMappedEvent() public { @@ -191,7 +194,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B function test_RevertIf_mapTokenCalledWithIMXAddress() public { bytes memory data = abi.encode( - childBridge.MAP_TOKEN_SIG(), IMX_TOKEN, "ImmutableX", "IMX", 18 + childBridge.MAP_TOKEN_SIG(), ROOT_IMX_TOKEN, "ImmutableX", "IMX", 18 ); vm.expectRevert(CantMapIMX.selector); childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); @@ -225,6 +228,59 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B childBridge.updateBridgeAdaptor(address(0)); } + //Deposit ETH + + function test_onMessageReceive_DepositETH_EmitsETHDepositEvent() public { + address sender = address(100); + address receiver = address(200); + uint256 amount = 1000; + + bytes memory depositData = abi.encode(childBridge.DEPOSIT_SIG(), address(NATIVE_ETH), sender, receiver, amount); + + address predictedChildETHToken = Clones.predictDeterministicAddress( + address(childTokenTemplate), keccak256(abi.encodePacked(NATIVE_ETH)), address(childBridge) + ); + + vm.expectEmit(address(childBridge)); + emit NativeEthDeposit(address(NATIVE_ETH), predictedChildETHToken, sender, receiver, amount); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, depositData); + } + + function test_onMessageReceive_DepositETH_TransfersTokensToReceiver() public { + address sender = address(100); + address receiver = address(200); + uint256 amount = 1000; + + bytes memory depositData = abi.encode(childBridge.DEPOSIT_SIG(), address(NATIVE_ETH), sender, receiver, amount); + + address predictedChildETHToken = Clones.predictDeterministicAddress( + address(childTokenTemplate), keccak256(abi.encodePacked(NATIVE_ETH)), address(childBridge) + ); + + uint256 receiverPreBal = ChildERC20(predictedChildETHToken).balanceOf(receiver); + + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, depositData); + + assertEq(ChildERC20(predictedChildETHToken).balanceOf(receiver), receiverPreBal + amount, "receiver balance not increased"); + } + + function test_onMessageReceive_DepositETH_IncreasesTotalSupply() public { + address sender = address(100); + address receiver = address(200); + uint256 amount = 1000; + + bytes memory depositData = abi.encode(childBridge.DEPOSIT_SIG(), address(NATIVE_ETH), sender, receiver, amount); + + address predictedChildETHToken = Clones.predictDeterministicAddress( + address(childTokenTemplate), keccak256(abi.encodePacked(NATIVE_ETH)), address(childBridge) + ); + uint256 totalSupplyPre = ChildERC20(predictedChildETHToken).totalSupply(); + + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, depositData); + + assertEq(ChildERC20(predictedChildETHToken).totalSupply(), totalSupplyPre + amount, "totalSupply not increased"); + } + //Deposit function test_onMessageReceive_DepositIMX_EmitsIMXDepositEvent() public { @@ -236,10 +292,10 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B address receiver = address(200); uint256 amount = 1 ether; - bytes memory depositData = abi.encode(childBridge.DEPOSIT_SIG(), IMX_TOKEN, sender, receiver, amount); + bytes memory depositData = abi.encode(childBridge.DEPOSIT_SIG(), ROOT_IMX_TOKEN, sender, receiver, amount); vm.expectEmit(address(childBridge)); - emit IMXDeposit(IMX_TOKEN, sender, receiver, amount); + emit IMXDeposit(ROOT_IMX_TOKEN, sender, receiver, amount); childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, depositData); } @@ -251,7 +307,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B address receiver = address(200); uint256 amount = 1 ether; - bytes memory depositData = abi.encode(childBridge.DEPOSIT_SIG(), IMX_TOKEN, sender, receiver, amount); + bytes memory depositData = abi.encode(childBridge.DEPOSIT_SIG(), ROOT_IMX_TOKEN, sender, receiver, amount); childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, depositData); @@ -267,7 +323,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B address receiver = address(200); uint256 amount = 10 ether; - bytes memory depositData = abi.encode(childBridge.DEPOSIT_SIG(), IMX_TOKEN, sender, receiver, amount); + bytes memory depositData = abi.encode(childBridge.DEPOSIT_SIG(), ROOT_IMX_TOKEN, sender, receiver, amount); vm.expectRevert("Address: insufficient balance"); childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, depositData); diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 8acf4883..950cc6b8 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -212,12 +212,12 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid rootBridge.depositETH{value: amount+depositFee}(amount); } - function test_depositETHEmitsNativeDepositEvent() public { + function test_depositETHEmitsNativeEthDepositEvent() 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); + emit NativeEthDeposit(NATIVE_TOKEN, CHILD_ETH_TOKEN, address(this), address(this), amount); rootBridge.depositETH{value: amount+depositFee}(amount); } @@ -246,13 +246,13 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid rootBridge.depositToETH{value: amount+depositFee}(receiver, amount); } - function test_depositToETHEmitsNativeDepositEvent() public { + function test_depositToETHEmitsNativeEthDepositEvent() 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); + emit NativeEthDeposit(NATIVE_TOKEN, CHILD_ETH_TOKEN, address(this), receiver, amount); rootBridge.depositToETH{value: amount+depositFee}(receiver, amount); }