Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/HubPortal.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.26;
import { IERC20 } from "../lib/common/src/interfaces/IERC20.sol";
import { TransceiverStructs } from "../lib/native-token-transfers/evm/src/libraries/TransceiverStructs.sol";
import { IMTokenLike } from "./interfaces/IMTokenLike.sol";
import { IRegistrarLike } from "./interfaces/IRegistrarLike.sol";
import { IHubPortal } from "./interfaces/IHubPortal.sol";
import { IMerkleTreeBuilder } from "./interfaces/IMerkleTreeBuilder.sol";
import { Portal } from "./Portal.sol";
import { PayloadEncoder } from "./libs/PayloadEncoder.sol";
import { TypeConverter } from "./libs/TypeConverter.sol";
/**
* @title Portal residing on Ethereum Mainnet handling sending/receiving M and pushing the M index and Registrar keys.
* @author M^0 Labs
*/
contract HubPortal is IHubPortal, Portal {
using TypeConverter for address;
/* ============ Variables ============ */
/// @inheritdoc IHubPortal
bool public wasEarningEnabled;
/// @inheritdoc IHubPortal
uint128 public disableEarningIndex;
/// @inheritdoc IHubPortal
address public merkleTreeBuilder;
/* ============ Constructor ============ */
/**
* @notice Constructs the contract.
* @param mToken_ The address of the M token to bridge.
* @param registrar_ The address of the Registrar.
* @param swapFacility_ The address of Swap Facility.
* @param chainId_ Wormhole chain id.
*/
constructor(
address mToken_,
address registrar_,
address swapFacility_,
uint16 chainId_
) Portal(mToken_, registrar_, swapFacility_, Mode.LOCKING, chainId_) {}
/* ============ Interactive Functions ============ */
/// @inheritdoc IHubPortal
function sendMTokenIndex(
uint16 destinationChainId_,
bytes32 refundAddress_,
bytes memory transceiverInstructions_
) external payable returns (bytes32 messageId_) {
uint128 index_ = _currentIndex();
messageId_ = _isSVM(destinationChainId_)
? _sendMTokenIndexToSVM(destinationChainId_, index_, refundAddress_, transceiverInstructions_)
: _sendCustomMessage(
destinationChainId_,
refundAddress_,
PayloadEncoder.encodeIndex(index_, destinationChainId_),
transceiverInstructions_
);
emit MTokenIndexSent(destinationChainId_, messageId_, index_);
}
/// @inheritdoc IHubPortal
function sendRegistrarKey(
uint16 destinationChainId_,
bytes32 key_,
bytes32 refundAddress_,
bytes memory transceiverInstructions_
) external payable returns (bytes32 messageId_) {
// Sending Registrar key to SVM chains is not supported at this time.
// To propagate earners to SVM chains call `sendEarnersMerkleRoot`.
if (_isSVM(destinationChainId_)) revert UnsupportedDestinationChain(destinationChainId_);
bytes32 value_ = IRegistrarLike(registrar).get(key_);
bytes memory payload_ = PayloadEncoder.encodeKey(key_, value_, destinationChainId_);
messageId_ = _sendCustomMessage(destinationChainId_, refundAddress_, payload_, transceiverInstructions_);
emit RegistrarKeySent(destinationChainId_, messageId_, key_, value_);
}
/// @inheritdoc IHubPortal
function sendRegistrarListStatus(
uint16 destinationChainId_,
bytes32 listName_,
address account_,
bytes32 refundAddress_,
bytes memory transceiverInstructions_
) external payable returns (bytes32 messageId_) {
// Sending Registrar list status to SVM chains is not supported at this time.
// To propagate earners to SVM chains call `sendEarnersMerkleRoot`.
if (_isSVM(destinationChainId_)) revert UnsupportedDestinationChain(destinationChainId_);
bool status_ = IRegistrarLike(registrar).listContains(listName_, account_);
bytes memory payload_ = PayloadEncoder.encodeListUpdate(listName_, account_, status_, destinationChainId_);
messageId_ = _sendCustomMessage(destinationChainId_, refundAddress_, payload_, transceiverInstructions_);
emit RegistrarListStatusSent(destinationChainId_, messageId_, listName_, account_, status_);
}
/// @inheritdoc IHubPortal
function sendEarnersMerkleRoot(
uint16 destinationChainId_,
bytes32 refundAddress_,
bytes memory transceiverInstructions_
) external payable returns (bytes32 messageId_) {
if (!_isSVM(destinationChainId_)) revert UnsupportedDestinationChain(destinationChainId_);
bytes32 destinationToken_ = destinationMToken[destinationChainId_];
// TODO: verify if a separate Merkle root needed for each SVM chain
bytes32 earnersMerkleRoot_ = IMerkleTreeBuilder(merkleTreeBuilder).getRoot(_SOLANA_EARNER_LIST);
bytes memory additionalPayload_ = PayloadEncoder.encodeAdditionalPayload(
_currentIndex(),
destinationToken_,
earnersMerkleRoot_
);
(, messageId_) = _transferNativeToken(
0,
token,
destinationChainId_,
destinationToken_,
refundAddress_, // recipient doesn't matter since transfer amount is 0
refundAddress_,
additionalPayload_,
transceiverInstructions_
);
emit EarnersMerkleRootSent(destinationChainId_, messageId_, earnersMerkleRoot_);
}
/// @inheritdoc IHubPortal
function setMerkleTreeBuilder(address merkleTreeBuilder_) external onlyOwner {
if ((merkleTreeBuilder = merkleTreeBuilder_) == address(0)) revert ZeroMerkleTreeBuilder();
emit MerkleTreeBuilderSet(merkleTreeBuilder_);
}
/// @inheritdoc IHubPortal
function enableEarning() external {
if (_isEarningEnabled()) revert EarningIsEnabled();
if (wasEarningEnabled) revert EarningCannotBeReenabled();
wasEarningEnabled = true;
IMTokenLike(mToken()).startEarning();
emit EarningEnabled(IMTokenLike(mToken()).currentIndex());
}
/// @inheritdoc IHubPortal
function disableEarning() external {
if (!_isEarningEnabled()) revert EarningIsDisabled();
uint128 currentMIndex_ = IMTokenLike(mToken()).currentIndex();
disableEarningIndex = currentMIndex_;
IMTokenLike(mToken()).stopEarning(address(this));
emit EarningDisabled(currentMIndex_);
}
/* ============ Internal Interactive Functions ============ */
/**
* @dev Unlocks M tokens to `recipient_`.
* @param recipient_ The account to unlock/transfer M tokens to.
* @param amount_ The amount of M Token to unlock to the recipient.
*/
function _mintOrUnlock(address recipient_, uint256 amount_, uint128) internal override {
if (recipient_ != address(this)) {
IERC20(mToken()).transfer(recipient_, amount_);
}
}
/// @dev Sends a custom (not a transfer) message to the destination chain.
function _sendCustomMessage(
uint16 destinationChainId_,
bytes32 refundAddress_,
bytes memory payload_,
bytes memory transceiverInstructions_
) private returns (bytes32 messageId_) {
if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress();
TransceiverStructs.NttManagerMessage memory message_ = TransceiverStructs.NttManagerMessage(
bytes32(uint256(_useMessageSequence())),
msg.sender.toBytes32(),
payload_
);
_sendMessage(destinationChainId_, refundAddress_, message_, transceiverInstructions_);
messageId_ = TransceiverStructs.nttManagerMessageDigest(chainId, message_);
}
/// @dev A workaround to send M Token Index to SVM chains as an additional payload with zero token transfer
function _sendMTokenIndexToSVM(
uint16 destinationChainId_,
uint128 index_,
bytes32 refundAddress_,
bytes memory transceiverInstructions_
) private returns (bytes32 messageId_) {
bytes32 destinationToken_ = destinationMToken[destinationChainId_];
bytes memory additionalPayload_ = PayloadEncoder.encodeAdditionalPayload(index_, destinationToken_);
(, messageId_) = _transferNativeToken(
0,
token,
destinationChainId_,
destinationToken_,
refundAddress_, // recipient doesn't matter since transfer amount is 0
refundAddress_,
additionalPayload_,
transceiverInstructions_
);
}
/* ============ Internal View/Pure Functions ============ */
/// @dev If earning is enabled returns the current M token index,
/// otherwise, returns the index at the time when earning was disabled.
function _currentIndex() internal view override returns (uint128) {
return _isEarningEnabled() ? IMTokenLike(mToken()).currentIndex() : disableEarningIndex;
}
/// @dev Returns whether earning was enabled for HubPortal or not.
function _isEarningEnabled() internal view returns (bool) {
return wasEarningEnabled && disableEarningIndex == 0;
}
}
"
},
"lib/common/src/interfaces/IERC20.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.20 <0.9.0;
/**
* @title ERC20 Token Standard.
* @author M^0 Labs
* @dev The interface as defined by EIP-20: https://eips.ethereum.org/EIPS/eip-20
*/
interface IERC20 {
/* ============ Events ============ */
/**
* @notice Emitted when `spender` has been approved for `amount` of the token balance of `account`.
* @param account The address of the account.
* @param spender The address of the spender being approved for the allowance.
* @param amount The amount of the allowance being approved.
*/
event Approval(address indexed account, address indexed spender, uint256 amount);
/**
* @notice Emitted when `amount` tokens is transferred from `sender` to `recipient`.
* @param sender The address of the sender who's token balance is decremented.
* @param recipient The address of the recipient who's token balance is incremented.
* @param amount The amount of tokens being transferred.
*/
event Transfer(address indexed sender, address indexed recipient, uint256 amount);
/* ============ Interactive Functions ============ */
/**
* @notice Allows a calling account to approve `spender` to spend up to `amount` of its token balance.
* @dev MUST emit an `Approval` event.
* @param spender The address of the account being allowed to spend up to the allowed amount.
* @param amount The amount of the allowance being approved.
* @return Whether or not the approval was successful.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @notice Allows a calling account to transfer `amount` tokens to `recipient`.
* @param recipient The address of the recipient who's token balance will be incremented.
* @param amount The amount of tokens being transferred.
* @return Whether or not the transfer was successful.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @notice Allows a calling account to transfer `amount` tokens from `sender`, with allowance, to a `recipient`.
* @param sender The address of the sender who's token balance will be decremented.
* @param recipient The address of the recipient who's token balance will be incremented.
* @param amount The amount of tokens being transferred.
* @return Whether or not the transfer was successful.
*/
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
/* ============ View/Pure Functions ============ */
/**
* @notice Returns the allowance `spender` is allowed to spend on behalf of `account`.
* @param account The address of the account who's token balance `spender` is allowed to spend.
* @param spender The address of an account allowed to spend on behalf of `account`.
* @return The amount `spender` can spend on behalf of `account`.
*/
function allowance(address account, address spender) external view returns (uint256);
/**
* @notice Returns the token balance of `account`.
* @param account The address of some account.
* @return The token balance of `account`.
*/
function balanceOf(address account) external view returns (uint256);
/// @notice Returns the number of decimals UIs should assume all amounts have.
function decimals() external view returns (uint8);
/// @notice Returns the name of the contract/token.
function name() external view returns (string memory);
/// @notice Returns the symbol of the token.
function symbol() external view returns (string memory);
/// @notice Returns the current total supply of the token.
function totalSupply() external view returns (uint256);
}
"
},
"lib/native-token-transfers/evm/src/libraries/TransceiverStructs.sol": {
"content": "// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;
import "wormhole-solidity-sdk/libraries/BytesParsing.sol";
import "./TrimmedAmount.sol";
library TransceiverStructs {
using BytesParsing for bytes;
using TrimmedAmountLib for TrimmedAmount;
/// @notice Error thrown when the payload length exceeds the allowed maximum.
/// @dev Selector 0xa3419691.
/// @param size The size of the payload.
error PayloadTooLong(uint256 size);
/// @notice Error thrown when the prefix of an encoded message
/// does not match the expected value.
/// @dev Selector 0x56d2569d.
/// @param prefix The prefix that was found in the encoded message.
error IncorrectPrefix(bytes4 prefix);
/// @notice Error thrown when the transceiver instructions aren't
/// encoded with strictly increasing indices
/// @dev Selector 0x0555a4b9.
/// @param lastIndex Last parsed instruction index
/// @param instructionIndex The instruction index that was unordered
error UnorderedInstructions(uint256 lastIndex, uint256 instructionIndex);
/// @notice Error thrown when a transceiver instruction index
/// is greater than the number of registered transceivers
/// @dev We index from 0 so if providedIndex == numTransceivers then we're out-of-bounds too
/// @dev Selector 0x689f5016.
/// @param providedIndex The index specified in the instruction
/// @param numTransceivers The number of registered transceivers
error InvalidInstructionIndex(uint256 providedIndex, uint256 numTransceivers);
/// @dev Prefix for all NativeTokenTransfer payloads
/// This is 0x99'N''T''T'
bytes4 constant NTT_PREFIX = 0x994E5454;
/// @dev Message emitted and received by the nttManager contract.
/// The wire format is as follows:
/// - id - 32 bytes
/// - sender - 32 bytes
/// - payloadLength - 2 bytes
/// - payload - `payloadLength` bytes
struct NttManagerMessage {
/// @notice unique message identifier
/// @dev This is incrementally assigned on EVM chains, but this is not
/// guaranteed on other runtimes.
bytes32 id;
/// @notice original message sender address.
bytes32 sender;
/// @notice payload that corresponds to the type.
bytes payload;
}
function nttManagerMessageDigest(
uint16 sourceChainId,
NttManagerMessage memory m
) public pure returns (bytes32) {
return _nttManagerMessageDigest(sourceChainId, encodeNttManagerMessage(m));
}
function _nttManagerMessageDigest(
uint16 sourceChainId,
bytes memory encodedNttManagerMessage
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(sourceChainId, encodedNttManagerMessage));
}
function encodeNttManagerMessage(
NttManagerMessage memory m
) public pure returns (bytes memory encoded) {
if (m.payload.length > type(uint16).max) {
revert PayloadTooLong(m.payload.length);
}
uint16 payloadLength = uint16(m.payload.length);
return abi.encodePacked(m.id, m.sender, payloadLength, m.payload);
}
/// @notice Parse a NttManagerMessage.
/// @param encoded The byte array corresponding to the encoded message
/// @return nttManagerMessage The parsed NttManagerMessage struct.
function parseNttManagerMessage(
bytes memory encoded
) public pure returns (NttManagerMessage memory nttManagerMessage) {
uint256 offset = 0;
(nttManagerMessage.id, offset) = encoded.asBytes32Unchecked(offset);
(nttManagerMessage.sender, offset) = encoded.asBytes32Unchecked(offset);
uint256 payloadLength;
(payloadLength, offset) = encoded.asUint16Unchecked(offset);
(nttManagerMessage.payload, offset) = encoded.sliceUnchecked(offset, payloadLength);
encoded.checkLength(offset);
}
/// @dev Native Token Transfer payload.
/// The wire format is as follows:
/// - NTT_PREFIX - 4 bytes
/// - numDecimals - 1 byte
/// - amount - 8 bytes
/// - sourceToken - 32 bytes
/// - to - 32 bytes
/// - toChain - 2 bytes
/// - additionalPayloadLength - 2 bytes, optional
/// - additionalPayload - `additionalPayloadLength` bytes
struct NativeTokenTransfer {
/// @notice Amount being transferred (big-endian u64 and u8 for decimals)
TrimmedAmount amount;
/// @notice Source chain token address.
bytes32 sourceToken;
/// @notice Address of the recipient.
bytes32 to;
/// @notice Chain ID of the recipient
uint16 toChain;
/// @notice Custom payload
/// @dev Recommended that the first 4 bytes are a unique prefix
bytes additionalPayload;
}
function encodeNativeTokenTransfer(
NativeTokenTransfer memory m
) public pure returns (bytes memory encoded) {
// The `amount` and `decimals` fields are encoded in reverse order compared to how they are declared in the
// `TrimmedAmount` type. This is consistent with the Rust NTT implementation.
TrimmedAmount transferAmount = m.amount;
if (m.additionalPayload.length > 0) {
if (m.additionalPayload.length > type(uint16).max) {
revert PayloadTooLong(m.additionalPayload.length);
}
uint16 additionalPayloadLength = uint16(m.additionalPayload.length);
return abi.encodePacked(
NTT_PREFIX,
transferAmount.getDecimals(),
transferAmount.getAmount(),
m.sourceToken,
m.to,
m.toChain,
additionalPayloadLength,
m.additionalPayload
);
}
return abi.encodePacked(
NTT_PREFIX,
transferAmount.getDecimals(),
transferAmount.getAmount(),
m.sourceToken,
m.to,
m.toChain
);
}
/// @dev Parse a NativeTokenTransfer.
/// @param encoded The byte array corresponding to the encoded message
/// @return nativeTokenTransfer The parsed NativeTokenTransfer struct.
function parseNativeTokenTransfer(
bytes memory encoded
) public pure returns (NativeTokenTransfer memory nativeTokenTransfer) {
uint256 offset = 0;
bytes4 prefix;
(prefix, offset) = encoded.asBytes4Unchecked(offset);
if (prefix != NTT_PREFIX) {
revert IncorrectPrefix(prefix);
}
// The `amount` and `decimals` fields are parsed in reverse order compared to how they are declared in the
// `TrimmedAmount` struct. This is consistent with the Rust NTT implementation.
uint8 numDecimals;
(numDecimals, offset) = encoded.asUint8Unchecked(offset);
uint64 amount;
(amount, offset) = encoded.asUint64Unchecked(offset);
nativeTokenTransfer.amount = packTrimmedAmount(amount, numDecimals);
(nativeTokenTransfer.sourceToken, offset) = encoded.asBytes32Unchecked(offset);
(nativeTokenTransfer.to, offset) = encoded.asBytes32Unchecked(offset);
(nativeTokenTransfer.toChain, offset) = encoded.asUint16Unchecked(offset);
// The additional payload may be omitted, but if it is included, it is prefixed by a u16 for its length.
// If there are at least 2 bytes remaining, attempt to parse the additional payload.
if (encoded.length >= offset + 2) {
uint256 payloadLength;
(payloadLength, offset) = encoded.asUint16Unchecked(offset);
(nativeTokenTransfer.additionalPayload, offset) =
encoded.sliceUnchecked(offset, payloadLength);
}
encoded.checkLength(offset);
}
/// @dev Message emitted by Transceiver implementations.
/// Each message includes an Transceiver-specified 4-byte prefix.
/// The wire format is as follows:
/// - prefix - 4 bytes
/// - sourceNttManagerAddress - 32 bytes
/// - recipientNttManagerAddress - 32 bytes
/// - nttManagerPayloadLength - 2 bytes
/// - nttManagerPayload - `nttManagerPayloadLength` bytes
/// - transceiverPayloadLength - 2 bytes
/// - transceiverPayload - `transceiverPayloadLength` bytes
struct TransceiverMessage {
/// @notice Address of the NttManager contract that emitted this message.
bytes32 sourceNttManagerAddress;
/// @notice Address of the NttManager contract that receives this message.
bytes32 recipientNttManagerAddress;
/// @notice Payload provided to the Transceiver contract by the NttManager contract.
bytes nttManagerPayload;
/// @notice Optional payload that the transceiver can encode and use for its own message passing purposes.
bytes transceiverPayload;
}
// @notice Encodes an Transceiver message for communication between the
// NttManager and the Transceiver.
// @param m The TransceiverMessage struct containing the message details.
// @return encoded The byte array corresponding to the encoded message.
// @custom:throw PayloadTooLong if the length of transceiverId, nttManagerPayload,
// or transceiverPayload exceeds the allowed maximum.
function encodeTransceiverMessage(
bytes4 prefix,
TransceiverMessage memory m
) public pure returns (bytes memory encoded) {
if (m.nttManagerPayload.length > type(uint16).max) {
revert PayloadTooLong(m.nttManagerPayload.length);
}
uint16 nttManagerPayloadLength = uint16(m.nttManagerPayload.length);
if (m.transceiverPayload.length > type(uint16).max) {
revert PayloadTooLong(m.transceiverPayload.length);
}
uint16 transceiverPayloadLength = uint16(m.transceiverPayload.length);
return abi.encodePacked(
prefix,
m.sourceNttManagerAddress,
m.recipientNttManagerAddress,
nttManagerPayloadLength,
m.nttManagerPayload,
transceiverPayloadLength,
m.transceiverPayload
);
}
function buildAndEncodeTransceiverMessage(
bytes4 prefix,
bytes32 sourceNttManagerAddress,
bytes32 recipientNttManagerAddress,
bytes memory nttManagerMessage,
bytes memory transceiverPayload
) public pure returns (TransceiverMessage memory, bytes memory) {
TransceiverMessage memory transceiverMessage = TransceiverMessage({
sourceNttManagerAddress: sourceNttManagerAddress,
recipientNttManagerAddress: recipientNttManagerAddress,
nttManagerPayload: nttManagerMessage,
transceiverPayload: transceiverPayload
});
bytes memory encoded = encodeTransceiverMessage(prefix, transceiverMessage);
return (transceiverMessage, encoded);
}
/// @dev Parses an encoded message and extracts information into an TransceiverMessage struct.
/// @param encoded The encoded bytes containing information about the TransceiverMessage.
/// @return transceiverMessage The parsed TransceiverMessage struct.
/// @custom:throw IncorrectPrefix if the prefix of the encoded message does not
/// match the expected prefix.
function parseTransceiverMessage(
bytes4 expectedPrefix,
bytes memory encoded
) internal pure returns (TransceiverMessage memory transceiverMessage) {
uint256 offset = 0;
bytes4 prefix;
(prefix, offset) = encoded.asBytes4Unchecked(offset);
if (prefix != expectedPrefix) {
revert IncorrectPrefix(prefix);
}
(transceiverMessage.sourceNttManagerAddress, offset) = encoded.asBytes32Unchecked(offset);
(transceiverMessage.recipientNttManagerAddress, offset) = encoded.asBytes32Unchecked(offset);
uint16 nttManagerPayloadLength;
(nttManagerPayloadLength, offset) = encoded.asUint16Unchecked(offset);
(transceiverMessage.nttManagerPayload, offset) =
encoded.sliceUnchecked(offset, nttManagerPayloadLength);
uint16 transceiverPayloadLength;
(transceiverPayloadLength, offset) = encoded.asUint16Unchecked(offset);
(transceiverMessage.transceiverPayload, offset) =
encoded.sliceUnchecked(offset, transceiverPayloadLength);
// Check if the entire byte array has been processed
encoded.checkLength(offset);
}
/// @dev Parses the payload of an Transceiver message and returns
/// the parsed NttManagerMessage struct.
/// @param expectedPrefix The prefix that should be encoded in the nttManager message.
/// @param payload The payload sent across the wire.
function parseTransceiverAndNttManagerMessage(
bytes4 expectedPrefix,
bytes memory payload
) public pure returns (TransceiverMessage memory, NttManagerMessage memory) {
// parse the encoded message payload from the Transceiver
TransceiverMessage memory parsedTransceiverMessage =
parseTransceiverMessage(expectedPrefix, payload);
// parse the encoded message payload from the NttManager
NttManagerMessage memory parsedNttManagerMessage =
parseNttManagerMessage(parsedTransceiverMessage.nttManagerPayload);
return (parsedTransceiverMessage, parsedNttManagerMessage);
}
/// @dev Variable-length transceiver-specific instruction that can be passed by the caller to the nttManager.
/// The index field refers to the index of the registeredTransceiver that this instruction should be passed to.
/// The serialization format is:
/// - index - 1 byte
/// - payloadLength - 1 byte
/// - payload - `payloadLength` bytes
struct TransceiverInstruction {
uint8 index;
bytes payload;
}
function encodeTransceiverInstruction(
TransceiverInstruction memory instruction
) public pure returns (bytes memory) {
if (instruction.payload.length > type(uint8).max) {
revert PayloadTooLong(instruction.payload.length);
}
uint8 payloadLength = uint8(instruction.payload.length);
return abi.encodePacked(instruction.index, payloadLength, instruction.payload);
}
function parseTransceiverInstructionUnchecked(
bytes memory encoded,
uint256 offset
) public pure returns (TransceiverInstruction memory instruction, uint256 nextOffset) {
(instruction.index, nextOffset) = encoded.asUint8Unchecked(offset);
uint8 instructionLength;
(instructionLength, nextOffset) = encoded.asUint8Unchecked(nextOffset);
(instruction.payload, nextOffset) = encoded.sliceUnchecked(nextOffset, instructionLength);
}
function parseTransceiverInstructionChecked(
bytes memory encoded
) public pure returns (TransceiverInstruction memory instruction) {
uint256 offset = 0;
(instruction, offset) = parseTransceiverInstructionUnchecked(encoded, offset);
encoded.checkLength(offset);
}
/// @dev Encode an array of multiple variable-length transceiver-specific instructions.
/// The serialization format is:
/// - instructionsLength - 1 byte
/// - `instructionsLength` number of serialized `TransceiverInstruction` types.
function encodeTransceiverInstructions(
TransceiverInstruction[] memory instructions
) public pure returns (bytes memory) {
if (instructions.length > type(uint8).max) {
revert PayloadTooLong(instructions.length);
}
uint256 instructionsLength = instructions.length;
bytes memory encoded;
for (uint256 i = 0; i < instructionsLength; i++) {
bytes memory innerEncoded = encodeTransceiverInstruction(instructions[i]);
encoded = bytes.concat(encoded, innerEncoded);
}
return abi.encodePacked(uint8(instructionsLength), encoded);
}
function parseTransceiverInstructions(
bytes memory encoded,
uint256 numRegisteredTransceivers
) public pure returns (TransceiverInstruction[] memory) {
uint256 offset = 0;
uint256 instructionsLength;
(instructionsLength, offset) = encoded.asUint8Unchecked(offset);
// We allocate an array with the length of the number of registered transceivers
// This gives us the flexibility to not have to pass instructions for transceivers that
// don't need them
TransceiverInstruction[] memory instructions =
new TransceiverInstruction[](numRegisteredTransceivers);
uint256 lastIndex = 0;
for (uint256 i = 0; i < instructionsLength; i++) {
TransceiverInstruction memory instruction;
(instruction, offset) = parseTransceiverInstructionUnchecked(encoded, offset);
uint8 instructionIndex = instruction.index;
// The instructions passed in have to be strictly increasing in terms of transceiver index
if (i != 0 && instructionIndex <= lastIndex) {
revert UnorderedInstructions(lastIndex, instructionIndex);
}
// Instruction index is out of bounds
if (instructionIndex >= numRegisteredTransceivers) {
revert InvalidInstructionIndex(instructionIndex, numRegisteredTransceivers);
}
lastIndex = instructionIndex;
instructions[instructionIndex] = instruction;
}
encoded.checkLength(offset);
return instructions;
}
struct TransceiverInit {
bytes4 transceiverIdentifier;
bytes32 nttManagerAddress;
uint8 nttManagerMode;
bytes32 tokenAddress;
uint8 tokenDecimals;
}
function encodeTransceiverInit(
TransceiverInit memory init
) public pure returns (bytes memory) {
return abi.encodePacked(
init.transceiverIdentifier,
init.nttManagerAddress,
init.nttManagerMode,
init.tokenAddress,
init.tokenDecimals
);
}
function decodeTransceiverInit(
bytes memory encoded
) public pure returns (TransceiverInit memory init) {
uint256 offset = 0;
(init.transceiverIdentifier, offset) = encoded.asBytes4Unchecked(offset);
(init.nttManagerAddress, offset) = encoded.asBytes32Unchecked(offset);
(init.nttManagerMode, offset) = encoded.asUint8Unchecked(offset);
(init.tokenAddress, offset) = encoded.asBytes32Unchecked(offset);
(init.tokenDecimals, offset) = encoded.asUint8Unchecked(offset);
encoded.checkLength(offset);
}
struct TransceiverRegistration {
bytes4 transceiverIdentifier;
uint16 transceiverChainId;
bytes32 transceiverAddress;
}
function encodeTransceiverRegistration(
TransceiverRegistration memory registration
) public pure returns (bytes memory) {
return abi.encodePacked(
registration.transceiverIdentifier,
registration.transceiverChainId,
registration.transceiverAddress
);
}
function decodeTransceiverRegistration(
bytes memory encoded
) public pure returns (TransceiverRegistration memory registration) {
uint256 offset = 0;
(registration.transceiverIdentifier, offset) = encoded.asBytes4Unchecked(offset);
(registration.transceiverChainId, offset) = encoded.asUint16Unchecked(offset);
(registration.transceiverAddress, offset) = encoded.asBytes32Unchecked(offset);
encoded.checkLength(offset);
}
}
"
},
"src/interfaces/IMTokenLike.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.26;
/**
* @title Subset of M Token interface required for Portal contracts.
* @author M^0 Labs
*/
interface IMTokenLike {
/// @notice The current index that would be written to storage if `updateIndex` is called.
function currentIndex() external view returns (uint128);
/**
* @notice Checks if account is an earner.
* @param account The account to check.
* @return True if account is an earner, false otherwise.
*/
function isEarning(address account) external view returns (bool);
/// @notice Starts earning for caller if allowed by TTG.
function startEarning() external;
/// @notice Stops earning for the account.
function stopEarning(address account) external;
}
"
},
"src/interfaces/IRegistrarLike.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.26;
/**
* @title Subset of Registrar interface required for Portal contracts.
* @author M^0 Labs
*/
interface IRegistrarLike {
/**
* @notice Adds `account` to `list`.
* @param list The key for some list.
* @param account The address of some account to be added.
*/
function addToList(bytes32 list, address account) external;
/**
* @notice Removes `account` from `list`.
* @param list The key for some list.
* @param account The address of some account to be removed.
*/
function removeFromList(bytes32 list, address account) external;
/**
* @notice Sets `key` to `value`.
* @param key Some key.
* @param value Some value.
*/
function setKey(bytes32 key, bytes32 value) external;
/**
* @notice Returns the value of `key`.
* @param key Some key.
* @return Some value.
*/
function get(bytes32 key) external view returns (bytes32);
/**
* @notice Returns whether `list` contains `account`.
* @param list The key for some list.
* @param account The address of some account.
* @return Whether `list` contains `account`.
*/
function listContains(bytes32 list, address account) external view returns (bool);
/// @notice Returns the address of the Portal contract.
function portal() external view returns (address);
}
"
},
"src/interfaces/IHubPortal.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.26;
import { IPortal } from "./IPortal.sol";
/**
* @title HubPortal interface.
* @author M^0 Labs
*/
interface IHubPortal is IPortal {
/* ============ Events ============ */
/**
* @notice Emitted when earning is enabled for the Hub Portal.
* @param index The index at which earning was enabled.
*/
event EarningEnabled(uint128 index);
/**
* @notice Emitted when earning is disabled for the Hub Portal.
* @param index The index at which earning was disabled.
*/
event EarningDisabled(uint128 index);
/**
* @notice Emitted when the M token index is sent to a destination chain.
* @param destinationChainId The Wormhole destination chain ID.
* @param messageId The unique identifier for the sent message.
* @param index The the M token index.
*/
event MTokenIndexSent(uint16 indexed destinationChainId, bytes32 messageId, uint128 index);
/**
* @notice Emitted when the Registrar key is sent to a destination chain.
* @param destinationChainId The Wormhole destination chain ID.
* @param messageId The unique identifier for the sent message.
* @param key The key that was sent.
* @param value The value that was sent.
*/
event RegistrarKeySent(uint16 indexed destinationChainId, bytes32 messageId, bytes32 indexed key, bytes32 value);
/**
* @notice Emitted when the Registrar list status for an account is sent to a destination chain.
* @param destinationChainId The Wormhole destination chain ID.
* @param messageId The unique identifier for the sent message.
* @param listName The name of the list.
* @param account The account.
* @param status The status of the account in the list.
*/
event RegistrarListStatusSent(
uint16 indexed destinationChainId,
bytes32 messageId,
bytes32 indexed listName,
address indexed account,
bool status
);
/**
* @notice Emitted when Merkle Tree Builder contract is set.
* @param merkleTreeBuilder The address of Merkle Tree Builder contract.
*/
event MerkleTreeBuilderSet(address merkleTreeBuilder);
/**
* @notice Emitted when earners Merkle root is sent to Solana.
* @param destinationChainId The Wormhole chain ID for the destination.
* @param messageId The unique identifier for the sent message.
* @param earnersMerkleRoot The Merkle root of earners.
*/
event EarnersMerkleRootSent(uint16 indexed destinationChainId, bytes32 messageId, bytes32 earnersMerkleRoot);
/* ============ Custom Errors ============ */
/// @notice Emitted when trying to enable earning after it has been explicitly disabled.
error EarningCannotBeReenabled();
/// @notice Emitted when performing an operation that is not allowed when earning is disabled.
error EarningIsDisabled();
/// @notice Emitted when performing an operation that is not allowed when earning is enabled.
error EarningIsEnabled();
/// @notice Emitted when calling `setMerkleTreeBuilder` if Merkle Tree Builder address is 0x0.
error ZeroMerkleTreeBuilder();
/// @notice Emitted when the destination chain is not supported
error UnsupportedDestinationChain(uint16 destinationChainId);
/* ============ View/Pure Functions ============ */
/// @notice Indicates whether earning for HubPortal was ever enabled.
function wasEarningEnabled() external returns (bool);
/// @notice Returns the value of M Token index when earning for HubPortal was disabled.
function disableEarningIndex() external returns (uint128);
/// @notice Returns the address of Merkle tree builder.
function merkleTreeBuilder() external returns (address);
/* ============ Interactive Functions ============ */
/**
* @notice Sends the M token index to the destination chain.
* @param destinationChainId The Wormhole destination chain ID.
* @param refundAddress The refund address to receive excess native gas.
* @param transceiverInstructions The transceiver specific instructions for quoting and sending.
* @return messageId The ID uniquely identifying the message.
*/
function sendMTokenIndex(
uint16 destinationChainId,
bytes32 refundAddress,
bytes memory transceiverInstructions
) external payable returns (bytes32 messageId);
/**
* @notice Sends the Registrar key to the destination chain.
* @dev Not supported for SVM chains.
* @param destinationChainId The Wormhole destination chain ID.
* @param key The key to dispatch.
* @param refundAddress The refund address to receive excess native gas.
* @param transceiverInstructions The transceiver specific instructions for quoting and sending.
* @return messageId The ID uniquely identifying the message
*/
function sendRegistrarKey(
uint16 destinationChainId,
bytes32 key,
bytes32 refundAddress,
bytes memory transceiverInstructions
) external payable returns (bytes32 messageId);
/**
* @notice Sends the Registrar list status for an account to the destination chain.
* @dev Not supported for SVM chains.
* @param destinationChainId The Wormhole destination chain ID.
* @param listName The name of the list.
* @param account The account.
* @param refundAddress The refund address to receive excess native gas.
* @param transceiverInstructions The transceiver specific instructions for quoting and sending.
* @return messageId The ID uniquely identifying the message.
*/
function sendRegistrarListStatus(
uint16 destinationChainId,
bytes32 listName,
address account,
bytes32 refundAddress,
bytes memory transceiverInstructions
) external payable returns (bytes32 messageId);
/**
* @notice Sends earners Merkle root to SVM chains.
* @param destinationChainId The Wormhole destination chain ID.
* @param refundAddress The refund address to receive excess native gas.
* @param transceiverInstructions The transceiver specific instructions for quoting and sending.
* @return messageId The ID uniquely identifying the message.
*/
function sendEarnersMerkleRoot(
uint16 destinationChainId,
bytes32 refundAddress,
bytes memory transceiverInstructions
) external payable returns (bytes32 messageId);
/**
* @notice Sets Merkle Tree Builder contract.
* @param merkleTreeBuilder The address of Merkle Tree Builder contract.
*/
function setMerkleTreeBuilder(address merkleTreeBuilder) external;
/// @notice Enables earning for the Hub Portal if allowed by TTG.
function enableEarning() external;
/// @notice Disables earning for the Hub Portal if disallowed by TTG.
function disableEarning() external;
}
"
},
"src/interfaces/IMerkleTreeBuilder.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.26;
/**
* @title MerkleTreeBuilder interface.
* @author M^0 Labs
* @dev This contract allows constructing Merkle Trees from values set on the TTGRegistrar.
* The reason for this is to allow propagating these values, via the Merkle tree roots,
* to other chains in a way that is efficient and trustless.
*/
interface IMerkleTreeBuilder {
/* ========== ERRORS ========== */
error ListAlreadyExists();
error InvalidList();
error InvalidAdd();
error InvalidRemove();
error NotInList();
error ValueInList();
error ValueNotInList();
/* ========== EVENTS ========== */
event RootUpdated(bytes32 indexed list, bytes32 root);
/* ========== MANAGE LISTS ========== */
/**
* @notice Adds a value to a list.
* @dev The value must be included in the list on the TTGRegistrar.
* All registrar values are bytes32 => bytes32 mappings.
* In the case of lists, the key is the hash of abi.encodePacked("VALUE", list, value).
* @param list The list to add the value to.
* @param before The value immediately before the position in the list where the new value should be inserted.
* @param value The value to add to the list.
*/
function addToList(bytes32 list, bytes32 before, bytes32 value) external;
/**
* @notice Removes a value from a list.
* @dev The value must have been removed from the list on the TTGRegistrar.
* @param list The list to remove the value from.
* @param before The value immediately before the value in list.
* @param value The value to remove from the list.
*/
function removeFromList(bytes32 list, bytes32 before, bytes32 value) external;
/* ========== MERKLE TREE ========== */
/**
* @notice Updates the Merkle tree root for a list and stores it for later retrieval.
* @dev This should be called after adding or removing values from the list.
* @param list The list to update the root for.
*/
function updateRoot(bytes32 list) external;
/* ========== VIEWS ========== */
/**
* @notice Retrieves the value following the provided value in the list.
* @dev This is useful for iterating over the list off-chain.
*/
function getNext(bytes32 list, bytes32 value) external view returns (bytes32);
/**
* @notice Retrieves the length of the list.
* @dev This is useful for iterating over the list off-chain.
*/
function getLen(bytes32 list) external view returns (uint256);
/**
* @notice Retrieves the root of the Merkle tree for a list.
*/
function getRoot(bytes32 list) external view returns (bytes32);
/**
* @notice Retrieves the list of values in the provided list.
* @dev This is useful for retrieving smaller lists off-chain in one go.
* Larger lists may run into the gas limit at which point
* the list should be retrieved using `getNext`.
*/
function getList(bytes32 list) external view returns (bytes32[] memory);
/**
* @notice Returns whether or not a value is in the provided list on this contract.
*/
function contains(bytes32 list, bytes32 value) external view returns (bool);
}
"
},
"src/Portal.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.26;
import { IERC20 } from "../lib/common/src/interfaces/IERC20.sol";
import { IndexingMath } from "../lib/common/src/libs/IndexingMath.sol";
import { TrimmedAmount, TrimmedAmountLib } from "../lib/native-token-transfers/evm/src/libraries/TrimmedAmount.sol";
import { TransceiverStructs } from "../lib/native-token-transfers/evm/src/libraries/TransceiverStructs.sol";
import {
NttManagerNoRateLimiting
} from "../lib/native-token-transfers/evm/src/NttManager/NttManagerNoRateLimiting.sol";
import { IPortal } from "./interfaces/IPortal.sol";
import { ISwapFacilityLike } from "./interfaces/ISwapFacilityLike.sol";
import { TypeConverter } from "./libs/TypeConverter.sol";
import { PayloadType, PayloadEncoder } from "./libs/PayloadEncoder.sol";
import { ReentrancyLock } from "../lib/uniswap-v4-periphery/src/base/ReentrancyLock.sol";
/**
* @title Base Portal contract inherited by HubPortal and SpokePortal.
* @author M^0 Labs
*/
abstract contract Portal is NttManagerNoRateLimiting, ReentrancyLock, IPortal {
using TypeConverter for *;
using PayloadEncoder for bytes;
using TrimmedAmountLib for *;
uint16 internal constant _SOLANA_WORMHOLE_CHAIN_ID = 1;
uint16 internal constant _FOGO_WORMHOLE_CHAIN_ID = 51;
bytes32 internal constant _SOLANA_EARNER_LIST = bytes32("solana-earners");
/// @inheritdoc IPortal
address public immutable registrar;
/// @inheritdoc IPortal
address public immutable swapFacility;
/// @inheritdoc IPortal
mapping(address sourceToken => mapping(uint16 destinationChainId => mapping(bytes32 destinationToken => bool supported)))
public supportedBridgingPath;
/// @inheritdoc IPortal
mapping(uint16 destinationChainId => bytes32 mToken) public destinationMToken;
/* ============ Constructor ============ */
/**
* @notice Constructs the contract.
* @param mToken_ The address of the M token to bridge.
* @param registrar_ The address of the Registrar.
* @param swapFacility_ The address of Swap Facility.
* @param mode_ The NttManager token transfer mode - LOCKING or BURNING.
* @param chainId_ The Wormhole chain id.
*/
constructor(
address mToken_,
address registrar_,
address swapFacility_,
Mode mode_,
uint16 chainId_
) NttManagerNoRateLimiting(mToken_, mode_, chainId_) {
if (mToken_ == address(0)) revert ZeroMToken();
if ((registrar = registrar_) == address(0)) revert ZeroRegistrar();
if ((swapFacility = swapFacility_) == address(0)) revert ZeroSwapFacility();
}
/* ============ View/Pure Functions ============ */
/// @inheritdoc IPortal
function mToken() public view returns (address) {
return token;
}
/// @inheritdoc IPortal
function currentIndex() external view returns (uint128) {
return _currentIndex();
}
/// @inheritdoc IPortal
function msgSender() external view returns (address) {
return _getLocker();
}
/* ============ External Interactive Functions ============ */
/// @inheritdoc IPortal
function setDestinationMToken(uint16 destinationChainId_, bytes32 mToken_) external onlyOwner {
if (destinationChainId_ == chainId) revert InvalidDestinationChain(destinationChainId_);
if (mToken_ == bytes32(0)) revert ZeroMToken();
destinationMToken[destinationChainId_] = mToken_;
emit DestinationMTokenSet(destinationChainId_, mToken_);
}
/// @inheritdoc IPortal
function setSupportedBridgingPath(
address sourceToken_,
uint16 destinationChainId_,
bytes32 destinationToken_,
bool supported_
) external onlyOwner {
if (sourceToken_ == address(0)) revert ZeroSourceToken();
if (destinationChainId_ == chainId) revert InvalidDestinationChain(destinationChainId_);
if (destinationToken_ == bytes32(0)) revert ZeroDestinationToken();
supportedBridgingPath[sourceToken_][destinationChainId_][destinationToken_] = supported_;
emit SupportedBridgingPathSet(sourceToken_, destinationChainId_, destinationToken_, supported_);
}
/// @inheritdoc IPortal
function transferMLikeToken(
uint256 amount_,
address sourceToken_,
uint16 destinationChainId_,
bytes32 destinationToken_,
bytes32 recipient_,
bytes32 refundAddress_,
bytes memory transceiverInstructions_
) external payable isNotLocked whenNotPaused returns (uint64 sequence_) {
if (!supportedBridgingPath[sourceToken_][destinationChainId_][destinationToken_]) {
revert UnsupportedBridgingPath(sourceToken_, destinationChainId_, destinationToken_);
}
sequence_ = _transferMLikeToken(
amount_,
sourceToken_,
destinationChainId_,
destinationToken_,
recipient_,
refundAddress_,
transceiverInstructions_
);
}
/* ============ Internal/Private Interactive Functions ============ */
/**
* @dev Called from NTTManager `transfer` function to transfer M token
* @dev Overridden to reduce code duplication, optimize gas cost and prevent Yul stack too deep
* @param amount_ The amount of tokens to transfer.
* @param destinationChainId_ The Wormhole destination chain ID.
* @param recipient_ The account to receive tokens.
* @param refundAddress_ The address to receive excess native gas on the destination chain.
* @return sequence_ The message sequence.
*/
function _transferEntryPoint(
uint256 amount_,
uint16 destinationChainId_,
bytes32 recipient_,
bytes32 refundAddress_,
bool, // shouldQueue_
bytes memory transceiverInstructions_
) internal override isNotLocked returns (uint64 sequence_) {
sequence_ = _transferMLikeToken(
amount_,
token, // M Token
destinationChainId_,
destinationMToken[destinationChainId_], // M Token on destination
recipient_,
refundAddress_,
transceiverInstructions_
);
}
/**
* @dev Transfers M or Wrapped M Token to the destination chain.
* @param amount_ The amount of tokens to transfer.
* @param sourceToken_ The address of the token (M or Wrapped M) on the source chain.
* @param destinationChainId_ The Wormhole destination chain ID.
* @param destinationToken_ The address of the token (M or Wrapped M) on the destination chain.
* @param recipient_ The account to receive tokens.
* @param refundAddress_ The address to receive excess native gas on the destination chain.
* @param transceiverInstructions_ The transceiver specific instructions for quoting and sending.
* @return sequence_ The message sequence.
*/
function _transferMLikeToken(
uint256 amount_,
address sourceToken_,
uint16 destinationChainId_,
bytes32 destinationToken_,
bytes32 recipient_,
bytes32 refundAddress_,
bytes memory transceiverInstructions_
) private returns (uint64 sequence_) {
_verifyTransferAmount(amount_);
if (destinationToken_ == bytes32(0)) revert ZeroDestinationToken();
if (recipient_ == bytes32(0)) revert InvalidRecipient();
if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress();
IERC20 mToken_ = IERC20(token);
uint256 startingBalance_ = mToken_.balanceOf(address(this));
// transfer source token from the sender
IERC20(sourceToken_).transferFrom(msg.sender, address(this), amount_);
// if the source token isn't M token, unwrap it
if (sourceToken_ != address(mToken_)) {
IERC20(sourceToken_).approve(swapFacility, amount_);
ISwapFacilityLike(swapFacility).swapOutM(sourceToken_, amount_, address(this));
}
// The actual amount of M tokens that Portal received from the sender.
// Accounts for potential rounding errors when transferring between earners and non-earners,
// as well as potential fee-on-transfer functionality in the source token.
uint256 actualAmount_ = mToken_.balanceOf(address(this)) - startingBalance_;
if (amount_ > actualAmount_) {
unchecked {
// If the difference between the specified transfer amount and the actual amount exceeds
// the maximum acceptable rounding error (e.g., due to fee-on-transfer in an extension token)
// transfer the actual amount, not the specified.
// Otherwise, the specified amount will be transferred and the deficit caused by rounding error will
// be covered from the yield earned by HubPortal.
if (amount_ - actualAmount_ > _getMaxRoundingError()) {
amount_ = actualAmount_;
_verifyTransferAmount(amount_);
}
}
}
// Burn the actual amount of M tokens on Spoke.
// In case of Hub, do nothing, as tokens are already transferred.
_burnOrLock(actualAmount_);
// Prevent stack too deep error
bytes32 destinationToken = destinationToken_;
(sequence_, ) = _transferNativeToken(
amount_,
sourceToken_,
destinationChainId_,
destinationToken,
recipient_,
refundAddress_,
PayloadEncoder.encodeAdditionalPayload(_currentIndex(), destinationToken),
transceiverInstructions_
);
}
/**
* @dev Transfers M or Wrapped M Token to the destination chain.
* @dev adapted from NttManager `_transfer` function.
* @dev https://github.com/wormhole-foundation/native-token-transfers/blob/main/evm/src/NttManager/NttManager.sol#L521
* @param amount_ The amount of tokens to transfer.
* @param sourceToken_ The address of the token (M or Wrapped M) on the source chain.
* @param destinationChainId_ The Wormhole destination chain ID.
* @param destinationToken_ The address of the token (M or Wrapped M) on the destination chain.
* @param recipient_ The account to receive tokens.
* @param refundAddress_ The address to receive excess native gas on the destination chain.
* @param additionalPayload_ The additional payload to sent with tokens transfer.
* @param transceiverInstructions_ The transceiver specific instructions for quoting and sending.
* @return sequence_ The message sequence.
*/
function _transferNativeToken(
uint256 amount_,
address sourceToken_,
uint16 destinationChainId_,
bytes32 destinationToken_,
bytes32 recipient_,
bytes32 refundAddress_,
bytes memory additionalPayload_,
bytes memory transceiverInstructions_
) internal returns (uint64 sequence_, bytes32 messageId_) {
sequence_ = _useMessageSequence();
TransceiverStructs.NttManagerMessage memory message_;
(message_, messageId_) = _encodeTokenTransfer(
_trimTransferAmount(amount_, destinationChainId_),
destinationChainId_,
msg.sender,
recipient_,
additionalPayload_,
sequence_
);
uint256 totalPriceQuote_ = _sendMessage(
destinationChainId_,
refundAddress_,
message_,
transceiverInstructions_
);
// prevent stack too deep
uint256 transferAmount_ = amount_;
emit MTokenSent(
sourceToken_,
destinationChainId_,
destinationToken_,
msg.sender,
recipient_,
transferAmount_,
_currentIndex(),
messageId_
);
// emit NTT events
emit TransferSent(
recipient_,
refundAddress_,
transferAmount_,
totalPriceQuote_,
destinationChainId_,
sequence_
);
emit TransferSent(messageId_);
}
/**
* @dev Encodes transfer information into NTT format.
* @param amount_ The amount of tokens to transfer.
* @param destinationChainId_ The Wormhole destination chain ID.
* @param sender_ The message sender.
* @param recipient_ The account to receive tokens.
* @param additionalPayload_ The additional payload to send with tokens transfer.
* @param sequence_ The message sequence.
* @return message_ The message in NTT format.
* @return messageId_ The message Id.
*/
function _encodeTokenTransfer(
TrimmedAmount amount_,
uint16 destinationChainId_,
address sender_,
bytes32 recipient_,
bytes memory additionalPayload_,
uint64 sequence_
) internal view returns (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) {
TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer_ = TransceiverStructs.NativeTokenTransfer(
amount_,
token.toBytes32(),
recipient_,
destinationChainId_,
additionalPayload_
);
message_ = TransceiverStructs.NttManagerMessage(
bytes32(uint256(sequence_)),
sender_.toBytes32(),
TransceiverStructs.encodeNativeTokenTransfer(nativeTokenTransfer_)
);
messageId_ = TransceiverStructs.nttManagerMessageDigest(chainId, message_);
}
/**
* @dev Sends a generic message to the destination chain.
* The implementation is adapted from `NttManager` `_transfer` function.
* @param destinationChainId_ The Wormhole destination chain ID.
* @param refundAddress_ The address to receive excess native gas on the destination chain.
* @param message_ The message to send.
* @param transceiverInstructions_ The transceiver specific instructions for quoting and sending.
* @return totalPriceQuote_ The price to deliver the message to the destination chain.
*/
function _sendMessage(
uint16 destinationChainId_,
bytes32 refundAddress_,
TransceiverStructs.NttManagerMessage memory message_,
bytes memory transceiverInstructions_
) internal returns (uint256 totalPriceQuote_) {
_verifyIfChainForked();
address[] memory enabledTransceivers_;
TransceiverStructs.TransceiverInstruction[] memory instructions_;
uint256[] memory priceQuotes_;
(enabledTransceivers_, instructions_, priceQuotes_, totalPriceQuote_) = _prepareForTransfer(
destinationChainId_,
transceiverInstructions_
);
// send a message
_sendMessageToTransceivers(
destinationChainId_,
refundAddress_,
_getPeersStorage()[destinationChainId_].peerAddress,
priceQuotes_,
instructions_,
enabledTransceivers_,
TransceiverStructs.encodeNttManagerMessage(message_)
);
}
/**
* @dev Handles token transfer with an additional payload and custom payload types on the destination.
* @param sourceChainId_ The Wormhole source chain ID.
* @param message_ The message.
*/
function _handleMsg(
uint16 sourceChainId_,
bytes32, // sourceNttManagerAddress
TransceiverStructs.NttManagerMessage memory message_,
bytes32 messageId_ // digest
) internal override {
bytes memory payload_ = message_.payload;
PayloadType payloadType_ = message_.payload.getPayloadType();
_verifyIfChainForked();
if (payloadType_ == PayloadType.Token) {
_receiveMToken(sourceChainId_, messageId_, message_.sender, payload_);
return;
}
_receiveCustomPayload(messageId_, payloadType_, payload_);
}
/**
* @dev Handles token transfer message on the destination.
* @param sourceChainId_ The Wormhole source chain ID.
* @param messageId_ The message ID.
* @param sender_ The address of the message sender.
* @param payload_ The message payload.
*/
function _receiveMToken(uint16 sourceChainId_, bytes32 messageId_, bytes32 sender_, bytes memory payload_) private {
(
TrimmedAmount trimmedAmount_,
uint128 index_,
address destinationToken_,
address recipient_,
uint16 destinationChainId_
) = payload_.decodeTokenTransfer();
_verifyDestinationChain(destinationChainId_);
// NOTE: Assumes that token.decimals() are the same on all chains.
uint256 amount_ = trimmedAmount_.untrim(tokenDecimals());
emit MTokenReceived(sourceChainId_, destinationToken_, sender_, recipient_, amount_, index_, messageId_);
// Emitting `INttManager.TransferRedeemed` to
Submitted on: 2025-10-03 09:19:11
Comments
Log in to comment.
No comments yet.