Skip to content

Commit

Permalink
Merge pull request #12 from immutable/SMR-1834-WETH-Bridging
Browse files Browse the repository at this point in the history
SMR-1834-WETH-Bridging
  • Loading branch information
proletesseract authored Oct 26, 2023
2 parents 275d3b6 + d4bc8cb commit ff42ce1
Show file tree
Hide file tree
Showing 13 changed files with 460 additions and 102 deletions.
4 changes: 3 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ ROOT_GAS_SERVICE_ADDRESS=
CHILD_GAS_SERVICE_ADDRESS=
ROOT_CHAIN_NAME=
CHILD_CHAIN_NAME=
ROOT_IMX_ADDRESS=
ROOT_IMX_ADDRESS=
ROOT_WETH_ADDRESS=
ENVIRONMENT=
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Compiler files
cache/
out/
.idea/

# Ignores development broadcast logs
!/broadcast
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,11 @@ ROOT_GATEWAY_ADDRESS="0x013459EC3E8Aeced878C5C4bFfe126A366cd19E9"
CHILD_GATEWAY_ADDRESS="0xc7B788E88BAaB770A6d4936cdcCcd5250E1bbAd8"
ROOT_GAS_SERVICE_ADDRESS="0x28f8B50E1Be6152da35e923602a2641491E71Ed8"
CHILD_GAS_SERVICE_ADDRESS="0xC573c722e21eD7fadD38A8f189818433e01Ae466"

ENVIRONMENT="local"
```
(Note that `{ROOT,CHILD}_PRIVATE_KEY` can be any of the standard localhost private keys that get funded)
(Note that `ROOT_IMX_ADDRESS` is not currently used in this local environment. Therefore, any non-zero address is fine.)
(Note that `ENVIRONMENT` if the environment is not set to "local" then ROOT_WETH_ADDRESS will need to be manually set in the .env file since it is expected WETH to be already deployed on testnet or mainnet)

3. In a separate terminal window, deploy the smart contracts
```shell
Expand Down
7 changes: 7 additions & 0 deletions deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function get_deployed_contract() {
}

