angkor wat

a brief tour of programming in anchor

disclaimer: i wrote this against anchor 0.18.0. theres breaking changes all the time as the devs figure out the best ways to do this stuff. im not gonna commit to an update schedule (in fact i still didnt update my solana post!!) so keep that in mind if youre reading this in the future

* * *

introduction

oki i know i said id write other posts first but ive been working in anchor more days than not for a few months now. so i thought itd be good to do this one. like i said anchor is still a moving target but its been gaining coherency lately (i dont have to work as hard as i used to when i update my codebases every like two weeks omg) so i think this should be useful for a bit in the future too

this post is aimed at programmers who are already basically familiar with solana. you should know what accounts are, whats the difference between transactions and instructions, shit like that. if not then theres a terrible post by some moron to get you through it. although anchor makes a lot of things easier im strongly of the opinion you need to understand what youre abstracting over

if youre decent at programming but dont know rust you can probably follow along anyway. rust is like c on training wheels plus haskell on training wheels with the commitment to minimalism of c++ and the syntactical beauty of javascript. but its actually not a bad language to write and the documentation and tooling is top notch

code examples are illustrative not comprehensive. this isnt a replacement for the anchor tutorial or the docs. im just trying to give you a high level overview so you can see whats up

cpi = cross-program invocation, pda = program-derived address, otherwise im not weird about acronyms

* * *

words

solana is fucking complicated. it is that way for good reasons, but there is a lot of shit it expects you to just deal with that any reasonable person would refuse

but we arent reasonable people. we are faang cringelords desperately larping as iconoclasts, vc losers scared of not getting rich fast enough, gambling addicts trying to pose as technically minded, and emotionally crippled autistic cats distracted by laser pointers. that is why we are here

the overarching point to anchors existence is ergonomic. the main things it does are:

anchor could be compared to solidity sorta. it cant quite replicate the easy back and forth you get with solidity contracts because reading from and writing to solana are fundamentally distinct operations. but its clearly been made with solidity in mind and solves a lot of the same problems

(this does not mean vanilla solana is like evm bytecode if you say that i will cry. ebpf bytecode is like evm bytecode. anchor is just extra rust on top of rust)

personally i use anchor for everything lately. its super fast to prototype in: when i have "i wonder what happens if..." thoughts, i often just write it out instead of reading source or asking around. its good to develop in in general too. at this point id only start a new project in raw solana if i knew from the beginning i would need to optimize the shit out of it

a couple months ago i was more skeptical. the abstractions were weirder and more fucked up (associated accounts, state singletons, three different account types...) and anchor got in my way enough i used it for the interface boundary and otherwise wrote vanilla solana

but now its really good. it does a lot more. ive consistently been deleting code and replacing it with account annotations that do the same things behind the scenes every few releases. and as of 0.18.0 constraints can have readable errors instead of meangingless hex codes!! so i dont have to feel bad sacrificing user friendliness for dev readability. but ill get to what that means in a bit

* * *

code

again, code examples are more a visual aid than teaching tool. the anchor tutorial is good and kept up to date version to version. you can get an idea here and then fill out the picture with that, or you can start with that and use this as a quick reference. whatever suits your style

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
#[deny(unused_must_use)]
pub mod example {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {
        Ok(())  
    }
}

#[derive(Accounts)]
pub struct Initialize {}

this is a full working program, almost exactly the one anchor init spits out

you have to declare your program id with declare_id!, its a huge pain honestly. the bulk of the program goes in a module under the #[program] macro. i always add #[deny(unused_must_use)] to elevate ignoring Result<T> from a compiler warning to a compiler error

every pub fn in the module is exposed as a client-callable program function. the first arg is always Context<T> and corresponds to the solana accounts array and program id

then below that is the (presently useless) struct for those accounts, derive Accounts and dont ask questions

account structs can be reused for multiple functions, this is sometimes useful like if two calls are inverse operations of each other

const anchor = require("@project-serum/anchor");

anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Example;
await program.rpc.initialize();

this is the minimal js needed to call that program

Provider encapsulates connection and wallet, its actually kinda nice because it can handle getting signatures from wallet extensions that hold their own keys. anchor.workspace loads the program but doesnt work in the browser lmao. program.rpc is the simplest interface with a program and calls it directly with the default provider. there are also program.transaction and program.instruction namespaces to get the relevant objects if you need to do extra stuff with them

{
  "version": "0.0.0",
  "name": "example",
  "instructions": [
    {
      "name": "initialize",
      "accounts": [],
      "args": []
    }
  ]
}

after running anchor build you get a file target/idl/example.json that looks like this. this is called the idl and it is what you want to load and pass to Program to instantiate the object in a browser. if you deploy the address will be written in there but i would not rely on it because it goes away again if you rebuild

from this point forward code will be relevant snippets only. you can see full programs in the tutorial

pub fn initialize(ctx: Context<Initialize>, n: u64, s: String) -> ProgramResult
await program.rpc.initialize(new anchor.BN(1), "hi");

arguments can be added to functions. on the client, you pass args to your rpc call as shown. when you also have an accounts object, args go first

you may need to wrap numbers as bignums but otherwise the rust/js correspondences are usually straightforward. an Option<T> arg can be passed as just the value for Some or null for None. the only weird one is enums. if you have like pub enum Color { Black, White } then you pass it in js like { Black: {} }. this sounds like a joke but its not

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(
        init,
        payer = authority,
    )]
    pub state: Account<'info, State>, 
    pub rent: Sysvar<'info, Rent>,
    pub system_program: Program<'info, System>,
}

here is an actually useful accounts struct

the Signer account type and #[account(mut)] macro correspond to isSigner and isWritable flags. the second macro over state implicitly calls create_account inside the program to allocate an account of the appropriate size for a serialized State struct. you can alternatively add the create instruction and annotate the account #[account(zero)] to assert the data is uninitialized

also note sysvars and programs are both subtyped and these types act as constraints. you can use UncheckedAccount to pass arbitrary addresses in. you can wrap accounts in Box if (when) you have enough that anchor blows the stack deserializing them

#[account]
#[derive(Default)]
pub struct State {
    bump: u8,
    authority: Pubkey,
}
"an account of the appropriate size" means eight bytes for a header called the discriminator plus however many bytes for the struct data. you can derive Default to let anchor automatically calculate it. be careful using this with variable-length types like strings and lists because their default value is usually mempty. alternatively you can size the buffer by hand in the constraint with space but remember to add eight
function discriminator(name) {
    let hash = sha256("account:" + name);
    return Buffer.from(hash.substring(0, 16), "hex");
}

here is how you calculate account discriminators, where name is the struct name as a string literal. maybe it will come in handy someday. i like to use them for pda seeds to cut down on hardcoded magic numbers

program.rpc.initialize({
    accounts: {
        authority: authority.publicKey,
        state: stateKey,
        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
        systemProgram: anchor.web3.SystemProgram.programId,
    },
    signers: [authority.payer],
});

calling our new version of initialize from js looks like this

thats pretty much everything essential about the client so the rest will be just programs

#[error]
pub enum AdobeError {
    #[msg("borrow requires an equivalent repay")]
    NoRepay,
    #[msg("non-repay adobe calls after borrow are disallowed")]
    ExtraCall,
}
return Err(AdobeError::NoRepay.into());

you can declare and return custom errors like so. whatever you put in #[msg(...)] gets surfaced to the client

#[derive(Accounts)]
#[instruction(pool_bump: u8)]
pub struct AddPool<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(
        seeds = [&State::discriminator()[..]],
        bump = state.bump,
        has_one = authority,
    )]
    pub state: Account<'info, State>,
    pub token_mint: Account<'info, Mint>,
    #[account(
        init,
        seeds = [&Pool::discriminator()[..], token_mint.key().as_ref()],
        bump = pool_bump,
        payer = authority,
    )]
    pub pool: Account<'info, Pool>,
    #[account(
        init,
        seeds = [TOKEN_NAMESPACE, token_mint.key().as_ref()],
        bump,
        token::mint = token_mint,
        token::authority = state,
        payer = authority,
    )]
    pub pool_token: Account<'info, TokenAccount>,
    #[account(
        init,
        seeds = [VOUCHER_NAMESPACE, token_mint.key().as_ref()],
        bump,
        mint::authority = state,
        mint::decimals = token_mint.decimals,
        payer = authority,
    )]
    pub voucher_mint: Account<'info, Mint>,
    pub rent: Sysvar<'info, Rent>,
    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
}

constraints are the most important shit in anchor that anchor doesnt just do for you. this is a lot but lets walk through it

first, the #[instruction(...)] macro deserializes program data so you can use your function args in constraints. this starts from the ninth byte of the buffer (the first eight are used for dispatch) and is not checked whatsoever so be careful

seeds and bump assert the account address matches the pda generated by them. you dont need to give an arg to bump and it will just calculate it

has_one is honestly a tiny bit confusing but its a shorthand to check that state has a field authority whose value matches the key of the account named authority

futher down above pool_token and voucher_mint you see we initialize an spl token account and mint. the TokenAccount type is imported from a library but it works just fine. token::mint token::authority et al values are assigned as shown. this fully initializes the accounts and no cpi calls to the token program need be made by hand

#[account(mut, address = pool.pool_token)]
pub pool_token: Account<'info, TokenAccount>,
#[account(mut, constraint =  user_token.mint == pool.token_mint)]
pub user_token: Account<'info, TokenAccount>,

in a different instruction from the same program, we can see address constraints, which are a more explicit and backwards has_one, and raw constraint which evaluates any boolean expression. constraint can also return custom errors now by appending @ and the error

you can see all valid account annotations in the anchor docs. i am putting that link there mostly because i find it by going to docs.rs and typing "account" and randomly clicking the dozens of results until i get the right one. so now i can just open this post and click my link instead

im not gonna cover cpi but its pretty simple and you can see it in the tutorial

* * *

words again

i dont really have much bad to say about anchor. if i had to come up with critiques tho itd be like...

anchor is still a very fast moving target. expect to constantly be porting your program to new versions. this isnt as bad as it once was but the api is still under active development

anchor adds a substantial compute cost, especially serializing all account data into rust types. when working on programs of sufficient complexity i worry about having to junk parts of my anchor code and fall back to unchecked accounts and raw buffer manipulation. like it makes me feel i should have just done it in solana and dealt with the complexity to have a coherent codebase, rather than a weird frankenstein of the two styles. (update: they have zero-copy wrappers which may very well solve my problems here!)

its not clear to me how safe anchor actually is. this is not a veiled accusation, this is a confession of ignorance. ive dabbled in langsec enough to be afraid of macro magic and automatic deserialization and the possibility of constraint annotations degrading to permissiveness etc. solana is a weird machine and this is weird machines on weird machines. i have nothing of substance to say here it just kinda makes my hairs stand up a lil sometimes

aside from that i guess my biggest concern about anchor is that it may have a ruby on rails effect. anchor, like rails, is a tool by domain experts primarily written to reduce boilerplate. it fundamentally exists to erase work that youve already done too many times and dont want to do again

but because it does this, that also makes it very easy for new people to come on without actually understanding what was erased. what i hope is that anchor is a way for more people to gradually get onboarded to solana, figure out the basics, and then dive deeper when theyre ready. what i dont hope is that they never peek under the sheets, living in the abstraction. because thats when everything seems to be going fine until suddenly one day it very much is not

on the one hand shitty broken rails apps gave us twitter and github! on the other hand imagine if every fail whale was potentially a multimillion dollar loss of user funds. web2 is forgiving because unless you have uptime guarantees nothing bad actually happens if you accidentally break your site. whereas in web3 its uhhh a little bit different!!

* * *

unrelated words

anyway! i hope this has been helpful. i really like anchor and think its one of the best things in the broader solana ecosystem

last time i promised to write about weird tricksy things i thought about solana but im not sure if or when im gonna do that one. i had some neat ideas of possible attacks, bizarre shit you could do with accounts, but... i think is kinda navel gazing unless i can use it in the wild. i dont want to just list off a bunch of hypothetical things that i havent tried or really thought through

soo ive been thinking of trying to vuln hunt public solana contracts to see ive id be any good at bug bounties or audits. id like to dig into the runtime code at some point, actually understand how the weird machine ticks. im also interested in cosmos again lately and might do a similar grand tour of that tech stack. and writing lots of program code, and thinking about trading, and kinda wanna write strategies and bots... too much, too much! but its all in good fun

home