Skip to main content

PayloadUtils Library

The PayloadUtils library provides standardized ABI encoding and decoding for cross-chain transfer payloads used by the Treasury two-chain system.

Overview

Library Type: Pure functions License: EVVM-NONCOMMERCIAL-1.0 Import Path: @evvm/testnet-contracts/library/treasuryTwoChains/lib/PayloadUtils.sol Solidity Version: ^0.8.0

Key Features

  • Standardized payload format for cross-chain transfers
  • ABI encoding for reliable data transmission
  • Compatible with Hyperlane, LayerZero, and Axelar protocols
  • Supports both ERC20 tokens and native ETH

Functions

encodePayload

Function Type: internal pure Function Signature: encodePayload(address token, address toAddress, uint256 amount) returns (bytes memory payload)

Encodes transfer parameters into a standardized cross-chain payload.

Parameters

ParameterTypeDescription
tokenaddressToken contract address. Use address(0) for native ETH
toAddressaddressRecipient address on the destination chain
amountuint256Amount of tokens to transfer (in token's native decimals)

Return Value

TypeDescription
bytesABI-encoded payload containing the transfer parameters

Implementation

function encodePayload(
address token,
address toAddress,
uint256 amount
) internal pure returns (bytes memory payload) {
payload = abi.encode(token, toAddress, amount);
}

Example

// Encode an ERC20 transfer
bytes memory payload = PayloadUtils.encodePayload(
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, // USDC
0x1234567890abcdef1234567890abcdef12345678,
1000000 // 1 USDC (6 decimals)
);

// Encode a native ETH transfer
bytes memory ethPayload = PayloadUtils.encodePayload(
address(0), // address(0) indicates native ETH
0x1234567890abcdef1234567890abcdef12345678,
1 ether
);

decodePayload

Function Type: internal pure Function Signature: decodePayload(bytes memory payload) returns (address token, address toAddress, uint256 amount)

Decodes a cross-chain payload back into transfer parameters.

Parameters

ParameterTypeDescription
payloadbytesABI-encoded bytes received from cross-chain protocols

Return Value

TypeDescription
tokenToken contract address (address(0) indicates native ETH)
toAddressRecipient address extracted from the payload
amountAmount of tokens to transfer in token's native decimals

Implementation

function decodePayload(
bytes memory payload
) internal pure returns (address token, address toAddress, uint256 amount) {
(token, toAddress, amount) = abi.decode(
payload,
(address, address, uint256)
);
}

Example

// Decode a received cross-chain payload
(address token, address toAddress, uint256 amount) = PayloadUtils.decodePayload(
receivedPayload
);

if (token == address(0)) {
// Native ETH transfer
payable(toAddress).transfer(amount);
} else {
// ERC20 transfer
IERC20(token).transfer(toAddress, amount);
}

Payload Structure

The payload uses standard ABI encoding with the following layout:

┌─────────────────────────────────────────────────────────┐
│ Payload Layout │
├─────────────────────────────────────────────────────────┤
│ Bytes 0-31 │ token address (address, 20 bytes) │
│ Bytes 32-63 │ toAddress (address, 20 bytes) │
│ Bytes 64-95 │ amount (uint256, 32 bytes) │
├─────────────────────────────────────────────────────────┤
│ Total: 96 bytes (32 bytes × 3 fields) │
└─────────────────────────────────────────────────────────┘

Usage in Cross-Chain Operations

TreasuryHostChainStation

Used when sending tokens from the EVVM host chain to an external chain:

// Encoding for cross-chain withdrawal
bytes memory payload = PayloadUtils.encodePayload(
token,
toAddress,
amount
);

// Send via Hyperlane, LayerZero, or Axelar

TreasuryExternalChainStation

Used when receiving tokens on the external chain from the host chain:

// Decoding received cross-chain deposit
(address token, address toAddress, uint256 amount) =
PayloadUtils.decodePayload(message);

// Credit the recipient's balance

Protocol Compatibility

The PayloadUtils library works with all three cross-chain protocols supported by EVVM:

ProtocolHandler FunctionPayload Format
Hyperlanehandle(uint32, bytes32, bytes)ABI-encoded
LayerZero_lzReceive(uint32, bytes, address, bytes)ABI-encoded
Axelar_execute(string, string, bytes)ABI-encoded

All three protocols use the same PayloadUtils format for transfer data.

Security Considerations

1. Payload Integrity

Cross-chain payloads should be validated before execution:

function _execute(
string memory,
string memory,
bytes memory payload
) internal override {
(address token, address toAddress, uint256 amount) =
PayloadUtils.decodePayload(payload);

// Validate decoded data
require(toAddress != address(0), "Invalid recipient");
require(amount > 0, "Zero amount");

// Execute transfer
_creditBalance(toAddress, token, amount);
}

2. Native ETH Handling

When token == address(0), the payload represents a native ETH transfer. Ensure proper handling:

if (token == address(0)) {
// Native ETH: use payable transfer
(bool success, ) = toAddress.call{value: amount}("");
require(success, "ETH transfer failed");
} else {
// ERC20: use safe transfer
IERC20(token).safeTransfer(toAddress, amount);
}

3. Decoding Failures

If the payload is malformed, abi.decode will revert. Wrap decoding in try/catch if needed:

try PayloadUtils.decodePayload(payload) returns (
address token,
address toAddress,
uint256 amount
) {
// Process transfer
} catch {
revert InvalidPayload();
}

Gas Costs

OperationApproximate GasNotes
encodePayload~500ABI encoding overhead
decodePayload~500-800ABI decoding overhead