Solana

Solana Overview

As the underlying Solana environment is different than that of Ethereum, Solidity inner workings have been modified to function properly. For example, A Solidity contract on Solana utilizes two accounts: a data account and a program account. The program account stores the contract’s executable binary and owns the data account, which holds all the storage variables. On Ethereum a single account can store executable code and data.

Contract upgrades

Provided that the data layout from a new contract is compatible with that of an old one, it is possible to update the binary in the program account and retain the same data, rendering contract upgrades implemented in Solidity unnecessary. Solana’s CLI tool provides a command to both do an initial deploy of a program, and redeploy it later.:

solana program deploy --program-id <KEYPAIR_FILEPATH> <PROGRAM_FILEPATH>

where <KEYPAIR_FILEPATH> is the program’s keypair json file and <PROGRAM_FILEPATH> is the program binary .so file. For more information about redeploying a program, check Solana’s documentation.

Data types

  • An account address consists of a 32-bytes key, which is represented by the address type. This data model differs from Ethereum 20-bytes addresses.

  • Solana’s virtual machine registers are 64-bit wide, so 64-bit integers uint64 and int64 are preferable over uint256 and int256. An operation with types wider than 64-bits is split into multiple operations, making it slower and consuming more compute units. This is the case, for instance, with multiplication, division and modulo using uint256.

  • Likewise, all balances and values on Solana are 64-bit wide, so the builtin functions for address .balance, .transfer() and .send() use 64-bit integers.

  • An address literal has to be specified using the address"36VtvSbE6jVGGQytYWSaDPG7uZphaxEjpJHUUpuUbq4D" syntax.

  • Ethereum syntax for addresses 0xE0f5206BBD039e7b0592d8918820024e2a7437b9 is not supported.

Runtime

  • The Solana target requires Solana v1.8.1.

  • Function selectors are eight bytes wide and known as discriminators.

  • Solana provides different builtins, e.g. block.slot and tx.accounts.

  • When calling an external function or invoking a contract’s constructor, one needs to provide the necessary accounts for the transaction.

  • The keyword this returns the contract’s program account, also know as program id.

  • Contracts cannot be types on Solana and calls to contracts follow a different syntax.

  • Accounts can be declared on functions using annotations.

  • Retrieving the balance and transferring values utilize the AccountInfo struct.

Compute budget

On Ethereum, when calling a smart contract function, one needs to specify the amount of gas the operation is allowed to use. Gas serves to pay for a contract execution on chain and can be a way for giving a contract priority execution when extra gas is offered in a transaction. Each EVM instruction has an associated gas value, which translates to real ETH cost. Provided that one can afford all the gas expenses, there is no upper boundary for the amount of gas limit one can provide in a transaction, so Solidity for Ethereum has gas builtins, like gasleft, block.gaslimit, tx.gasprice or the Yul gas() builtin, which returns the amount of gas left for execution.

On the other hand, Solana is optimized for low latency and high transaction throughput and has an equivalent concept to gas: compute unit. Every smart contract function is allowed the same quantity of compute units (currently that value is 200k), and every instruction of a contract consumes exactly one compute unit. There is no need to provide an amount of compute units for a transaction and they are not charged, except when one wants priority execution on chain, in which case one would pay per compute unit consumed. Therefore, functions for gas are not available on Solidity for Solana.

Solidity for Solana incompatibilities with Solidity for Ethereum

  • msg.sender is not available on Solana.

  • There is no ecrecover() builtin function because Solana does not use the ECDSA algorithm, but there is a signatureVerify() function, which can check ed25519 signatures. As a consequence, it is not possible to recover a signer from a signature.

  • Try-catch statements do not work on Solana. If any external call or contract creation fails, the runtime will halt execution and revert the entire transaction.

  • Error definitions and reverts with error messages are not yet working for Solana.

  • Value transfer with function call does not work.

  • Many Yul builtins are not available, as specified in the availability table.

  • External calls on Solana require that accounts be specified, as in this example.

  • The ERC-20 interface is not compatible with Solana at the moment.

Getting started on Solana

Please follow the Solang Getting Started Guide.

For more examples, see the solang’s integration tests.

Using the Anchor client library

