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 1935 erc20 withdraw l1 #18

Merged
merged 13 commits into from
Nov 6, 2023
10 changes: 5 additions & 5 deletions script/DeployRootContracts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ contract DeployRootContracts is Script {
uint256 rootPrivateKey = vm.envUint("ROOT_PRIVATE_KEY");
string memory rootRpcUrl = vm.envString("ROOT_RPC_URL");
string memory deployEnvironment = vm.envString("ENVIRONMENT");
address rootGateway = vm.envAddress("ROOT_GATEWAY_ADDRESS");

/**
* DEPLOY ROOT CHAIN CONTRACTS
Expand All @@ -36,18 +37,17 @@ contract DeployRootContracts is Script {

RootERC20Bridge rootERC20BridgeImplementation = new RootERC20Bridge();
rootERC20BridgeImplementation.initialize(
address(1), address(1), "filler", address(1), address(1), address(1), 1
address(1), address(1), "filler", address(1), address(1), address(1), "filler_child_name", 1
);
TransparentUpgradeableProxy rootERC20BridgeProxy = new TransparentUpgradeableProxy(
address(rootERC20BridgeImplementation),
address(proxyAdmin),
""
);

RootAxelarBridgeAdaptor rootBridgeAdaptorImplementation = new RootAxelarBridgeAdaptor();
rootBridgeAdaptorImplementation.initialize(
address(rootERC20BridgeImplementation), "Filler", address(1), address(1)
);
RootAxelarBridgeAdaptor rootBridgeAdaptorImplementation = new RootAxelarBridgeAdaptor(rootGateway);
rootBridgeAdaptorImplementation.initialize(address(rootERC20BridgeImplementation), "Filler", address(1));

TransparentUpgradeableProxy rootBridgeAdaptorProxy = new TransparentUpgradeableProxy(
address(rootBridgeAdaptorImplementation),
address(proxyAdmin),
Expand Down
4 changes: 1 addition & 3 deletions script/InitializeRootContracts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ struct InitializeRootContractsParams {
address rootIMXToken;
address rootWETHToken;
string childChainName;
address rootGateway;
address rootGasService;
uint256 initialIMXCumulativeDepositLimit;
}
Expand All @@ -42,7 +41,6 @@ contract InitializeRootContracts is Script {
rootIMXToken: vm.envAddress("ROOT_IMX_ADDRESS"),
rootWETHToken: vm.envAddress("ROOT_WETH_ADDRESS"),
childChainName: vm.envString("CHILD_CHAIN_NAME"),
rootGateway: vm.envAddress("ROOT_GATEWAY_ADDRESS"),
rootGasService: vm.envAddress("ROOT_GAS_SERVICE_ADDRESS"),
initialIMXCumulativeDepositLimit: vm.envUint("INITIAL_IMX_CUMULATIVE_DEPOSIT_LIMIT")
});
Expand All @@ -63,13 +61,13 @@ contract InitializeRootContracts is Script {
params.rootChainChildTokenTemplate,
params.rootIMXToken,
params.rootWETHToken,
params.childChainName,
params.initialIMXCumulativeDepositLimit
);

params.rootBridgeAdaptor.initialize(
address(params.rootERC20Bridge), // root bridge
params.childChainName, // child chain name
params.rootGateway, // axelar gateway
params.rootGasService // axelar gas service
);

Expand Down
4 changes: 1 addition & 3 deletions src/child/ChildAxelarBridgeAdaptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ contract ChildAxelarBridgeAdaptor is
/// @notice Address of bridge to relay messages to.
IChildERC20Bridge public childBridge;
IAxelarGasService public gasService;
string public rootBridgeAdaptor;
string public rootChain;

constructor(address _gateway) AxelarExecutable(_gateway) {}
Expand All @@ -39,7 +38,6 @@ contract ChildAxelarBridgeAdaptor is
childBridge = IChildERC20Bridge(_childBridge);
rootChain = _rootChain;
gasService = IAxelarGasService(_gasService);
rootBridgeAdaptor = childBridge.rootERC20BridgeAdaptor();
}

/**
Expand All @@ -54,7 +52,7 @@ contract ChildAxelarBridgeAdaptor is
}

// Load from storage.
string memory _rootBridgeAdaptor = rootBridgeAdaptor;
string memory _rootBridgeAdaptor = childBridge.rootERC20BridgeAdaptor();
string memory _rootChain = rootChain;

gasService.payNativeGasForContractCall{value: msg.value}(
Expand Down
26 changes: 26 additions & 0 deletions src/interfaces/root/IRootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IER

interface IRootERC20Bridge {
function childBridgeAdaptor() external view returns (string memory);
/**
* @notice Receives a bridge message from child chain, parsing the message type then executing.
* @param sourceChain The chain the message originated from.
* @param sourceAddress The address the message originated from.
* @param data The data payload of the message.
*/
function onMessageReceive(string calldata sourceChain, string calldata sourceAddress, bytes calldata data)
external;

/**
* @notice Initiate sending a mapToken message to the child chain.
Expand Down Expand Up @@ -64,6 +72,14 @@ interface IRootERC20BridgeEvents {
address indexed receiver,
uint256 amount
);

event RootChainERC20Withdraw(
address indexed rootToken,
address indexed childToken,
address withdrawer,
address indexed receiver,
uint256 amount
);
}

interface IRootERC20BridgeErrors {
Expand All @@ -73,6 +89,8 @@ interface IRootERC20BridgeErrors {
error ZeroAmount();
/// @notice Error when a zero address is given when not valid.
error ZeroAddress();
/// @notice Error when the child chain name is invalid.
error InvalidChildChain();
/// @notice Error when a token is already mapped.
error AlreadyMapped();
/// @notice Error when a token is not mapped when it should be.
Expand All @@ -87,6 +105,14 @@ interface IRootERC20BridgeErrors {
error BalanceInvariantCheckFailed(uint256 actualBalance, uint256 expectedBalance);
/// @notice Error when the given child chain bridge adaptor is invalid.
error InvalidChildERC20BridgeAdaptor();
/// @notice Error when a message received has invalid data.
error InvalidData();
/// @notice Error when a message received has invalid source address.
error InvalidSourceAddress();
/// @notice Error when a message received has invalid source chain.
error InvalidSourceChain();
/// @notice Error when caller is not the root bridge adaptor but should be.
error NotBridgeAdaptor();
/// @notice Error when the total IMX deposit limit is exceeded
error ImxDepositLimitExceeded();
/// @notice Error when the IMX deposit limit is set below the amount of IMX already deposited
Expand Down
32 changes: 20 additions & 12 deletions src/root/RootAxelarBridgeAdaptor.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache 2.0
pragma solidity ^0.8.21;

import {AxelarExecutable} from "@axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
Expand All @@ -20,41 +21,38 @@ import {IRootERC20Bridge} from "../interfaces/root/IRootERC20Bridge.sol";
* @notice RootAxelarBridgeAdaptor is a bridge adaptor that allows the RootERC20Bridge to communicate with the Axelar Gateway.
*/
contract RootAxelarBridgeAdaptor is
AxelarExecutable,
Initializable,
IRootERC20BridgeAdaptor,
IRootAxelarBridgeAdaptorEvents,
IRootAxelarBridgeAdaptorErrors
{
using SafeERC20 for IERC20Metadata;

address public rootBridge;
IRootERC20Bridge public rootBridge;
string public childBridgeAdaptor;
string public childChain;
IAxelarGateway public axelarGateway;
IAxelarGasService public gasService;
mapping(uint256 => string) public chainIdToChainName;

constructor(address _gateway) AxelarExecutable(_gateway) {}

/**
* @notice Initilization function for RootAxelarBridgeAdaptor.
* @param _rootBridge Address of root bridge contract.
* @param _childChain Name of child chain.
* @param _axelarGateway Address of Axelar Gateway contract.
* @param _gasService Address of Axelar Gas Service contract.
*/
function initialize(address _rootBridge, string memory _childChain, address _axelarGateway, address _gasService)
public
initializer
{
if (_rootBridge == address(0) || _axelarGateway == address(0) || _gasService == address(0)) {
function initialize(address _rootBridge, string memory _childChain, address _gasService) public initializer {
if (_rootBridge == address(0) || _gasService == address(0)) {
revert ZeroAddresses();
}

if (bytes(_childChain).length == 0) {
revert InvalidChildChain();
}
rootBridge = _rootBridge;
rootBridge = IRootERC20Bridge(_rootBridge);
childChain = _childChain;
axelarGateway = IAxelarGateway(_axelarGateway);
gasService = IAxelarGasService(_gasService);
}

Expand All @@ -66,7 +64,7 @@ contract RootAxelarBridgeAdaptor is
if (msg.value == 0) {
revert NoGas();
}
if (msg.sender != rootBridge) {
if (msg.sender != address(rootBridge)) {
revert CallerNotBridge();
}

Expand All @@ -79,7 +77,17 @@ contract RootAxelarBridgeAdaptor is
address(this), _childChain, _childBridgeAdaptor, payload, refundRecipient
);

axelarGateway.callContract(_childChain, _childBridgeAdaptor, payload);
gateway.callContract(_childChain, _childBridgeAdaptor, payload);
emit AxelarMessage(_childChain, _childBridgeAdaptor, payload);
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit:

  • for better observability, should we have a corresponding event when a message is received (in _execute())?
  • suggest we rename AxelarMessage to AxelarMessageSent so its a bit more descriptive

}

/**
* @dev This function is called by the parent `AxelarExecutable` contract to execute the payload.
*/
function _execute(string calldata sourceChain_, string calldata sourceAddress_, bytes calldata payload_)
internal
override
{
rootBridge.onMessageReceive(sourceChain_, sourceAddress_, payload_);
}
}
68 changes: 68 additions & 0 deletions src/root/RootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {IAxelarGateway} from "@axelar-cgp-solidity/contracts/interfaces/IAxelarGateway.sol";
import {IRootERC20Bridge, IERC20Metadata} from "../interfaces/root/IRootERC20Bridge.sol";
import {IRootERC20BridgeEvents, IRootERC20BridgeErrors} from "../interfaces/root/IRootERC20Bridge.sol";
Expand Down Expand Up @@ -38,6 +39,7 @@ contract RootERC20Bridge is
uint256 public constant NO_DEPOSIT_LIMIT = 0;
bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN");
bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT");
bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW");
address public constant NATIVE_ETH = address(0xeee);

IRootERC20BridgeAdaptor public rootBridgeAdaptor;
Expand All @@ -53,6 +55,8 @@ contract RootERC20Bridge is
address public childETHToken;
/// @dev The address of the wETH ERC20 token on L1.
address public rootWETHToken;
/// @dev The name of the chain that this bridge is connected to.
string public childChain;
/// @dev The maximum cumulative amount of IMX that can be deposited into the bridge.
/// @dev A limit of zero indicates unlimited.
uint256 public imxCumulativeDepositLimit;
Expand All @@ -65,6 +69,7 @@ contract RootERC20Bridge is
* @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.
* @param newChildChain Name of child chain.
* @param newImxCumulativeDepositLimit The cumulative IMX deposit limit.
* @dev Can only be called once.
*/
Expand All @@ -75,6 +80,7 @@ contract RootERC20Bridge is
address newChildTokenTemplate,
address newRootIMXToken,
address newRootWETHToken,
string memory newChildChain,
uint256 newImxCumulativeDepositLimit
) public initializer {
if (
Expand All @@ -86,6 +92,10 @@ contract RootERC20Bridge is
if (bytes(newChildBridgeAdaptor).length == 0) {
revert InvalidChildERC20BridgeAdaptor();
}
if (bytes(newChildChain).length == 0) {
revert InvalidChildChain();
}

childERC20Bridge = newChildERC20Bridge;
childTokenTemplate = newChildTokenTemplate;
rootIMXToken = newRootIMXToken;
Expand All @@ -95,6 +105,7 @@ contract RootERC20Bridge is
);
rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor);
childBridgeAdaptor = newChildBridgeAdaptor;
childChain = newChildChain;
imxCumulativeDepositLimit = newImxCumulativeDepositLimit;
}

