The AddressUtils library provides functions for converting between EVM addresses and bytes32 format. This enables cross-chain compatibility, allowing EVM chains to interact with non-EVM chains that use different address formats.
Overview
CCTP uses bytes32 for all address fields in messages to maintain compatibility across chains:
- EVM chains: 20-byte addresses are left-padded with 12 zero bytes
- Non-EVM chains: Native address formats can be encoded as bytes32
Two versions of the library exist:
- AddressUtils: Internal functions for use within contracts
- AddressUtilsExternal: External functions for off-chain or library linking
AddressUtils (Internal)
Source: ~/workspace/source/src/messages/v2/AddressUtils.sol
Internal helper functions for address conversion.
toBytes32
function toBytes32(address addr) internal pure returns (bytes32)
Converts an EVM address to bytes32 by left-padding with zeros (alignment-preserving cast).
Parameters:
addr: The 20-byte EVM address to convert
Returns: The address as bytes32 (left-padded with 12 zero bytes)
Implementation:
return bytes32(uint256(uint160(addr)));
Example:
import {AddressUtils} from "./messages/v2/AddressUtils.sol";
address wallet = 0x1234567890123456789012345678901234567890;
bytes32 walletBytes = AddressUtils.toBytes32(wallet);
// Result: 0x0000000000000000000000001234567890123456789012345678901234567890
// |<--- 12 zero bytes --->|<------- 20 bytes address -------->|
toAddress
function toAddress(bytes32 _buf) internal pure returns (address)
Converts bytes32 to an EVM address by taking the rightmost 20 bytes (alignment-preserving cast).
Parameters:
_buf: The bytes32 value to convert
Returns: The rightmost 20 bytes as an EVM address
Implementation:
return address(uint160(uint256(_buf)));
Important Security Consideration:Different bytes32 values can map to the same address due to truncation of the leftmost 12 bytes. If address uniqueness is critical for your use case, you MUST validate that the first 12 bytes are zero-padding.// Both map to the same address:
bytes32 value1 = 0x0000000000000000000000001234...7890;
bytes32 value2 = 0xFFFFFFFFFFFFFFFFFFFFFFFF1234...7890;
address addr1 = AddressUtils.toAddress(value1); // 0x1234...7890
address addr2 = AddressUtils.toAddress(value2); // 0x1234...7890 (same!)
// To enforce uniqueness:
require(
uint256(_buf) >> 160 == 0,
"First 12 bytes must be zero"
);
Example:
import {AddressUtils} from "./messages/v2/AddressUtils.sol";
bytes32 recipientBytes = 0x0000000000000000000000001234567890123456789012345678901234567890;
address recipient = AddressUtils.toAddress(recipientBytes);
// Result: 0x1234567890123456789012345678901234567890
AddressUtilsExternal (External)
Source: ~/workspace/source/src/messages/v2/AddressUtilsExternal.sol
External versions of the same functions, intended for:
- Off-chain use (via eth_call)
- Contract-to-contract library calls
- Deployment as a separate library contract
addressToBytes32
function addressToBytes32(address addr) external pure returns (bytes32)
External equivalent of toBytes32(). Converts address to bytes32.
Gas Cost: ~200 gas (external call overhead)
bytes32ToAddress
function bytes32ToAddress(bytes32 _buf) external pure returns (address)
External equivalent of toAddress(). Converts bytes32 to address.
Same truncation warning applies: different input values can map to the same address. Validate zero-padding if uniqueness is required.
Gas Cost: ~200 gas (external call overhead)
Legacy Message Library Functions
The original Message library also includes conversion functions:
Source: ~/workspace/source/src/messages/Message.sol:146-158
// Legacy external function
function addressToBytes32(address addr) external pure returns (bytes32) {
return bytes32(uint256(uint160(addr)));
}
// Legacy public function
function bytes32ToAddress(bytes32 _buf) public pure returns (address) {
return address(uint160(uint256(_buf)));
}
These are functionally identical to AddressUtilsExternal but located in the Message library.
Usage Examples
Basic Conversion
import {AddressUtils} from "./messages/v2/AddressUtils.sol";
contract MyContract {
using AddressUtils for address;
using AddressUtils for bytes32;
function convertAddress(address addr) public pure returns (bytes32) {
return AddressUtils.toBytes32(addr);
}
function convertBytes32(bytes32 buf) public pure returns (address) {
return AddressUtils.toAddress(buf);
}
}
Secure Conversion with Validation
import {AddressUtils} from "./messages/v2/AddressUtils.sol";
function secureConversion(bytes32 _buf) internal pure returns (address) {
// Validate first 12 bytes are zero
require(
uint256(_buf) >> 160 == 0,
"Invalid address format: non-zero padding"
);
return AddressUtils.toAddress(_buf);
}
Cross-Chain Message Construction
import {Message} from "./messages/Message.sol";
import {BurnMessage} from "./messages/BurnMessage.sol";
import {AddressUtils} from "./messages/v2/AddressUtils.sol";
function createCrossChainTransfer(
address sender,
address recipient,
address token,
uint256 amount
) internal pure returns (bytes memory) {
// Convert all addresses to bytes32
bytes32 senderBytes = AddressUtils.toBytes32(sender);
bytes32 recipientBytes = AddressUtils.toBytes32(recipient);
bytes32 tokenBytes = AddressUtils.toBytes32(token);
// Create burn message
bytes memory burnMsg = BurnMessage._formatMessage(
0, // version
tokenBytes,
recipientBytes,
amount,
senderBytes
);
// Create main message
return Message._formatMessage(
0, // version
0, // source domain
1, // destination domain
12345, // nonce
senderBytes,
recipientBytes,
bytes32(0), // any caller
burnMsg
);
}
Decoding Received Messages
import {Message} from "./messages/Message.sol";
import {AddressUtils} from "./messages/v2/AddressUtils.sol";
import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol";
using TypedMemView for bytes;
using Message for bytes29;
function processReceivedMessage(bytes memory message) internal {
bytes29 messageView = message.ref(0);
// Extract bytes32 addresses
bytes32 senderBytes = messageView._sender();
bytes32 recipientBytes = messageView._recipient();
// Convert to EVM addresses
address sender = AddressUtils.toAddress(senderBytes);
address recipient = AddressUtils.toAddress(recipientBytes);
// Validate addresses if needed
require(recipient == expectedRecipient, "Wrong recipient");
// Process message...
}
Off-Chain Address Conversion
Using AddressUtilsExternal via eth_call:
const { ethers } = require('ethers');
// Contract ABI
const abi = [
'function addressToBytes32(address addr) external pure returns (bytes32)',
'function bytes32ToAddress(bytes32 buf) external pure returns (address)'
];
// Connect to deployed library
const addressUtils = new ethers.Contract(
'0x...AddressUtilsExternal address...',
abi,
provider
);
// Convert address to bytes32
const address = '0x1234567890123456789012345678901234567890';
const bytes32 = await addressUtils.addressToBytes32(address);
console.log(bytes32);
// 0x0000000000000000000000001234567890123456789012345678901234567890
// Convert bytes32 to address
const convertedAddress = await addressUtils.bytes32ToAddress(bytes32);
console.log(convertedAddress);
// 0x1234567890123456789012345678901234567890
Non-EVM Chain Considerations
When integrating with non-EVM chains:
Solana Example
Solana addresses are 32-byte public keys (already bytes32):
// Solana address fits perfectly in bytes32
bytes32 solanaAddress = 0x1234... // 32-byte Solana public key
// No conversion needed - use directly in messages
For chains with different address lengths:
- Shorter addresses: Right-pad with zeros
- Longer addresses: Hash to 32 bytes (not reversible)
- Different encoding: Define chain-specific conversion logic
For production cross-chain applications, establish clear address encoding standards for each supported chain in your protocol documentation.
Gas Optimization
Internal functions are more gas-efficient:
// Internal: ~20 gas (inlined)
AddressUtils.toBytes32(addr);
// External: ~200+ gas (external call)
addressUtilsExternal.addressToBytes32(addr);
Use internal functions within contracts, external functions for off-chain reads.
See Also