Some notes on using the anchor javascript npm library.

  • Solidity function names are converted to camelCase. This means that if in Solidity a function is called foo_bar(), you must write fooBar() in your javascript.

  • Anchor only allows you to call .view() on Solidity functions which are declared view or pure.

  • Named return values in Solidity are also converted to camelCase. Unnamed returned are given the name return0, return1, etc, depending on the position in the returns values.

  • Only return values from view and pure functions can be decoded. Return values from other functions and are not accessible. This is a limitation in the Anchor library. Possibly this can be fixed.

  • In the case of an error, no return data is decoded. This means that the reason provided in revert('reason'); is not available as a return value.

  • Number arguments for functions are expressed as BN values and not plain javascript Number or BigInt.

Calling Anchor Programs from Solidity

It is possible to call Anchor Programs from Solidity. You first have to generate a Solidity interface file from the IDL file using the Generate Solidity interface from IDL. Then, import the Solidity file in your Solidity using the import "..."; syntax. Say you have an anchor program called bobcat with a function pounce, you can call it like so:

import "./bobcat.sol";
import "solana";

contract example {
    function test(address a, address b) public {
        // The list of accounts to pass into the Anchor program must be passed
        // as an array of AccountMeta with the correct writable/signer flags set
        AccountMeta[2] am = [
            AccountMeta({pubkey: a, is_writable: true, is_signer: false}),
            AccountMeta({pubkey: b, is_writable: false, is_signer: false})
        ];

        // Any return values are decoded automatically
        int64 res = bobcat.pounce{accounts: am}();
    }
}

Setting the program_id for a contract

When developing contracts for Solana, programs are usually deployed to a well known account. The account can be specified in the source code using an annotation @program_id if it is known beforehand. If you want to call a contract via an external call, either the contract must have a @program_id annotation or the {program_id: ..} call argument must be present.

@program_id("Foo5mMfYo5RhRcWa4NZ2bwFn4Kdhe8rNK5jchxsKrivA")
contract Foo {
    function say_hello() public pure {
        print("Hello from foo");
    }
}

contract OtherFoo {
    function say_bye() public pure {
        print("Bye from other foo");
    }
}

contract Bar {
    function create_foo() external {
        Foo.new();
    }

    function call_foo() public {
        Foo.say_hello{accounts: []}();
    }

    function foo_at_another_address(address other_foo_id) external {
        OtherFoo.new{program_id: other_foo_id}();
        OtherFoo.say_bye{program_id: other_foo_id}();
    }
}

Note

The program_id Foo5mMfYo5RhRcWa4NZ2bwFn4Kdhe8rNK5jchxsKrivA was generated using the command line:

solana-keygen grind --starts-with Foo:1

Setting the payer, seeds, bump, and space for a contract

When a contract is instantiated, there are two accounts required: the program account to hold the executable code and the data account to save the state variables of the contract. The program account is deployed once and can be reused for updating the contract. When each Solidity contract is instantiated (also known as deployed), the data account has to be created. This can be done by the client-side code, and then the created blank account is passed to the transaction that runs the constructor code.

Alternatively, the data account can be created by the constructor, on chain. When this method is used, some parameters must be specified for the account using annotations. Annotations placed above a constructor can only contain literals or constant expressions, as is the case for first @seed and @space in the following example. Annotations can also refer to constructor arguments when placed next to them, as the second @seed and the @bump examples below. The @payer annotation is a special annotation that declares an account.

If the contract has no constructor, annotations can be paired with an empty constructor.

@program_id("Foo5mMfYo5RhRcWa4NZ2bwFn4Kdhe8rNK5jchxsKrivA")
contract Foo {

    @space(500 + 12)
    @seed("Foo")
    @payer(payer)
    constructor(@seed bytes seed_val, @bump bytes1 bump_val) {
        // ...
    }
}

Creating an account needs a payer, so at a minimum the @payer annotation must be specified. If it is missing, then the data account must be created client-side. The @payer annotation declares a Solana account that must be passed in the transaction.

The size of the data account can be specified with @space. This is a uint64 expression which can either be a constant or use one of the constructor arguments. The @space should at least be the size given when you run solang -v:

$ solang compile --target solana -v examples/solana/flipper.sol
...
info: contract flipper uses at least 17 bytes account data
...

If the data account is going to be a program derived address, then the seeds and bump have to be provided. There can be multiple seeds, and an optional single bump. If the bump is not provided, then the seeds must not create an account that falls on the curve. When placed above the constructor, the @seed can be a string literal, or a hex string with the format hex"4142". If before an argument, the seed annotation must refer to an argument of type bytes, address, or fixed length byte array of bytesN. The @bump must a single byte of type bytes1.

Transferring native value with a function call

