Solana for Non Smart Contract Developers

An introduction to Solana smart contracts for developers with no smart contract experience and smart contract developers with no Solana experience.

Krešimir Klas
14 min readJul 29, 2022

This article introduces the most important concepts and describes the programming and security models of smart contract programming on Solana. It’s aimed at experienced developers who may or may not have prior smart contract development experience on other blockchains. Familiarity with Rust is assumed.

The goal of this article is to cover all the important concepts that you’d need to get started in smart contract development on Solana and describe how they all tie together. For conciseness, I don’t expand on any of the concepts in too much detail. The links I’ve included in the “Conclusion” section at the end of the article are a good starting point to dig deeper into any of the topics.

Programming Model

Solana’s smart contract programming model is based on programs and accounts.

Note: in Solana, smart contracts are commonly referred to as “programs”. So when you hear “programs” just think smart contracts (these terms are used interchangeably). Also, forget what you know about EVM because some of the terminologies here will be confusing.

Programs (smart contracts) define the logic that will be processed when the program is called in a transaction. You can pass arbitrary arguments to a program call, but programs are pure in the sense that on their own they cannot persist, read or write, any state. To enable persistence and state, the programs work with accounts. Program calls are made by clients (usually web apps, but you can also manually call them directly).

Accounts are essentially what stores the program state. Programs write to and read from accounts and this data is persisted across transactions. Each account is uniquely identified by its address (the public key of an Ed25519 key pair). Accounts are of a specified size and store arbitrary data. Additionally, there is metadata associated with each account which carries important information about the account. The programs themselves (executables) are also stored in accounts and have an address (referred to as program ID). There‘s no inherent structure to account data — from runtime’s perspective it’s just an array of bytes of specified size.

You can think of Solana’s account space as a global key-value store where keys are account addresses and values are account data. The programs then operate on top of this key-value store by reading and modifying its values.

It’s important to note that for each program call, the client (usually a web app) needs to specify a priori which accounts the program will access in the call and whether it wants to read from it or also write. This is very important for transaction processing performance — since the runtime now knows which accounts each transaction will modify, it can schedule non-overlapping transactions to be executed in parallel while guaranteeing data consistency. This is one of the key reasons why Solana can maintain high throughput and also when you hear people say “EVM doesn’t scale” this is largely what they’re referring to — it can’t do parallelization of TX processing. You can read more about Solana’s TX processing runtime in Anatoly Yakovenko’s article from 2019.

Security Model

Account Ownerhsip

Solana accounts have a notion of ownership. Each account is strictly owned by one (and only one) program. This means that only the program which owns the account can ever modify it. No other programs can modify it, only read from it. Account ownership information is stored in its metadata and is not modifiable by user programs.

Account Signatures

The clients can, along with accounts, also pass in account signatures in the program calls (remember that each account is associated with an Ed25519 key pair). When you own a private key of an account you can sign a transaction with it and then this account is marked as a “signer” in the program runtime. This information can then be used arbitrarily by programs to implement ownership and authority functionality. In fact, this is how wallets are implemented on Solana. Each account has an amount of SOL associated with it (this is also stored in the account metadata). When you want to transfer SOL from one account to another, you need to sign the transaction with the account’s private key (where the public key is the account’s address). So Solana wallets are, essentially, accounts for which you own private keys.

Note: it is not necessary for accounts to actually store any data. Very often accounts are used only for their private key signing capabilities — their allocated size is 0 and they store no data but their signatures are used e.g. to implement authority and ownership functionalities.

CPI calls

You can also call programs from other programs via the CPI (cross-program invocation) interface. CPI calls are very similar to client program calls — you reference the program you want to call into by its program ID and pass in the arguments and required accounts. Internally, CPI calls are implemented via a syscall which makes the runtime execute the specified program in pretty much the same way it would if you called it directly from the client side.

PDAs

Programs themselves also have the ability to provide account signatures in CPI calls. This is implemented through PDA (program derived address) accounts. PDA accounts are special types of accounts that can only be signed by a program. They are program specific and each program can generate as many PDAs as it wants to. Users or other programs can never provide signatures for PDAs created for a specific program. PDAs are important because they allow for programs to have authority and ownership capabilities. For example, they allow programs to own tokens. This is how e.g. token vaults are implemented where only the program (and no users or other programs) have the authority to withdraw tokens from the vault. Because of the guarantees that PDAs provide, the vault is fully protected by the program’s logic — i.e. if the program is correct we know that the vault is safe.

It’s important to recognize the difference between account ownership and signatures. Accounts are owned by programs and it gives the owner program permission to mutate it. Account private keys are held by users which allows the user who has the private key of an account to sign a transaction with it and this will mark the account as “signed” during program execution. Accounts being signed or not doesn’t have any special semantics in the runtime and it’s fully up to the implementation of the program to give it meaning.

For example, in the SPL Token Program (which implements ERC20-like functionality, and is separate from SOL transfer mechanics described at the beginning of this section) each token account is owned by the Token Program meaning only the token program can change the values in those accounts (e.g. add or subtract amounts). But in each token account, the Token Program stores a field about the address (public key) of the authority account which is allowed to spend the tokens (and which is different from the account owner stored in the account metadata). So when you do a transfer call in the Token Program, you need to provide the signature which corresponds to the authority field of the specified source token account. The Token Program will, during execution, check whether the provided signature matches the authority stored in the token account (by checking that the correct account is marked as a signer), and if it does it will allow the transfer. So only when you own the private key of the authority can you spend from this account. When the authority is a PDA, it means that only the program corresponding to that PDA can spend the tokens from that account (via a CPI call). On top of that, the Solana runtime will check that the token account which is being mutated (amounts are being subtracted) is owned by the Token Program. This is so that the Token Program is prevented from mutating any accounts it doesn’t own.

In summary

  • accounts are owned by programs which gives them the sole permission to mutate them
  • you can sign a transaction with the account’s private key and the runtime will mark this account as a “signer” that the program implementation defines the semantics of
  • the programs can call each other using CPI calls
  • PDAs allow programs to also provide account signatures

These four things are the basic building blocks for composable and secure smart contract programming on Solana. Using this you can build pretty much anything you need.

Smart Contract Programming on Solana

Solana smart contracts can be written in any programming language that compiles to SBF (Solana Bytecode Format, a modification of eBPF), but generally, they’re written in Rust. All programs on Solana define an entrypoint which looks like this:

When the program gets called the runtime will call the entrypoint function by passing in:

  • the program id of the program being called (address of the account which stores the program) — because sometimes (very rarely) it’s useful to deploy the same program to different addresses (program IDs)
  • arbitrary accounts specified by the client
  • arbitrary instruction data specified by the client (normally used to encode arguments)

The program then needs to process these inputs and execute its logic.

Note: each transaction can contain multiple sequential program calls (to the same or different programs) referred to as “instructions”. Transactions are atomic in the sense that if any of the instructions (program calls) fails, the whole transaction will fail and it will have no effect on the global state.

Each AccountInfo struct contains the account data as well as some relevant metadata — e.g. which program is its owner, can the account be mutated, has the transaction been signed with its private key, etc…

Input Safety

So we can see right off the bat that since the client can pass any accounts and any instruction data, the program needs to be very careful when processing these inputs to make sure that adversarially crafted inputs cannot affect program execution in unexpected ways.

For example, when processing a transfer call, the Token Program needs to check whether the client-provided source token account is a valid token account — i.e. belongs to the Token Program and is of the correct type (the Token Program also owns mint accounts that store different data). Since token accounts can only be created via an initialize account instruction call in the Token Program, after doing these checks we know that using this account is safe. Otherwise, one could specially craft a custom token account using a custom program, pass it in as a real one, and effectively mint an arbitrary amount of tokens because the Token Program would allow transfers of tokens from this fake account depositing them as real tokens into the destination account. In fact an exploit of the same kind (passing in a fake account) is what lead to Wormhole’s $326M loss of funds (and numerous other similar hacks).

Instructions

Most of the time we want to implement multiple different instruction calls in the same program — e.g. the Token Program implements InitializeAccount, InitializeMint, Transfer, and numerous other instruction calls — each of which requires different call arguments. A common pattern is to encode a discriminator in the first byte of the instruction_data argument in the entrypoint function. Then at the beginning of the program, we decode the first byte which will tell us which instruction the user is trying to call (with one byte we can differentiate up to 256 different instructions). We can encode the instruction arguments in the remaining bytes of the instruction_data byte slice. The Token Program is a good example of this pattern.

In summary, here is a rough flow of Solana programs:

  • define the entrypoint function
  • process instruction data to decode the instruction call and its arguments
  • call the relevant instruction handler
  • do account and argument checks (super important to do this properly as these inputs are unsanitized)
  • process the instruction, decode account data as necessary

Anchor

Solana programs are written in Rust and Rust is a very powerful, flexible, yet safe language. Rust gives us a blank canvas, but we have seen in the previous chapter there is a common theme and patterns when it comes to implementing smart contracts, namely:

  • decoding instructions and their arguments from instruction_data
  • implementing handlers for each instruction
  • doing account checks
  • differentiating between multiple different account types
  • encoding and decoding of data into accounts

These are things that pretty much all programs do but Rust itself doesn’t give us any tools to streamline at least some of this. This is where Anchor comes in…

Anchor is a framework for Solana smart contract development in Rust. It fills some of these gaps that we have with raw Rust and provides safe defaults. It makes Solana smart contract development much more ergonomic and safer — not only because it removes footguns and provides safe defaults, but also because it makes safety-critical things more explicit and readable. It also provides a lot of tooling which saves time.

What Anchor provides out of the box:

  • instruction discrimination and argument parsing
  • safe account serialization and deserialization
  • embedded DSL for doing account checks
  • library code for CPI calls
  • errors and events
  • trivial composability with other Anchor programs
  • IDL generation and client SDKs
  • workspace scaffolding generation

