@neonevm/call-solana

1.0.2 • Public • Published

Solidity libraries for composability with Solana programs through NeonEVM

Overview

Installation

npm install @neonevm/call-solana

Usage

Once you have installed the package, you can use the Solidity libraries by importing them in your contracts:

pragma solidity 0.8.28;

import { LibSPLTokenData } from "@neonevm/call-solana/composability/libraries/spl-token-program/LibSPLTokenData.sol";

contract CallSPLTokenProgram {
  /// @param tokenAccount The 32 bytes SPL token account public key
  /// @return token account balance as uint64
  function getSPLTokenAccountBalance(bytes32 tokenAccount) external view returns(uint64) {
    return LibSPLTokenData.getSPLTokenAccountBalance(tokenAccount);
  }
}

NeonEVM's composability feature

NeonEVM is a Solana network extension enabling EVM dApps to tap into Solana's user base and liquidity. It comes with a set of precompiled smart contracts acting as an interface between EVM dApps on NeonEVM and Solana's accounts and programs.

The composability feature allows EVM dApps deployed on NeonEVM to interact with Solana programs, which involves formatting Solana instructions in ways that are specific to each program.

Here we provide a set of Solidity libraries which make it possible to easily implement secure interactions with the following Solana programs:

  • System program: LibSystemProgram, LibSystemData and LibSystemErrors libraries
  • SPL Token program: LibSPLTokenProgram, LibSPLTokenData and LibSPLTokenErrors libraries
  • Associated Token program: : LibAssociatedTokenProgram and LibAssociatedTokenData libraries
  • Metaplex program: LibMetaplexProgram, LibMetaplexData and LibMetaplexErrors libraries
  • Raydium program: LibRaydiumProgram, LibRaydiumData and LibRaydiumErrors libraries

We also provide a set of example smart-contracts implementing typical use cases for these libraries and best practices when it comes to user authentication and Solana accounts management.

[!CAUTION] The following contracts have not been audited yet and are here for educational purposes.

Examples

Minting an SPL Token

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import { CallSolanaHelperLib } from "@neonevm/call-solana/utils/CallSolanaHelperLib.sol";
import { Constants } from "@neonevm/call-solana/composability/libraries/Constants.sol";
import { LibSystemData } from "@neonevm/call-solana/composability/libraries/system-program/LibSystemData.sol";
import { LibSPLTokenData } from "@neonevm/call-solana/composability/libraries/spl-token-program/LibSPLTokenData.sol";
import { LibSPLTokenProgram } from "@neonevm/call-solana/composability/libraries/spl-token-program/LibSPLTokenProgram.sol";

import { ICallSolana } from '@neonevm/call-solana/precompiles/ICallSolana.sol';

contract CallSPLTokenProgram {
    ICallSolana public constant CALL_SOLANA = ICallSolana(0xFF00000000000000000000000000000000000006);

    function createInitializeTokenMint(bytes memory seed, uint8 decimals) external {
        // Create SPL token mint account: msg.sender and a seed are used to calculate the salt used to derive the token
        // mint account, allowing for future authentication when interacting with this token mint. Note that it is
        // entirely possible to calculate the salt in a different manner and to use a different approach for
        // authentication
        bytes32 tokenMint = CALL_SOLANA.createResource(
            sha256(abi.encodePacked(
                msg.sender, // msg.sender is included here for future authentication
                seed // using different seeds allows msg.sender to create different token mint accounts
            )), // salt
            LibSPLTokenData.SPL_TOKEN_MINT_SIZE, // space
            LibSystemData.getRentExemptionBalance(
                LibSPLTokenData.SPL_TOKEN_MINT_SIZE,
                LibSystemData.getSystemAccountData(
                    Constants.getSysvarRentPubkey(),
                    LibSystemData.getSpace(Constants.getSysvarRentPubkey())
                )
            ), // lamports
            Constants.getTokenProgramId() // Owner must be SPL Token program
        );

        // This contract is mint/freeze authority
        bytes32 authority = CALL_SOLANA.getNeonAddress(address(this));
        // Format initializeMint2 instruction
        (   bytes32[] memory accounts,
            bool[] memory isSigner,
            bool[] memory isWritable,
            bytes memory data
        ) = LibSPLTokenProgram.formatInitializeMint2Instruction(
            decimals,
            tokenMint,
            authority,
            authority
        );

        // Prepare initializeMint2 instruction
        bytes memory initializeMint2Ix = CallSolanaHelperLib.prepareSolanaInstruction(
            Constants.getTokenProgramId(),
            accounts,
            isSigner,
            isWritable,
            data
        );

        // Execute initializeMint2 instruction
        CALL_SOLANA.execute(0, initializeMint2Ix);
    }

    function mint(
        bytes memory seed,
        bytes32 recipientATA,
        uint64 amount
    ) external {
        // Authentication: we derive the token mint account from msg.sender and seed
        bytes32 tokenMint = getTokenMintAccount(msg.sender, seed);
        // Format mintTo instruction
        (   bytes32[] memory accounts,
            bool[] memory isSigner,
            bool[] memory isWritable,
            bytes memory data
        ) = LibSPLTokenProgram.formatMintToInstruction(
            tokenMint,
            recipientATA,
            amount
        );
        // Prepare mintTo instruction
        bytes memory mintToIx = CallSolanaHelperLib.prepareSolanaInstruction(
            Constants.getTokenProgramId(),
            accounts,
            isSigner,
            isWritable,
            data
        );
        // Execute mintTo instruction
        CALL_SOLANA.execute(0, mintToIx);
    }
}