The Solidity language on Ethereum allows value transfers with an external call or constructor, using the auction.bid{value: 501}() syntax. Solana Cross Program Invocation (CPI) does not support this, which means that:

  • Specifying value: on an external call or constructor is not permitted

  • The payable keyword has no effect

  • msg.value is not supported

Note

A naive way to implement this is to let the caller transfer native balance and then inform the callee about the amount transferred by specifying this in the instruction data. However, it would be trivial to forge such an operation.

Receive function

In Solidity the receive() function, when defined, is called whenever the native balance for an account gets credited, for example through a contract calling account.transfer(value);. On Solana, there is no method that implements this. The balance of an account can be credited without any code being executed.

receive() functions are not permitted on the Solana target.

msg.sender not available on Solana

On Ethereum, msg.sender is used to identify either the account that submitted the transaction, or the caller when one contract calls another. On Ethereum, each contract execution can only use a single account, which provides the code and data. On Solana, each contract execution uses many accounts. Consider a rust contract which calls a Solidity contract: the rust contract can access a few data accounts, and which of those would be considered the caller? So in many cases there is not a single account which can be identified as a caller. In addition to that, the Solana VM has no mechanism for fetching the caller accounts. This means there is no way to implement msg.sender.

The way to implement this on Solana is to have an authority account for the contract that must be a signer for the transaction (note that on Solana there can be many signers too). This is a common construct on Solana contracts.

import 'solana';

contract AuthorityExample {
    address authority;
    uint64 counter;

    constructor(address initial_authority) {
        authority = initial_authority;
    }

    @signer(authorityAccount)
    function set_new_authority(address new_authority) external {
        assert(tx.accounts.authorityAccount.key == authority && tx.accounts.authorityAccount.is_signer);
        authority = new_authority;
    }

    @signer(authorityAccount)
    function inc() external {
        assert(tx.accounts.authorityAccount.key == authority && tx.accounts.authorityAccount.is_signer);
        counter += 1;
    }

    function get() public view returns (uint64) {
        return counter;
    }
}

Builtin Imports

Some builtin functionality is only available after importing. The following structs can be imported via the special builtin import file solana.

import {AccountMeta, AccountInfo} from 'solana';

Note that {AccountMeta, AccountInfo} can be omitted, renamed or imported via import object.

// Now AccountMeta will be known as AM
import {AccountMeta as AM} from 'solana';

// Now AccountMeta will be available as solana.AccountMeta
import 'solana' as solana;

Note

The import file solana is only available when compiling for the Solana target.

Builtin AccountInfo

The account info of all the accounts passed into the transaction. AccountInfo is a builtin structure with the following fields:

address key

The address (or public key) of the account

uint64 lamports

The lamports of the accounts. This field can be modified, however the lamports need to be balanced for all accounts by the end of the transaction.

bytes data

The account data. This field can be modified, but use with caution.

address owner

The program that owns this account

uint64 rent_epoch

The next epoch when rent is due.

bool is_signer

Did this account sign the transaction

bool is_writable

Is this account writable in this transaction

bool executable

Is this account a program

Builtin AccountMeta

When doing an external call (aka CPI), AccountMeta specifies which accounts should be passed to the callee.

address pubkey

The address (or public key) of the account

bool is_writable

Can the callee write to this account

bool is_signer

Can the callee assume this account signed the transaction

Builtin create_program_address

This function returns the program derived address for a program address and the provided seeds. See the Solana documentation on program derived addresses.

import {create_program_address} from 'solana';

contract pda {
    address token = address"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";

    function create_pda(bytes seed2) public returns (address) {
        return create_program_address(["kabang", seed2], token);
    }
}

Builtin try_find_program_address

This function returns the program derived address for a program address and the provided seeds, along with a seed bump. See the Solana documentation on program derived addresses.

import {try_find_program_address} from 'solana';

contract pda {
    address token = address"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";

    function create_pda(bytes seed2) public returns (address, bytes1) {
        return try_find_program_address(["kabang", seed2], token);
    }
}

Solana Library

In Solang’s Github repository, there is a directory called solana-library. It contains libraries for Solidity contracts to interact with Solana specific instructions. We provide two libraries: one for SPL tokens and another for Solana’s system instructions. In order to use those functionalities, copy the correspondent library file to your project and import it.

SPL-token

spl-token is the Solana native way of creating tokens, minting, burning and transferring token. This is the Solana equivalent of ERC-20 and ERC-721. Solang’s repository contains a library SplToken to use spl-token from Solidity. The file spl_token.sol should be copied into your source tree, and then imported in your solidity files where it is required. The SplToken library has doc comments explaining how it should be used.

