// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.13; import {BytesLib} from "./BytesLib.sol"; import {IHandler} from "../../interfaces/IHandler.sol"; import "./BaseMessage.sol"; /// @title MessageLib /// @author The Warp Team /// @notice A library for encoding and decoding cross-chain messages according /// to the Basic Abstract Messaging specification. library MessageLib { using BytesLib for bytes; /// @notice A cross-chain message. /// @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. struct Message { uint64 index; uint16 targetChain; bytes targetAddress; bytes sender; bytes contents; } /// @notice A 1-byte message type indicator. uint8 public constant MESSAGE_TYPE = 0; /// @notice The minimum length of an encoded message. /// @dev Composed of: /// - magicByte - 1 byte. /// - version - 1 byte. /// - messageType - 1 byte. /// - index - 8 bytes. /// - targetChain - 2 bytes. /// - targetAddressLen - 2 bytes. /// - smallest possible targetAddress - 1 byte. /// - senderLen - 2 bytes. /// - contentsLen - 2 bytes. /// @dev Equals 20 bytes. uint256 public constant MINIMUM_ENCODED_LEN = 1 + 1 + 1 + 8 + 2 + 2 + 1 + 2 + 2; /// @notice Details for the index in the encoding scheme. uint256 private constant INDEX_OFFSET = MESSAGE_TYPE_OFFSET + MESSAGE_TYPE_SIZE; uint256 private constant INDEX_SIZE = 8; /// @notice Details for the targetChain in the encoding scheme. uint256 private constant TARGET_CHAIN_OFFSET = INDEX_OFFSET + INDEX_SIZE; uint256 private constant TARGET_CHAIN_SIZE = 2; /// @notice Offset for the target address length-prefixed bytearray in the /// encoding scheme. uint256 private constant TARGET_ADDRESS_OFFSET = TARGET_CHAIN_OFFSET + TARGET_CHAIN_SIZE; /// @notice Encode a message into bytes. /// @dev The message 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 /// @custom:reverts As encodeBody. /// @return The encoded message as a bytearray. function encode(Message memory message) internal pure returns (bytes memory) { return abi.encodePacked(encodeHeader(MESSAGE_TYPE), encodeBody(message)); } /// @notice Encode a message body into bytes. /// @dev The message body follows a simple specification: /// 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 /// @custom:reverts Errors when the message, targetAddress, or sender is too /// large; or if the targetAddress is empty. /// @return The encoded message body as a bytearray. function encodeBody(Message memory message) internal pure returns (bytes memory) { if (message.targetAddress.length > type(uint16).max || message.targetAddress.length == 0) { revert AddressLength(); } if (message.contents.length > type(uint16).max) revert ContentsLength(); if (message.sender.length > type(uint16).max) revert SenderLength(); // length-prepend the bytearrays bytes memory targetAddress = abi.encodePacked(uint16(message.targetAddress.length), message.targetAddress); bytes memory sender = abi.encodePacked(uint16(message.sender.length), message.sender); bytes memory contents = abi.encodePacked(uint16(message.contents.length), message.contents); return abi.encodePacked(message.index, message.targetChain, targetAddress, sender, contents); } /// @notice Decode a message from a bytes representation, without checking /// the header. /// @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 Message /// struct. function decodeBody(bytes memory encoded) internal pure returns (Message memory decoded, uint256 consumed) { // 8 bytes at position 3 decoded.index = encoded.toUint64(INDEX_OFFSET); // 2 bytes at position 11 decoded.targetChain = encoded.toUint16(TARGET_CHAIN_OFFSET); // length-prefixed bytearray at position 13 decoded.targetAddress = readU16PrefixedBytes(encoded, TARGET_ADDRESS_OFFSET); // We are now in the dynamic section, so we start tracking our offset // in the encoded data. consumed = TARGET_ADDRESS_OFFSET + 2 + decoded.targetAddress.length; // computed as the targetOffset + 2 (the size of the length prefix in // bytes) + the value of the length prefix (the length of the // targetAddress in bytes) decoded.sender = readU16PrefixedBytes(encoded, consumed); consumed += 2 + decoded.sender.length; // computed as the senderOffset + 2 (the size of the length prefix in // bytes) + the value of the length prefix (the length of the // sender in bytes) decoded.contents = readU16PrefixedBytes(encoded, consumed); consumed += 2 + decoded.contents.length; } /// @notice Decode a message from a bytes representation, /// checking the 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 (Message 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(Message memory message) internal pure returns (IHandler) { return extractAddress(message.targetAddress); } }