From 9a96b3ec304eda3ae972d7c0cc176edcf354d794 Mon Sep 17 00:00:00 2001 From: skyc1e Date: Mon, 15 Jun 2026 17:30:48 +0200 Subject: [PATCH] docs(bridge): clarify Solana-to-Base recipient encoding --- base/src/libraries/TokenLib.sol | 27 +++++++++++++++++-- base/test/libraries/TokenLib.t.sol | 17 ++++++++++++ .../solana_to_base/state/outgoing_message.rs | 2 ++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/base/src/libraries/TokenLib.sol b/base/src/libraries/TokenLib.sol index a0294a5f..b5dbdb99 100644 --- a/base/src/libraries/TokenLib.sol +++ b/base/src/libraries/TokenLib.sol @@ -13,7 +13,9 @@ import {Pubkey} from "./SVMLib.sol"; /// /// @custom:field localToken Address of the ERC20 token on this chain. /// @custom:field remoteToken Pubkey of the remote token on Solana. -/// @custom:field to Address of the recipient on the target chain. EVM address on Base, Solana pubkey on Solana. +/// @custom:field to Recipient on the target chain. For Base recipients, encode the EVM address with +/// `TokenLib.encodeEvmRecipient`, i.e. left-aligned as `bytes32(bytes20(recipient))`. +/// For Solana recipients, use the Solana pubkey. /// @custom:field remoteAmount Amount of tokens being bridged (expressed in Solana units). struct Transfer { address localToken; @@ -114,6 +116,27 @@ library TokenLib { } } + /// @notice Encodes an EVM recipient for a Solana-to-Base transfer. + /// + /// @dev The Solana message format stores recipients in a bytes32 field, but Base finalization reads the first + /// 20 bytes. This must be left-aligned, not ABI-style right-aligned. + /// + /// @param recipient Address to receive tokens on Base. + /// + /// @return encoded The recipient encoded as left-aligned bytes20 inside bytes32. + function encodeEvmRecipient(address recipient) internal pure returns (bytes32 encoded) { + return bytes32(bytes20(recipient)); + } + + /// @notice Decodes a Base recipient from a Solana-to-Base transfer. + /// + /// @param encoded Recipient encoded with `encodeEvmRecipient`. + /// + /// @return recipient Address to receive tokens on Base. + function decodeEvmRecipient(bytes32 encoded) internal pure returns (address recipient) { + return address(bytes20(encoded)); + } + /// @notice Initializes a token transfer. /// /// @dev IMPORTANT: For native ERC20 tokens with transfer fees, the `transfer.remoteAmount` field might be modified @@ -224,7 +247,7 @@ library TokenLib { function finalizeTransfer(Transfer memory transfer, address crossChainErc20Factory) internal { TokenLibStorage storage $ = getTokenLibStorage(); - address to = address(bytes20(transfer.to)); + address to = decodeEvmRecipient(transfer.to); uint256 localAmount; if (transfer.localToken == ETH_ADDRESS) { diff --git a/base/test/libraries/TokenLib.t.sol b/base/test/libraries/TokenLib.t.sol index b7e48a92..40bccc1d 100644 --- a/base/test/libraries/TokenLib.t.sol +++ b/base/test/libraries/TokenLib.t.sol @@ -647,6 +647,23 @@ contract TokenLibTest is CommonTest { ); } + function test_evmRecipientEncoding_usesLeftAlignedBytes20() public pure { + address recipient = address(uint160(0x1234567890abcdef1234567890abcdef12345678)); + + bytes32 encoded = TokenLib.encodeEvmRecipient(recipient); + bytes32 rightAligned = bytes32(uint256(uint160(recipient))); + + assertEq(encoded, bytes32(bytes20(recipient)), "EVM recipient should be left-aligned"); + assertEq(TokenLib.decodeEvmRecipient(encoded), recipient, "Left-aligned recipient should round trip"); + + assertNotEq(rightAligned, encoded, "ABI-style right-aligned encoding should differ"); + assertNotEq( + TokenLib.decodeEvmRecipient(rightAligned), + recipient, + "Right-aligned encoding decodes to the wrong Base recipient" + ); + } + ////////////////////////////////////////////////////////////// /// Integration Tests /// ////////////////////////////////////////////////////////////// diff --git a/solana/programs/bridge/src/solana_to_base/state/outgoing_message.rs b/solana/programs/bridge/src/solana_to_base/state/outgoing_message.rs index 89d96752..7634a39f 100644 --- a/solana/programs/bridge/src/solana_to_base/state/outgoing_message.rs +++ b/solana/programs/bridge/src/solana_to_base/state/outgoing_message.rs @@ -11,6 +11,8 @@ pub trait MessageSpace { #[derive(Debug, Clone, Eq, PartialEq, AnchorSerialize, AnchorDeserialize)] pub struct Transfer { /// The recipient address on Base that will receive the bridged tokens. + /// When relayed into the Solidity `Transfer.to` bytes32 field, these 20 bytes + /// occupy the first 20 bytes and are decoded by `TokenLib.decodeEvmRecipient`. pub to: [u8; 20], /// The token mint address on Solana that is being bridged.