function main() {

forge script script/DeployRootContracts.s.sol:DeployRootContracts --broadcast
forge script script/DeployChildContracts.s.sol:DeployChildContracts --broadcast

Expand All @@ -23,6 +24,11 @@ function main() {
root_erc20_bridge=$( get_deployed_contract "$root_filename" "RootERC20Bridge" )
root_bridge_adaptor=$( get_deployed_contract "$root_filename" "RootAxelarBridgeAdaptor" )
root_chain_child_token_template=$( get_deployed_contract "$root_filename" "ChildERC20" )

if [ "$ENVIRONMENT" = "local" ]; then
root_weth_contract=$( get_deployed_contract "$root_filename" "WETH" )
export ROOT_WETH_ADDRESS=$root_weth_contract
fi

child_erc20_bridge=$( get_deployed_contract "$child_filename" "ChildERC20Bridge" )
child_bridge_adaptor=$( get_deployed_contract "$child_filename" "ChildAxelarBridgeAdaptor" )
Expand All @@ -31,6 +37,7 @@ function main() {
export ROOT_ERC20_BRIDGE=$root_erc20_bridge
export ROOT_BRIDGE_ADAPTOR=$root_bridge_adaptor
export ROOTCHAIN_CHILD_TOKEN_TEMPLATE=$root_chain_child_token_template

export CHILD_BRIDGE_ADAPTOR=$child_bridge_adaptor
export CHILD_ERC20_BRIDGE=$child_erc20_bridge
export CHILDCHAIN_CHILD_TOKEN_TEMPLATE=$child_chain_child_token_template
Expand Down
10 changes: 8 additions & 2 deletions script/DeployRootContracts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {RootAxelarBridgeAdaptor} from "../src/root/RootAxelarBridgeAdaptor.sol";
import {ChildERC20Bridge} from "../src/child/ChildERC20Bridge.sol";
import {ChildAxelarBridgeAdaptor} from "../src/child/ChildAxelarBridgeAdaptor.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {WETH} from "../src/test/root/WETH.sol";

// TODO update private key usage to be more secure: https://book.getfoundry.sh/reference/forge/forge-script#wallet-options---raw

Expand All @@ -19,6 +20,7 @@ contract DeployRootContracts is Script {
address rootGateway = vm.envAddress("ROOT_GATEWAY_ADDRESS");
address rootGasService = vm.envAddress("ROOT_GAS_SERVICE_ADDRESS");
string memory childChainName = vm.envString("CHILD_CHAIN_NAME");
string memory deployEnvironment = vm.envString("ENVIRONMENT");

/**
* DEPLOY ROOT CHAIN CONTRACTS
Expand All @@ -39,11 +41,15 @@ contract DeployRootContracts is Script {
rootGasService // axelar gas service
);

if (Strings.equal(deployEnvironment, "local")) {
new WETH();
}

vm.stopBroadcast();

console2.log("====ROOT ADDRESSES====");
console2.log("Root ERC20 Bridge: %s", address(rootERC20Bridge));
console2.log("Root Axelar Bridge Adaptor: %s", address(rootBridgeAdaptor));
console2.log("ROOT CHAIN childTokenTemplate: %s", address(rootChainChildTokenTemplate));

vm.stopBroadcast();
}
}
6 changes: 4 additions & 2 deletions script/InitializeRootContracts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import {Utils} from "./Utils.sol";

contract InitializeRootContracts is Script {
function run() public {
RootERC20Bridge rootERC20Bridge = RootERC20Bridge(vm.envAddress("ROOT_ERC20_BRIDGE"));
RootERC20Bridge rootERC20Bridge = RootERC20Bridge(payable(vm.envAddress("ROOT_ERC20_BRIDGE")));
RootAxelarBridgeAdaptor rootBridgeAdaptor = RootAxelarBridgeAdaptor(vm.envAddress("ROOT_BRIDGE_ADAPTOR"));
address rootChainChildTokenTemplate = vm.envAddress("ROOTCHAIN_CHILD_TOKEN_TEMPLATE");
address childBridgeAdaptor = vm.envAddress("CHILD_BRIDGE_ADAPTOR");
address childERC20Bridge = vm.envAddress("CHILD_ERC20_BRIDGE");
string memory rootRpcUrl = vm.envString("ROOT_RPC_URL");
uint256 rootPrivateKey = vm.envUint("ROOT_PRIVATE_KEY");
address rootIMXToken = vm.envAddress("ROOT_IMX_ADDRESS");
address rootWETHToken = vm.envAddress("ROOT_WETH_ADDRESS");

string[] memory checksumInputs = Utils.getChecksumInputs(childBridgeAdaptor);
bytes memory checksumOutput = vm.ffi(checksumInputs);
Expand All @@ -38,7 +39,8 @@ contract InitializeRootContracts is Script {
childERC20Bridge,
childBridgeAdaptorChecksum,
rootChainChildTokenTemplate,
rootIMXToken
rootIMXToken,
rootWETHToken
);

rootBridgeAdaptor.setChildBridgeAdaptor();
Expand Down
9 changes: 9 additions & 0 deletions src/interfaces/root/IRootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ interface IRootERC20BridgeEvents {
uint256 amount
);
event IMXDeposit(address indexed rootToken, address depositor, address indexed receiver, uint256 amount);
event WETHDeposit(
address indexed rootToken,
address indexed childToken,
address depositor,
address indexed receiver,
uint256 amount
);
event NativeEthDeposit(
address indexed rootToken,
address indexed childToken,
Expand All @@ -70,6 +77,8 @@ interface IRootERC20BridgeErrors {
error CantMapIMX();
/// @notice Error when attempting to map ETH.
error CantMapETH();
/// @notice Error when attempting to map wETH.
error CantMapWETH();
/// @notice Error when token balance invariant check fails.
error BalanceInvariantCheckFailed(uint256 actualBalance, uint256 expectedBalance);
/// @notice Error when the given child chain bridge adaptor is invalid.
Expand Down
30 changes: 30 additions & 0 deletions src/interfaces/root/IWETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache 2.0
pragma solidity ^0.8.21;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
* @dev Interface of Wrapped ETH.
*/
interface IWETH is IERC20 {
/**
* @dev Emitted when `value` native ETH are deposited from `account`.
*/
event Deposit(address indexed account, uint256 value);

/**
* @dev Emitted when `value` wETH tokens are withdrawn to `account`.
*/
event Withdrawal(address indexed account, uint256 value);

/**
* @notice Deposit native ETH in the function call and mint the equal amount of wrapped ETH to msg.sender.
*/
function deposit() external payable;

/**
* @notice Withdraw given amount of native ETH to msg.sender and burn the equal amount of wrapped ETH.
* @param value The amount to withdraw.
*/
function withdraw(uint256 value) external;
}
113 changes: 77 additions & 36 deletions src/root/RootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {IRootERC20Bridge, IERC20Metadata} from "../interfaces/root/IRootERC20Bri
import {IRootERC20BridgeEvents, IRootERC20BridgeErrors} from "../interfaces/root/IRootERC20Bridge.sol";
import {IRootERC20BridgeAdaptor} from "../interfaces/root/IRootERC20BridgeAdaptor.sol";
import {IChildERC20} from "../interfaces/child/IChildERC20.sol";
import {IWETH} from "../interfaces/root/IWETH.sol";

/**
* @notice RootERC20Bridge is a bridge that allows ERC20 tokens to be transferred from the root chain to the child chain.
Expand All @@ -21,6 +22,7 @@ import {IChildERC20} from "../interfaces/child/IChildERC20.sol";
* @dev Because of this pattern, any checks or logic that is agnostic to the messaging protocol should be done in RootERC20Bridge.
* @dev Any checks or logic that is specific to the underlying messaging protocol should be done in the bridge adaptor.
*/

contract RootERC20Bridge is
Ownable2Step,
Initializable,
Expand Down Expand Up @@ -48,6 +50,8 @@ contract RootERC20Bridge is
address public rootIMXToken;
/// @dev The address of the ETH ERC20 token on L2.
address public childETHToken;
/// @dev The address of the wETH ERC20 token on L1.
address public rootWETHToken;

/**
* @notice Initilization function for RootERC20Bridge.
Expand All @@ -56,18 +60,20 @@ contract RootERC20Bridge is
* @param newChildBridgeAdaptor Address of child bridge adaptor to communicate with (As a checksummed string).
* @param newChildTokenTemplate Address of child token template to clone.
* @param newRootIMXToken Address of ERC20 IMX on the root chain.
* @param newRootWETHToken Address of ERC20 WETH on the root chain.
* @dev Can only be called once.
*/
function initialize(
address newRootBridgeAdaptor,
address newChildERC20Bridge,
string memory newChildBridgeAdaptor,
address newChildTokenTemplate,
address newRootIMXToken
address newRootIMXToken,
address newRootWETHToken
) public initializer {
if (
newRootBridgeAdaptor == address(0) || newChildERC20Bridge == address(0)
|| newChildTokenTemplate == address(0) || newRootIMXToken == address(0)
|| newChildTokenTemplate == address(0) || newRootIMXToken == address(0) || newRootWETHToken == address(0)
) {
revert ZeroAddress();
}
Expand All @@ -77,14 +83,26 @@ contract RootERC20Bridge is
childERC20Bridge = newChildERC20Bridge;
childTokenTemplate = newChildTokenTemplate;
rootIMXToken = newRootIMXToken;

rootWETHToken = newRootWETHToken;
childETHToken = Clones.predictDeterministicAddress(
childTokenTemplate, keccak256(abi.encodePacked(NATIVE_ETH)), childERC20Bridge
);
rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor);
childBridgeAdaptor = newChildBridgeAdaptor;
}

function updateRootBridgeAdaptor(address newRootBridgeAdaptor) external onlyOwner {
if (newRootBridgeAdaptor == address(0)) {
revert ZeroAddress();
}
rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor);
}

/**
* @dev method to receive the ETH back from the WETH contract when it is unwrapped
*/
receive() external payable {}

/**
* @inheritdoc IRootERC20Bridge
* @dev TODO when this becomes part of the deposit flow on a token's first bridge, this logic will need to be mostly moved into an internal function.
Expand All @@ -97,15 +115,27 @@ contract RootERC20Bridge is
}

function depositETH(uint256 amount) external payable {
//override removed?
_depositETH(msg.sender, amount);
}

function depositToETH(address receiver, uint256 amount) external payable {
//override removed?
_depositETH(receiver, amount);
}

/**
* @inheritdoc IRootERC20Bridge
*/
function deposit(IERC20Metadata rootToken, uint256 amount) external payable override {
_depositToken(rootToken, msg.sender, amount);
}

/**
* @inheritdoc IRootERC20Bridge
*/
function depositTo(IERC20Metadata rootToken, address receiver, uint256 amount) external payable override {
_depositToken(rootToken, receiver, amount);
}

function _depositETH(address receiver, uint256 amount) private {
if (msg.value < amount) {
revert InsufficientValue();
Expand All @@ -121,18 +151,27 @@ contract RootERC20Bridge is
}
}

/**
* @inheritdoc IRootERC20Bridge
*/
function deposit(IERC20Metadata rootToken, uint256 amount) external payable override {
_depositERC20(rootToken, msg.sender, amount);
function _depositToken(IERC20Metadata rootToken, address receiver, uint256 amount) private {
if (address(rootToken) == rootWETHToken) {
_depositWrappedETH(receiver, amount);
} else {
_depositERC20(rootToken, receiver, amount);
}
}

/**
* @inheritdoc IRootERC20Bridge
*/
function depositTo(IERC20Metadata rootToken, address receiver, uint256 amount) external payable override {
_depositERC20(rootToken, receiver, amount);
function _depositWrappedETH(address receiver, uint256 amount) private {
uint256 expectedBalance = address(this).balance + amount;

IERC20Metadata erc20WETH = IERC20Metadata(rootWETHToken);

erc20WETH.safeTransferFrom(msg.sender, address(this), amount);
IWETH(rootWETHToken).withdraw(amount);

// invariant check to ensure that the root native balance has increased by the amount deposited
if (address(this).balance != expectedBalance) {
revert BalanceInvariantCheckFailed(address(this).balance, expectedBalance);
}
_deposit(IERC20Metadata(rootWETHToken), receiver, amount);
}

function _depositERC20(IERC20Metadata rootToken, address receiver, uint256 amount) private {
Expand All @@ -157,6 +196,10 @@ contract RootERC20Bridge is
revert CantMapETH();
}

if (address(rootToken) == rootWETHToken) {
revert CantMapWETH();
}

if (rootTokenToChildToken[address(rootToken)] != address(0)) {
revert AlreadyMapped();
}
Expand All @@ -181,52 +224,50 @@ contract RootERC20Bridge is
if (receiver == address(0) || address(rootToken) == address(0)) {
revert ZeroAddress();
}

if (amount == 0) {
revert ZeroAmount();
}

address childToken;
uint256 feeAmount;
// ETH, WETH and IMX do not need to be mapped since it should have been mapped on initialization
// ETH also cannot be transferred since it was received in the payable function call
// WETH is also not transferred here since it was earlier unwrapped to ETH

// The native token does not need to be mapped since it should have been mapped on initialization
// The native token also cannot be transferred since it was received in the payable function call
// TODO We can call _mapToken here, but ordering in the GMP is not guaranteed.
// Therefore, we need to decide how to handle this and it may be a UI decision to wait until map token message is executed on child chain.
// Discuss this, and add this decision to the design doc.
if (address(rootToken) != NATIVE_ETH) {
// Therefore, we need to decide how to handle this and it may be a UI decision to wait until map token message is executed on child chain.
// Discuss this, and add this decision to the design doc.

address childToken;
uint256 feeAmount = msg.value;
address payloadToken = address(rootToken);

if (address(rootToken) == NATIVE_ETH) {
feeAmount = msg.value - amount;
} else if (address(rootToken) == rootWETHToken) {
payloadToken = NATIVE_ETH;
} else {
if (address(rootToken) != rootIMXToken) {
childToken = rootTokenToChildToken[address(rootToken)];
if (childToken == address(0)) {
revert NotMapped();
}
}
// ERC20 must be transferred explicitly
rootToken.safeTransferFrom(msg.sender, address(this), amount);
feeAmount = msg.value;
} else {
feeAmount = msg.value - amount;
}

// Deposit sig, root token address, depositor, receiver, amount
bytes memory payload = abi.encode(DEPOSIT_SIG, rootToken, msg.sender, receiver, amount);
// TODO investigate using delegatecall to keep the axelar message sender as the bridge contract, since adaptor can change.
bytes memory payload = abi.encode(DEPOSIT_SIG, payloadToken, msg.sender, receiver, amount);

// TODO investigate using delegatecall to keep the axelar message sender as the bridge contract, since adaptor can change.
rootBridgeAdaptor.sendMessage{value: feeAmount}(payload, msg.sender);

if (address(rootToken) == NATIVE_ETH) {
emit NativeEthDeposit(address(rootToken), childETHToken, msg.sender, receiver, amount);
} else if (address(rootToken) == rootWETHToken) {
emit WETHDeposit(address(rootToken), childETHToken, msg.sender, receiver, amount);
} else if (address(rootToken) == rootIMXToken) {
emit IMXDeposit(address(rootToken), msg.sender, receiver, amount);
} else {
emit ERC20Deposit(address(rootToken), childToken, msg.sender, receiver, amount);
}
}

function updateRootBridgeAdaptor(address newRootBridgeAdaptor) external onlyOwner {
if (newRootBridgeAdaptor == address(0)) {
revert ZeroAddress();
}
rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor);
}
}
Loading

0 comments on commit ff42ce1

Please sign in to comment.