sRFC 00003: On-chain interface account resolution

On-chain interface account resolution

Summary

As the Solana program ecosystem matures, program interfaces will become the main means of building the composable future. Developers will still be able to innovate with their protocols to do anything, but by having their programs also adhere to interfaces, they can get immediate support with the rest of the ecosystem.

For example, as long as a program implements instruction processors for all of the possible “spl-token” instructions, and their structures conform to the Mint and TokenAccount types, then any marketplace or trading program can also use that program without any additional work.

This approach currently works, but it’s very limited. For example, if a program that implements the “spl-token” interface needs one more account to properly process a transfer (for example, the instructions sysvar), then both the client and on-chain programs need to figure out how to resolve the required accounts. SRFC 00002 solves the problem during transaction creation, but programs must also be able to construct CPI instructions on-chain. To put it differently, given a list of accounts, a program must be able to construct instructions in order to perform a CPI, without the program knowing everything about the target program.

Let’s walk through a concrete example.

Permissioned transfer for two different token types

Let’s say there’s a token marketplace program with some form of bids and asks on tokens. It doesn’t matter how exactly the program works, but at some point, it needs to transfer two different token types in one instruction, which we’ll call A and B. A and B could belong to different token programs, that all implement the “spl-token” interface.

As part of the “spl-token” transfer interface, a token program may also CPI into another program, which adheres to a “permission-transfer-check” interface. This nested “permissioned-transfer-check” interface requires at least the program, the token mint account, a PDA derived from the mint, and any number of additional accounts to validate the transfer.

Let’s assume that the client has properly constructed the transaction, so that all necessary accounts are available to the program. How does the program figure out which accounts are needed to construct the CPI instruction to transfer token A?

Possible solution

The “spl-token” transfer interface specifies certain “guaranteed” accounts: source token account, destination token account, mint, and authority. Also, the accounts in the program (the token account and mint) must conform to a certain structural definition.

After that, any other required account must be derivable from those required four. Additional account addresses may be derived as new program-derived addresses or read from account data. These additional accounts must also be provided after all of the “guaranteed” accounts in the marketplace interface.

For example, in Rust pseudo-code, that could be:

MarketplaceSwap {
    token_a_source: TokenAccount,
    token_a_destination: TokenAccount,
    token_a_mint: Mint,
    token_a_authority: Signer,
    token_b_source: TokenAccount,
    token_b_destination: TokenAccount,
    token_b_mint: Mint,
    token_b_authority: Signer,
    additional_accounts: &[AccountInfo]
}

To create an instruction to transfer token A, the token interface exposes an instruction creator:

fn create_transfer_instruction(
    program_id: &Pubkey,
    source: &TokenAccount,
    destination: &TokenAccount,
    mint: &Mint,
    authority: &AccountInfo,
    additional_accounts: &[AccountInfo]
) -> Instruction;

The interface instruction creator looks inside the mint, finds that it needs a CPI into the “permission-transfer-check” interface, finds the program in additional_accounts, along with a required PDA, and passes it down to one more nested function to extract the next level of additional accounts:

fn get_additional_accounts_for_permission_transfer_check(
    program_id: &Pubkey,
    mint: &Mint,
    additional_accounts: &[AccountInfo],
) -> [AccountMeta];

Given all of these, the marketplace program can construct the full instruction with all of the required accounts, and finally pass them all to the token program.

One level deeper, the token program will need to perform just one round on-chain account resolution to get the “permission-transfer-check” instruction:

fn create_permission_transfer_check_instruction(
    program_id: &Pubkey,
    mint: &Mint,
    additional_accounts: &[AccountInfo],
) -> Instruction;

This function is very similar to get_additional_accounts_for_permission_transfer_check, but instead it actually gives the full instruction, not just the additional account metas.

Conclusion

While this is just one approach to dynamic on-chain instruction creation / account resolution, with well-defined interfaces, this recursive approach allows programs to construct even the most complicated instructions while on-chain, and without additional CPIs into other programs. Everything must be derivable from the interface, the program, and the required accounts, or the whole model fails.

Implementation: still WIP, will update when it’s ready!

4 Likes

Hey Jon!

Here’s a proof of concept that does an on-chain “round trip” for account resolution written in Anchor, but doesn’t require introspecting Mint data.

I think this is a super cool paradigm that can be extended beyond Token implementations.

For those interested typescript tests to drive this on-chain account resolution:

test with program A:

test with program B:

2 Likes