Skip to content

Commit

Permalink
Merge pull request #9 from immutable/SMR-1782-eth-bridging-child
Browse files Browse the repository at this point in the history
SMR-1782 ETH Child Bridge
  • Loading branch information
Benjimmutable authored Oct 25, 2023
2 parents 52a7243 + 56737b4 commit 8ee323f
Show file tree
Hide file tree
Showing 13 changed files with 364 additions and 101 deletions.
3 changes: 1 addition & 2 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@ ROOT_GAS_SERVICE_ADDRESS=
CHILD_GAS_SERVICE_ADDRESS=
ROOT_CHAIN_NAME=
CHILD_CHAIN_NAME=
ROOT_IMX_ADDRESS=
CHILD_ETH_ADDRESS=
ROOT_IMX_ADDRESS=
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ docs/
.vscode/

# Contract addresses
addresses.json
addresses.json

/node_modules
2 changes: 1 addition & 1 deletion script/DeployChildContracts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 1 addition & 7 deletions script/InitializeRootContracts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ 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 childETHToken = vm.envAddress("CHILD_ETH_ADDRESS");

/**
* INITIALIZE ROOT CHAIN CONTRACTS
Expand All @@ -31,12 +30,7 @@ contract InitializeRootContracts is Script {
vm.startBroadcast(rootPrivateKey);

rootERC20Bridge.initialize(
address(rootBridgeAdaptor),
childERC20Bridge,
childBridgeAdaptor,
rootChainChildTokenTemplate,
rootIMXToken,
childETHToken
address(rootBridgeAdaptor), childERC20Bridge, childBridgeAdaptor, rootChainChildTokenTemplate, rootIMXToken
);

rootBridgeAdaptor.setChildBridgeAdaptor();
Expand Down
56 changes: 41 additions & 15 deletions src/child/ChildERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,43 @@ contract ChildERC20Bridge is
{
using SafeERC20 for IERC20Metadata;

/// @dev leave this as the first param for the integration tests
mapping(address => address) public rootTokenToChildToken;

bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN");
bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT");
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.
string public rootERC20BridgeAdaptor;
/// @dev The address of the token template that will be cloned to create tokens.
address public childTokenTemplate;
/// @dev The name of the chain that this bridge is connected to.
string public rootChain;
mapping(address => address) public rootTokenToChildToken;

bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN");
bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT");
address public imxToken;
/// @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.
* @param newBridgeAdaptor Address of StateSender to send deposit information to.
* @param newRootERC20BridgeAdaptor Stringified address of root ERC20 bridge adaptor to communicate with.
* @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 newIMXToken Address of ECR20 IMX on the root chain.
* @param newRootIMXToken Address of ECR20 IMX on the root chain.
* @dev Can only be called once.
*/
function initialize(
address newBridgeAdaptor,
string memory newRootERC20BridgeAdaptor,
address newChildTokenTemplate,
string memory newRootChain,
address newIMXToken
address newRootIMXToken
) public initializer {
if (newBridgeAdaptor == address(0) || newChildTokenTemplate == address(0) || newIMXToken == address(0)) {
if (newBridgeAdaptor == address(0) || newChildTokenTemplate == address(0) || newRootIMXToken == address(0)) {
revert ZeroAddress();
}

Expand All @@ -78,7 +85,12 @@ contract ChildERC20Bridge is
childTokenTemplate = newChildTokenTemplate;
bridgeAdaptor = IChildERC20BridgeAdaptor(newBridgeAdaptor);
rootChain = newRootChain;
imxToken = newIMXToken;
rootIMXToken = newRootIMXToken;

IChildERC20 clonedETHToken =
IChildERC20(Clones.cloneDeterministic(childTokenTemplate, keccak256(abi.encodePacked(NATIVE_ETH))));
clonedETHToken.initialize(NATIVE_ETH, "Ethereum", "ETH", 18);
childETHToken = address(clonedETHToken);
}

/**
Expand Down Expand Up @@ -120,10 +132,14 @@ contract ChildERC20Bridge is
revert ZeroAddress();
}

if (address(rootToken) == imxToken) {
if (address(rootToken) == rootIMXToken) {
revert CantMapIMX();
}

if (address(rootToken) == NATIVE_ETH) {
revert CantMapETH();
}

if (rootTokenToChildToken[rootToken] != address(0)) {
revert AlreadyMapped();
}
Expand All @@ -147,19 +163,29 @@ contract ChildERC20Bridge is

address childToken;

if (address(rootToken) != imxToken) {
childToken = rootTokenToChildToken[address(rootToken)];
if (childToken == address(0)) {
revert NotMapped();
if (address(rootToken) != rootIMXToken) {
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();
}

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);
Expand Down
4 changes: 3 additions & 1 deletion src/interfaces/child/IChildERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ interface IChildERC20BridgeEvents {
uint256 amount
);
event IMXDeposit(address indexed rootToken, address depositor, address indexed receiver, uint256 amount);
event NativeDeposit(
event NativeEthDeposit(
address indexed rootToken,
address indexed childToken,
address depositor,
Expand All @@ -58,6 +58,8 @@ interface IChildERC20BridgeErrors {
error NotMapped();
/// @notice Error when attempting to map IMX.
error CantMapIMX();
/// @notice Error when attempting to map ETH.
error CantMapETH();
/// @notice Error when a token is already mapped.
error AlreadyMapped();
/// @notice Error when a message is given to the bridge from an address not the designated bridge adaptor.
Expand Down
4 changes: 3 additions & 1 deletion src/interfaces/root/IRootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ interface IRootERC20BridgeEvents {
uint256 amount
);
event IMXDeposit(address indexed rootToken, address depositor, address indexed receiver, uint256 amount);
event NativeDeposit(
event NativeEthDeposit(
address indexed rootToken,
address indexed childToken,
address depositor,
Expand All @@ -68,6 +68,8 @@ interface IRootERC20BridgeErrors {
error NotMapped();
/// @notice Error when attempting to map IMX.
error CantMapIMX();
/// @notice Error when attempting to map ETH.
error CantMapETH();
/// @notice Error when token balance invariant check fails.
error BalanceInvariantCheckFailed(uint256 actualBalance, uint256 expectedBalance);
}
32 changes: 20 additions & 12 deletions src/root/RootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {IAxelarGateway} from "@axelar-cgp-solidity/contracts/interfaces/IAxelarG
import {IRootERC20Bridge, IERC20Metadata} from "../interfaces/root/IRootERC20Bridge.sol";
import {IRootERC20BridgeEvents, IRootERC20BridgeErrors} from "../interfaces/root/IRootERC20Bridge.sol";
import {IRootERC20BridgeAdaptor} from "../interfaces/root/IRootERC20BridgeAdaptor.sol";
import {IChildERC20} from "../interfaces/child/IChildERC20.sol";

/**
* @notice RootERC20Bridge is a bridge that allows ERC20 tokens to be transferred from the root chain to the child chain.
Expand All @@ -29,19 +30,20 @@ contract RootERC20Bridge is
{
using SafeERC20 for IERC20Metadata;

/// @dev leave this as the first param for the integration tests
mapping(address => address) public rootTokenToChildToken;

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.
/// @dev Stringified version of address.
string public childBridgeAdaptor;
/// @dev The address that will be minting tokens on the child chain.
address public childERC20Bridge;
/// @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.
Expand All @@ -54,28 +56,29 @@ contract RootERC20Bridge is
* @param newChildBridgeAdaptor Address of child bridge adaptor to communicate with.
* @param newChildTokenTemplate Address of child token template to clone.
* @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,
address newChildBridgeAdaptor,
address newChildTokenTemplate,
address newRootIMXToken,
address newChildETHToken
address newRootIMXToken
) public initializer {
if (
newRootBridgeAdaptor == address(0) || newChildERC20Bridge == address(0)
|| newChildTokenTemplate == address(0) || newChildBridgeAdaptor == address(0)
|| newRootIMXToken == address(0) || newChildETHToken == address(0)
|| newRootIMXToken == address(0)
) {
revert ZeroAddress();
}
childERC20Bridge = newChildERC20Bridge;
childTokenTemplate = newChildTokenTemplate;
rootIMXToken = newRootIMXToken;
childETHToken = newChildETHToken;

childETHToken = Clones.predictDeterministicAddress(
childTokenTemplate, keccak256(abi.encodePacked(NATIVE_ETH)), childERC20Bridge
);
rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor);
childBridgeAdaptor = Strings.toHexString(newChildBridgeAdaptor);
}
Expand Down Expand Up @@ -108,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) {
Expand Down Expand Up @@ -147,6 +150,11 @@ contract RootERC20Bridge is
if (address(rootToken) == rootIMXToken) {
revert CantMapIMX();
}

if (address(rootToken) == NATIVE_ETH) {
revert CantMapETH();
}

if (rootTokenToChildToken[address(rootToken)] != address(0)) {
revert AlreadyMapped();
}
Expand Down Expand Up @@ -184,7 +192,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)) {
Expand All @@ -204,8 +212,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 {
Expand Down
54 changes: 50 additions & 4 deletions test/integration/child/ChildAxelarBridge.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {Utils} from "../../utils.t.sol";
contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils {
string public ROOT_ADAPTOR_ADDRESS = Strings.toHexString(address(1));
string public ROOT_CHAIN_NAME = "ROOT_CHAIN";
address constant IMX_TOKEN = address(9);
address constant IMX_TOKEN_ADDRESS = address(0xccc);
address constant NATIVE_ETH = address(0xeee);

ChildERC20Bridge public childERC20Bridge;
ChildERC20 public childERC20;
Expand All @@ -35,7 +36,11 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil
new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway), address(childERC20Bridge));

childERC20Bridge.initialize(
address(childAxelarBridgeAdaptor), ROOT_ADAPTOR_ADDRESS, address(childERC20), ROOT_CHAIN_NAME, IMX_TOKEN
address(childAxelarBridgeAdaptor),
ROOT_ADAPTOR_ADDRESS,
address(childERC20),
ROOT_CHAIN_NAME,
IMX_TOKEN_ADDRESS
);
}

Expand Down Expand Up @@ -143,6 +148,47 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil
);
}

function test_deposit_EmitsNativeDeposit() public {
address sender = address(0xff);
address receiver = address(0xee);
uint256 amount = 100;

address predictedChildETHToken = Clones.predictDeterministicAddress(
address(childERC20), keccak256(abi.encodePacked(NATIVE_ETH)), address(childERC20Bridge)
);

bytes32 commandId = bytes32("testCommandId");

vm.expectEmit(address(childERC20Bridge));
emit NativeEthDeposit(NATIVE_ETH, predictedChildETHToken, sender, receiver, amount);

childAxelarBridgeAdaptor.execute(
commandId,
ROOT_CHAIN_NAME,
ROOT_ADAPTOR_ADDRESS,
abi.encode(childERC20Bridge.DEPOSIT_SIG(), NATIVE_ETH, sender, receiver, amount)
);
}

function test_deposit_EmitsIMXDeposit() public {
address sender = address(0xff);
address receiver = address(0xee);
uint256 amount = 100;
bytes32 commandId = bytes32("testCommandId");

vm.deal(address(childERC20Bridge), 1 ether);

vm.expectEmit(address(childERC20Bridge));
emit IMXDeposit(IMX_TOKEN_ADDRESS, sender, receiver, amount);

childAxelarBridgeAdaptor.execute(
commandId,
ROOT_CHAIN_NAME,
ROOT_ADAPTOR_ADDRESS,
abi.encode(childERC20Bridge.DEPOSIT_SIG(), IMX_TOKEN_ADDRESS, sender, receiver, amount)
);
}

function test_deposit_TransfersTokenToReceiver() public {
address rootTokenAddress = address(456);
address sender = address(0xff);
Expand Down Expand Up @@ -246,8 +292,8 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil
bytes32 depositSig = childERC20Bridge.DEPOSIT_SIG();
address rootAddress = address(0x123);
{
// Slot is 6 because of the Ownable, Initializable contracts coming first.
uint256 rootTokenToChildTokenMappingSlot = 6;
// Slot is 2 because of the Ownable, Initializable contracts coming first.
uint256 rootTokenToChildTokenMappingSlot = 2;
address childAddress = address(444444);
bytes32 slot = getMappingStorageSlotFor(rootAddress, rootTokenToChildTokenMappingSlot);
bytes32 data = bytes32(uint256(uint160(childAddress)));
Expand Down
Loading

0 comments on commit 8ee323f

Please sign in to comment.