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-1834-WETH-Bridging #12

Merged
merged 17 commits into from
Oct 26, 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
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");

Comment on lines 25 to 27
Copy link
Contributor

@Benjimmutable Benjimmutable Oct 25, 2023

Choose a reason for hiding this comment

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

Could we add the steps into ./deploy.sh that deploy WETH (if local), and instructions in README for how to trigger this local deployment of WETH, and explaining that it needs to be manually set iff ./deploy.sh is not going to be run with the local flag

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;
}
proletesseract marked this conversation as resolved.
Show resolved Hide resolved

// 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