diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 03468768..90c76c42 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,19 @@ on: branches: [main] jobs: + forge-test: + name: Run Forge Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Run tests + run: forge test -vvv hardhat-test: name: Run Hardhat Tests runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 1ec01ef3..2e96d52f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ dist/ # Forge files foundry-out/ + +# VSCode Config +.vscode/ diff --git a/contracts/bridge/child/ChildAxelarBridgeAdaptor.sol b/contracts/bridge/child/ChildAxelarBridgeAdaptor.sol new file mode 100644 index 00000000..6c22af4c --- /dev/null +++ b/contracts/bridge/child/ChildAxelarBridgeAdaptor.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {AxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol"; +import {IChildERC20Bridge} from "../interfaces/child/IChildERC20Bridge.sol"; +import {IChildAxelarBridgeAdaptorErrors} from "../interfaces/child/IChildAxelarBridgeAdaptor.sol"; + +contract ChildAxelarBridgeAdaptor is AxelarExecutable, IChildAxelarBridgeAdaptorErrors { + /// @notice Address of bridge to relay messages to. + IChildERC20Bridge public immutable CHILD_BRIDGE; + + constructor(address _gateway, address _childBridge) AxelarExecutable(_gateway) { + if (_childBridge == address(0)) { + revert ZeroAddress(); + } + + CHILD_BRIDGE = IChildERC20Bridge(_childBridge); + } + + /** + * @dev This function is called by the parent `AxelarExecutable` contract to execute the payload. + * @custom:assumes `sourceAddress_` is a 20 byte address. + */ + function _execute( + string calldata sourceChain_, + string calldata sourceAddress_, + bytes calldata payload_ + ) internal override { + CHILD_BRIDGE.onMessageReceive(sourceChain_, sourceAddress_, payload_); + } +} diff --git a/contracts/bridge/child/ChildERC20.sol b/contracts/bridge/child/ChildERC20.sol new file mode 100644 index 00000000..f0839f0b --- /dev/null +++ b/contracts/bridge/child/ChildERC20.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol) +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../lib/EIP712MetaTransaction.sol"; +import "../interfaces/child/IChildERC20.sol"; + +/** + * @title ChildERC20 + * @author Polygon Technology (@QEDK) + * @notice Child token template for ChildERC20 predicate deployments + * @dev All child tokens are clones of this contract. Burning and minting is controlled by respective predicates only. + */ +// solhint-disable reason-string +contract ChildERC20 is EIP712MetaTransaction, ERC20Upgradeable, IChildERC20 { + address private _bridge; + address private _rootToken; + uint8 private _decimals; + + modifier onlyBridge() { + require(msg.sender == _bridge, "ChildERC20: Only bridge can call"); + _; + } + + /** + * @inheritdoc IChildERC20 + */ + function initialize( + address rootToken_, + string calldata name_, + string calldata symbol_, + uint8 decimals_ + ) external initializer { + require( + rootToken_ != address(0) && bytes(name_).length != 0 && bytes(symbol_).length != 0, + "ChildERC20: BAD_INITIALIZATION" + ); + _rootToken = rootToken_; + _decimals = decimals_; + _bridge = msg.sender; + __ERC20_init(name_, symbol_); + _initializeEIP712(name_, "1"); + } + + /** + * @notice Returns the decimals places of the token + * @return uint8 Returns the decimals places of the token. + */ + function decimals() public view virtual override(ERC20Upgradeable, IERC20MetadataUpgradeable) returns (uint8) { + return _decimals; + } + + /** + * @inheritdoc IChildERC20 + */ + function bridge() external view virtual returns (address) { + return _bridge; + } + + /** + * @inheritdoc IChildERC20 + */ + function rootToken() external view virtual returns (address) { + return _rootToken; + } + + /** + * @inheritdoc IChildERC20 + */ + function mint(address account, uint256 amount) external virtual onlyBridge returns (bool) { + _mint(account, amount); + + return true; + } + + /** + * @inheritdoc IChildERC20 + */ + function burn(address account, uint256 amount) external virtual onlyBridge returns (bool) { + _burn(account, amount); + + return true; + } + + function _msgSender() internal view virtual override(EIP712MetaTransaction, ContextUpgradeable) returns (address) { + return EIP712MetaTransaction._msgSender(); + } +} diff --git a/contracts/bridge/child/ChildERC20Bridge.sol b/contracts/bridge/child/ChildERC20Bridge.sol new file mode 100644 index 00000000..91be6aa6 --- /dev/null +++ b/contracts/bridge/child/ChildERC20Bridge.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; // TODO hardhat config compiles with 0.8.17. We should investigate upgrading this. + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {IChildERC20BridgeEvents, IChildERC20BridgeErrors, IChildERC20Bridge, IERC20Metadata} from "../interfaces/child/IChildERC20Bridge.sol"; +import {IChildERC20BridgeAdaptor} from "../interfaces/child/IChildERC20BridgeAdaptor.sol"; +import {IChildERC20} from "../interfaces/child/IChildERC20.sol"; + +/** + * @notice RootERC20Bridge is a bridge that allows ERC20 tokens to be transferred from the root chain to the child chain. + * @dev This contract is designed to be upgradeable. + * @dev Follows a pattern of using a bridge adaptor to communicate with the child chain. This is because the underlying communication protocol may change, + * and also allows us to decouple vendor-specific messaging logic from the bridge logic. + * @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 ChildERC20Bridge is + Ownable2Step, + Initializable, + IChildERC20BridgeErrors, + IChildERC20Bridge, + IChildERC20BridgeEvents +{ + using SafeERC20 for IERC20Metadata; + + IChildERC20BridgeAdaptor public bridgeAdaptor; + /// @dev The address that will be sending messages to, and receiving messages from, the child chain. + string public rootERC20BridgeAdaptor; + /// @dev The address of the token template that will be cloned to create tokens. + address public childTokenTemplate; + /// @dev The name of the chain that this bridge is connected to. + string public rootChain; + mapping(address => address) public rootTokenToChildToken; + + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + + /** + * @notice Initilization function for RootERC20Bridge. + * @param newBridgeAdaptor Address of StateSender to send deposit information to. + * @param newRootERC20BridgeAdaptor Stringified address of root ERC20 bridge adaptor to communicate with. + * @param newChildTokenTemplate Address of child token template to clone. + * @param newRootChain A stringified representation of the chain that this bridge is connected to. Used for validation. + * @dev Can only be called once. + */ + function initialize( + address newBridgeAdaptor, + string memory newRootERC20BridgeAdaptor, + address newChildTokenTemplate, + string memory newRootChain + ) public initializer { + if ( + newBridgeAdaptor == address(0) || + newChildTokenTemplate == address(0) + ) { + revert ZeroAddress(); + } + + if (bytes(newRootERC20BridgeAdaptor).length == 0) { + revert InvalidRootERC20BridgeAdaptor(); + } + + if (bytes(newRootChain).length == 0) { + revert InvalidRootChain(); + } + + rootERC20BridgeAdaptor = newRootERC20BridgeAdaptor; + childTokenTemplate = newChildTokenTemplate; + bridgeAdaptor = IChildERC20BridgeAdaptor(newBridgeAdaptor); + rootChain = newRootChain; + } + + /** + * @inheritdoc IChildERC20Bridge + * @dev This is only callable by the child chain bridge adaptor. + * @dev Validates `sourceAddress` is the root chain's bridgeAdaptor. + */ + function onMessageReceive( + string calldata messageSourceChain, + string calldata sourceAddress, + bytes calldata data + ) external override { + if (msg.sender != address(bridgeAdaptor)) { + revert NotBridgeAdaptor(); + } + if (!Strings.equal(messageSourceChain, rootChain)) { + revert InvalidSourceChain(); + } + if (!Strings.equal(sourceAddress, rootERC20BridgeAdaptor)) { + revert InvalidSourceAddress(); + } + if (data.length == 0) { + revert InvalidData(); + } + + if (bytes32(data[:32]) == MAP_TOKEN_SIG) { + _mapToken(data); + } else { + revert InvalidData(); + } + } + + function _mapToken(bytes calldata data) private { + (, address rootToken, string memory name, string memory symbol, uint8 decimals) = abi.decode( + data, + (bytes32, address, string, string, uint8) + ); + + if (address(rootToken) == address(0)) { + revert ZeroAddress(); + } + + if (rootTokenToChildToken[address(rootToken)] != address(0)) { + revert AlreadyMapped(); + } + + IChildERC20 childToken = IChildERC20( + Clones.cloneDeterministic(childTokenTemplate, keccak256(abi.encodePacked(rootToken))) + ); + + rootTokenToChildToken[rootToken] = address(childToken); + childToken.initialize(rootToken, name, symbol, decimals); + + emit L2TokenMapped(address(rootToken), address(childToken)); + } + + function updateBridgeAdaptor(address newBridgeAdaptor) external override onlyOwner { + if (newBridgeAdaptor == address(0)) { + revert ZeroAddress(); + } + bridgeAdaptor = IChildERC20BridgeAdaptor(newBridgeAdaptor); + } +} diff --git a/contracts/bridge/interfaces/child/IChildAxelarBridgeAdaptor.sol b/contracts/bridge/interfaces/child/IChildAxelarBridgeAdaptor.sol new file mode 100644 index 00000000..d9bed0cc --- /dev/null +++ b/contracts/bridge/interfaces/child/IChildAxelarBridgeAdaptor.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +interface IChildAxelarBridgeAdaptorErrors { + error ZeroAddress(); +} diff --git a/contracts/bridge/interfaces/child/IChildERC20.sol b/contracts/bridge/interfaces/child/IChildERC20.sol new file mode 100644 index 00000000..0b51ab65 --- /dev/null +++ b/contracts/bridge/interfaces/child/IChildERC20.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.17; + +import {IERC20MetadataUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; + +/** + * @dev Interface of IChildERC20 + */ +interface IChildERC20 is IERC20MetadataUpgradeable { + /** + * @dev Sets the values for {rootToken}, {name}, {symbol} and {decimals}. + * + * All these values are immutable: they can only be set once during + * initialization. + */ + function initialize(address rootToken_, string calldata name_, string calldata symbol_, uint8 decimals_) external; + + /** + * @notice Returns bridge address controlling the child token + * @return address Returns the address of the Bridge + */ + function bridge() external view returns (address); + + /** + * @notice Returns bridge address controlling the child token + * @return address Returns the address of the Bridge + */ + function rootToken() external view returns (address); + + /** + * @notice Mints an amount of tokens to a particular address + * @dev Can only be called by the predicate address + * @param account Account of the user to mint the tokens to + * @param amount Amount of tokens to mint to the account + * @return bool Returns true if function call is succesful + */ + function mint(address account, uint256 amount) external returns (bool); + + /** + * @notice Burns an amount of tokens from a particular address + * @dev Can only be called by the predicate address + * @param account Account of the user to burn the tokens from + * @param amount Amount of tokens to burn from the account + * @return bool Returns true if function call is succesful + */ + function burn(address account, uint256 amount) external returns (bool); +} \ No newline at end of file diff --git a/contracts/bridge/interfaces/child/IChildERC20Bridge.sol b/contracts/bridge/interfaces/child/IChildERC20Bridge.sol new file mode 100644 index 00000000..b9974ff2 --- /dev/null +++ b/contracts/bridge/interfaces/child/IChildERC20Bridge.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +interface IChildERC20Bridge { + /** + * @notice Receives a bridge message from root 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 Sets a new bridge adaptor address to receive and send function calls for L1 messages + * @param newBridgeAdaptor The new child chain bridge adaptor address. + */ + function updateBridgeAdaptor(address newBridgeAdaptor) external; +} + +interface IChildERC20BridgeEvents { + event L2TokenMapped(address rootToken, address childToken); +} + +interface IChildERC20BridgeErrors { + error ZeroAddress(); + error AlreadyMapped(); + error NotBridgeAdaptor(); + error InvalidData(); + error InvalidSourceChain(); + error InvalidSourceAddress(); + error InvalidRootChain(); + error InvalidRootERC20BridgeAdaptor(); +} diff --git a/contracts/bridge/interfaces/child/IChildERC20BridgeAdaptor.sol b/contracts/bridge/interfaces/child/IChildERC20BridgeAdaptor.sol new file mode 100644 index 00000000..9e3c652f --- /dev/null +++ b/contracts/bridge/interfaces/child/IChildERC20BridgeAdaptor.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +interface IChildERC20BridgeAdaptor {} diff --git a/contracts/bridge/interfaces/root/IRootAxelarBridgeAdaptor.sol b/contracts/bridge/interfaces/root/IRootAxelarBridgeAdaptor.sol new file mode 100644 index 00000000..fcca336e --- /dev/null +++ b/contracts/bridge/interfaces/root/IRootAxelarBridgeAdaptor.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +interface IRootAxelarBridgeAdaptorErrors { + error ZeroAddresses(); + error InvalidChildChain(); + error NoGas(); + error CallerNotBridge(); + error InvalidArrayLengths(); +} + +interface IRootAxelarBridgeAdaptorEvents { + event MapTokenAxelarMessage(string indexed childChain, string indexed childBridgeAdaptor, bytes indexed payload); +} diff --git a/contracts/bridge/interfaces/root/IRootERC20Bridge.sol b/contracts/bridge/interfaces/root/IRootERC20Bridge.sol new file mode 100644 index 00000000..b2f68a5e --- /dev/null +++ b/contracts/bridge/interfaces/root/IRootERC20Bridge.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +interface IRootERC20Bridge { + function mapToken(IERC20Metadata rootToken) external payable returns (address); +} + +interface IRootERC20BridgeEvents { + event L1TokenMapped(address rootToken, address childToken); +} + +interface IRootERC20BridgeErrors { + error ZeroAddress(); + error AlreadyMapped(); +} diff --git a/contracts/bridge/interfaces/root/IRootERC20BridgeAdaptor.sol b/contracts/bridge/interfaces/root/IRootERC20BridgeAdaptor.sol new file mode 100644 index 00000000..b92e08bc --- /dev/null +++ b/contracts/bridge/interfaces/root/IRootERC20BridgeAdaptor.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +// TODO: This is likely able to become a generic bridge adaptor, not just for ERC20 tokens. +interface IRootERC20BridgeAdaptor { + /** + * @notice Send an arbitrary message to the child 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; +} diff --git a/contracts/bridge/lib/EIP712MetaTransaction.sol b/contracts/bridge/lib/EIP712MetaTransaction.sol new file mode 100644 index 00000000..23a93916 --- /dev/null +++ b/contracts/bridge/lib/EIP712MetaTransaction.sol @@ -0,0 +1,113 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "./EIP712Upgradeable.sol"; + +// solhint-disable reason-string +contract EIP712MetaTransaction is EIP712Upgradeable { + bytes32 private constant META_TRANSACTION_TYPEHASH = + keccak256(bytes("MetaTransaction(uint256 nonce,address from,bytes functionSignature)")); + + event MetaTransactionExecuted(address userAddress, address relayerAddress, bytes functionSignature); + mapping(address => uint256) private nonces; + + /* + * Meta transaction structure. + * No point of including value field here as if user is doing value transfer then he has the funds to pay for gas + * He should call the desired function directly in that case. + */ + struct MetaTransaction { + uint256 nonce; + address from; + bytes functionSignature; + } + + function executeMetaTransaction( + address userAddress, + bytes calldata functionSignature, + bytes32 sigR, + bytes32 sigS, + uint8 sigV + ) external returns (bytes memory) { + bytes4 destinationFunctionSig = _convertBytesToBytes4(functionSignature); + + require(destinationFunctionSig != msg.sig, "functionSignature can not be of executeMetaTransaction method"); + + MetaTransaction memory metaTx = MetaTransaction({ + nonce: nonces[userAddress], + from: userAddress, + functionSignature: functionSignature + }); + + require(_verify(userAddress, metaTx, sigR, sigS, sigV), "Signer and signature do not match"); + + unchecked { + ++nonces[userAddress]; + } + // Append userAddress at the end to extract it from calling context + // slither-disable-next-line low-level-calls + (bool success, bytes memory returnData) = address(this).call(abi.encodePacked(functionSignature, userAddress)); // solhint-disable avoid-low-level-calls + + require(success, "Function call not successful"); + // slither-disable-next-line reentrancy-events + emit MetaTransactionExecuted(userAddress, msg.sender, functionSignature); + return returnData; + } + + /** + @dev Invalidates next "offset" number of nonces for the calling address + */ + function invalidateNext(uint256 offset) external { + nonces[msg.sender] += offset; + } + + function getNonce(address user) external view returns (uint256 nonce) { + nonce = nonces[user]; + } + + function _msgSender() internal view virtual returns (address sender) { + if (msg.sender == address(this)) { + bytes memory array = msg.data; + uint256 index = msg.data.length; + // slither-disable-next-line assembly + assembly { + // solhint-disable no-inline-assembly + // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. + sender := and(mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff) + } + } else { + sender = msg.sender; + } + return sender; + } + + function _verify( + address user, + MetaTransaction memory metaTx, + bytes32 sigR, + bytes32 sigS, + uint8 sigV + ) private view returns (bool) { + address signer = ecrecover(_hashTypedDataV4(_hashMetaTransaction(metaTx)), sigV, sigR, sigS); + require(signer != address(0), "Invalid signature"); + return signer == user; + } + + function _hashMetaTransaction(MetaTransaction memory metaTx) private pure returns (bytes32) { + return + keccak256( + abi.encode(META_TRANSACTION_TYPEHASH, metaTx.nonce, metaTx.from, keccak256(metaTx.functionSignature)) + ); + } + + function _convertBytesToBytes4(bytes memory inBytes) private pure returns (bytes4 outBytes4) { + if (inBytes.length == 0) { + return 0x0; + } + // slither-disable-next-line assembly + assembly { + // solhint-disable no-inline-assembly + outBytes4 := mload(add(inBytes, 32)) + } + } +} \ No newline at end of file diff --git a/contracts/bridge/lib/EIP712Upgradeable.sol b/contracts/bridge/lib/EIP712Upgradeable.sol new file mode 100644 index 00000000..cbd92b6d --- /dev/null +++ b/contracts/bridge/lib/EIP712Upgradeable.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +// slither-disable-start naming-convention +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/EIP712.sol) + +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/** + * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. + * + * The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible, + * thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding + * they need in their contracts using a combination of `abi.encode` and `keccak256`. + * + * This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding + * scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA + * ({_hashTypedDataV4}). + * + * The implementation of the domain separator was designed to be as efficient as possible while still properly updating + * the chain id to protect against replay attacks on an eventual fork of the chain. + * + * NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method + * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. + * + * _Available since v3.4._ + */ +abstract contract EIP712Upgradeable { + /* solhint-disable var-name-mixedcase */ + // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to + // invalidate the cached domain separator if the chain id changes. + bytes32 private _CACHED_DOMAIN_SEPARATOR; + uint256 private _CACHED_CHAIN_ID; + address private _CACHED_THIS; + + bytes32 private _HASHED_NAME; + bytes32 private _HASHED_VERSION; + bytes32 private _TYPE_HASH; + + /* solhint-enable var-name-mixedcase */ + + /** + * @dev Initializes the domain separator and parameter caches. + * + * The meaning of `name` and `version` is specified in + * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]: + * + * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol. + * - `version`: the current major version of the signing domain. + * + * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart + * contract upgrade]. + */ + function _initializeEIP712(string memory name, string memory version) internal { + bytes32 hashedName = keccak256(bytes(name)); + bytes32 hashedVersion = keccak256(bytes(version)); + bytes32 typeHash = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + _HASHED_NAME = hashedName; + _HASHED_VERSION = hashedVersion; + _CACHED_CHAIN_ID = block.chainid; + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion); + _CACHED_THIS = address(this); + _TYPE_HASH = typeHash; + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { + if (address(this) == _CACHED_THIS && block.chainid == _CACHED_CHAIN_ID) { + return _CACHED_DOMAIN_SEPARATOR; + } else { + return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION); + } + } + + function _buildDomainSeparator( + bytes32 typeHash, + bytes32 nameHash, + bytes32 versionHash + ) private view returns (bytes32) { + return keccak256(abi.encode(typeHash, nameHash, versionHash, block.chainid, address(this))); + } + + /** + * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this + * function returns the hash of the fully encoded EIP712 message for this domain. + * + * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: + * + * ```solidity + * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + * keccak256("Mail(address to,string contents)"), + * mailTo, + * keccak256(bytes(mailContents)) + * ))); + * address signer = ECDSA.recover(digest, signature); + * ``` + */ + function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { + return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); + } +} +// slither-disable-end naming-convention \ No newline at end of file diff --git a/contracts/bridge/root/RootAxelarBridgeAdaptor.sol b/contracts/bridge/root/RootAxelarBridgeAdaptor.sol new file mode 100644 index 00000000..f220253c --- /dev/null +++ b/contracts/bridge/root/RootAxelarBridgeAdaptor.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; +import {IAxelarGasService} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol"; +import {IRootERC20BridgeAdaptor} from "../interfaces/root/IRootERC20BridgeAdaptor.sol"; +import {IRootAxelarBridgeAdaptorEvents, IRootAxelarBridgeAdaptorErrors} from "../interfaces/root/IRootAxelarBridgeAdaptor.sol"; + +// TODO Note: this will have to be an AxelarExecutable contract in order to receive messages + +/** + * @notice RootAxelarBridgeAdaptor is a bridge adaptor that allows the RootERC20Bridge to communicate with the Axelar Gateway. + * @dev This is not an upgradeable contract, because it is trivial to deploy a new one if needed. + */ +contract RootAxelarBridgeAdaptor is IRootERC20BridgeAdaptor, IRootAxelarBridgeAdaptorEvents, IRootAxelarBridgeAdaptorErrors { + using SafeERC20 for IERC20Metadata; + + address public immutable ROOT_BRIDGE; + /// @dev childBridgeAdaptor & 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 immutable AXELAR_GATEWAY; + IAxelarGasService public immutable GAS_SERVICE; + mapping(uint256 => string) public chainIdToChainName; + + constructor( + address _rootBridge, + address _childBridgeAdaptor, + string memory _childChain, + address _axelarGateway, + address _gasService + ) { + if ( + _rootBridge == address(0) || + _childBridgeAdaptor == address(0) || + _axelarGateway == address(0) || + _gasService == address(0) + ) { + revert ZeroAddresses(); + } + + if (bytes(_childChain).length == 0) { + revert InvalidChildChain(); + } + ROOT_BRIDGE = _rootBridge; + childBridgeAdaptor = Strings.toHexString(_childBridgeAdaptor); + childChain = _childChain; + AXELAR_GATEWAY = IAxelarGateway(_axelarGateway); + GAS_SERVICE = IAxelarGasService(_gasService); + } + + /** + * @inheritdoc IRootERC20BridgeAdaptor + * @notice Sends an arbitrary message to the child chain, via the Axelar network. + */ + function sendMessage(bytes calldata payload, address refundRecipient) external payable override { + if (msg.value == 0) { + revert NoGas(); + } + if (msg.sender != ROOT_BRIDGE) { + revert CallerNotBridge(); + } + + // Load from storage. + string memory _childBridgeAdaptor = childBridgeAdaptor; + string memory _childChain = childChain; + + // TODO For other functions (depositing to chain), the refund recipient should be the user doing the deposit + GAS_SERVICE.payNativeGasForContractCall{value: msg.value}( + address(this), + _childChain, + _childBridgeAdaptor, + payload, + refundRecipient + ); + + AXELAR_GATEWAY.callContract(_childChain, _childBridgeAdaptor, payload); + emit MapTokenAxelarMessage(_childChain, _childBridgeAdaptor, payload); + } + + // TODO future tickets + function receiveWithdrawMessage(bytes calldata payload) external { + // TODO + } + + function sendDepositMessage(address l1Token, address recipient, uint256 amount) external { + // TODO + } +} diff --git a/contracts/bridge/root/RootERC20Bridge.sol b/contracts/bridge/root/RootERC20Bridge.sol new file mode 100644 index 00000000..aceff5c1 --- /dev/null +++ b/contracts/bridge/root/RootERC20Bridge.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; // TODO hardhat config compiles with 0.8.17. We should investigate upgrading this. + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; +import {IRootERC20Bridge, IERC20Metadata} from "../interfaces/root/IRootERC20Bridge.sol"; +import {IRootERC20BridgeEvents, IRootERC20BridgeErrors} from "../interfaces/root/IRootERC20Bridge.sol"; +import {IRootERC20BridgeAdaptor} from "../interfaces/root/IRootERC20BridgeAdaptor.sol"; + +/** + * @notice RootERC20Bridge is a bridge that allows ERC20 tokens to be transferred from the root chain to the child chain. + * @dev This contract is designed to be upgradeable. + * @dev Follows a pattern of using a bridge adaptor to communicate with the child chain. This is because the underlying communication protocol may change, + * and also allows us to decouple vendor-specific messaging logic from the bridge logic. + * @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, + IRootERC20Bridge, + IRootERC20BridgeEvents, + IRootERC20BridgeErrors +{ + using SafeERC20 for IERC20Metadata; + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + + IRootERC20BridgeAdaptor public bridgeAdaptor; + /// @dev The address that will be minting tokens on the child chain. + address public childERC20Bridge; + /// @dev The address of the token template that will be cloned to create tokens on the child chain. + address public childTokenTemplate; + mapping(address => address) public rootTokenToChildToken; + + /** + * @notice Initilization function for RootERC20Bridge. + * @param newBridgeAdaptor Address of StateSender to send bridge messages to, and receive messages from. + * @param newChildERC20Bridge Address of child ERC20 bridge to communicate with. + * @param newChildTokenTemplate Address of child token template to clone. + * @dev Can only be called once. + */ + function initialize( + address newBridgeAdaptor, + address newChildERC20Bridge, + address newChildTokenTemplate + ) public initializer { + if ( + newBridgeAdaptor == address(0) || newChildERC20Bridge == address(0) || newChildTokenTemplate == address(0) + ) { + revert ZeroAddress(); + } + childERC20Bridge = newChildERC20Bridge; + childTokenTemplate = newChildTokenTemplate; + bridgeAdaptor = IRootERC20BridgeAdaptor(newBridgeAdaptor); + } + + /** + * @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. + * Additionally, we need to investigate what the ordering guarantees are. i.e. if we send a map token message, then a bridge token message, + * in the same TX (or even very close but separate transactions), is it possible the order gets reversed? This could potentially make some + * first bridges break and we might then have to separate them and wait for the map to be confirmed. + */ + function mapToken(IERC20Metadata rootToken) external payable override returns (address) { + if (address(rootToken) == address(0)) { + revert ZeroAddress(); + } + if (rootTokenToChildToken[address(rootToken)] != address(0)) { + revert AlreadyMapped(); + } + + address childBridge = childERC20Bridge; + + address childToken = Clones.predictDeterministicAddress( + childTokenTemplate, + keccak256(abi.encodePacked(rootToken)), + childBridge + ); + + rootTokenToChildToken[address(rootToken)] = childToken; + + bytes memory payload = abi.encode( + MAP_TOKEN_SIG, + rootToken, + rootToken.name(), + rootToken.symbol(), + rootToken.decimals() + ); + // TODO investigate using delegatecall to keep the axelar message sender as the bridge contract, since adaptor can change. + bridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender); + + emit L1TokenMapped(address(rootToken), childToken); + return childToken; + } + + function updateBridgeAdaptor(address newBridgeAdaptor) external onlyOwner { + if (newBridgeAdaptor == address(0)) { + revert ZeroAddress(); + } + bridgeAdaptor = IRootERC20BridgeAdaptor(newBridgeAdaptor); + } +} diff --git a/contracts/bridge/test/child/MockChildAxelarGateway.sol b/contracts/bridge/test/child/MockChildAxelarGateway.sol new file mode 100644 index 00000000..c2f5655d --- /dev/null +++ b/contracts/bridge/test/child/MockChildAxelarGateway.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +contract MockChildAxelarGateway { + function validateContractCall( + bytes32 , + string calldata , + string calldata , + bytes32 + ) external pure returns (bool) { + return true; + } +} diff --git a/contracts/bridge/test/child/MockChildERC20Bridge.sol b/contracts/bridge/test/child/MockChildERC20Bridge.sol new file mode 100644 index 00000000..756df8a2 --- /dev/null +++ b/contracts/bridge/test/child/MockChildERC20Bridge.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +contract MockChildERC20Bridge { + function onMessageReceive( + string calldata, + string calldata, + bytes calldata + ) external {} +} diff --git a/contracts/bridge/test/root/MockAdaptor.sol b/contracts/bridge/test/root/MockAdaptor.sol new file mode 100644 index 00000000..49007c27 --- /dev/null +++ b/contracts/bridge/test/root/MockAdaptor.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +// @dev A contract for ensuring the Axelar Bridge Adaptor is called correctly during unit tests. +contract MockAdaptor { + function sendMessage(bytes calldata , address) external payable{} +} diff --git a/contracts/bridge/test/root/MockAxelarGasService.sol b/contracts/bridge/test/root/MockAxelarGasService.sol new file mode 100644 index 00000000..81108cb1 --- /dev/null +++ b/contracts/bridge/test/root/MockAxelarGasService.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +// @dev A contract for ensuring the Axelar gas service is called correctly during unit tests. +contract MockAxelarGasService { + function payNativeGasForContractCall( + address sender, + string calldata, + string calldata, + bytes calldata, + address refundAddress + ) external payable {} +} diff --git a/contracts/bridge/test/root/MockAxelarGateway.sol b/contracts/bridge/test/root/MockAxelarGateway.sol new file mode 100644 index 00000000..1673d970 --- /dev/null +++ b/contracts/bridge/test/root/MockAxelarGateway.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +// @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 {} +} diff --git a/package.json b/package.json index bb6ecdc0..34accd3f 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,10 @@ "typescript": "^4.9.5" }, "dependencies": { + "@axelar-network/axelar-gmp-sdk-solidity": "^5.3.3", "@openzeppelin/contracts": "^4.9.3", + "@openzeppelin/contracts-upgradeable": "^4.9.3", "solidity-bits": "^0.4.0", "solidity-bytes-utils": "^0.8.0" } -} +} \ No newline at end of file diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 00000000..fcb2f1d4 --- /dev/null +++ b/remappings.txt @@ -0,0 +1 @@ +@axelar-cgp-solidity=node_modules/@axelar-network/axelar-gmp-sdk-solidity \ No newline at end of file diff --git a/test/bridge/integration/child/ChildAxelarBridge.t.sol b/test/bridge/integration/child/ChildAxelarBridge.t.sol new file mode 100644 index 00000000..41e507ca --- /dev/null +++ b/test/bridge/integration/child/ChildAxelarBridge.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Test, console2} from "forge-std/Test.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {ChildAxelarBridgeAdaptor} from "../../../../contracts/bridge/child/ChildAxelarBridgeAdaptor.sol"; +import {ChildERC20Bridge, IChildERC20BridgeEvents, IERC20Metadata, IChildERC20BridgeErrors } from "../../../../contracts/bridge/child/ChildERC20Bridge.sol"; +import {IChildERC20, ChildERC20} from "../../../../contracts/bridge/child/ChildERC20.sol"; +import {MockChildAxelarGateway} from "../../../../contracts/bridge/test/child/MockChildAxelarGateway.sol"; + +contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors { + string public ROOT_ADAPTOR_ADDRESS = Strings.toHexString(address(1)); + string public ROOT_CHAIN_NAME = "ROOT_CHAIN"; + + ChildERC20Bridge public childERC20Bridge; + ChildERC20 public childERC20; + ChildAxelarBridgeAdaptor public childAxelarBridgeAdaptor; + MockChildAxelarGateway public mockChildAxelarGateway; + + function setUp() public { + childERC20 = new ChildERC20(); + childERC20.initialize(address(123), "Test", "TST", 18); + + childERC20Bridge = new ChildERC20Bridge(); + mockChildAxelarGateway = new MockChildAxelarGateway(); + childAxelarBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway), address(childERC20Bridge)); + + childERC20Bridge.initialize(address(childAxelarBridgeAdaptor), ROOT_ADAPTOR_ADDRESS, address(childERC20), ROOT_CHAIN_NAME); + } + + function test_ChildTokenMap() public { + address rootTokenAddress = address(456); + string memory name = "test name"; + string memory symbol = "TSTNME"; + uint8 decimals = 17; + + bytes32 commandId = bytes32("testCommandId"); + bytes memory payload = abi.encode(childERC20Bridge.MAP_TOKEN_SIG(), rootTokenAddress, name, symbol, decimals); + + address predictedAddress = Clones.predictDeterministicAddress(address(childERC20), keccak256(abi.encodePacked(rootTokenAddress)), address(childERC20Bridge)); + vm.expectEmit(true, true, false, false, address(childERC20Bridge)); + emit L2TokenMapped(rootTokenAddress, predictedAddress); + + // vm.prank(ROOT_ADAPTOR_ADDRESS); + childAxelarBridgeAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_ADAPTOR_ADDRESS, payload); + + assertEq(childERC20Bridge.rootTokenToChildToken(rootTokenAddress), predictedAddress); + + IChildERC20 childToken = IChildERC20(predictedAddress); + assertEq(childToken.name(), name); + assertEq(childToken.symbol(), symbol); + assertEq(childToken.decimals(), decimals); + } + + function test_RevertsIf_payloadDataNotValid() public { + bytes32 commandId = bytes32("testCommandId"); + bytes memory payload = abi.encode("invalid payload"); + + vm.expectRevert(InvalidData.selector); + childAxelarBridgeAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_ADAPTOR_ADDRESS, payload); + } + + function test_RevertsIf_rootTokenAddressIsZero() public { + bytes32 commandId = bytes32("testCommandId"); + bytes memory payload = abi.encode(childERC20Bridge.MAP_TOKEN_SIG(), address(0), "test name", "TSTNME", 17); + + vm.expectRevert(ZeroAddress.selector); + childAxelarBridgeAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_ADAPTOR_ADDRESS, payload); + } + + function test_RevertsIf_MapTwice() public { + bytes32 commandId = bytes32("testCommandId"); + bytes memory payload = abi.encode(childERC20Bridge.MAP_TOKEN_SIG(), address(456), "test name", "TSTNME", 17); + + childAxelarBridgeAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_ADAPTOR_ADDRESS, payload); + + vm.expectRevert(AlreadyMapped.selector); + childAxelarBridgeAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_ADAPTOR_ADDRESS, payload); + } + + function test_RevertsIf_EmptyData() public { + bytes32 commandId = bytes32("testCommandId"); + bytes memory payload = ""; + + vm.expectRevert(InvalidData.selector); + childAxelarBridgeAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_ADAPTOR_ADDRESS, payload); + } + + function test_RevertsIf_InvalidSourceChain() public { + bytes32 commandId = bytes32("testCommandId"); + bytes memory payload = abi.encode(childERC20Bridge.MAP_TOKEN_SIG(), address(456), "test name", "TSTNME", 17); + + vm.expectRevert(InvalidSourceChain.selector); + childAxelarBridgeAdaptor.execute(commandId, "FAKE_CHAIN", ROOT_ADAPTOR_ADDRESS, payload); + } +} diff --git a/test/bridge/integration/root/RootERC20Bridge.t.sol b/test/bridge/integration/root/RootERC20Bridge.t.sol new file mode 100644 index 00000000..374d8d43 --- /dev/null +++ b/test/bridge/integration/root/RootERC20Bridge.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../contracts/bridge/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../contracts/bridge/test/root/MockAxelarGasService.sol"; +import {RootERC20Bridge, IRootERC20BridgeEvents, IERC20Metadata} from "../../../../contracts/bridge/root/RootERC20Bridge.sol"; +import {RootAxelarBridgeAdaptor, IRootAxelarBridgeAdaptorEvents} from "../../../../contracts/bridge/root/RootAxelarBridgeAdaptor.sol"; +import {Utils} from "../../utils.t.sol"; + +contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAxelarBridgeAdaptorEvents, Utils { + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "test"; + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + + ERC20PresetMinterPauser public token; + RootERC20Bridge public rootBridge; + RootAxelarBridgeAdaptor public axelarAdaptor; + MockAxelarGateway public mockAxelarGateway; + MockAxelarGasService public axelarGasService; + + function setUp() public { + (token, rootBridge, axelarAdaptor, mockAxelarGateway, axelarGasService) = + integrationSetup(CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR, CHILD_CHAIN_NAME); + } + + /** + * @dev A future test will assert that the computed childToken is the same as what gets deployed on L2. + * This test uses the same code as the mapToken function does to calculate this address, so we can + * not consider it sufficient. + */ + function test_mapToken() public { + uint256 mapTokenFee = 300; + address childToken = + Clones.predictDeterministicAddress(address(token), keccak256(abi.encodePacked(token)), CHILD_BRIDGE); + + bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); + vm.expectEmit(true, true, true, false, address(axelarAdaptor)); + emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, Strings.toHexString(CHILD_BRIDGE_ADAPTOR), payload); + + vm.expectEmit(true, true, false, false, address(rootBridge)); + emit L1TokenMapped(address(token), childToken); + + // Instead of using expectCalls, we could use expectEmit in combination with mock contracts emitting events. + // expectCalls requires less boilerplate and is less dependant on mock code. + vm.expectCall( + address(axelarAdaptor), + mapTokenFee, + abi.encodeWithSelector( + axelarAdaptor.sendMessage.selector, payload, address(this) + ) + ); + + // These are calls that the axelarAdaptor should make. + vm.expectCall( + address(axelarGasService), + mapTokenFee, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + CHILD_CHAIN_NAME, + Strings.toHexString(CHILD_BRIDGE_ADAPTOR), + payload, + address(this) + ) + ); + + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + CHILD_CHAIN_NAME, + Strings.toHexString(CHILD_BRIDGE_ADAPTOR), + payload + ) + ); + + // Check that we pay mapTokenFee to the axelarGasService. + uint256 thisPreBal = address(this).balance; + uint256 axelarGasServicePreBal = address(axelarGasService).balance; + + rootBridge.mapToken{value: mapTokenFee}(token); + + // Should update ETH balances as gas payment for message. + assertEq(address(this).balance, thisPreBal - mapTokenFee); + assertEq(address(axelarGasService).balance, axelarGasServicePreBal + mapTokenFee); + + assertEq(rootBridge.rootTokenToChildToken(address(token)), childToken); + } +} diff --git a/test/bridge/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/bridge/unit/child/ChildAxelarBridgeAdaptor.t.sol new file mode 100644 index 00000000..b3ff1331 --- /dev/null +++ b/test/bridge/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {Test, console2} from "forge-std/Test.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {ChildAxelarBridgeAdaptor} from "../../../../contracts/bridge/child/ChildAxelarBridgeAdaptor.sol"; +import {MockChildERC20Bridge} from "../../../../contracts/bridge/test/child/MockChildERC20Bridge.sol"; +import {MockChildAxelarGateway} from "../../../../contracts/bridge/test/child/MockChildAxelarGateway.sol"; +import {IChildAxelarBridgeAdaptorErrors} from "../../../../contracts/bridge/interfaces/child/IChildAxelarBridgeAdaptor.sol"; + +contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErrors { + address public GATEWAY_ADDRESS = address(1); + + ChildAxelarBridgeAdaptor public childAxelarBridgeAdaptor; + MockChildERC20Bridge public mockChildERC20Bridge; + MockChildAxelarGateway public mockChildAxelarGateway; + + function setUp() public { + mockChildERC20Bridge = new MockChildERC20Bridge(); + mockChildAxelarGateway = new MockChildAxelarGateway(); + childAxelarBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway), address(mockChildERC20Bridge)); + } + + function test_Constructor_SetsValues() public { + assertEq(address(childAxelarBridgeAdaptor.CHILD_BRIDGE()), address(mockChildERC20Bridge)); + assertEq(address(childAxelarBridgeAdaptor.gateway()), address(mockChildAxelarGateway)); + } + + function test_RevertIf_ConstructorGivenZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + // Gateway address being zero is checked in Axelar's AxelarExecutable smart contract. + new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(0)); + } + + function test_Execute() public { + bytes32 commandId = bytes32("testCommandId"); + string memory sourceChain = "test"; + string memory sourceAddress = Strings.toHexString(address(123)); + bytes memory payload = abi.encodePacked("payload"); + + // We expect to call the bridge's onMessageReceive function. + vm.expectCall( + address(mockChildERC20Bridge), + abi.encodeWithSelector( + mockChildERC20Bridge.onMessageReceive.selector, + sourceChain, + sourceAddress, + payload + ) + ); + childAxelarBridgeAdaptor.execute(commandId, sourceChain, sourceAddress, payload); + } +} diff --git a/test/bridge/unit/child/ChildERC20Bridge.t.sol b/test/bridge/unit/child/ChildERC20Bridge.t.sol new file mode 100644 index 00000000..db416e76 --- /dev/null +++ b/test/bridge/unit/child/ChildERC20Bridge.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {Test, console2} from "forge-std/Test.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {ChildERC20Bridge, IChildERC20BridgeEvents, IERC20Metadata, IChildERC20BridgeErrors } from "../../../../contracts/bridge/child/ChildERC20Bridge.sol"; +import {ChildERC20} from "../../../../contracts/bridge/child/ChildERC20.sol"; +import {MockAdaptor} from "../../../../contracts/bridge/test/root/MockAdaptor.sol"; + +contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors { + address constant ROOT_BRIDGE = address(3); + string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + string constant ROOT_CHAIN_NAME = "test"; + + ChildERC20 public token; + ChildERC20 public rootToken; + ChildERC20Bridge public childBridge; + + function setUp() public { + rootToken = new ChildERC20(); + rootToken.initialize(address(456), "Test", "TST", 18); + + token = new ChildERC20(); + token.initialize(address(123), "Test", "TST", 18); + + childBridge = new ChildERC20Bridge(); + + childBridge.initialize(address(this), ROOT_BRIDGE_ADAPTOR, address(token), ROOT_CHAIN_NAME); + } + + function test_Initialize() public { + assertEq(address(childBridge.bridgeAdaptor()), address(address(this))); + assertEq(childBridge.rootERC20BridgeAdaptor(), ROOT_BRIDGE_ADAPTOR); + assertEq(childBridge.childTokenTemplate(), address(token)); + assertEq(childBridge.rootChain(), ROOT_CHAIN_NAME); + } + + function test_RevertIfInitializeTwice() public { + vm.expectRevert("Initializable: contract is already initialized"); + childBridge.initialize(address(this), ROOT_BRIDGE_ADAPTOR, address(token), ROOT_CHAIN_NAME); + } + + function test_RevertIf_InitializeWithAZeroAddress() public { + ChildERC20Bridge bridge = new ChildERC20Bridge(); + vm.expectRevert(ZeroAddress.selector); + bridge.initialize(address(0), ROOT_BRIDGE_ADAPTOR, address(0), ROOT_CHAIN_NAME); + } + + function test_RevertIf_InitializeWithAnEmptyBridgeAdaptorString() public { + ChildERC20Bridge bridge = new ChildERC20Bridge(); + vm.expectRevert(InvalidRootERC20BridgeAdaptor.selector); + bridge.initialize(address(this), "", address(token), ROOT_CHAIN_NAME); + } + + + function test_RevertIf_InitializeWithAnEmptyChainNameString() public { + ChildERC20Bridge bridge = new ChildERC20Bridge(); + vm.expectRevert(InvalidRootChain.selector); + bridge.initialize(address(this), ROOT_BRIDGE_ADAPTOR, address(token), ""); + } + + function test_onMessageReceive_EmitsTokenMappedEvent() public { + address childToken = + Clones.predictDeterministicAddress(address(token), keccak256(abi.encodePacked(rootToken)), address(childBridge)); + + bytes memory data = abi.encode(childBridge.MAP_TOKEN_SIG(), address(rootToken), rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + vm.expectEmit(true, true, false, false, address(childBridge)); + emit L2TokenMapped(address(rootToken), childToken); + + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); + + } + + function test_onMessageReceive_SetsTokenMapping() public { + address childToken = + Clones.predictDeterministicAddress(address(token), keccak256(abi.encodePacked(rootToken)), address(childBridge)); + + bytes memory data = abi.encode(childBridge.MAP_TOKEN_SIG(), address(rootToken), rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); + assertEq(childBridge.rootTokenToChildToken(address(rootToken)), childToken); + } + + function test_onMessageReceive_DeploysERC20() public { + address childToken = + Clones.predictDeterministicAddress(address(token), keccak256(abi.encodePacked(rootToken)), address(childBridge)); + + bytes memory data = abi.encode(childBridge.MAP_TOKEN_SIG(), address(rootToken), rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); + + assertEq(ChildERC20(childToken).symbol(), rootToken.symbol()); + } + + + function test_RevertsIf_onMessageReceiveCalledWithMsgSenderNotBridgeAdaptor() public { + bytes memory data = abi.encode(childBridge.MAP_TOKEN_SIG(), address(rootToken), rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + vm.expectRevert(NotBridgeAdaptor.selector); + vm.prank(address(123)); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); + } + function test_RevertsIf_onMessageReceiveCalledWithSourceChainNotRootChain() public { + bytes memory data = abi.encode(childBridge.MAP_TOKEN_SIG(), address(rootToken), rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + vm.expectRevert(InvalidSourceChain.selector); + childBridge.onMessageReceive("FAKE_CHAIN", ROOT_BRIDGE_ADAPTOR, data); + } + + function test_RevertsIf_onMessageReceiveCalledWithSourceAddressNotRootAdaptor() public { + bytes memory data = abi.encode(childBridge.MAP_TOKEN_SIG(), address(rootToken), rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + vm.expectRevert(InvalidSourceAddress.selector); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, Strings.toHexString(address(456)), data); + } + + function test_RevertsIf_onMessageReceiveCalledWithDataLengthZero() public { + bytes memory data = ""; + vm.expectRevert(InvalidData.selector); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); + } + + function test_RevertsIf_onMessageReceiveCalledWithDataInvalid() public { + bytes memory data = abi.encode("FAKEDATA", address(rootToken), rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + vm.expectRevert(InvalidData.selector); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); + } + + function test_RevertsIf_onMessageReceiveCalledWithZeroAddress() public { + bytes memory data = abi.encode(childBridge.MAP_TOKEN_SIG(), address(0), rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + vm.expectRevert(ZeroAddress.selector); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); + } + + function test_RevertsIf_onMessageReceiveCalledTwice() public { + bytes memory data = abi.encode(childBridge.MAP_TOKEN_SIG(), address(rootToken), rootToken.name(), rootToken.symbol(), rootToken.decimals()); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); + vm.expectRevert(AlreadyMapped.selector); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); + } + + function test_updateBridgeAdaptor() public { + address newAdaptorAddress = address(0x11111); + + assertEq(address(childBridge.bridgeAdaptor()), address(this)); + childBridge.updateBridgeAdaptor(newAdaptorAddress); + assertEq(address(childBridge.bridgeAdaptor()), newAdaptorAddress); + } + + function test_RevertsIf_updateBridgeAdaptorCalledByNonOwner() public { + vm.prank(address(0xf00f00)); + vm.expectRevert("Ownable: caller is not the owner"); + childBridge.updateBridgeAdaptor(address(0x11111)); + } + + function test_RevertsIf_updateBridgeAdaptorCalledWithZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + childBridge.updateBridgeAdaptor(address(0)); + } +} + diff --git a/test/bridge/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/bridge/unit/root/RootAxelarBridgeAdaptor.t.sol new file mode 100644 index 00000000..00f006a8 --- /dev/null +++ b/test/bridge/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../contracts/bridge/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../contracts/bridge/test/root/MockAxelarGasService.sol"; +import {RootAxelarBridgeAdaptor, IRootAxelarBridgeAdaptorEvents, IRootAxelarBridgeAdaptorErrors} from "../../../../contracts/bridge/root/RootAxelarBridgeAdaptor.sol"; + +contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IRootAxelarBridgeAdaptorErrors { + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "test"; + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + + ERC20PresetMinterPauser public token; + RootAxelarBridgeAdaptor public axelarAdaptor; + MockAxelarGateway public mockAxelarGateway; + MockAxelarGasService public axelarGasService; + + function setUp() public { + token = new ERC20PresetMinterPauser("Test", "TST"); + mockAxelarGateway = new MockAxelarGateway(); + axelarGasService = new MockAxelarGasService(); + + axelarAdaptor = new RootAxelarBridgeAdaptor( + address(this), // set rootBridge to address(this) for unit testing + CHILD_BRIDGE_ADAPTOR, + CHILD_CHAIN_NAME, + address(mockAxelarGateway), + address(axelarGasService) + ); + } + + function test_Constructor() public { + assertEq(axelarAdaptor.ROOT_BRIDGE(), address(this)); + assertEq(axelarAdaptor.childBridgeAdaptor(), Strings.toHexString(CHILD_BRIDGE_ADAPTOR)); + assertEq(axelarAdaptor.childChain(), CHILD_CHAIN_NAME); + assertEq(address(axelarAdaptor.AXELAR_GATEWAY()), address(mockAxelarGateway)); + assertEq(address(axelarAdaptor.GAS_SERVICE()), address(axelarGasService)); + } + + function test_RevertsWhen_ConstructorGivenZeroAddress() public { + vm.expectRevert(ZeroAddresses.selector); + new RootAxelarBridgeAdaptor( + address(0), + CHILD_BRIDGE_ADAPTOR, + CHILD_CHAIN_NAME, + address(mockAxelarGateway), + address(axelarGasService) + ); + } + + function test_RevertsWhen_ConstructorGivenEmptyChildChainName() public { + vm.expectRevert(InvalidChildChain.selector); + new RootAxelarBridgeAdaptor( + address(this), + CHILD_BRIDGE_ADAPTOR, + "", + address(mockAxelarGateway), + address(axelarGasService) + ); + } + + /// @dev For this unit test we just want to make sure the correct functions are called on the Axelar Gateway and Gas Service. + function test_sendMessage_CallsGasService() public { + address refundRecipient = address(123); + bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); + uint256 callValue = 300; + + vm.expectCall( + address(axelarGasService), + callValue, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + CHILD_CHAIN_NAME, + Strings.toHexString(CHILD_BRIDGE_ADAPTOR), + payload, + refundRecipient + ) + ); + + axelarAdaptor.sendMessage{value: callValue}(payload, refundRecipient); + } + + function test_sendMessage_CallsGateway() public { + bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); + uint256 callValue = 300; + + vm.expectCall( + address(mockAxelarGateway), + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + CHILD_CHAIN_NAME, + Strings.toHexString(CHILD_BRIDGE_ADAPTOR), + payload + ) + ); + + axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); + } + + function test_sendMessage_EmitsMapTokenAxelarMessageEvent() public { + bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); + uint256 callValue = 300; + + vm.expectEmit(true, true, true, false, address(axelarAdaptor)); + emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, Strings.toHexString(CHILD_BRIDGE_ADAPTOR), payload); + + axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); + } + + function testFuzz_sendMessage_PaysGasToGasService(uint256 callValue) public { + vm.assume(callValue < address(this).balance); + vm.assume(callValue > 0); + + bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); + + uint256 thisPreBal = address(this).balance; + uint256 axelarGasServicePreBal = address(axelarGasService).balance; + + axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); + + assertEq(address(this).balance, thisPreBal - callValue); + assertEq(address(axelarGasService).balance, axelarGasServicePreBal + callValue); + } + + function test_sendMessage_GivesCorrectRefundRecipient() public { + address refundRecipient = address(0x3333); + uint256 callValue = 300; + + bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); + + vm.expectCall( + address(axelarGasService), + callValue, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + CHILD_CHAIN_NAME, + Strings.toHexString(CHILD_BRIDGE_ADAPTOR), + payload, + refundRecipient + ) + ); + + axelarAdaptor.sendMessage{value: callValue}(payload, refundRecipient); + } + + function test_RevertsIf_mapTokenCalledByNonRootBridge() public { + address payable prankster = payable(address(0x33)); + uint256 value = 300; + bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); + + // Have to call these above so the expectRevert works on the call to mapToken. + prankster.transfer(value); + vm.prank(prankster); + vm.expectRevert(CallerNotBridge.selector); + axelarAdaptor.sendMessage{value: value}(payload, address(123)); + } + + function test_RevertsIf_mapTokenCalledWithNoValue() public { + bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); + vm.expectRevert(NoGas.selector); + axelarAdaptor.sendMessage{value: 0}(payload, address(123)); + } +} diff --git a/test/bridge/unit/root/RootERC20Bridge.t.sol b/test/bridge/unit/root/RootERC20Bridge.t.sol new file mode 100644 index 00000000..744926cc --- /dev/null +++ b/test/bridge/unit/root/RootERC20Bridge.t.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {RootERC20Bridge, IRootERC20BridgeEvents, IERC20Metadata, IRootERC20BridgeErrors } from "../../../../contracts/bridge/root/RootERC20Bridge.sol"; +import {MockAxelarGateway} from "../../../../contracts/bridge/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../contracts/bridge/test/root/MockAxelarGasService.sol"; +import {MockAdaptor} from "../../../../contracts/bridge/test/root/MockAdaptor.sol"; + +contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20BridgeErrors { + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "test"; + + ERC20PresetMinterPauser public token; + RootERC20Bridge public rootBridge; + MockAdaptor public mockAxelarAdaptor; + MockAxelarGateway public mockAxelarGateway; + MockAxelarGasService public axelarGasService; + + function setUp() public { + token = new ERC20PresetMinterPauser("Test", "TST"); + + rootBridge = new RootERC20Bridge(); + mockAxelarGateway = new MockAxelarGateway(); + axelarGasService = new MockAxelarGasService(); + + mockAxelarAdaptor = new MockAdaptor(); + + // The specific ERC20 token template does not matter for these unit tests + rootBridge.initialize(address(mockAxelarAdaptor), CHILD_BRIDGE, address(token)); + } + + function test_InitializeBridge() public { + assertEq(address(rootBridge.bridgeAdaptor()), address(mockAxelarAdaptor)); + assertEq(rootBridge.childERC20Bridge(), CHILD_BRIDGE); + assertEq(rootBridge.childTokenTemplate(), address(token)); + } + + function test_RevertIfInitializeTwice() public { + vm.expectRevert("Initializable: contract is already initialized"); + rootBridge.initialize(address(mockAxelarAdaptor), CHILD_BRIDGE, address(token)); + } + + function test_RevertIf_InitializeWithAZeroAddress() public { + RootERC20Bridge bridge = new RootERC20Bridge(); + vm.expectRevert(ZeroAddress.selector); + bridge.initialize(address(0), address(0), address(0)); + } + + function test_mapToken_EmitsTokenMappedEvent() public { + uint256 mapTokenFee = 300; + address childToken = + Clones.predictDeterministicAddress(address(token), keccak256(abi.encodePacked(token)), CHILD_BRIDGE); + + vm.expectEmit(true, true, false, false, address(rootBridge)); + emit L1TokenMapped(address(token), childToken); + + rootBridge.mapToken{value: mapTokenFee}(token); + + } + + function test_mapToken_CallsAdaptor() public { + uint256 mapTokenFee = 300; + + bytes memory payload = abi.encode(rootBridge.MAP_TOKEN_SIG(), token, token.name(), token.symbol(), token.decimals()); + + vm.expectCall( + address(mockAxelarAdaptor), + mapTokenFee, + abi.encodeWithSelector( + mockAxelarAdaptor.sendMessage.selector, payload, address(this) + ) + ); + + rootBridge.mapToken{value: mapTokenFee}(token); + } + + function test_mapToken_SetsTokenMapping() public { + uint256 mapTokenFee = 300; + address childToken = + Clones.predictDeterministicAddress(address(token), keccak256(abi.encodePacked(token)), CHILD_BRIDGE); + + rootBridge.mapToken{value: mapTokenFee}(token); + + assertEq(rootBridge.rootTokenToChildToken(address(token)), childToken); + } + + function testFuzz_mapToken_UpdatesEthBalance(uint256 mapTokenFee) public { + vm.assume(mapTokenFee < address(this).balance); + vm.assume(mapTokenFee > 0); + uint256 thisPreBal = address(this).balance; + uint256 rootBridgePreBal = address(rootBridge).balance; + uint256 adaptorPreBal = address(mockAxelarAdaptor).balance; + + rootBridge.mapToken{value: mapTokenFee}(token); + + /* + * Because this is a unit test, the adaptor is mocked. This adaptor would typically + * pay the ETH to the gas service, but in this mocked case it will keep the ETH. + */ + + // User pays + assertEq(address(this).balance, thisPreBal - mapTokenFee); + assertEq(address(mockAxelarAdaptor).balance, adaptorPreBal + mapTokenFee); + assertEq(address(rootBridge).balance, rootBridgePreBal); + } + + function test_RevertsIf_mapTokenCalledWithZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + rootBridge.mapToken{value: 300}(IERC20Metadata(address(0))); + } + + function test_RevertsIf_mapTokenCalledTwice() public { + rootBridge.mapToken{value: 300}(token); + vm.expectRevert(AlreadyMapped.selector); + rootBridge.mapToken{value: 300}(token); + } + + function test_updateBridgeAdaptor() public { + address newAdaptorAddress = address(0x11111); + + assertEq(address(rootBridge.bridgeAdaptor()), address(mockAxelarAdaptor)); + rootBridge.updateBridgeAdaptor(newAdaptorAddress); + assertEq(address(rootBridge.bridgeAdaptor()), newAdaptorAddress); + } + + function test_RevertsIf_updateBridgeAdaptorCalledByNonOwner() public { + vm.prank(address(0xf00f00)); + vm.expectRevert("Ownable: caller is not the owner"); + rootBridge.updateBridgeAdaptor(address(0x11111)); + } + + function test_RevertsIf_updateBridgeAdaptorCalledWithZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + rootBridge.updateBridgeAdaptor(address(0)); + } +} diff --git a/test/bridge/utils.t.sol b/test/bridge/utils.t.sol new file mode 100644 index 00000000..162332b7 --- /dev/null +++ b/test/bridge/utils.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {MockAxelarGateway} from "../../contracts/bridge/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../contracts/bridge/test/root/MockAxelarGasService.sol"; +import {RootERC20Bridge} from "../../contracts/bridge/root/RootERC20Bridge.sol"; +import {RootAxelarBridgeAdaptor} from "../../contracts/bridge/root/RootAxelarBridgeAdaptor.sol"; + +contract Utils is Test { + function integrationSetup(address childBridge, address childBridgeAdaptor, string memory childBridgeName) + public + returns ( + ERC20PresetMinterPauser token, + RootERC20Bridge rootBridge, + RootAxelarBridgeAdaptor axelarAdaptor, + MockAxelarGateway mockAxelarGateway, + MockAxelarGasService axelarGasService + ) + { + token = new ERC20PresetMinterPauser("Test", "TST"); + token.mint(address(this), 1000000 ether); + + rootBridge = new RootERC20Bridge(); + mockAxelarGateway = new MockAxelarGateway(); + axelarGasService = new MockAxelarGasService(); + + axelarAdaptor = new RootAxelarBridgeAdaptor( + address(rootBridge), + childBridgeAdaptor, + childBridgeName, + address(mockAxelarGateway), + address(axelarGasService) + ); + + rootBridge.initialize(address(axelarAdaptor), childBridge, address(token)); + } +} diff --git a/yarn.lock b/yarn.lock index 8465a95c..4a4077b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@axelar-network/axelar-gmp-sdk-solidity@^5.3.3": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-5.3.3.tgz#8dcd7fde107d1e1c8b7810f224a80b058fc6cc16" + integrity sha512-QG2u1OkPrHjVfJo0ZpvmhHvbWwNXl7CQgM6lJa2ZAsZ6eRoKhEQ406zsgIOd2MtvgG4kH8vPHTb0K3y5ovMBIA== + "@babel/code-frame@^7.0.0": version "7.22.13" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" @@ -1142,6 +1147,11 @@ find-up "^4.1.0" fs-extra "^8.1.0" +"@openzeppelin/contracts-upgradeable@^4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.3.tgz#ff17a80fb945f5102571f8efecb5ce5915cc4811" + integrity sha512-jjaHAVRMrE4UuZNfDwjlLGDxTHWIOwTJS2ldnc278a0gevfXfPr8hxKEVBGFBE96kl2G3VHDZhUimw/+G3TG2A== + "@openzeppelin/contracts@^4.9.3": version "4.9.3" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.3.tgz#00d7a8cf35a475b160b3f0293a6403c511099364"