/solana-poc-framework

A framework for creating PoC's for Solana Smart Contracts in a painless and intuitive way

Primary LanguageRust

Solana PoC Framework

DISCLAIMER: any illegal usage of this framework is heavily discouraged. Most projects on Solana offer a more than generous bug bounty. Also you don't want your kneecaps broken.

Usage

To get started, just add the following line to the dependencies section in your Cargo.toml:

[dependencies]
poc-framework = { git = "https://github.com/neodyme-labs/solana-poc-framework.git", branch = "1.16" }

This crate already re-exports every Solana dependency you should need.

If your program needs a specific Solana version, you can just specify the corresponding branch. Currently the framework supports the following minor versions:

  • 1.11
  • 1.13
  • 1.14
  • 1.16

What this framework is for

This framework was made for security researchers, to facilitate a fast and convenient development of Proof-of-Concepts for bugs in Solana smart contracts or even Solana core. The generic Environment interface allows for exploits to be developed locally, and then tested on Testnet or Devnet.

Feature overview

Utility

This framework offers many utility functions that proved very useful time and time again for the PoC's we developed for the smart contracts we audited at Neodyme.

The first thing you want to do in any PoC is setup logging. This is especially useful if you use a local environment, as it is the only way to figure out why a transaction could not be executed (if for example signers are missing):

setup_logging(LogLevel::DEBUG);

Afterwards you want to define what keys you will use. Keys should easily be identifiable when printing a transaction. This purpose gets fulfilled by the keypair(n: u8) function. The framework contains 256 pre-ground keys that start with Kxxx, where xxx is the 3-digit representation of the argument n. Note that the base58 charset does not contain 0, which is why we used o instead:

let authority = keypair(0);   // KoooVyhdpoRPA6gpn7xr3cmjqAvtpHcjcBX6JBKu1nf
let target    = keypair(1);   // Koo1BQTQYawwKVBg71J2sru7W51EJgfbyyHsTFCssRW
let mint      = keypair(2);   // Koo2SZ393psmp7ags3hMz59ciV3XWLj1GkPousNgTH1
let victim    = keypair(137); // K137jwH7CncXBTadHbLDsHNWUhuLDN4ddegJL2hmn6u

There is also a random_keypair function if you don't care about recognising a keypair.

Also very valuable for debugging purposes is the ability to print the result of a transaction in a neat way. For this the framework provides the trait PrintableTransaction, which it implements both for ConfirmedTransaction as well as EncodedConfirmedTransaction. This trait provides the function print, which can conveniently be chained to the end of any env.execute_transaction call:

env.execute_as_transaction(&[...], &[...]).print();

Environment

At the core if this framework is the Environment trait. This encapsulates the ability to execute transactions on some chain state, as well as the utility of having a payer that pays for all fee and rent expenditures.

There are currently two different implementations: the RemoteEnvironment which executes all transactions on a cluster, and the LocalEnvironment, which executes all transactions locally on an arbitrary chain state.

The Environment trait also provides many useful shortcuts for sending transactions, like interacting with spl-token accounts or even creating accounts with arbitrary content (but obviously with a fixed owner).

RemoteEnvironment

To construct a remote environment, you require an RpcClient. These can be conveniently constructed using devnet_client()/testnet_client()/localhost_client(). We do not condone using this framework on mainnet. Airdrops are also implemented, with the new_with_airdrop and airdrop functions.

let payer = read_keypair_file("big-fat-wallet.json").unwrap();
let client = devnet_client();
let mut env = RemoteEnvironment::new(client, payer);

LocalEnvironment

Constructing a local environment usually takes some effort, as one has to first clone the relevant chain state. The framework offers many different ways of doing this. From deploying a contract from a file to inserting an arbitrary account up to cloning accounts and even whole upgradable programs from a cluster:

let mut env = LocalEnvironment::builder()
    .add_account_with_lamports(authority, system_program::ID, sol_to_lamports(10.0))
    .add_token_mint(mint, Some(authority), 0, 1, None)
    .add_associated_token_account(authority, mint, 1337)
    .clone_upgradable_program_from_cluster(client, my_program::ID)
    .build();

Note however that it is possible to craft state that is not legal on the chain using this builder (for example accounts that belong to a program that contain state that the program itself would never write to it), leading to exploits that are only reproducible locally. Try to use transactions on the environment for as many things as possible to prevent these pitfalls.