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 1813 withdraw erc20s #16

Merged
merged 12 commits into from
Nov 3, 2023
3 changes: 2 additions & 1 deletion script/InitializeChildContracts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ contract InitializeChildContracts is Script {
string memory childRpcUrl = vm.envString("CHILD_RPC_URL");
string memory rootChainName = vm.envString("ROOT_CHAIN_NAME");
address rootIMXToken = vm.envAddress("ROOT_IMX_ADDRESS");
address childGasService = vm.envAddress("CHILD_GAS_SERVICE_ADDRESS"); // Not yet used.

/**
* INITIALIZE CHILD CONTRACTS
Expand All @@ -35,7 +36,7 @@ contract InitializeChildContracts is Script {
address(childAxelarBridgeAdaptor), rootBridgeAdaptorString, childTokenTemplate, rootChainName, rootIMXToken
);

childAxelarBridgeAdaptor.initialize(address(childERC20Bridge));
childAxelarBridgeAdaptor.initialize(rootChainName, address(childERC20Bridge), childGasService);

vm.stopBroadcast();
}
Expand Down
47 changes: 44 additions & 3 deletions src/child/ChildAxelarBridgeAdaptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,67 @@
pragma solidity ^0.8.21;

import {AxelarExecutable} from "@axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol";
import {IAxelarGasService} from "@axelar-cgp-solidity/contracts/interfaces/IAxelarGasService.sol";
import {IAxelarGateway} from "@axelar-cgp-solidity/contracts/interfaces/IAxelarGateway.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {IChildERC20Bridge} from "../interfaces/child/IChildERC20Bridge.sol";
import {IChildAxelarBridgeAdaptorErrors} from "../interfaces/child/IChildAxelarBridgeAdaptor.sol";
import {
IChildAxelarBridgeAdaptorErrors,
IChildAxelarBridgeAdaptorEvents
} from "../interfaces/child/IChildAxelarBridgeAdaptor.sol";
import {IChildERC20BridgeAdaptor} from "../interfaces/child/IChildERC20BridgeAdaptor.sol";

contract ChildAxelarBridgeAdaptor is AxelarExecutable, Initializable, IChildAxelarBridgeAdaptorErrors {
contract ChildAxelarBridgeAdaptor is
AxelarExecutable,
IChildERC20BridgeAdaptor,
Initializable,
IChildAxelarBridgeAdaptorErrors,
IChildAxelarBridgeAdaptorEvents
{
/// @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) {}

/**
* @notice Initializes the contract.
* @param _childBridge Address of the child bridge contract.
*/
function initialize(address _childBridge) external initializer {
function initialize(string memory _rootChain, address _childBridge, address _gasService) external initializer {
if (_childBridge == address(0)) {
revert ZeroAddress();
}

childBridge = IChildERC20Bridge(_childBridge);
rootChain = _rootChain;
gasService = IAxelarGasService(_gasService);
rootBridgeAdaptor = childBridge.rootERC20BridgeAdaptor();
}

/**
* TODO
*/
function sendMessage(bytes calldata payload, address refundRecipient) external payable override {
if (msg.value == 0) {
revert NoGas();
}
if (msg.sender != address(childBridge)) {
revert CallerNotBridge();
}

// Load from storage.
string memory _rootBridgeAdaptor = rootBridgeAdaptor;
string memory _rootChain = rootChain;

gasService.payNativeGasForContractCall{value: msg.value}(
address(this), _rootChain, _rootBridgeAdaptor, payload, refundRecipient
);

gateway.callContract(_rootChain, _rootBridgeAdaptor, payload);
emit AxelarMessage(_rootChain, _rootBridgeAdaptor, payload);
}

/**
Expand Down
47 changes: 46 additions & 1 deletion src/child/ChildERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ contract ChildERC20Bridge is

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);

IChildERC20BridgeAdaptor public bridgeAdaptor;
Expand Down Expand Up @@ -125,6 +126,50 @@ contract ChildERC20Bridge is
}
}

function withdraw(IChildERC20 childToken, uint256 amount) external payable {
_withdraw(childToken, msg.sender, amount);
}

function withdrawTo(IChildERC20 childToken, address receiver, uint256 amount) external payable {
_withdraw(childToken, receiver, amount);
}

function _withdraw(IChildERC20 childToken, address receiver, uint256 amount) private {
if (address(childToken).code.length == 0) {
revert EmptyTokenContract();
}

address rootToken = childToken.rootToken();

if (rootTokenToChildToken[rootToken] != address(childToken)) {
revert NotMapped();
}

// A mapped token should never have root token unset
if (rootToken == address(0)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should never happen, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

revert ZeroAddressRootToken();
}

// A mapped token should never have the bridge unset
if (childToken.bridge() != address(this)) {
revert BridgeNotSet();
}

if (!childToken.burn(msg.sender, amount)) {
revert BurnFailed();
}

// TODO Should we enforce receiver != 0? old poly contracts don't
Benjimmutable marked this conversation as resolved.
Show resolved Hide resolved

// Encode the message payload
bytes memory payload = abi.encode(WITHDRAW_SIG, rootToken, msg.sender, receiver, amount);

// Send the message to the bridge adaptor and up to root chain
bridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender);

emit ChildChainERC20Withdraw(rootToken, address(childToken), msg.sender, receiver, amount);
}

function _mapToken(bytes calldata data) private {
(, address rootToken, string memory name, string memory symbol, uint8 decimals) =
abi.decode(data, (bytes32, address, string, string, uint8));
Expand Down Expand Up @@ -185,7 +230,7 @@ contract ChildERC20Bridge is
if (address(rootToken) == NATIVE_ETH) {
emit NativeEthDeposit(address(rootToken), childToken, sender, receiver, amount);
} else {
emit ERC20Deposit(address(rootToken), childToken, sender, receiver, amount);
emit ChildChainERC20Deposit(address(rootToken), childToken, sender, receiver, amount);
}
} else {
Address.sendValue(payable(receiver), amount);
Expand Down
9 changes: 9 additions & 0 deletions src/interfaces/child/IChildAxelarBridgeAdaptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,13 @@ pragma solidity ^0.8.21;
interface IChildAxelarBridgeAdaptorErrors {
/// @notice Error when a zero address is given when not valid.
error ZeroAddress();
/// @notice Error when a message is sent with no gas payment.
error NoGas();
/// @notice Error when the caller is not the bridge.
error CallerNotBridge();
}

interface IChildAxelarBridgeAdaptorEvents {
/// @notice Emitted when an Axelar message is sent to the root chain.
event AxelarMessage(string indexed rootChain, string indexed rootBridgeAdaptor, bytes indexed payload);
}
16 changes: 15 additions & 1 deletion src/interfaces/child/IChildERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ interface IChildERC20BridgeEvents {
/// @notice Emitted when a map token message is received from the root chain and executed successfully.
event L2TokenMapped(address rootToken, address childToken);

event ERC20Deposit(
event ChildChainERC20Withdraw(
address indexed rootToken,
address indexed childToken,
address depositor,
address indexed receiver,
uint256 amount
);

event ChildChainERC20Deposit(
address indexed rootToken,
address indexed childToken,
address depositor,
Expand Down Expand Up @@ -70,4 +78,10 @@ interface IChildERC20BridgeErrors {
error InvalidSourceChain();
/// @notice Error when the source chain's message sender is not a recognised address.
error InvalidSourceAddress();
/// @notice Error when a given child token's root token is the zero address.
error ZeroAddressRootToken();
/// @notice Error when a given child token's bridge address is not set.
error BridgeNotSet();
/// @notice Error when a call to the given child token's `burn` function fails.
error BurnFailed();
}
11 changes: 9 additions & 2 deletions src/interfaces/child/IChildERC20BridgeAdaptor.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// SPDX-License-Identifier: Apache 2.0
pragma solidity ^0.8.21;

// TODO to be used when sending messages L2 -> L1
interface IChildERC20BridgeAdaptor {}
interface IChildERC20BridgeAdaptor {
/**
* @notice Send an arbitrary message to the root chain via the message passing protocol.
* @param payload The message to send, encoded in a `bytes` array.
* @param refundRecipient Used if the message passing protocol requires fees & pays back excess to a refund recipient.
* @dev `payable` because the message passing protocol may require a fee to be paid.
*/
function sendMessage(bytes calldata payload, address refundRecipient) external payable;
}
2 changes: 1 addition & 1 deletion src/interfaces/root/IRootAxelarBridgeAdaptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ interface IRootAxelarBridgeAdaptorErrors {

interface IRootAxelarBridgeAdaptorEvents {
/// @notice Emitted when an Axelar message is sent to the child chain.
event MapTokenAxelarMessage(string indexed childChain, string indexed childBridgeAdaptor, bytes indexed payload);
event AxelarMessage(string indexed childChain, string indexed childBridgeAdaptor, bytes indexed payload);
}
2 changes: 1 addition & 1 deletion src/interfaces/root/IRootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface IRootERC20BridgeEvents {
/// @notice Emitted when a map token message is sent to the child chain.
event L1TokenMapped(address indexed rootToken, address indexed childToken);
/// @notice Emitted when an ERC20 deposit message is sent to the child chain.
event ERC20Deposit(
event ChildChainERC20Deposit(
address indexed rootToken,
address indexed childToken,
address depositor,
Expand Down
5 changes: 2 additions & 3 deletions src/root/RootAxelarBridgeAdaptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ contract RootAxelarBridgeAdaptor is
using SafeERC20 for IERC20Metadata;

address public rootBridge;
/// @dev childChain could be immutable, but as of writing this Solidity does not support immutable strings.
/// see: https://ethereum.stackexchange.com/questions/127622/typeerror-immutable-variables-cannot-have-a-non-value-type
string public childBridgeAdaptor;
string public childChain;
IAxelarGateway public axelarGateway;
IAxelarGasService public gasService;
Expand Down Expand Up @@ -81,6 +80,6 @@ contract RootAxelarBridgeAdaptor is
);

axelarGateway.callContract(_childChain, _childBridgeAdaptor, payload);
emit MapTokenAxelarMessage(_childChain, _childBridgeAdaptor, payload);
emit AxelarMessage(_childChain, _childBridgeAdaptor, payload);
}
}
2 changes: 1 addition & 1 deletion src/root/RootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ contract RootERC20Bridge is
} else if (address(rootToken) == rootIMXToken) {
emit IMXDeposit(address(rootToken), msg.sender, receiver, amount);
} else {
emit ERC20Deposit(address(rootToken), childToken, msg.sender, receiver, amount);
emit ChildChainERC20Deposit(address(rootToken), childToken, msg.sender, receiver, amount);
}
}
}
18 changes: 18 additions & 0 deletions src/test/child/ChildERC20FailOnBurn.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache 2.0
// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.21;

