diff --git a/deployment-config/chains/1.json b/deployment-config/chains/1.json index c8f6327..68ff550 100644 --- a/deployment-config/chains/1.json +++ b/deployment-config/chains/1.json @@ -3,6 +3,7 @@ "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "opMessenger": "0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1", "lzEndpoint": "0x1a44076050125825900e736c501f859c50fE728c", + "mailbox": "0xc005dc82818d67AF737725bD4bf75435d065D239", "assetToRateProviderAndPriceFeed": { "NOTE_0_CHAINLINK_1_REDSTONE_2_GENERIC": 0, "NOTE_THESE_KEYS_MUST_NOT_BE_CHECKSUMMED": 0, diff --git a/deployment-config/chains/11155111.json b/deployment-config/chains/11155111.json index e900a9a..fe48613 100644 --- a/deployment-config/chains/11155111.json +++ b/deployment-config/chains/11155111.json @@ -1,5 +1,6 @@ { "name": "Ethereum Sepolia", "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", - "lzEndpoint": "0x6EDCE65403992e310A62460808c4b910D972f10f" + "lzEndpoint": "0x6EDCE65403992e310A62460808c4b910D972f10f", + "mailbox": "0xfFAEF09B3cd11D9b20d1a19bECca54EEC2884766" } \ No newline at end of file diff --git a/deployment-config/chains/11155420.json b/deployment-config/chains/11155420.json new file mode 100644 index 0000000..12a0374 --- /dev/null +++ b/deployment-config/chains/11155420.json @@ -0,0 +1,6 @@ +{ + "name": "Optimism Sepolia", + "balancerVault": "0x0000000000000000000000000000000000000000", + "lzEndpoint": "0x0000000000000000000000000000000000000000", + "mailbox": "0x6966b0E55883d49BFB24539356a2f8A673E02039" + } \ No newline at end of file diff --git a/deployment-config/chains/288.json b/deployment-config/chains/288.json index 21e0d49..4b08c7c 100644 --- a/deployment-config/chains/288.json +++ b/deployment-config/chains/288.json @@ -1,5 +1,6 @@ { "name": "Boba", "balancerVault": "0x0000000000000000000000000000000000000000", - "lzEndpoint": "0x0000000000000000000000000000000000000000" + "lzEndpoint": "0x0000000000000000000000000000000000000000", + "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E" } \ No newline at end of file diff --git a/deployment-config/eth-sepolia-hyperlane-L1.json b/deployment-config/eth-sepolia-hyperlane-L1.json new file mode 100644 index 0000000..2e45916 --- /dev/null +++ b/deployment-config/eth-sepolia-hyperlane-L1.json @@ -0,0 +1,46 @@ +{ + "base": "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9", + "protocolAdmin": "0x94544835Cf97c631f101c5f538787fE14E2E04f6", + "boringVaultAndBaseDecimals": "18", + "boringVault": { + "boringVaultSalt": "0x1ddd634c506ad203da17ff00000000000000000000000000000000000000001c", + "boringVaultName": "Nucleus Vault", + "boringVaultSymbol": "NV", + "address": "0x0000000000000000000000000000000000000000" + }, + "manager": { + "managerSalt": "0x30432d4b4ec00003b4a25000000000000000000000000000000000000000001c", + "address": "0x0000000000000000000000000000000000000000" + }, + "accountant": { + "accountantSalt": "0x6a184dbea6f3cc0318679f00000000000000000000000000000000000000001c", + "payoutAddress": "0x94544835Cf97c631f101c5f538787fE14E2E04f6", + "allowedExchangeRateChangeUpper": "10003", + "allowedExchangeRateChangeLower": "10000", + "minimumUpdateDelayInSeconds": "3600", + "managementFee": "2000", + "address": "0x0000000000000000000000000000000000000000" + }, + "teller": { + "tellerSalt": "0x51f8968749a56d01202c9100000000000000000000000000000000000000001c", + "maxGasForPeer": 100000, + "minGasForPeer": 0, + "peerEid": 0, + "peerDomainId": 11155420, + "tellerContractName": "MultiChainHyperlaneTellerWithMultiAssetSupport", + "assets": [ + ], + "address": "0x0000000000000000000000000000000000000000" + }, + "rolesAuthority": { + "rolesAuthoritySalt": "0x66bbc3b3b3000b01466a3a00000000000000000000000000000000000000001c", + "strategist": "0x94544835Cf97c631f101c5f538787fE14E2E04f6", + "exchangeRateBot": "0x94544835Cf97c631f101c5f538787fE14E2E04f6", + "pauser": "0x94544835Cf97c631f101c5f538787fE14E2E04f6", + "address": "0x0000000000000000000000000000000000000000" + }, + "decoder": { + "decoderSalt": "0x48b53893da2e0b0248268c00000000000000000000000000000000000000001c", + "address": "0x0000000000000000000000000000000000000000" + } + } \ No newline at end of file diff --git a/deployment-config/op-sepolia-hyperlane-L2.json b/deployment-config/op-sepolia-hyperlane-L2.json new file mode 100644 index 0000000..b5d7d28 --- /dev/null +++ b/deployment-config/op-sepolia-hyperlane-L2.json @@ -0,0 +1,46 @@ +{ + "base": "0x1BDD24840e119DC2602dCC587Dd182812427A5Cc", + "protocolAdmin": "0x94544835Cf97c631f101c5f538787fE14E2E04f6", + "boringVaultAndBaseDecimals": "18", + "boringVault": { + "boringVaultSalt": "0x1ddd634c506ad203da17ff00000000000000000000000000000000000000001c", + "boringVaultName": "Nucleus Vault", + "boringVaultSymbol": "NV", + "address": "0x0000000000000000000000000000000000000000" + }, + "manager": { + "managerSalt": "0x30432d4b4ec00003b4a25000000000000000000000000000000000000000001c", + "address": "0x0000000000000000000000000000000000000000" + }, + "accountant": { + "accountantSalt": "0x6a184dbea6f3cc0318679f00000000000000000000000000000000000000001c", + "payoutAddress": "0x94544835Cf97c631f101c5f538787fE14E2E04f6", + "allowedExchangeRateChangeUpper": "10003", + "allowedExchangeRateChangeLower": "10000", + "minimumUpdateDelayInSeconds": "3600", + "managementFee": "2000", + "address": "0x0000000000000000000000000000000000000000" + }, + "teller": { + "tellerSalt": "0x51f8968749a56d01202c9100000000000000000000000000000000000000001c", + "maxGasForPeer": 100000, + "minGasForPeer": 0, + "peerEid": 0, + "peerDomainId": 11155111, + "tellerContractName": "MultiChainHyperlaneTellerWithMultiAssetSupport", + "assets": [ + ], + "address": "0x0000000000000000000000000000000000000000" + }, + "rolesAuthority": { + "rolesAuthoritySalt": "0x66bbc3b3b3000b01466a3a00000000000000000000000000000000000000001c", + "strategist": "0x94544835Cf97c631f101c5f538787fE14E2E04f6", + "exchangeRateBot": "0x94544835Cf97c631f101c5f538787fE14E2E04f6", + "pauser": "0x94544835Cf97c631f101c5f538787fE14E2E04f6", + "address": "0x0000000000000000000000000000000000000000" + }, + "decoder": { + "decoderSalt": "0x48b53893da2e0b0248268c00000000000000000000000000000000000000001c", + "address": "0x0000000000000000000000000000000000000000" + } + } \ No newline at end of file diff --git a/script/ConfigReader.s.sol b/script/ConfigReader.s.sol index e19a516..699f4cf 100644 --- a/script/ConfigReader.s.sol +++ b/script/ConfigReader.s.sol @@ -39,6 +39,8 @@ library ConfigReader { uint64 maxGasForPeer; uint64 minGasForPeer; address lzEndpoint; + address mailbox; + uint32 peerDomainId; bytes32 rolesAuthoritySalt; address manager; address teller; @@ -88,13 +90,21 @@ library ConfigReader { config.minGasForPeer = uint64(_config.readUint(".teller.minGasForPeer")); config.tellerContractName = _config.readString(".teller.tellerContractName"); config.assets = _config.readAddressArray(".teller.assets"); - config.peerEid = uint32(_config.readUint(".teller.peerEid")); - config.requiredDvns = _config.readAddressArray(".teller.dvnIfNoDefault.required"); - config.optionalDvns = _config.readAddressArray(".teller.dvnIfNoDefault.optional"); - config.dvnBlockConfirmationsRequired = - uint64(_config.readUint(".teller.dvnIfNoDefault.blockConfirmationsRequiredIfNoDefault")); - config.optionalDvnThreshold = uint8(_config.readUint(".teller.dvnIfNoDefault.optionalThreshold")); + // layerzero + if (compareStrings(config.tellerContractName, "MultiChainLayerZeroTellerWithMultiAssetSupport")) { + config.lzEndpoint = _chainConfig.readAddress(".lzEndpoint"); + + config.peerEid = uint32(_config.readUint(".teller.peerEid")); + config.requiredDvns = _config.readAddressArray(".teller.dvnIfNoDefault.required"); + config.optionalDvns = _config.readAddressArray(".teller.dvnIfNoDefault.optional"); + config.dvnBlockConfirmationsRequired = + uint64(_config.readUint(".teller.dvnIfNoDefault.blockConfirmationsRequiredIfNoDefault")); + config.optionalDvnThreshold = uint8(_config.readUint(".teller.dvnIfNoDefault.optionalThreshold")); + } else if (compareStrings(config.tellerContractName, "MultiChainHyperlaneTellerWithMultiAssetSupport")) { + config.mailbox = _chainConfig.readAddress(".mailbox"); + config.peerDomainId = uint32(_config.readUint(".teller.peerDomainId")); + } // Reading from the 'rolesAuthority' section config.rolesAuthority = _config.readAddress(".rolesAuthority.address"); @@ -109,8 +119,11 @@ library ConfigReader { // Reading from the 'chainConfig' section config.balancerVault = _chainConfig.readAddress(".balancerVault"); - config.lzEndpoint = _chainConfig.readAddress(".lzEndpoint"); return config; } + + function compareStrings(string memory a, string memory b) internal pure returns (bool) { + return (keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b))); + } } diff --git a/script/deploy/deployAll.s.sol b/script/deploy/deployAll.s.sol index c9d0bfe..a6079e5 100644 --- a/script/deploy/deployAll.s.sol +++ b/script/deploy/deployAll.s.sol @@ -15,6 +15,7 @@ import { DeployCrossChainOPTellerWithMultiAssetSupport } from "./single/05a_DeployCrossChainOPTellerWithMultiAssetSupport.s.sol"; import { DeployMultiChainLayerZeroTellerWithMultiAssetSupport } from "./single/05b_DeployMultiChainLayerZeroTellerWithMultiAssetSupport.s.sol"; +import { DeployMultiChainHyperlaneTeller } from "./single/05c_DeployMultiChainHyperlaneTeller.s.sol"; import { DeployRolesAuthority } from "./single/06_DeployRolesAuthority.s.sol"; import { TellerSetup } from "./single/07_TellerSetup.s.sol"; import { SetAuthorityAndTransferOwnerships } from "./single/08_SetAuthorityAndTransferOwnerships.s.sol"; @@ -102,6 +103,8 @@ contract DeployAll is BaseScript { teller = new DeployCrossChainOPTellerWithMultiAssetSupport().deploy(config); } else if (compareStrings(config.tellerContractName, "MultiChainLayerZeroTellerWithMultiAssetSupport")) { teller = new DeployMultiChainLayerZeroTellerWithMultiAssetSupport().deploy(config); + } else if (compareStrings(config.tellerContractName, "MultiChainHyperlaneTellerWithMultiAssetSupport")) { + teller = new DeployMultiChainHyperlaneTeller().deploy(config); } else if (compareStrings(config.tellerContractName, "TellerWithMultiAssetSupport")) { teller = new DeployTellerWithMultiAssetSupport().deploy(config); } else { diff --git a/script/deploy/single/04_DeployAccountantWithRateProviders.s.sol b/script/deploy/single/04_DeployAccountantWithRateProviders.s.sol index 90ae876..ffcafc0 100644 --- a/script/deploy/single/04_DeployAccountantWithRateProviders.s.sol +++ b/script/deploy/single/04_DeployAccountantWithRateProviders.s.sol @@ -26,7 +26,7 @@ contract DeployAccountantWithRateProviders is BaseScript { require(config.base != address(0), "base address must not be zero"); require(config.allowedExchangeRateChangeUpper > 1e4, "allowedExchangeRateChangeUpper"); require(config.allowedExchangeRateChangeUpper <= 1.003e4, "allowedExchangeRateChangeUpper upper bound"); - require(config.allowedExchangeRateChangeLower < 1e4, "allowedExchangeRateChangeLower"); + require(config.allowedExchangeRateChangeLower <= 1e4, "allowedExchangeRateChangeLower"); require(config.allowedExchangeRateChangeLower >= 0.997e4, "allowedExchangeRateChangeLower lower bound"); require(config.minimumUpdateDelayInSeconds >= 3600, "minimumUpdateDelayInSeconds"); require(config.managementFee < 1e4, "managementFee"); diff --git a/script/deploy/single/05c_DeployMultiChainHyperlaneTeller.s.sol b/script/deploy/single/05c_DeployMultiChainHyperlaneTeller.s.sol new file mode 100644 index 0000000..389d3f7 --- /dev/null +++ b/script/deploy/single/05c_DeployMultiChainHyperlaneTeller.s.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { AccountantWithRateProviders } from "./../../../src/base/Roles/AccountantWithRateProviders.sol"; +import { MultiChainHyperlaneTellerWithMultiAssetSupport } from + "./../../../src/base/Roles/CrossChain/MultiChainHyperlaneTellerWithMultiAssetSupport.sol"; +import { IMailbox } from "./../../../src/interfaces/hyperlane/IMailbox.sol"; +import { BaseScript } from "./../../Base.s.sol"; +import { stdJson as StdJson } from "@forge-std/StdJson.sol"; +import { ConfigReader } from "../../ConfigReader.s.sol"; +import { console2 } from "@forge-std/console2.sol"; + +contract DeployMultiChainHyperlaneTeller is BaseScript { + using StdJson for string; + + function run() public returns (address teller) { + return deploy(getConfig()); + } + + function deploy(ConfigReader.Config memory config) public override broadcast returns (address) { + // Require config Values + require(config.boringVault.code.length != 0, "boringVault must have code"); + require(config.accountant.code.length != 0, "accountant must have code"); + require(config.tellerSalt != bytes32(0), "tellerSalt"); + require(config.boringVault != address(0), "boringVault"); + require(config.accountant != address(0), "accountant"); + + // Create Contract + bytes memory creationCode = type(MultiChainHyperlaneTellerWithMultiAssetSupport).creationCode; + MultiChainHyperlaneTellerWithMultiAssetSupport teller = MultiChainHyperlaneTellerWithMultiAssetSupport( + CREATEX.deployCreate3( + config.tellerSalt, + abi.encodePacked( + creationCode, abi.encode(broadcaster, config.boringVault, config.accountant, config.mailbox) + ) + ) + ); + + teller.addChain(config.peerDomainId, true, true, address(teller), config.maxGasForPeer, config.minGasForPeer); + + IMailbox mailbox = teller.mailbox(); + + // Post Deploy Checks + require(teller.shareLockPeriod() == 0, "share lock period must be zero"); + require(teller.isPaused() == false, "the teller must not be paused"); + require( + AccountantWithRateProviders(teller.accountant()).vault() == teller.vault(), + "the accountant vault must be the teller vault" + ); + require(address(mailbox) == config.mailbox, "mailbox must be set"); + + return address(teller); + } +} diff --git a/src/base/Roles/CrossChain/Hyperlane/StandardHookMetadata.sol b/src/base/Roles/CrossChain/Hyperlane/StandardHookMetadata.sol new file mode 100644 index 0000000..26564ff --- /dev/null +++ b/src/base/Roles/CrossChain/Hyperlane/StandardHookMetadata.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +/** + * Format of metadata: + * + * [0:2] variant + * [2:34] msg.value + * [34:66] Gas limit for message (IGP) + * [66:86] Refund address for message (IGP) + * [86:] Custom metadata + */ +library StandardHookMetadata { + struct Metadata { + uint16 variant; + uint256 msgValue; + uint256 gasLimit; + address refundAddress; + } + + uint8 private constant VARIANT_OFFSET = 0; + uint8 private constant MSG_VALUE_OFFSET = 2; + uint8 private constant GAS_LIMIT_OFFSET = 34; + uint8 private constant REFUND_ADDRESS_OFFSET = 66; + uint256 private constant MIN_METADATA_LENGTH = 86; + + uint16 public constant VARIANT = 1; + + /** + * @notice Returns the variant of the metadata. + * @param _metadata ABI encoded standard hook metadata. + * @return variant of the metadata as uint8. + */ + function variant(bytes calldata _metadata) internal pure returns (uint16) { + if (_metadata.length < VARIANT_OFFSET + 2) return 0; + return uint16(bytes2(_metadata[VARIANT_OFFSET:VARIANT_OFFSET + 2])); + } + + /** + * @notice Returns the specified value for the message. + * @param _metadata ABI encoded standard hook metadata. + * @param _default Default fallback value. + * @return Value for the message as uint256. + */ + function msgValue(bytes calldata _metadata, uint256 _default) internal pure returns (uint256) { + if (_metadata.length < MSG_VALUE_OFFSET + 32) return _default; + return uint256(bytes32(_metadata[MSG_VALUE_OFFSET:MSG_VALUE_OFFSET + 32])); + } + + /** + * @notice Returns the specified gas limit for the message. + * @param _metadata ABI encoded standard hook metadata. + * @param _default Default fallback gas limit. + * @return Gas limit for the message as uint256. + */ + function gasLimit(bytes calldata _metadata, uint256 _default) internal pure returns (uint256) { + if (_metadata.length < GAS_LIMIT_OFFSET + 32) return _default; + return uint256(bytes32(_metadata[GAS_LIMIT_OFFSET:GAS_LIMIT_OFFSET + 32])); + } + + /** + * @notice Returns the specified refund address for the message. + * @param _metadata ABI encoded standard hook metadata. + * @param _default Default fallback refund address. + * @return Refund address for the message as address. + */ + function refundAddress(bytes calldata _metadata, address _default) internal pure returns (address) { + if (_metadata.length < REFUND_ADDRESS_OFFSET + 20) return _default; + return address(bytes20(_metadata[REFUND_ADDRESS_OFFSET:REFUND_ADDRESS_OFFSET + 20])); + } + + /** + * @notice Returns any custom metadata. + * @param _metadata ABI encoded standard hook metadata. + * @return Custom metadata. + */ + function getCustomMetadata(bytes calldata _metadata) internal pure returns (bytes calldata) { + if (_metadata.length < MIN_METADATA_LENGTH) return _metadata[0:0]; + return _metadata[MIN_METADATA_LENGTH:]; + } + + /** + * @notice Formats the specified gas limit and refund address into standard hook metadata. + * @param _msgValue msg.value for the message. + * @param _gasLimit Gas limit for the message. + * @param _refundAddress Refund address for the message. + * @param _customMetadata Additional metadata to include in the standard hook metadata. + * @return ABI encoded standard hook metadata. + */ + function formatMetadata( + uint256 _msgValue, + uint256 _gasLimit, + address _refundAddress, + bytes memory _customMetadata + ) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(VARIANT, _msgValue, _gasLimit, _refundAddress, _customMetadata); + } + + /** + * @notice Formats the specified gas limit and refund address into standard hook metadata. + * @param _msgValue msg.value for the message. + * @return ABI encoded standard hook metadata. + */ + function overrideMsgValue(uint256 _msgValue) internal view returns (bytes memory) { + return formatMetadata(_msgValue, uint256(0), msg.sender, ""); + } + + /** + * @notice Formats the specified gas limit and refund address into standard hook metadata. + * @param _gasLimit Gas limit for the message. + * @return ABI encoded standard hook metadata. + */ + function overrideGasLimit(uint256 _gasLimit) internal view returns (bytes memory) { + return formatMetadata(uint256(0), _gasLimit, msg.sender, ""); + } + + /** + * @notice Formats the specified refund address into standard hook metadata. + * @param _refundAddress Refund address for the message. + * @return ABI encoded standard hook metadata. + */ + function overrideRefundAddress(address _refundAddress) internal pure returns (bytes memory) { + return formatMetadata(uint256(0), uint256(0), _refundAddress, ""); + } +} diff --git a/src/base/Roles/CrossChain/MultiChainHyperlaneTellerWithMultiAssetSupport.sol b/src/base/Roles/CrossChain/MultiChainHyperlaneTellerWithMultiAssetSupport.sol new file mode 100644 index 0000000..dad88a2 --- /dev/null +++ b/src/base/Roles/CrossChain/MultiChainHyperlaneTellerWithMultiAssetSupport.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { + MultiChainTellerBase, + MultiChainTellerBase_MessagesNotAllowedFrom, + MultiChainTellerBase_MessagesNotAllowedFromSender, + Chain +} from "./MultiChainTellerBase.sol"; +import { BridgeData, ERC20 } from "./CrossChainTellerBase.sol"; +import { StandardHookMetadata } from "./Hyperlane/StandardHookMetadata.sol"; +import { IMailbox } from "../../../interfaces/hyperlane/IMailbox.sol"; +import { IInterchainSecurityModule } from "../../../interfaces/hyperlane/IInterchainSecurityModule.sol"; +import { IPostDispatchHook } from "../../../interfaces/hyperlane/IPostDispatchHook.sol"; + +/** + * @title MultiChainHyperlaneTellerWithMultiAssetSupport + * @notice Hyperlane implementation of MultiChainTeller + * @custom:security-contact security@molecularlabs.io + */ +contract MultiChainHyperlaneTellerWithMultiAssetSupport is MultiChainTellerBase { + // ========================================= STATE ========================================= + + /** + * @notice The Hyperlane mailbox contract. + */ + IMailbox public immutable mailbox; + + /** + * @notice The Hyperlane interchain security module. + * @dev If `address(0)`, uses the mailbox's default ISM. + */ + IInterchainSecurityModule public interchainSecurityModule; + + /** + * @notice The hook invoked after `dispatch`. + */ + IPostDispatchHook public hook; + + /** + * @notice A nonce used to generate unique message IDs. + */ + uint128 public nonce; + + //============================== EVENTS =============================== + + event SetInterChainSecurityModule(address _interchainSecurityModule); + event SetPostDispatchHook(address _hook); + + //============================== ERRORS =============================== + + error MultiChainHyperlaneTellerWithMultiAssetSupport_InvalidBridgeFeeToken(); + error MultiChainHyperlaneTellerWithMultiAssetSupport_CallerMustBeMailbox(address caller); + error MultiChainHyperlaneTellerWithMultiAssetSupport_InvalidBytes32Address(bytes32 _address); + error MultiChainHyperlaneTellerWithMultiAssetSupport_ZeroAddressDestinationReceiver(); + + constructor( + address _owner, + address _vault, + address _accountant, + IMailbox _mailbox + ) + MultiChainTellerBase(_owner, _vault, _accountant) + { + mailbox = _mailbox; + } + + /** + * @notice Sets the post dispatch hook for Hyperlane mailbox. + */ + function setHook(IPostDispatchHook _hook) external requiresAuth { + hook = _hook; + emit SetPostDispatchHook(address(_hook)); + } + + /** + * @notice Sets a custom interchain security module for Hyperlane. + */ + function setInterchainSecurityModule(IInterchainSecurityModule _interchainSecurityModule) external requiresAuth { + interchainSecurityModule = _interchainSecurityModule; + emit SetInterChainSecurityModule(address(_interchainSecurityModule)); + } + + /** + * @notice function override to return the fee quote + * @param shareAmount to be sent as a message + * @param data Bridge data + */ + function _quote(uint256 shareAmount, BridgeData calldata data) internal view override returns (uint256) { + uint256 nextNonce = nonce + 1; + bytes32 messageId = keccak256(abi.encodePacked(nextNonce, address(this), block.chainid)); + + bytes memory _payload = abi.encode(shareAmount, data.destinationChainReceiver, messageId); + + bytes32 msgRecipient = _addressToBytes32(selectorToChains[data.chainSelector].targetTeller); + + return mailbox.quoteDispatch( + data.chainSelector, msgRecipient, _payload, StandardHookMetadata.overrideGasLimit(data.messageGas), hook + ); + } + + /** + * @notice Called when data is received from the protocol. It overrides the equivalent function in the parent + * contract. + * Protocol messages are defined as packets, comprised of the following parameters. + * @param origin A struct containing information about where the packet came from. + * @param sender The contract that sent this message. + * @param payload Encoded message. + */ + function handle(uint32 origin, bytes32 sender, bytes calldata payload) external payable { + _beforeReceive(); + + Chain memory chain = selectorToChains[origin]; + + // Three things must be checked. + // 1. This function must only be called by the mailbox + // 2. The sender must be the teller from the source chain + // 3. The origin aka chainSelector must be allowed to send message to this + // contract through the `Chain` config. + if (msg.sender != address(mailbox)) { + revert MultiChainHyperlaneTellerWithMultiAssetSupport_CallerMustBeMailbox(msg.sender); + } + + if (sender != _addressToBytes32(chain.targetTeller)) { + revert MultiChainTellerBase_MessagesNotAllowedFromSender(uint256(origin), _bytes32ToAddress(sender)); + } + + if (!chain.allowMessagesFrom) { + revert MultiChainTellerBase_MessagesNotAllowedFrom(origin); + } + + (uint256 shareAmount, address receiver, bytes32 messageId) = abi.decode(payload, (uint256, address, bytes32)); + + // This should never be the case since zero address + // `destinationChainReceiver` in `_bridge` is not allowed, but we have + // this as a sanity check. + if (receiver == address(0)) { + revert MultiChainHyperlaneTellerWithMultiAssetSupport_ZeroAddressDestinationReceiver(); + } + + vault.enter(address(0), ERC20(address(0)), 0, receiver, shareAmount); + + _afterReceive(shareAmount, receiver, messageId); + } + + /** + * @notice bridge override to allow bridge logic to be done for bridge() and depositAndBridge() + * @param shareAmount to be moved across chain + * @param data BridgeData + * @return messageId a unique hash for the message + */ + function _bridge(uint256 shareAmount, BridgeData calldata data) internal override returns (bytes32 messageId) { + // We create our own guid and pass it into the payload for it to be + // parsed in `handle`. There is no way to pass the return `messageId` + // from `dispatch` to `handle`. + unchecked { + messageId = keccak256(abi.encodePacked(++nonce, address(this), block.chainid)); + } + + if (address(data.bridgeFeeToken) != NATIVE) { + revert MultiChainHyperlaneTellerWithMultiAssetSupport_InvalidBridgeFeeToken(); + } + + if (data.destinationChainReceiver == address(0)) { + revert MultiChainHyperlaneTellerWithMultiAssetSupport_ZeroAddressDestinationReceiver(); + } + + bytes memory _payload = abi.encode(shareAmount, data.destinationChainReceiver, messageId); + + // Unlike L0 that has a built in peer check, this contract must + // constrain the message recipient itself. We do this by our own + // configuration. + bytes32 msgRecipient = _addressToBytes32(selectorToChains[data.chainSelector].targetTeller); + + mailbox.dispatch{ value: msg.value }( + data.chainSelector, // must be `destinationDomain` on hyperlane + msgRecipient, // must be the teller address left-padded to bytes32 + _payload, + StandardHookMetadata.overrideGasLimit(data.messageGas), // Sets the refund address to msg.sender, sets + // `_msgValue` to zero + hook + ); + } + + function _addressToBytes32(address _address) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_address))); + } + + function _bytes32ToAddress(bytes32 _address) internal pure returns (address) { + if (uint256(_address) > uint256(type(uint160).max)) { + revert MultiChainHyperlaneTellerWithMultiAssetSupport_InvalidBytes32Address(_address); + } + + return address(uint160(uint256(_address))); + } +} diff --git a/src/base/Roles/CrossChain/MultiChainTellerBase.sol b/src/base/Roles/CrossChain/MultiChainTellerBase.sol index 2f47905..6fd104e 100644 --- a/src/base/Roles/CrossChain/MultiChainTellerBase.sol +++ b/src/base/Roles/CrossChain/MultiChainTellerBase.sol @@ -14,6 +14,8 @@ struct Chain { error MultiChainTellerBase_MessagesNotAllowedFrom(uint32 chainSelector); error MultiChainTellerBase_MessagesNotAllowedFromSender(uint256 chainSelector, address sender); error MultiChainTellerBase_MessagesNotAllowedTo(uint256 chainSelector); +error MultiChainTellerBase_TargetTellerIsZeroAddress(); +error MultiChainTellerBase_DestinationChainReceiverIsZeroAddress(); error MultiChainTellerBase_ZeroMessageGasLimit(); error MultiChainTellerBase_GasLimitExceeded(); error MultiChainTellerBase_GasTooLow(); @@ -172,15 +174,25 @@ abstract contract MultiChainTellerBase is CrossChainTellerBase { * @param data bridge data */ function _beforeBridge(BridgeData calldata data) internal override { - if (!selectorToChains[data.chainSelector].allowMessagesTo) { + Chain memory chain = selectorToChains[data.chainSelector]; + + if (!chain.allowMessagesTo) { revert MultiChainTellerBase_MessagesNotAllowedTo(data.chainSelector); } - if (data.messageGas > selectorToChains[data.chainSelector].messageGasLimit) { + if (chain.targetTeller == address(0)) { + revert MultiChainTellerBase_TargetTellerIsZeroAddress(); + } + + if (data.destinationChainReceiver == address(0)) { + revert MultiChainTellerBase_DestinationChainReceiverIsZeroAddress(); + } + + if (data.messageGas > chain.messageGasLimit) { revert MultiChainTellerBase_GasLimitExceeded(); } - if (data.messageGas < selectorToChains[data.chainSelector].minimumMessageGas) { + if (data.messageGas < chain.minimumMessageGas) { revert MultiChainTellerBase_GasTooLow(); } } diff --git a/src/interfaces/hyperlane/IInterchainSecurityModule.sol b/src/interfaces/hyperlane/IInterchainSecurityModule.sol new file mode 100644 index 0000000..8b7c5f8 --- /dev/null +++ b/src/interfaces/hyperlane/IInterchainSecurityModule.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +interface IInterchainSecurityModule { + enum Types { + UNUSED, + ROUTING, + AGGREGATION, + LEGACY_MULTISIG, + MERKLE_ROOT_MULTISIG, + MESSAGE_ID_MULTISIG, + NULL, // used with relayer carrying no metadata + CCIP_READ, + ARB_L2_TO_L1, + WEIGHTED_MERKLE_ROOT_MULTISIG, + WEIGHTED_MESSAGE_ID_MULTISIG, + OP_L2_TO_L1 + } + + /** + * @notice Returns an enum that represents the type of security model + * encoded by this ISM. + * @dev Relayers infer how to fetch and format metadata. + */ + function moduleType() external view returns (uint8); + + /** + * @notice Defines a security model responsible for verifying interchain + * messages based on the provided metadata. + * @param _metadata Off-chain metadata provided by a relayer, specific to + * the security model encoded by the module (e.g. validator signatures) + * @param _message Hyperlane encoded interchain message + * @return True if the message was verified + */ + function verify(bytes calldata _metadata, bytes calldata _message) external returns (bool); +} + +interface ISpecifiesInterchainSecurityModule { + function interchainSecurityModule() external view returns (IInterchainSecurityModule); +} diff --git a/src/interfaces/hyperlane/IMailbox.sol b/src/interfaces/hyperlane/IMailbox.sol new file mode 100644 index 0000000..b0fe1d7 --- /dev/null +++ b/src/interfaces/hyperlane/IMailbox.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import { IInterchainSecurityModule } from "./IInterchainSecurityModule.sol"; +import { IPostDispatchHook } from "./IPostDispatchHook.sol"; + +interface IMailbox { + // ============ Events ============ + /** + * @notice Emitted when a new message is dispatched via Hyperlane + * @param sender The address that dispatched the message + * @param destination The destination domain of the message + * @param recipient The message recipient address on `destination` + * @param message Raw bytes of message + */ + event Dispatch(address indexed sender, uint32 indexed destination, bytes32 indexed recipient, bytes message); + + /** + * @notice Emitted when a new message is dispatched via Hyperlane + * @param messageId The unique message identifier + */ + event DispatchId(bytes32 indexed messageId); + + /** + * @notice Emitted when a Hyperlane message is processed + * @param messageId The unique message identifier + */ + event ProcessId(bytes32 indexed messageId); + + /** + * @notice Emitted when a Hyperlane message is delivered + * @param origin The origin domain of the message + * @param sender The message sender address on `origin` + * @param recipient The address that handled the message + */ + event Process(uint32 indexed origin, bytes32 indexed sender, address indexed recipient); + + function localDomain() external view returns (uint32); + + function delivered(bytes32 messageId) external view returns (bool); + + function defaultIsm() external view returns (IInterchainSecurityModule); + + function defaultHook() external view returns (IPostDispatchHook); + + function requiredHook() external view returns (IPostDispatchHook); + + function latestDispatchedId() external view returns (bytes32); + + function dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody + ) + external + payable + returns (bytes32 messageId); + + function quoteDispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody + ) + external + view + returns (uint256 fee); + + function dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata body, + bytes calldata defaultHookMetadata + ) + external + payable + returns (bytes32 messageId); + + function quoteDispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody, + bytes calldata defaultHookMetadata + ) + external + view + returns (uint256 fee); + + function dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata body, + bytes calldata customHookMetadata, + IPostDispatchHook customHook + ) + external + payable + returns (bytes32 messageId); + + function quoteDispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody, + bytes calldata customHookMetadata, + IPostDispatchHook customHook + ) + external + view + returns (uint256 fee); + + function process(bytes calldata metadata, bytes calldata message) external payable; + + function recipientIsm(address recipient) external view returns (IInterchainSecurityModule module); +} diff --git a/src/interfaces/hyperlane/IPostDispatchHook.sol b/src/interfaces/hyperlane/IPostDispatchHook.sol new file mode 100644 index 0000000..26fcedc --- /dev/null +++ b/src/interfaces/hyperlane/IPostDispatchHook.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +interface IPostDispatchHook { + enum Types { + UNUSED, + ROUTING, + AGGREGATION, + MERKLE_TREE, + INTERCHAIN_GAS_PAYMASTER, + FALLBACK_ROUTING, + ID_AUTH_ISM, + PAUSABLE, + PROTOCOL_FEE, + LAYER_ZERO_V1, + RATE_LIMITED, + ARB_L2_TO_L1, + OP_L2_TO_L1 + } + + /** + * @notice Returns an enum that represents the type of hook + */ + function hookType() external view returns (uint8); + + /** + * @notice Returns whether the hook supports metadata + * @param metadata metadata + * @return Whether the hook supports metadata + */ + function supportsMetadata(bytes calldata metadata) external view returns (bool); + + /** + * @notice Post action after a message is dispatched via the Mailbox + * @param metadata The metadata required for the hook + * @param message The message passed from the Mailbox.dispatch() call + */ + function postDispatch(bytes calldata metadata, bytes calldata message) external payable; + + /** + * @notice Compute the payment required by the postDispatch call + * @param metadata The metadata required for the hook + * @param message The message passed from the Mailbox.dispatch() call + * @return Quoted payment for the postDispatch call + */ + function quoteDispatch(bytes calldata metadata, bytes calldata message) external view returns (uint256); +} diff --git a/test/CrossChain/MultiChainHyperlaneTellerWithMultiAssetSupport.t.sol b/test/CrossChain/MultiChainHyperlaneTellerWithMultiAssetSupport.t.sol new file mode 100644 index 0000000..bfb94fe --- /dev/null +++ b/test/CrossChain/MultiChainHyperlaneTellerWithMultiAssetSupport.t.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { + MultiChainTellerBase_MessagesNotAllowedFrom, + MultiChainTellerBase_MessagesNotAllowedFromSender +} from "src/base/Roles/CrossChain/MultiChainTellerBase.sol"; + +import { MultiChainBaseTest, MultiChainTellerBase, ERC20, BridgeData } from "./MultiChainBase.t.sol"; +import { MultiChainHyperlaneTellerWithMultiAssetSupport } from + "src/base/Roles/CrossChain/MultiChainHyperlaneTellerWithMultiAssetSupport.sol"; +import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; +import { IMailbox } from "src/interfaces/hyperlane/IMailbox.sol"; + +import { TellerWithMultiAssetSupport } from "src/base/Roles/TellerWithMultiAssetSupport.sol"; + +import { FixedPointMathLib } from "@solmate/utils/FixedPointMathLib.sol"; + +// Is only testing the function calls created from a single source chain. These +// tests do not guarantee correct behavior on the destination chain. +contract MultiChainHyperlaneTellerWithMultiAssetSupportTest is MultiChainBaseTest { + using SafeTransferLib for ERC20; + using FixedPointMathLib for uint256; + + IMailbox constant ETHEREUM_MAILBOX = IMailbox(0xc005dc82818d67AF737725bD4bf75435d065D239); + + uint32 constant SOURCE_DOMAIN = 1; // Ethereum + uint32 constant DESTINATION_DOMAIN = 42_161; // Arbitrum for Testing + + MultiChainHyperlaneTellerWithMultiAssetSupport sourceTeller; + MultiChainHyperlaneTellerWithMultiAssetSupport destinationTeller; + + function setUp() public virtual override(MultiChainBaseTest) { + MultiChainBaseTest.setUp(); + + sourceTeller = MultiChainHyperlaneTellerWithMultiAssetSupport(sourceTellerAddr); + destinationTeller = MultiChainHyperlaneTellerWithMultiAssetSupport(destinationTellerAddr); + } + + function testBridgingShares(uint256 sharesToBridge) external virtual { + sharesToBridge = uint96(bound(sharesToBridge, 1, 1000e18)); + uint256 startingShareBalance = boringVault.balanceOf(address(this)); + + // Setup chains on bridge. + sourceTeller.addChain(DESTINATION_DOMAIN, true, true, destinationTellerAddr, CHAIN_MESSAGE_GAS_LIMIT, 0); + + destinationTeller.addChain(SOURCE_DOMAIN, true, true, sourceTellerAddr, CHAIN_MESSAGE_GAS_LIMIT, 0); + + // Bridge shares. + address to = vm.addr(1); + + BridgeData memory data = BridgeData({ + chainSelector: DESTINATION_DOMAIN, + destinationChainReceiver: to, + bridgeFeeToken: ERC20(NATIVE), + messageGas: 80_000, + data: "" + }); + + uint256 quote = _getTypedTeller(sourceTellerAddr).previewFee(sharesToBridge, data); + assertTrue(quote != 0, "Quote should not be 0"); + + bytes32 id = sourceTeller.bridge{ value: quote }(sharesToBridge, data); + + assertEq( + boringVault.balanceOf(address(this)), + startingShareBalance - sharesToBridge, + "Should have burned shares on source chain" + ); + } + + function testDepositAndBridgeFailsWithShareLockTime(uint256 amount) external virtual { + sourceTeller.addChain(DESTINATION_DOMAIN, true, true, destinationTellerAddr, CHAIN_MESSAGE_GAS_LIMIT, 0); + sourceTeller.setShareLockPeriod(60); + + amount = bound(amount, 0.0001e18, 10_000e18); + // make a user and give them WETH + address user = makeAddr("A user"); + address userChain2 = makeAddr("A user on chain 2"); + deal(address(WETH), user, amount); + + // approve teller to spend WETH + vm.startPrank(user); + vm.deal(user, 10e18); + WETH.approve(address(boringVault), amount); + + // perform depositAndBridge + BridgeData memory data = BridgeData({ + chainSelector: DESTINATION_DOMAIN, + destinationChainReceiver: userChain2, + bridgeFeeToken: ERC20(NATIVE), + messageGas: 80_000, + data: "" + }); + + uint256 ONE_SHARE = 10 ** boringVault.decimals(); + + // so you don't really need to know exact shares in reality + // just need to pass in a number roughly the same size to get quote + // I still get the real number here for testing + uint256 shares = amount.mulDivDown(ONE_SHARE, accountant.getRateInQuoteSafe(WETH)); + uint256 quote = sourceTeller.previewFee(shares, data); + + vm.expectRevert( + bytes( + abi.encodeWithSelector( + TellerWithMultiAssetSupport.TellerWithMultiAssetSupport__SharesAreLocked.selector + ) + ) + ); + sourceTeller.depositAndBridge{ value: quote }(WETH, amount, shares, data); + } + + function testDepositAndBridge(uint256 amount) external virtual { + sourceTeller.addChain(DESTINATION_DOMAIN, true, true, destinationTellerAddr, CHAIN_MESSAGE_GAS_LIMIT, 0); + destinationTeller.addChain(SOURCE_DOMAIN, true, true, sourceTellerAddr, CHAIN_MESSAGE_GAS_LIMIT, 0); + + amount = bound(amount, 0.0001e18, 10_000e18); + + // make a user and give them WETH + address user = makeAddr("A user"); + address userChain2 = makeAddr("A user on chain 2"); + deal(address(WETH), user, amount); + + // approve teller to spend WETH + vm.startPrank(user); + vm.deal(user, 10e18); + WETH.approve(address(boringVault), amount); + + // perform depositAndBridge + BridgeData memory data = BridgeData({ + chainSelector: DESTINATION_DOMAIN, + destinationChainReceiver: userChain2, + bridgeFeeToken: ERC20(NATIVE), + messageGas: 80_000, + data: "" + }); + + uint256 ONE_SHARE = 10 ** boringVault.decimals(); + + // so you don't really need to know exact shares in reality + // just need to pass in a number roughly the same size to get quote + // I still get the real number here for testing + uint256 shares = amount.mulDivDown(ONE_SHARE, accountant.getRateInQuoteSafe(WETH)); + uint256 quote = sourceTeller.previewFee(shares, data); + uint256 wethBefore = WETH.balanceOf(address(boringVault)); + + sourceTeller.depositAndBridge{ value: quote }(WETH, amount, shares, data); + + assertEq(boringVault.balanceOf(user), 0, "Should have burned shares."); + + assertEq(WETH.balanceOf(address(boringVault)), wethBefore + shares); + vm.stopPrank(); + } + + function testReverts() public virtual override { + super.testReverts(); + + // if the token is not NATIVE, should revert + address NOT_NATIVE = 0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3; + BridgeData memory data = + BridgeData(DESTINATION_DOMAIN, address(this), ERC20(NOT_NATIVE), 80_000, abi.encode(DESTINATION_DOMAIN)); + sourceTeller.addChain(DESTINATION_DOMAIN, true, true, destinationTellerAddr, CHAIN_MESSAGE_GAS_LIMIT, 0); + + vm.expectRevert( + abi.encodeWithSelector( + MultiChainHyperlaneTellerWithMultiAssetSupport + .MultiChainHyperlaneTellerWithMultiAssetSupport_InvalidBridgeFeeToken + .selector + ) + ); + sourceTeller.bridge(1e18, data); + + // Call now succeeds. + data = BridgeData(DESTINATION_DOMAIN, address(this), ERC20(NATIVE), 80_000, abi.encode(DESTINATION_DOMAIN)); + uint256 quote = sourceTeller.previewFee(1e18, data); + + sourceTeller.bridge{ value: quote }(1e18, data); + } + + function testRevertsOnHandle() public { + bytes memory payload = ""; + + // If the caller on `handle` is not mailbox, should revert. + vm.expectRevert( + abi.encodeWithSelector( + MultiChainHyperlaneTellerWithMultiAssetSupport + .MultiChainHyperlaneTellerWithMultiAssetSupport_CallerMustBeMailbox + .selector, + address(this) + ) + ); + destinationTeller.handle( + SOURCE_DOMAIN, + _addressToBytes32(sourceTellerAddr), // correct sender + payload + ); + + // If the `sender` param is not the teller, should revert. + vm.startPrank(address(ETHEREUM_MAILBOX)); + vm.expectRevert( + abi.encodeWithSelector( + MultiChainTellerBase_MessagesNotAllowedFromSender.selector, uint256(SOURCE_DOMAIN), address(this) + ) + ); + destinationTeller.handle( + SOURCE_DOMAIN, + _addressToBytes32(address(this)), // wrong sender + payload + ); + vm.stopPrank(); + + // Set `SOURCE_DOMAIN`'s `allowMessagesFrom` to be false + vm.prank(destinationTeller.owner()); + destinationTeller.addChain(SOURCE_DOMAIN, false, true, sourceTellerAddr, CHAIN_MESSAGE_GAS_LIMIT, 0); + + vm.startPrank(address(ETHEREUM_MAILBOX)); + vm.expectRevert( + abi.encodeWithSelector(MultiChainTellerBase_MessagesNotAllowedFrom.selector, uint256(SOURCE_DOMAIN)) + ); + destinationTeller.handle( + SOURCE_DOMAIN, // now disallowed + _addressToBytes32(sourceTellerAddr), // correct sender + payload + ); + vm.stopPrank(); + } + + function testRevertOnInvalidBytes32Address() public { + bytes32 invalidSender = bytes32(uint256(type(uint168).max)); + + vm.startPrank(address(ETHEREUM_MAILBOX)); + vm.expectRevert( + abi.encodeWithSelector( + MultiChainHyperlaneTellerWithMultiAssetSupport + .MultiChainHyperlaneTellerWithMultiAssetSupport_InvalidBytes32Address + .selector, + invalidSender + ) + ); + destinationTeller.handle(SOURCE_DOMAIN, invalidSender, ""); + vm.stopPrank(); + } + + /** + * Trying to bridge token to the zero address should fail as it will simply + * burn the token. We don't want to allow this in a bridging context. + */ + function testRevertOnInvalidDestinationReceiver() public { + deal(address(WETH), address(this), 1e18); + + sourceTeller.addChain(DESTINATION_DOMAIN, true, true, destinationTellerAddr, CHAIN_MESSAGE_GAS_LIMIT, 0); + + WETH.approve(address(boringVault), 1e18); + + vm.expectRevert( + abi.encodeWithSelector( + MultiChainHyperlaneTellerWithMultiAssetSupport + .MultiChainHyperlaneTellerWithMultiAssetSupport_ZeroAddressDestinationReceiver + .selector + ) + ); + sourceTeller.depositAndBridge( + WETH, 1e18, 1e18, BridgeData(DESTINATION_DOMAIN, address(0), ERC20(NATIVE), 80_000, "") + ); + } + + function _getTypedTeller(address addr) internal returns (MultiChainHyperlaneTellerWithMultiAssetSupport) { + return MultiChainHyperlaneTellerWithMultiAssetSupport(addr); + } + + function _deploySourceAndDestinationTeller() internal virtual override { + sourceTellerAddr = address( + new MultiChainHyperlaneTellerWithMultiAssetSupport( + address(this), address(boringVault), address(accountant), ETHEREUM_MAILBOX + ) + ); + + destinationTellerAddr = address( + new MultiChainHyperlaneTellerWithMultiAssetSupport( + address(this), address(boringVault), address(accountant), ETHEREUM_MAILBOX + ) + ); + } + + function _addressToBytes32(address _address) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_address))); + } +}