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 7 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: 2 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ ROOT_GAS_SERVICE_ADDRESS=
CHILD_GAS_SERVICE_ADDRESS=
ROOT_CHAIN_NAME=
CHILD_CHAIN_NAME=
ROOT_IMX_ADDRESS=
ROOT_IMX_ADDRESS=
ROOT_WETH_ADDRESS=
3 changes: 2 additions & 1 deletion .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 All @@ -22,4 +23,4 @@ docs/
# Contract addresses
addresses.json

/node_modules
/node_modules
10 changes: 8 additions & 2 deletions script/InitializeRootContracts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import {Strings} from "@openzeppelin/contracts/utils/Strings.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

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

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

rootBridgeAdaptor.setChildBridgeAdaptor();
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/root/IRootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,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);
}
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;
}
65 changes: 50 additions & 15 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,26 +60,28 @@ 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 newRootWETHToken Address of ERC20 IMX on the root chain.
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
* @dev Can only be called once.
*/
function initialize(
address newRootBridgeAdaptor,
address newChildERC20Bridge,
address newChildBridgeAdaptor,
address newChildTokenTemplate,
address newRootIMXToken
address newRootIMXToken,
address newRootWETHToken
) public initializer {
if (
newRootBridgeAdaptor == address(0) || newChildERC20Bridge == address(0)
|| newChildTokenTemplate == address(0) || newChildBridgeAdaptor == address(0)
|| newRootIMXToken == address(0)
|| newRootIMXToken == address(0) || newRootWETHToken == address(0)
) {
revert ZeroAddress();
}
childERC20Bridge = newChildERC20Bridge;
childTokenTemplate = newChildTokenTemplate;
rootIMXToken = newRootIMXToken;

rootWETHToken = newRootWETHToken;
childETHToken = Clones.predictDeterministicAddress(
childTokenTemplate, keccak256(abi.encodePacked(NATIVE_ETH)), childERC20Bridge
);
Expand All @@ -95,23 +101,39 @@ 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);
}

function _depositUnwrappedETH(address receiver, uint256 amount) private {
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
_deposit(IERC20Metadata(NATIVE_ETH), receiver, amount, msg.value);
}

proletesseract marked this conversation as resolved.
Show resolved Hide resolved
function _depositETH(address receiver, uint256 amount) private {
if (msg.value < amount) {
revert InsufficientValue();
}

uint256 expectedBalance = address(this).balance - (msg.value - amount);

_deposit(IERC20Metadata(NATIVE_ETH), receiver, amount);
_deposit(IERC20Metadata(NATIVE_ETH), receiver, amount, msg.value - 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);
}
}

function _unwrapWETH(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) {
Expand All @@ -123,19 +145,28 @@ contract RootERC20Bridge is
* @inheritdoc IRootERC20Bridge
*/
function deposit(IERC20Metadata rootToken, uint256 amount) external payable override {
_depositERC20(rootToken, msg.sender, amount);
_depositToken(rootToken, msg.sender, amount);
}

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

function _depositToken(IERC20Metadata rootToken, address receiver, uint256 amount) private {
if (address(rootToken) == rootWETHToken) {
_unwrapWETH(amount);
_depositUnwrappedETH(receiver, amount);
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
} else {
_depositERC20(rootToken, receiver, amount);
}
}

function _depositERC20(IERC20Metadata rootToken, address receiver, uint256 amount) private {
uint256 expectedBalance = rootToken.balanceOf(address(this)) + amount;
_deposit(rootToken, receiver, amount);
_deposit(rootToken, receiver, amount, msg.value);
// invariant check to ensure that the root token balance has increased by the amount deposited
// slither-disable-next-line incorrect-equality
if (rootToken.balanceOf(address(this)) != expectedBalance) {
Expand All @@ -155,6 +186,10 @@ contract RootERC20Bridge is
revert CantMapETH();
}

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

if (rootTokenToChildToken[address(rootToken)] != address(0)) {
revert AlreadyMapped();
}
Expand All @@ -175,17 +210,15 @@ contract RootERC20Bridge is
return childToken;
}

function _deposit(IERC20Metadata rootToken, address receiver, uint256 amount) private {
function _deposit(IERC20Metadata rootToken, address receiver, uint256 amount, uint256 feeAmount) private {
if (receiver == address(0) || address(rootToken) == address(0)) {
revert ZeroAddress();
}

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

address childToken;
uint256 feeAmount;

// 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
Expand All @@ -201,9 +234,6 @@ contract RootERC20Bridge is
}
// 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
Expand All @@ -227,4 +257,9 @@ contract RootERC20Bridge is
}
rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor);
}

/**
* @dev method to receive the ETH back from the WETH contract when it is unwrapped
*/
receive() external payable {}
}
98 changes: 98 additions & 0 deletions src/test/root/WETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.21;

import {IWETH} from "../../interfaces/root/IWETH.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

/**
* @notice WETH is a wrapped ETH contract that allows users to wrap their native ETH.
* @dev This contract is adapted from the official Wrapped ETH contract.
*/
contract WETH is IWETH {
string public name = "Wrapped ETH";
string public symbol = "WETH";
uint8 public decimals = 18;

mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

/**
* @notice Fallback function on recieving native ETH.
*/
receive() external payable {
deposit();
}

/**
* @notice Deposit native ETH in the function call and mint the equal amount of wrapped ETH to msg.sender.
*/
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}

/**
* @notice Withdraw given amount of native ETH to msg.sender and burn the equal amount of wrapped ETH.
* @param wad The amount to withdraw.
*/
function withdraw(uint256 wad) public {
require(balanceOf[msg.sender] >= wad, "Wrapped ETH: Insufficient balance");
balanceOf[msg.sender] -= wad;

Address.sendValue(payable(msg.sender), wad);
emit Withdrawal(msg.sender, wad);
}

/**
* @notice Obtain the current total supply of wrapped ETH.
* @return uint The amount of supplied wrapped ETH.
*/
function totalSupply() public view returns (uint256) {
return address(this).balance;
}

/**
* @notice Approve given spender the ability to spend a given amount of msg.sender's tokens.
* @param guy Approved spender.
* @param wad Amount of allowance.
* @return bool Returns true if function call is successful.
*/
function approve(address guy, uint256 wad) public returns (bool) {
allowance[msg.sender][guy] = wad;
emit Approval(msg.sender, guy, wad);
return true;
}

/**
* @notice Transfer given amount of tokens from msg.sender to given destination.
* @param dst Destination of this transfer.
* @param wad Amount of this transfer.
* @return bool Returns true if function call is successful.
*/
function transfer(address dst, uint256 wad) public returns (bool) {
return transferFrom(msg.sender, dst, wad);
}

/**
* @notice Transfer given amount of tokens from given source to given destination.
* @param src Source of this transfer.
* @param dst Destination of this transfer.
* @param wad Amount of this transfer.
* @return bool Returns true if function call is successful.
*/
function transferFrom(address src, address dst, uint256 wad) public returns (bool) {
require(balanceOf[src] >= wad, "Wrapped ETH: Insufficient balance");

if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) {
require(allowance[src][msg.sender] >= wad, "Wrapped ETH: Insufficient allowance");
allowance[src][msg.sender] -= wad;
}

balanceOf[src] -= wad;
balanceOf[dst] += wad;

emit Transfer(src, dst, wad);

return true;
}
}
Loading