import "../../child/ChildERC20.sol";

/**
* @title ChildERC20FailOnBurn
* @author Immutable (@Benjimmutable)
* @notice ChildERC20 contract, except burn always returns false. Used for testing.
* @dev USED FOR TESTING
*/
// solhint-disable reason-string
contract ChildERC20FailOnBurn is ChildERC20 {
function burn(address account, uint256 amount) public virtual override returns (bool) {
return false;
}
}
12 changes: 12 additions & 0 deletions src/test/child/MockChildAxelarGasService.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache 2.0
pragma solidity ^0.8.21;

contract MockChildAxelarGasService {
function payNativeGasForContractCall(
address sender,
string calldata destinationChain,
string calldata destinationAddress,
bytes calldata payload,
address refundAddress
) external payable {}
}
2 changes: 2 additions & 0 deletions src/test/child/MockChildAxelarGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ contract MockChildAxelarGateway {
function validateContractCall(bytes32, string calldata, string calldata, bytes32) external pure returns (bool) {
return true;
}

function callContract(string memory childChain, string memory childBridgeAdaptor, bytes memory payload) external {}
}
14 changes: 10 additions & 4 deletions test/integration/child/ChildAxelarBridge.t.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: Apache 2.0
pragma solidity ^0.8.21;

import {Test, console2} from "forge-std/Test.sol";
Expand All @@ -13,6 +13,7 @@ import {
} from "../../../src/child/ChildERC20Bridge.sol";
import {IChildERC20, ChildERC20} from "../../../src/child/ChildERC20.sol";
import {MockChildAxelarGateway} from "../../../src/test/child/MockChildAxelarGateway.sol";
import {MockChildAxelarGasService} from "../../../src/test/child/MockChildAxelarGasService.sol";
import {Utils} from "../../utils.t.sol";

contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils {
Expand All @@ -25,13 +26,15 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil
ChildERC20 public childERC20;
ChildAxelarBridgeAdaptor public childAxelarBridgeAdaptor;
MockChildAxelarGateway public mockChildAxelarGateway;
MockChildAxelarGasService public mockChildAxelarGasService;

function setUp() public {
childERC20 = new ChildERC20();
childERC20.initialize(address(123), "Test", "TST", 18);

childERC20Bridge = new ChildERC20Bridge();
mockChildAxelarGateway = new MockChildAxelarGateway();
mockChildAxelarGasService = new MockChildAxelarGasService();
childAxelarBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway));

childERC20Bridge.initialize(
Expand All @@ -42,7 +45,9 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil
IMX_TOKEN_ADDRESS
);

childAxelarBridgeAdaptor.initialize(address(childERC20Bridge));
childAxelarBridgeAdaptor.initialize(
ROOT_CHAIN_NAME, address(childERC20Bridge), address(mockChildAxelarGasService)
);
}

function test_ChildTokenMap() public {
Expand Down Expand Up @@ -130,7 +135,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil
/*
* DEPOSIT
*/
function test_deposit_EmitsERC20Deposit() public {
function test_deposit_EmitsChildChainERC20Deposit() public {
address rootTokenAddress = address(456);
address sender = address(0xff);
address receiver = address(0xee);
Expand All @@ -139,7 +144,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil
bytes32 commandId = bytes32("testCommandId");

vm.expectEmit(address(childERC20Bridge));
emit ERC20Deposit(rootTokenAddress, childToken, sender, receiver, amount);
emit ChildChainERC20Deposit(rootTokenAddress, childToken, sender, receiver, amount);

childAxelarBridgeAdaptor.execute(
commandId,
Expand Down Expand Up @@ -294,6 +299,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil
address rootAddress = address(0x123);
{
// Slot is 2 because of the Ownable, Initializable contracts coming first.
// Found by running `forge inspect src/child/ChildERC20Bridge.sol:ChildERC20Bridge storageLayout | grep -B3 -A5 -i "rootTokenToChildToken"`
uint256 rootTokenToChildTokenMappingSlot = 2;
address childAddress = address(444444);
bytes32 slot = getMappingStorageSlotFor(rootAddress, rootTokenToChildTokenMappingSlot);
Expand Down
Loading