// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {BytesLib} from "./BytesLib.sol"; import {MessageLib} from "./MessageLib.sol"; import "./BaseMessage.sol"; /// @title TokenMessageLib /// @author The Warp Team /// @notice A library for encoding and decoding cross-chain messages with value /// according to the Wormhole Abstract Messaging specification. library TokenMessageLib { using BytesLib for bytes; /// @notice A cross-chain message with expected value. /// @dev The fields are as follows. /// - index - The index of the message in its Outbox's message history. /// - targetChain - The chain ID of the destination chain. /// - targetAddress - The address of the recipient on the targetChain. /// - sender - The address of the sender on the source chain. /// - contents - The message contents. /// - bridge - An application-specified identifier for a bridge. /// - assetIdentifier - An application-specified identifier for an /// asset, which may be in a bridge-specific format. /// - amount - The amount of the asset to be transferred, which may be /// in a bridge- or asset-specific format. struct TokenMessage { uint64 index; uint16 targetChain; bytes targetAddress; bytes sender; bytes contents; uint8 bridge; bytes assetIdentifier; uint256 amount; } /// @notice A 1-byte message type indicator. uint8 public constant MESSAGE_TYPE = 1; /// @notice The minimum length of an encoded message. /// @dev Composed of: /// - MessageLib.MINIMUM_ENCODED_LEN - 20 bytes. /// - bridge - 1 byte. /// - assetIdentifierLen - 2 bytes. /// - smallest possible assetIdentifier - 1 byte. /// - amount - 32 bytes. /// @dev Equals 56 bytes. uint256 private constant MINIMUM_ENCODED_LEN = MessageLib.MINIMUM_ENCODED_LEN + 1 + 2 + 1 + 32; /// @notice An error indicating that the asset identifier is too large to /// encode OR the asset identifier is empty. /// @dev Selector 0x9b29342f. error AssetIdentifierLength(); /// @notice An error indicating that the amount about to be encoded is zero. /// @dev Selector 0x1f2a2005. error ZeroAmount(); /// @notice Encode a message into a bytes representation. /// @dev Because `MessageLib.Message` is a prefix of `TokenMessage`, /// this function simply calls `MessageLib.encodeBody` to encode shared message fields. /// @dev The message with tokens follows a simple specification: /// HEADER /// - magicByte - 1 byte /// - version - 1 byte /// - messageType - 1 byte /// BODY /// - index - 8 bytes /// - targetChain - 2 bytes /// - targetAddress - 2 bytes length + targetAddress bytearray /// - sender - 2 bytes length + sender bytearray /// - contents - 2 bytes length + contents bytearray /// WITH TOKENS /// - bridge - 1 byte. /// - assetIdentifier - 2 bytes + assetIdentifier bytearray /// - amount - 32 bytes. /// @custom:reverts As MessageLib.encodeBody. /// @custom:reverts As encodeWithTokens /// @return The encoded message as a bytearray. function encode(TokenMessage memory message) internal pure returns (bytes memory) { return abi.encodePacked( encodeHeader(MESSAGE_TYPE), MessageLib.encodeBody(toMessage(message)), encodeWithTokens(message) ); } /// @notice Encode the WithTokens portion of a message into bytes. /// @dev The WithTokens portion of a message follows a simple specification: /// WITH TOKENS /// - bridge - 1 byte. /// - assetIdentifier - 2 bytes + assetIdentifier bytearray /// - amount - 32 bytes. /// @custom:reverts AssetIdentifierLength() if the asset identifier is // too large or empty. /// @return The WithTokens portion of the message encoded as a bytearray. function encodeWithTokens(TokenMessage memory message) internal pure returns (bytes memory) { if (message.assetIdentifier.length > type(uint16).max || message.assetIdentifier.length == 0) { revert AssetIdentifierLength(); } if (message.amount == 0) { revert ZeroAmount(); } // length-prepend the assetIdentifier bytearray bytes memory assetIdentifier = abi.encodePacked(uint16(message.assetIdentifier.length), message.assetIdentifier); // encode the additional WithTokens fields on the end return abi.encodePacked(message.bridge, assetIdentifier, message.amount); } /// @notice Decode a message from a bytes representation, without checking /// the header. /// @dev Because `MessageLib.Message` is a prefix of `TokenMessage`, /// this function simply calls `MessageLib.decodeBody` to parse shared fields. /// @param encoded The encoded message as a bytearray. /// @return decoded The decoded message. /// @return consumed The number of bytes consumed during decoding. /// @custom:reverts Errors if the input is not a valid encoded /// MessageWithValue struct. function decodeBody(bytes memory encoded) internal pure returns (TokenMessage memory decoded, uint256 consumed) { // decode the shared fields using MessageLib MessageLib.Message memory message; (message, consumed) = MessageLib.decodeBody(encoded); decoded = fromMessage(message); // Decode the WithTokens fields added on the end // In this segment we use the `consumed` variable to track the offset // into the encoded message. decoded.bridge = encoded.toUint8(consumed); consumed += 1; decoded.assetIdentifier = readU16PrefixedBytes(encoded, consumed); consumed += decoded.assetIdentifier.length + 2; decoded.amount = encoded.toUint256(consumed); consumed += 32; } /// @notice Decode a message from a bytes representation, /// checking header and erroring if any bytes are left over. /// @param encoded The encoded message as a bytearray. /// @return decoded The decoded message. /// @custom:reverts as expectHeader() /// @custom:reverts as decodeBody() /// @custom:reverts Overrun() If the encoded message is too short. /// @custom:reverts LeftoverBytes() If decoding consumes less than the /// entire input. function decode(bytes memory encoded) internal pure returns (TokenMessage memory decoded) { if (encoded.length < MINIMUM_ENCODED_LEN) revert Overrun(); expectHeader(MESSAGE_TYPE, encoded); uint256 consumed; (decoded, consumed) = decodeBody(encoded); if (consumed != encoded.length) revert LeftoverBytes(); } /// @notice Extract the target address from a message, reverting if it is /// not an EVM address. /// @dev Wrapper for `extractAddress(bytes)` /// @param message The message from which to extract the target address. /// @return decoded An IHandler interface at the decoded EVM address. /// @custom:reverts as `extractAddress(bytes)` function extractEvmTarget(TokenMessage memory message) internal pure returns (IHandler) { return extractAddress(message.targetAddress); } /// @notice Transcribe shared fields from a Message into a TokenMessage /// @param message The Message. /// @return tokMessage The transcribed TokenMessage. /// @dev In the returned TokenMessage, fields bridge, assetIdentifier, and amount will be empty function fromMessage(MessageLib.Message memory message) private pure returns (TokenMessage memory tokMessage) { tokMessage.index = message.index; tokMessage.targetChain = message.targetChain; tokMessage.targetAddress = message.targetAddress; tokMessage.sender = message.sender; tokMessage.contents = message.contents; } /// @notice Transcribe shared fields from a TokenMessage into a Message /// @param tokMessage The TokenMessage. /// @return message The transcribed Message. /// @dev In the returned Message, fields bridge, assetIdentifier, and amount will have been erased function toMessage(TokenMessage memory tokMessage) private pure returns (MessageLib.Message memory message) { message.index = tokMessage.index; message.targetChain = tokMessage.targetChain; message.targetAddress = tokMessage.targetAddress; message.sender = tokMessage.sender; message.contents = tokMessage.contents; } }