Deploying a Raydium pool

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import { Constants } from "@neonevm/call-solana/composability/libraries/Constants.sol";
import { CallSolanaHelperLib } from "@neonevm/call-solana/utils/CallSolanaHelperLib.sol";
import { LibAssociatedTokenData } from "@neonevm/call-solana/composability/libraries/associated-token-program/LibAssociatedTokenData.sol";
import { LibRaydiumProgram } from "@neonevm/call-solana/composability/libraries/raydium-cpmm-program/LibRaydiumCPMMProgram.sol";

import { ICallSolana } from "@neonevm/call-solana/precompiles/ICallSolana.sol";

interface IERC20ForSpl {
    function transferSolana(bytes32 to, uint64 amount) external returns(bool);
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external;
    function tokenMint() external view returns(bytes32);
}

contract CallRaydiumProgram {
    ICallSolana public constant CALL_SOLANA = ICallSolana(0xFF00000000000000000000000000000000000006);

    error InvalidTokens();

    function createPool(
        address tokenA,
        address tokenB,
        uint64 mintAAmount,
        uint64 mintBAmount,
        uint64 startTime
    ) public returns(bytes32) {
        bytes32 tokenAMint = IERC20ForSpl(tokenA).tokenMint();
        bytes32 tokenBMint = IERC20ForSpl(tokenB).tokenMint();
        bytes32 payerAccount = CALL_SOLANA.getPayer();
        bytes32 tokenA_ATA = LibAssociatedTokenData.getAssociatedTokenAccount(tokenAMint, payerAccount);
        bytes32 tokenB_ATA = LibAssociatedTokenData.getAssociatedTokenAccount(tokenBMint, payerAccount);

        IERC20ForSpl(tokenA).transferFrom(msg.sender, address(this), mintAAmount);
        IERC20ForSpl(tokenA).transferSolana(
            tokenA_ATA,
            mintAAmount
        );

        IERC20ForSpl(tokenB).transferFrom(msg.sender, address(this), mintBAmount);
        IERC20ForSpl(tokenB).transferSolana(
            tokenB_ATA,
            mintBAmount
        );

        bytes32[] memory premadeAccounts = new bytes32[](20);
        premadeAccounts[0] = payerAccount;
        premadeAccounts[7] = tokenA_ATA;
        premadeAccounts[8] = tokenB_ATA;

        (
            uint64 lamports,
            bytes32[] memory accounts,
            bool[] memory isSigner,
            bool[] memory isWritable,
            bytes memory data
        ) = LibRaydiumProgram.createPoolInstruction(tokenAMint, tokenBMint, mintAAmount, mintBAmount, startTime, 0, true, premadeAccounts);

        CALL_SOLANA.execute(
            lamports,
            CallSolanaHelperLib.prepareSolanaInstruction(
                Constants.getCreateCPMMPoolProgramId(),
                accounts,
                isSigner,
                isWritable,
                data
            )
        );

        return accounts[3]; // poolId
    }
}

Supported Solana programs

System program

SPL Token program

Metaplex program

Associated Token program

Raydium CPMM program

Composability helper contracts

  • Constants.sol provides commonly used constants for formatting instructions to be executed by Solana programs
  • CallSolanaHelperLib.sol provides helper functions to prepare formatted instructions right before they are executed on Solana
  • SolanaDataConverterLib.sol provides helper functions for casting data to and from various types commonly used on Solana
  • ICallSolana.sol provides an interfacte to the CallSolana precompiled contract which is the cornerstone of NeonEVM's composability with Solana. See: ICallSolana interface documentation .
  • QueryAccount.sol provides a set of getter function for reading Solana's state by querying data stored on Solana accounts

