Skip to content

feat(solana): implement SolanaIFTSendCallConstructor#959

Open
mariuszzak wants to merge 62 commits intomainfrom
mariuszzak/solidity-solana-constructor
Open

feat(solana): implement SolanaIFTSendCallConstructor#959
mariuszzak wants to merge 62 commits intomainfrom
mariuszzak/solidity-solana-constructor

Conversation

@mariuszzak
Copy link
Collaborator

@mariuszzak mariuszzak commented Feb 27, 2026

Description

  • Adds SolanaIFTSendCallConstructor, a Solidity contract that builds the complete ABI-encoded GmpSolanaPayload
    (accounts + instruction data) on-chain at send time, eliminating the relayer-controlled hint account
  • Adds eth-to-solana and solana-to-eth relayer modules for bidirectional Ethereum-Solana relay
  • Replaces manual ABI parsing in ics27-gmp with alloy-sol-types sol! macro
  • Adds E2E tests covering single-token and two-token IFT transfers between Ethereum and Solana

How it works

The constructor stores 6 Solana PDAs as immutables at deploy time. When a user sends tokens, constructMintCall packs these PDAs together with the dynamic receiver (wallet + ATA) into the GmpSolanaPayload format that Solana's GMP program expects. The entire execution payload is committed in the IBC packet on Ethereum — the relayer just reads it and forwards it.

Why the receiver encodes both wallet and ATA

On Solana, wallets don't hold SPL tokens directly. Each token balance lives in a dedicated Associated Token Account (ATA) — a PDA derived from the wallet address, the token program and the mint (the on-chain account that defines the token type, analogous to an ERC-20 contract address on Ethereum). The receiver field must include both the wallet and its ATA because the IFT mint instruction uses init_if_needed to create the ATA if it doesn't exist yet.

Creating or validating an ATA requires the wallet as the authority seed — the ATA address alone is not enough.

// programs/solana/programs/ift/src/instructions/ift_mint.rs
#[account(
    init_if_needed,
    payer = payer,
    associated_token::mint = mint,
    associated_token::authority = receiver_owner, // wallet needed as seed
    associated_token::token_program = token_program,
)]
pub receiver_token_account: InterfaceAccount<'info, TokenAccount>,

Flow Diagram