There is an example in our integration tests of how this should be used. See token.sol and token.spec.ts.

System Instructions

Solana’s system instructions enable developers to interact with Solana’s System Program. There are functions to create new accounts, allocate account data, assign accounts to owning programs, transfer lamports from System Program owned accounts and pay transaction fees. More information about the functions offered can be found both on Solana documentation and on Solang’s system_instruction.sol file.

The usage of system instructions needs the correct setting of writable and signer accounts when interacting with Solidity contracts on chain. Examples are available on Solang’s integration tests. See system_instruction_example.sol and system_instruction.spec.ts

Minimum balance

In order to instantiate a contract, you need the minimum balance required for a Solana account of a given size. There is a function minimum_balance(uint64 space) defined in minimum_balance.sol to calculate this.

Solana Account Management

In a contract constructor, one can optionally write the @payer annotation, which receives a character sequence as an argument. This annotation defines a Solana account that is going to pay for the initialization of the contract’s data account. The syntax @payer(my_account) declares an account named my_account, which will be required for every call to the constructor.

Similarly, for external functions in a contract, one can declare the necessary accounts, using function annotations. @account(myAcc) declares a read only account myAcc, while @mutableAccount(otherAcc) declares a mutable account otherAcc. For signer accounts, the annotations follow the syntax @signer(mySigner) and @mutableSigner(myOtherSigner).

Accessing accounts’ data

Accounts declared on a constructor using the @payer annotation are available for access inside it. Likewise, accounts declared on external functions with any of the aforementioned annotations are also available in the tx.accounts vector for easy access. For an account declared as @account(funder), the access follows the syntax tx.accounts.funder, which returns the AccountInfo builtin struct.

contract Foo {
    @account(oneAccount)
    @signer(mySigner)
    @mutableAccount(otherAccount)
    @mutableSigner(otherSigner)
    function bar() external returns (uint64) {
        assert(tx.accounts.mySigner.is_signer);
        assert(tx.accounts.otherSigner.is_signer);
        assert(tx.accounts.otherSigner.is_writable);
        assert(tx.accounts.otherAccount.is_writable);

        tx.accounts.otherAccount.data[0] = 0xca;
        tx.accounts.otherSigner.data[1] = 0xfe;

        return tx.accounts.oneAccount.lamports;
    }
}

External calls with accounts

In any Solana cross program invocation, including constructor calls, all the accounts a transaction needs must be informed. If the {accounts: ...} call argument is missing from an external call, the compiler will automatically generate the AccountMeta array that satisfies such a requirement. Currently, that only works if the call is done in an function declared external, as shown in the example below. In any other case, the AccountMeta array must be manually created, following the account ordering the IDL file specifies. If a certain call does not need any accounts, an empty vector must be passed {accounts: []}.

The following example shows two correct ways of calling a contract. Note that the IDL for the BeingBuilt contract has an instruction called new, representing the contract’s constructor, whose accounts are specified in the following order: dataAccount, payer_account, systemAccount. That is the order one must follow when invoking such a constructor.

import 'solana';

@program_id("SoLDxXQ9GMoa15i4NavZc61XGkas2aom4aNiWT6KUER")
contract Builder {
    function build_this() external {
        // When calling a constructor from an external function, the data account for the contract
        // 'BeingBuilt' should be passed as the 'BeingBuilt_dataAccount' in the client code.
        BeingBuilt.new("my_seed");
    }

    function build_that(address data_account, address payer_account) public {
        // In non-external functions, developers need to manually create the account metas array.
        // The order of the accounts must match the order from the BeingBuilt IDL file for the "new"
        // instruction.
        AccountMeta[3] metas = [
            AccountMeta({
                pubkey: data_account,
                is_signer: true,
                is_writable: true
                }),
            AccountMeta({
                pubkey: payer_account,
                is_signer: true,
                is_writable: true
                }),
            AccountMeta({
                pubkey: address"11111111111111111111111111111111",
                is_writable: false,
                is_signer: false
                })
        ];
        BeingBuilt.new{accounts: metas}("my_seed");


        // No accounts are needed in this call, so we pass an empty vector.
        BeingBuilt.say_this{accounts: []}("It's summertime!");
    }
}


@program_id("SoLGijpEqEeXLEqa9ruh7a6Lu4wogd6rM8FNoR7e3wY")
contract BeingBuilt {
    @space(1024)
    @payer(payer_account)
    constructor(@seed bytes my_seed) {}

    function say_this(string text) public pure {
        print(text);
    }
}