Solana specifics

See: Common Solana terminology

Solana Token accounts

Associated token accounts vs Arbitrary token accounts

Arbitrary token accounts are derived using a seed which includes the token account owner's public key and an arbitrary nonce (among other parameters). By using different nonce values it is possible to derive different arbitrary token accounts for the same owner which can be useful for some use cases.

The CallSPLTokenProgram contract provides its users with methods to create and initialize SPL token mints and arbitrary token accounts as well as to mint and transfer tokens using those accounts. It features a built-in authentication logic ensuring that users remain in control of created accounts.

However, there exists a canonical way of deriving a SPL token account for a specific owner and this token account is called an Associated Token account. Associated Token accounts are used widely by application s running on Solana and it is generally expected that token transfers are made to and from Associated Token accounts.

The CallAssociatedTokenProgram contract provides a method to create and initialize canonical Associated Token accounts for third party Solana users. This method can also be used to create and initialize canonical Associated Token accounts owned by this contract.

Ownership and authentication

SPL token mint ownership and authentication

The CallSPLTokenProgram.createInitializeTokenMint function takes a seed parameter as input which is used along with msg.sender to derive the created token mint account. While the CallSPLTokenProgram contract is given mint/freeze authority on the created token mint account, the mintTokens function grants msg.sender permission to mint tokens by providing the seed that was used to create the token mint account.

Metadata accounts ownership and authentication

The CallMetaplexProgram.createTokenMetadataAccount function takes a seed parameter as input which is used along with msg.sender to derive a token mint account. Created token metadata account is associated with this token mint account which must have been created and initialized beforehand by the same msg.sender. That same msg.sender is also granted permission to update the token metadata account in the future, provided that it is set as mutable upon creation.

Arbitrary token accounts ownership and authentication

Using arbitrary SPL Token accounts created via the CallSPLTokenProgram contract deployed on NeonEVM allows for cheap and easy authentication of NeonEVM users to let them interact with and effectively control those token accounts securely via this contract while this contract is the actual owner of those token accounts on Solana. It is also possible to create and initialize an arbitrary SPL Token accounts for third party Solana users, granting them full ownership of created accounts on Solana.

The CallSPLTokenProgram.createInitializeArbitraryTokenAccount function can be used for three different purposes:

  • To create and initialize an arbitrary token account to be used by msg.sender to send tokens through the CallSPLTokenProgram contract. In this case, both the owner and tokenOwner parameters passed to the function should be left empty. The arbitrary token account to be created is derived from msg.sender and a nonce (that can be incremented to create different arbitrary token accounts). Only msg.sender is allowed to perform state changes to the created token account via this contract. The transferTokens function grants msg.sender permission to transfer tokens from this arbitrary token account by providing the nonce that was used to create the arbitrary token account.

  • To create and initialize an arbitrary token account to be used by a third party user NeonEVM account through the CallSPLTokenProgram contract. In this case, the owner parameter passed to the function should be
    CallSPLTokenProgram.getNeonAddress(user) and the tokenOwner parameter should be left empty. The arbitrary token account to be created is derived from the user account and a nonce (that can be incremented to create different arbitrary token accounts). Only that user is allowed to perform state changes to the created token account via this contract. The transferTokens function grants user permission to transfer tokens from this arbitrary token account by providing the nonce that was used to create the arbitrary token account.

  • To create and initialize an arbitrary token account to be used by a third party solanaUser Solana account to send tokens directly on Solana without interacting with the CallSPLTokenProgram contract. In this case, both the owner and the tokenOwner parameters passed to the function should be solanaUser. The arbitrary token account to be created is derived from the solanaUser account and a nonce (that can be incremented to create different arbitrary token accounts). The owner of the arbitrary token account is the solanaUser account. The solanaUser account cannot transfer tokens from this arbitrary token account by interacting with the CallSPLTokenProgram contract, instead it must interact directly with the SPL Token program on Solana by signing and executing a transfer instruction.

License

This software is licensed under the MIT license

Package Sidebar

Install

npm i @neonevm/call-solana

Weekly Downloads

13

Version

1.0.2

License

MIT

Unpacked Size

178 kB

Total Files

26

Last publish

Collaborators

  • ahmed.gasanov
  • max_neon
  • ipneonevm