Expand Down Expand Up @@ -135,6 +146,35 @@ contract RootERC20Bridge is
*/
receive() external payable {}

/**
* @inheritdoc IRootERC20Bridge
* @dev This is only callable by the root chain bridge adaptor.
* @dev Validates `sourceAddress` is the child chain's bridgeAdaptor.
*/
function onMessageReceive(string calldata messageSourceChain, string calldata sourceAddress, bytes calldata data)
external
override
{
if (msg.sender != address(rootBridgeAdaptor)) {
revert NotBridgeAdaptor();
}
if (!Strings.equal(messageSourceChain, childChain)) {
revert InvalidSourceChain();
}
if (!Strings.equal(sourceAddress, childBridgeAdaptor)) {
revert InvalidSourceAddress();
}
if (data.length == 0) {
revert InvalidData();
}

if (bytes32(data[:32]) == WITHDRAW_SIG) {
_withdraw(data[32:]);
} else {
revert InvalidData();
}
}

/**
* @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 Down Expand Up @@ -308,4 +348,32 @@ contract RootERC20Bridge is
emit ChildChainERC20Deposit(address(rootToken), childToken, msg.sender, receiver, amount);
}
}

function _withdraw(bytes memory data) private {
(address rootToken, address withdrawer, address receiver, uint256 amount) =
abi.decode(data, (address, address, address, uint256));
address childToken = rootTokenToChildToken[rootToken];
if (childToken == address(0)) {
revert NotMapped();
}
_executeTransfer(rootToken, childToken, withdrawer, receiver, amount);
}

function _executeTransfer(
address rootToken,
address childToken,
address withdrawer,
address receiver,
uint256 amount
) internal {
// TODO when withdrawing ETH/WETH, this next section will also need to check for the withdrawal of WETH (i.e. rootToken == NATIVE_ETH || rootToken == CHILD_WETH)
// Tests for this NATIVE_ETH branch not yet written. This should come as part of that PR.
if (rootToken == NATIVE_ETH) {
Address.sendValue(payable(receiver), amount);
} else {
IERC20Metadata(rootToken).safeTransfer(receiver, amount);
}
// slither-disable-next-line reentrancy-events
emit RootChainERC20Withdraw(address(rootToken), childToken, withdrawer, receiver, amount);
Benjimmutable marked this conversation as resolved.
Show resolved Hide resolved
}
}
2 changes: 1 addition & 1 deletion src/test/child/ChildERC20FailOnBurn.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import "../../child/ChildERC20.sol";
*/
// solhint-disable reason-string
contract ChildERC20FailOnBurn is ChildERC20 {
function burn(address account, uint256 amount) public virtual override returns (bool) {
function burn(address, /*account*/ uint256 /*amount*/ ) public virtual override returns (bool) {
return false;
}
}
4 changes: 4 additions & 0 deletions src/test/root/MockAxelarGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ pragma solidity ^0.8.21;
// @dev A contract for ensuring the Axelar Gateway is called correctly during unit tests.
contract MockAxelarGateway {
function callContract(string memory childChain, string memory childBridgeAdaptor, bytes memory payload) external {}

function validateContractCall(bytes32, string calldata, string calldata, bytes32) external pure returns (bool) {
return true;
}
}
2 changes: 2 additions & 0 deletions src/test/root/StubRootBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ contract StubRootBridge {
function childBridgeAdaptor() external pure returns (string memory) {
return Strings.toHexString(address(9999));
}

function onMessageReceive(string calldata, string calldata, bytes calldata) external {}
}
Loading