Skip to content

Commit

Permalink
feat: multichain hyperlane teller contract
Browse files Browse the repository at this point in the history
  • Loading branch information
junkim012 committed Dec 6, 2024
1 parent 5af10cd commit e6d3cae
Show file tree
Hide file tree
Showing 5 changed files with 489 additions and 0 deletions.
141 changes: 141 additions & 0 deletions src/base/Roles/CrossChain/Hyperlane/StandardHookMetadata.sol
Original file line number Diff line number Diff line change
@@ -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, "");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// 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";

/**
* @title MultiChainHyperlaneTellerWithMultiAssetSupport
* @notice Hyperlane implementation of MultiChainTeller
* @custom:security-contact [email protected]
*/
contract MultiChainHyperlaneTellerWithMultiAssetSupport is MultiChainTellerBase {
// ========================================= STATE =========================================

/**
* @notice The hyperlane mailbox contract.
*/
IMailbox public immutable mailbox;

/**
* @notice A nonce used to generate unique message IDs.
*/
uint128 public nonce;

//============================== ERRORS ===============================

error MultiChainHyperlaneTeller_InvalidToken();
error MultiChainHyperlaneTeller_CallerMustBeMailbox(address caller);

constructor(
address _owner,
address _vault,
address _accountant,
IMailbox _mailbox
)
MultiChainTellerBase(_owner, _vault, _accountant)
{
mailbox = _mailbox;
}

/**
* @notice function override to return the fee quote
* @param shareAmount to be sent as a message
* @param data Bridge data
* @returns fee to be paid for bridging
*/
function _quote(uint256 shareAmount, BridgeData calldata data) internal view override returns (uint256) {
bytes memory _payload = abi.encode(shareAmount, data.destinationChainReceiver);
bytes32 msgRecipient = _addressToBytes32(selectorToChains[data.chainSelector].targetTeller);

return mailbox.quoteDispatch(data.chainSelector, msgRecipient, _payload); // TODO Should there be hook metadata
// for quoteDispatch?
}

/**
* @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.

// TODO How does setting the ISM work? Is it necessary?
if (msg.sender != address(mailbox)) {
revert MultiChainHyperlaneTeller_CallerMustBeMailbox(msg.sender);
}

// TODO check that bytes32 to address works properly
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));
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) {
unchecked {
messageId = keccak256(abi.encodePacked(++nonce, address(this), block.chainid));
}

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

bytes32 messageId = 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
);
}

function _addressToBytes32(address _address) internal pure returns (bytes32) {
return bytes32(uint256(uint160(_address)));
}

function _bytes32ToAddress(bytes32 _address) internal pure returns (address) {
return address(uint160(uint256(_address)));
}
}
40 changes: 40 additions & 0 deletions src/interfaces/hyperlane/IInterchainSecurityModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.6.11;

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

0 comments on commit e6d3cae

Please sign in to comment.