graph TB
    subgraph Deployment["Deployment Phase"]
        direction LR
        D1[Derive 6 PDAs off-chain] --> D2[Deploy Constructor]
        D2 --> D3["Store as immutables:<br/>APP_STATE, APP_MINT_STATE,<br/>IFT_BRIDGE, MINT,<br/>MINT_AUTHORITY,
GMP_ACCOUNT"]
    end

    U[User]

    subgraph Ethereum["Ethereum"]
        direction LR
        IFT[IFT Contract] -->|"2. constructMintCall(receiver, amount)"| CST[SolanaIFTSendCallConstructor]
        CST -->|"3. Pack 11 accounts<br/>(6 PDAs + wallet + ATA + 3 programs)"| PAYLOAD["4.
abi.encode(packedAccounts,<br/>instructionData, payerPosition)"]
        PAYLOAD -->|return| IFT
        IFT -->|"5. sendPacket"| IBC[IBC Router]
    end

    subgraph Relayer["Relayer"]
        direction LR
        R1{"encoding?"} -->|"ABI<br/>(Ethereum)"| ABI["abi_decode →<br/>parse 34-byte<br/>packed accounts"]
        R1 -->|"Protobuf<br/>(Cosmos)"| PB["protobuf decode →<br/>native account structs"]
        ABI --> TX["Build recv_packet tx:<br/>14 ICS26 accounts +<br/>[gmp_pda,
target_program,<br/>...execution_accounts]"]
        PB --> TX
    end

    subgraph Solana["Solana"]
        direction LR
        ROUTER["ICS26 Router"] -->|"CPI: on_recv_packet<br/>(dest_port = gmpport)"| GMP["ICS27 GMP Program"]
        GMP -->|"Decode payload,<br/>inject payer at payerPosition,<br/>CPI to target program"| IFTS[IFT Program]
        IFTS -->|Mint tokens| ATA[Receiver ATA]
    end

    Deployment -.-> Ethereum
    U -->|"1. iftTransfer(clientId, receiver, amount)"| IFT
    IBC --> Relayer
    TX -->|"recv_packet"| ROUTER

    style Deployment fill:#e8daef,stroke:#8e44ad,stroke-width:2px
    style Ethereum fill:#d4efdf,stroke:#27ae60,stroke-width:2px
    style Relayer fill:#fdebd0,stroke:#e67e22,stroke-width:2px
    style Solana fill:#d6eaf8,stroke:#2980b9,stroke-width:2px

    style U fill:#f9e79f,stroke:#f39c12,stroke-width:2px
    style IFT fill:#a9dfbf,stroke:#1e8449
    style CST fill:#a9dfbf,stroke:#1e8449
    style PAYLOAD fill:#a9dfbf,stroke:#1e8449
    style IBC fill:#a9dfbf,stroke:#1e8449
    style R1 fill:#fad7a0,stroke:#ca6f1e
    style ABI fill:#fad7a0,stroke:#ca6f1e
    style PB fill:#fad7a0,stroke:#ca6f1e
    style TX fill:#fad7a0,stroke:#ca6f1e
    style ROUTER fill:#aed6f1,stroke:#2471a3
    style GMP fill:#aed6f1,stroke:#2471a3
    style IFTS fill:#aed6f1,stroke:#2471a3
    style ATA fill:#aed6f1,stroke:#2471a3
    style D1 fill:#d2b4de,stroke:#7d3c98
    style D2 fill:#d2b4de,stroke:#7d3c98
    style D3 fill:#d2b4de,stroke:#7d3c98
Loading

Key changes

  • contracts/utils/SolanaIFTSendCallConstructor.sol — constructor contract with 6 immutable PDAs, builds packed
    accounts and Borsh-encoded instruction data using inline assembly
  • packages/relayer/modules/eth-to-solana/ — new relayer module for Ethereum to Solana packet relay, including
    ABI payload decoding and Solana transaction building
  • packages/relayer/modules/solana-to-eth/ — new relayer module for Solana to Ethereum relay
  • programs/solana/programs/ics27-gmp/src/abi.rs — replaced ~400 lines of manual ABI parsing with
    alloy-sol-types sol! macro types
  • e2e/interchaintestv8/ethereum_solana_ift_test.go — E2E tests for single and two-token transfers
  • scripts/DeploySolanaIFTConstructor.s.sol — forge deploy script that reads PDAs from env vars

This PR is based on #928 - proposes an alternative solution.

- Replace relayer-controlled SolanaPayloadHint with constructor-hardcoded
  PDAs in SolanaIFTSendCallConstructor (6 immutables set at deployment)
- Construct complete ABI-encoded GmpSolanaPayload on-chain in Ethereum,
  so Solana recv_packet decodes accounts directly from the IBC packet
- Remove payload_hint instruction and state from ics27-gmp program
- Simplify relayer payload_translator to extract accounts from ABI payload
  instead of building and storing hint transactions
- Add DeployIFTContract.s.sol and DeploySolanaIFTConstructor.s.sol scripts
- Add Test_EthSolana_IFT_TwoTokens E2E test verifying two independent
  tokens with separate bridges and constructors over the same client pair
- Define sol! struct types for GmpPacketData and GmpSolanaPayload
- Replace hand-rolled read_offset/read_dynamic_bytes/encode_dynamic
  helpers with alloy's abi_decode/abi_encode
- Add alloy-sol-types dependency to ics27-gmp
…ypes

- Rename tx_builder/payload_translator.rs to tx_builder/gmp.rs
- Move ABI_ENCODING and AbiGmpPacketData to src/gmp.rs alongside protobuf re-exports
- Update all imports in ift.rs, packets.rs and tx_builder.rs
- Extract GMP accounts inline based on payload encoding instead of
  pre-extracting from the original IBC proto message
- Remove AbiGmpAccountInfo wrapper, return Vec<AccountMeta> directly
- Simplify extract_abi_gmp_accounts to a standalone function
- Single gmp module for all GMP logic (protobuf re-exports + ABI types + extraction)
- Remove tx_builder/gmp.rs submodule
- Move ABI decoding into relayer-lib's solana_gmp module
- extract_gmp_accounts now handles both protobuf and ABI encodings
- Extract shared build_gmp_account_list for PDA derivation
- eth-to-solana/gmp.rs reduced to re-exports
- Callers use a single function regardless of encoding
@codecov
Copy link

codecov bot commented Feb 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.91%. Comparing base (06999ca) to head (e60c5e6).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #959   +/-   ##
=======================================
  Coverage   99.91%   99.91%           
=======================================
  Files          27       28    +1     
  Lines        1123     1191   +68     
=======================================
+ Hits         1122     1190   +68     
  Misses          1        1           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- Extract encoding-agnostic `gmp_packet_data::decode` from `abi.rs`
- Add `From<AbiGmpPacketData>` and `TryFrom<AbiGmpSolanaPayload>` to reuse existing proto conversions
- Add test coverage for protobuf path, invalid encoding and error cases
- Replace modulo check with chunks_exact().remainder() pattern
- Move ABI/protobuf dispatch for GmpSolanaPayload into gmp_solana_payload::decode
- Remove inline encoding branching from on_recv_packet
- Add unit tests for both encoding paths
- Add From<GmpPacketData> for AbiGmpPacketData conversion
- Add gmp_packet_data::encode dispatching ABI vs protobuf
- Remove encode_abi_gmp_packet helper, inline via From + abi_encode
- Replace inline encoding branch in send_call with unified encode call
@mariuszzak mariuszzak self-assigned this Mar 6, 2026
Adapt abi.rs and gmp_solana_payload.rs to the proto field rename
from payer_position to prefund_lamports introduced in main.
The GMP program no longer injects a payer at a position index. Instead,
the relayer pre-funds the GMP PDA before recv_packet. This commit:

- Adds the payer account (GMP_ACCOUNT, signer+writable) at index 8 in
  the Solidity constructor and bumps NUM_ACCOUNTS from 11 to 12
- Replaces PAYER_POSITION=8 with PREFUND_LAMPORTS=3_000_000
- Adds extract_gmp_prefund_lamports to the shared relayer lib handling
  both protobuf and ABI encodings, consolidating duplicated decode logic
- Wires a system_program::transfer pre-fund instruction into the
  eth-to-solana build_recv_packet_chunked flow
- Moves MAX_PREFUND_LAMPORTS to the shared lib to eliminate duplication
  between cosmos-to-solana and eth-to-solana
…ransfer

Align with #972 which removed ift_bridge from FinalizeTransfer accounts
in the IFT program. The cosmos-to-solana relayer was updated but
eth-to-solana was missed, causing AccountDiscriminatorMismatch (3002).
@mariuszzak mariuszzak marked this pull request as ready for review March 6, 2026 12:55
@mariuszzak mariuszzak requested a review from srdtrk as a code owner March 6, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants