Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SMR-1782 ETH Child Bridge #9

Merged
merged 16 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
Comment on lines 18 to 20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is some coverage that is missing in the child bridge integration tests now. Namely, making sure that if the root chain's ETH address is used, that the child ETH address is chosen, and that NativeEthDeposit is emitted (as these are different branches). They are tested in the unit test but think it could be nice adding here too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at adding these integration tests now.

Copy link
Contributor Author

@proletesseract proletesseract Oct 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ive added more integration tests to the root and child. also removed the need for the childETH address to be passed into the rootBridge initialize method and updated tests + deploy scripts to reflect that.

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