A token program written in Anchor could look something like this:

As you can tell, this example is not a complete program as it implements only two instructions —initialize_account and transfera full program would have to, at the very least, implement the initialize_mint instruction and a Mint account which is not covered here. Also, I have omitted the instruction bodies to focus on Anchor features rather than program-specific logic.

There’s a lot that Anchor does here under the hood so let’s unpack this.

Instructions

First, we can see that the token module is annotated with a #[program] macro (line 5). This macro will expand into an entrypoint function (see the previous chapter) and add code for discriminating between different instruction calls (in this case the initialize_account and transfer instructions). The instruction discriminator is encoded in the first 8 bytes of instruction data and is internally derived from the instruction name (and therefore instructions with different names will have different discriminators). The rest of the instruction data encodes the instruction arguments — e.g. for the transfer instruction call, the discriminator will be followed by the encoded amount parameter. Anchor uses Borsh serialization for encoding/decoding.

Contexts

Next, we can see that each of the instructions has a context as its first argument — e.g. in the initialize_account instruction we have ctx: Context<InitializeAccount>(line 9). These structs are similar to the &[AccountInfo] slice in the entrypoint (see the previous chapter) in that they hold accounts that were passed as inputs to the instruction call, but the main difference is that these accounts are validated. To understand what this means, let’s look at the corresponding InitializeAccount struct (line 30). This struct is annotated with the #[derive(Accounts)] macro and the fields represent the accounts that the initialize_account instruction expects as inputs. These accounts are new_token_account, system_program, and payer. When Anchor processes this instruction call it will try to load each account from the accounts input (&[AccountInfo] slice) into the corresponding field of the ctx struct (in sequential order — first account in the slice into the first field).

Let’s look at each account field individually:

new_token_account is the new token account that will be created to store the state. Its type is Account<'info, TokenAccount> . This tells Anchor that this account needs to be owned by this program (this assertion is done by the Account trait here), and the data encoded inside it is the TokenAccount struct (defined on line 21). This field is also annotated with an #[account(...) macro:

  • init — this tells Anchor that the account needs to be initialized. When the instruction is called Anchor will do a CPI call to the System Program to initialize this account. This is very convenient because otherwise, we’d have to do it manually and potentially make a mistake (there are a few nuances to initializing accounts which Anchor handles for us)
  • space — since we’re initializing a new account, we need to tell Anchor what size it needs to be (Anchor cannot infer this on its own). We create an account with the size of 80 bytes (8 + 32 + 32 + 8). The first 8 bytes are reserved for the account discriminator (more on that below) and the rest is space reserved for the TokenAccount fields (line 21). The authority and mint fields are of Pubkey type which is 32 bytes. The balance field is an u64 which is 8 bytes.
  • payer — this is the account that will pay for the account creation (rent). It refers to the payer account on line 39.

system_program references the System Program which is a special program in Solana used to create accounts, transfer SOL, etc. It needs to be passed in since we’re doing a CPI call in order to initialize a new token account (remember that all accounts that an instruction needs to access, and programs are also stored as accounts, need to be specified by the client in order for the runtime to be able to parallelize TX processing). The type of this field is Program<'info, SystemProgram> which will make Anchor assert that the account which was passed in corresponds to the System Program.

payer is needed because we’re initializing a new account (line 36). The type of this field is Signer<'info> which will make Anchor check that the transaction is signed with this account’s private key. This is needed since we’re going to be spending SOL from that account in order to pay for the token account initialization. The payer account field is also annotated with the #[account(mut)] attribute which means that the clients calling this instruction need to mark it as mutable (again because SOL is going to be subtracted from this account).

The Transfer ctx struct (line 42) is also very similar. The source account is the token account which we transfer the tokens from, destination is the account where the tokens will be deposited. It’s annotated with constraint = destination.mint == source.mint. This will check that the token accounts belong to the same token “type” (we should prevent transfers from a USDC token account to a SRM token account for example). The authority account is a Signer and with address = source.authority we check that this signer account matches the authority of the source account. This makes it impossible for anyone but the owner of the authority private key to spend from the source token account.

So I think that this illustrates really well how Anchor makes smart contract programming much more ergonomic and safer. Without Anchor we would have to do all this initialization, argument processing, encoding/decoding, account checks, etc. manually. And if you mess up in any of this it can easily lead to loss of user funds (as we’ve seen from numerous exploits that happened already).

For each program Anchor also generates an IDL which describes how the program should be called from the client side (similar to ABIs on Ethereum). Multiple available tools can use IDLs to simplify interaction with the program from the client side:

When starting a new smart contract on Solana it’s highly recommended that you use Anchor instead of raw Rust unless you have a really good reason not to.

Conclusion

This article gives an overview of the most important concepts in Solana smart contract development, describes the programming and security models, and gives an introduction to the Anchor framework.

For further reading I recommend the following resources:

I’d also recommend joining the Solana developer discord and the Solana Stack Exchange.

Follow me on Twitter: @kklas_

--

--