diff --git a/.github/workflows/anchor.yml b/.github/workflows/anchor.yml index 829932cfd..24c1ab449 100644 --- a/.github/workflows/anchor.yml +++ b/.github/workflows/anchor.yml @@ -193,17 +193,18 @@ jobs: ~/.cargo/bin/avm ~/.avm # Pinned to match the anchor-lang/anchor-spl crate version the programs depend on. - key: anchor-toolchain-${{ runner.os }}-1.1.2 - - name: Install Anchor 1.1.2 + key: anchor-toolchain-${{ runner.os }}-2.0.0-rc.1 + - name: Install Anchor 2.0.0-rc.1 run: | export PATH="$HOME/.cargo/bin:$HOME/.avm/bin:$PATH" # `anchor --version` can emit more than one line; read the first line only so # a multi-line value never reaches downstream parsing. current="$(anchor --version 2>/dev/null | head -n1 | awk '{print $2}' || true)" - if [ "$current" != "1.1.2" ]; then - cargo install --git https://github.com/coral-xyz/anchor avm --force - avm install 1.1.2 - avm use 1.1.2 + if [ "$current" != "2.0.0-rc.1" ]; then + cargo install --git https://github.com/solana-foundation/anchor avm --force + # 2.0.0-rc.1 is a pre-release, so avm needs it named explicitly. + avm install 2.0.0-rc.1 + avm use 2.0.0-rc.1 fi echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" echo "$HOME/.avm/bin" >> "$GITHUB_PATH" diff --git a/CHANGELOG.md b/CHANGELOG.md index 07a6002c3..61e0318df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,90 @@ All notable changes to this repository are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [2026-08-16] - Every Anchor example on Anchor v2.0.0-rc.1 + +All 55 Anchor examples build and pass their tests on 2.0.0-rc.1 (304 tests), +and `cargo fmt --check` and `cargo clippy -- -D warnings` are clean. + +### Changed + +- The remaining 39 Anchor examples, all of `tokens/`, `finance/` and + `compression/`, now build against `anchor-lang` 2.0.0-rc.1, joining the + `basics/` examples ported below. `.github/workflows/anchor.yml` installs + 2.0.0-rc.1, since `anchor build` under a v2 CLI will not build v1 programs. +- `docs/anchor-v2-migration.md` collects every difference the port ran into, + ordered by how often it bites. The rules the compiler will not catch for you + are called out: borrows held across CPIs, `Box`'s missing `cpi_handle_mut` + forwarding, and hand-built read-only handles over a live data account. +- `has_one` is deprecated in v2 and this repository's `rust.yml` runs + `cargo clippy -- -D warnings`, so every one of the 161 uses across 66 files + in the Anchor programs moves to the `address` constraint on the sibling field + it named. The Quasar crates keep `has_one`, which is still current there. +- The seven `transfer-hook` examples supply their own entrypoint. v2's + `#[program(interface, ...)]` generates a CPI client and no dispatch, so the + program declared that way builds to a ~900-byte object with no `entrypoint` + crate has no entrypoint symbol, while an executable `#[program]` limits + byte, which the transfer-hook interface's eight-byte values cannot use. Each + crate now builds with `no-entrypoint`, so anchor exports its dispatch as + `__anchor_dispatch`, and `src/entrypoint.rs` maps the interface + discriminators onto handlers before delegating. +- `tokens/pda-mint-authority` and `tokens/token-extensions/cpi-guard` build + their PDA by hand (`create_account` plus `initialize_mint2` / + `initialize_account3`). Both examples exist to show an account that is its own + authority, and a v2 SPL `init` constraint cannot name the account being + initialized. +- `finance/order-book` keeps its ~180 KB zero-copy critbit book zero-copy: v2's + `Account` derefs straight to `T`, so `load_init` / `load_mut` simply go + away rather than the state converting to borsh. +- Tests that asserted on an Anchor error *name* now assert on the numeric custom + code (the `#[error_code]` discriminant plus the default 6000 offset). v2 does + not log variant names, so the old assertions could never match. + +### Removed + +- `tokens/token-extensions/nft-meta-data-pointer` no longer depends on + `session-keys`. That crate is Anchor v1 only: its `Session` derive requires + `Option>`, and `SessionToken` is not `Pod`, so + v2's zero-copy `Account` cannot hold it either. The program reads the + session-token account layout itself (`src/session.rs`), checking owner, + discriminator and PDA, and spells out the `#[session_auth_or]` fallback in the + handler, so the gasless-session lesson and its security warning both survive. + +## [2026-08-13] - Anchor examples in `basics/` move to Anchor v2.0.0-rc.1 + +### Changed + +- Every Anchor example under `basics/` now builds against `anchor-lang` + 2.0.0-rc.1. v2 is a ground-up rewrite rather than a version bump: the crate is + `no_std` and built on pinocchio, so handlers take `&mut Context`, the + `<'info>` lifetime disappears from `#[derive(Accounts)]` structs and account + wrappers, `Pubkey` becomes `Address`, `.to_account_info()` becomes + `.cpi_handle_mut()` / `.cpi_handle()`, and `.key()` becomes `.address()`. +- `#[account]` is now zero-copy and requires a `Pod` layout. State holding + `String` or `Vec` moves to `#[account(borsh)]` plus `BorshAccount` + (`account-data`, `close-account`, `favorites`, `realloc`, `pyth`); state that + is already fixed-layout keeps the zero-copy default but must carry explicit + padding (`program-derived-addresses`) and cannot use `bool` + (`cross-program-invocation` uses `PodBool`). +- Instruction data is wincode-encoded rather than borsh. The `#[program]` macro + expands to `wincode` paths, so every program crate takes a direct `wincode` + dependency. `BorshConfig` keeps the wire format byte-identical to borsh, so + the checked-in account layouts and the tests that decode them with borsh are + unaffected. +- The only edit most LiteSVM tests needed: v2's `solana_program` compat shim has + no `system_program` submodule (the real module is at the crate root and + exposes `ID`, not `id()`) and no `pubkey::Pubkey` unless the `compat` feature + is on (`anchor_lang::Address` is the same 32-byte type). + +### Fixed + +- Anchor programs that put an `Address` in serialized state pin + `solana-address = ">=2.6, <2.7"`. anchor-lang 2.0.0-rc.1 is built against + wincode 0.5, but solana-address 2.7 moved to wincode 0.6; with both in the + graph, `Address`'s wincode impls belong to the version the `#[account(borsh)]` + derive is not using, and every `SchemaRead` / `SchemaWrite` bound fails. This + is the same class of split that the zeropod/quasar-lang pin below addresses. + ## [2026-08-04] - Oracle readers reject prices from before a cluster restart ### Added diff --git a/Cargo.lock b/Cargo.lock index 54e041455..5b8ff3334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,7 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -38,7 +39,7 @@ dependencies = [ "litesvm", "pinocchio 0.10.2", "pinocchio-log", - "pinocchio-system", + "pinocchio-system 0.5.0", "solana-keypair", "solana-message 4.0.0", "solana-native-token 3.0.0", @@ -196,182 +197,56 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "anchor-attribute-access-control" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc4b6b3c3f3e37a0b7d537e03a36bfb0415ee11b4443fa4190437cf26a874b6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "anchor-attribute-account" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e8b1468add67e6a69732883e58d23a18be51538994adaae69c9b3fe967e6be" -dependencies = [ - "anchor-syn", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "anchor-attribute-constant" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e1e3ade39ba05716ddc3b792859d838cccf43f5f4913ae8a163de7a2ed7f7e" -dependencies = [ - "anchor-syn", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "anchor-attribute-error" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e026f0ff09d740fd1821bbf97d9ac649c123ff92f04b9197b002494af4acf1" -dependencies = [ - "anchor-syn", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "anchor-attribute-event" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aafc8e26eb16a0c4129661f6857c42f301859fc2ee10f4e5287d571f0ea07f5" -dependencies = [ - "anchor-syn", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "anchor-attribute-program" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844e657ffe049b073389efb843bb281d08579b8a47ef294365f729b35ed3dca" -dependencies = [ - "anchor-lang-idl", - "anchor-syn", - "anyhow", - "heck", - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "anchor-derive-accounts" -version = "1.1.2" +version = "2.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c87a6769d7cf2deed8d3351d6d1d8aa05a3a07751f1418ab6b34aaa307bd34b" -dependencies = [ - "anchor-syn", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "anchor-derive-serde" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f014d99530ae48a81f710f150688c5c542fdeee3af8998010e147a64d7a697c" -dependencies = [ - "anchor-syn", - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "anchor-derive-space" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ba30c2d844e7440c491ad5f3e25f93115c6c9319df480eda460ce96030832b" +checksum = "46a0ce0bc596146ce0a73e2422e951d153cf88bf58af957c46a52e03d3048e4c" dependencies = [ + "anchor-lang-idl", + "bs58", + "curve25519-dalek", "proc-macro2", "quote", + "serde_json", + "sha2 0.10.9", + "solana-address 2.6.1", "syn 2.0.118", ] [[package]] name = "anchor-lang" -version = "1.1.2" +version = "2.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa6708f356398752b8d7a08fb701c9c17395e103786fb1b33615a7267a6f4774" +checksum = "163e7ec1855744f29f116b3fa4c469947b43f17008d086e558f9b9b047fbf47d" dependencies = [ - "anchor-attribute-access-control", - "anchor-attribute-account", - "anchor-attribute-constant", - "anchor-attribute-error", - "anchor-attribute-event", - "anchor-attribute-program", "anchor-derive-accounts", - "anchor-derive-serde", - "anchor-derive-space", - "anchor-lang-error", - "anchor-lang-idl", - "base64 0.21.7", - "bincode", - "borsh 1.7.0", "bytemuck", - "const-crypto", - "solana-account-info 3.1.1", - "solana-clock 3.1.1", - "solana-cpi 3.1.0", - "solana-define-syscall 3.0.0", - "solana-feature-gate-interface 3.1.0", + "pinocchio 0.11.2", + "pinocchio-system 0.6.1", + "sha2 0.10.9", + "solana-address 2.6.1", + "solana-define-syscall 5.1.0", "solana-instruction 3.2.0", - "solana-instructions-sysvar 3.0.0", - "solana-invoke", - "solana-loader-v3-interface 6.1.0", "solana-msg 3.1.0", - "solana-program-entrypoint 3.1.1", "solana-program-error 3.0.1", + "solana-program-log", "solana-program-memory 3.1.0", - "solana-program-option 3.1.0", - "solana-program-pack 3.1.0", - "solana-pubkey 3.0.0", - "solana-sdk-ids 3.1.0", - "solana-stake-interface 2.0.2", + "solana-sha256-hasher 3.1.0", "solana-system-interface 2.0.0", "solana-sysvar 3.1.1", - "solana-sysvar-id 3.1.0", - "thiserror 1.0.69", -] - -[[package]] -name = "anchor-lang-error" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20760a8d5b7ddd1aea7a347c43c702ea4e962ccb7b9908c952e4b372b2d9e1f7" -dependencies = [ - "anchor-attribute-error", - "borsh 1.7.0", - "solana-msg 3.1.0", - "solana-program-error 3.0.1", - "solana-pubkey 3.0.0", + "wincode 0.5.5", ] [[package]] name = "anchor-lang-idl" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47914b4290ae2bdf4ec203aa821e6eba86d7c78ef497918938038dcc6919f953" +checksum = "62076a3c298f5447488e31c0198675b2fcbada1f71381af7104333cf6ff2581d" dependencies = [ "anchor-lang-idl-spec", "anyhow", "heck", - "regex", "serde", "serde_json", "sha2 0.10.9", @@ -397,16 +272,25 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] name = "anchor-spl" -version = "1.1.2" +version = "2.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f698a45d424351ff695df413adcb3fa603f2c791a0b4a937ebcd2e93a052c77" +checksum = "29717fb1be787b9d71b085d960c217e009787dabd16b5d0fc31118f9a6534793" dependencies = [ "anchor-lang", - "spl-associated-token-account-interface", + "borsh 1.7.0", + "bytemuck", + "pinocchio 0.11.2", + "pinocchio-token", + "pinocchio-token-2022", + "solana-address 2.6.1", + "solana-instruction 3.2.0", + "solana-program-error 3.0.1", + "solana-pubkey 3.0.0", "spl-pod", "spl-token-2022-interface", "spl-token-group-interface", @@ -414,24 +298,6 @@ dependencies = [ "spl-token-metadata-interface", ] -[[package]] -name = "anchor-syn" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f6c61ef5b47db60700087bb6e419284b3461ba8f59040e1f6827aa9e9a5bc4" -dependencies = [ - "anyhow", - "bs58", - "cargo_toml", - "heck", - "proc-macro2", - "quote", - "serde", - "sha2 0.11.0", - "syn 2.0.118", - "thiserror 1.0.69", -] - [[package]] name = "ansi_term" version = "0.12.1" @@ -738,12 +604,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -1020,16 +880,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" -[[package]] -name = "cargo_toml" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" -dependencies = [ - "serde", - "toml 0.9.12+spec-1.1.0", -] - [[package]] name = "carnival" version = "0.1.0" @@ -1039,6 +889,7 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -1098,6 +949,7 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -1121,7 +973,7 @@ dependencies = [ "litesvm", "pinocchio 0.10.2", "pinocchio-log", - "pinocchio-system", + "pinocchio-system 0.5.0", "solana-instruction 3.2.0", "solana-keypair", "solana-native-token 3.0.0", @@ -1164,7 +1016,7 @@ dependencies = [ "pinocchio 0.10.2", "pinocchio-log", "pinocchio-pubkey", - "pinocchio-system", + "pinocchio-system 0.5.0", "solana-instruction 3.2.0", "solana-keypair", "solana-message 4.0.0", @@ -1181,9 +1033,11 @@ version = "0.1.0" dependencies = [ "anchor-lang", "litesvm", + "solana-address 2.6.1", "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -1225,28 +1079,12 @@ dependencies = [ "web-sys", ] -[[package]] -name = "const-crypto" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c06f1eb05f06cf2e380fdded278fbf056a38974299d77960555a311dcf91a52" -dependencies = [ - "keccak-const", - "sha2-const-stable", -] - [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - [[package]] name = "constant_time_eq" version = "0.4.2" @@ -1297,6 +1135,7 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -1338,7 +1177,7 @@ dependencies = [ "pinocchio 0.10.2", "pinocchio-log", "pinocchio-pubkey", - "pinocchio-system", + "pinocchio-system 0.5.0", "solana-instruction 3.2.0", "solana-keypair", "solana-native-token 3.0.0", @@ -1370,6 +1209,7 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -1405,7 +1245,7 @@ dependencies = [ "litesvm", "pinocchio 0.10.2", "pinocchio-log", - "pinocchio-system", + "pinocchio-system 0.5.0", "solana-instruction 3.2.0", "solana-keypair", "solana-native-token 3.0.0", @@ -1539,7 +1379,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid 0.9.6", + "const-oid", "zeroize", ] @@ -1576,7 +1416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.6", + "const-oid", "crypto-common 0.1.7", "subtle", ] @@ -1588,7 +1428,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", ] @@ -1745,7 +1584,7 @@ dependencies = [ "pinocchio 0.10.2", "pinocchio-log", "pinocchio-pubkey", - "pinocchio-system", + "pinocchio-system 0.5.0", "solana-instruction 3.2.0", "solana-keypair", "solana-native-token 3.0.0", @@ -1942,6 +1781,7 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -1995,6 +1835,7 @@ dependencies = [ "solana-kite", "solana-signer", "solana-transaction", + "wincode 0.5.5", ] [[package]] @@ -2162,12 +2003,6 @@ dependencies = [ "cpufeatures 0.2.17", ] -[[package]] -name = "keccak-const" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" - [[package]] name = "lazy_static" version = "1.5.0" @@ -2183,6 +2018,7 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -2377,9 +2213,11 @@ dependencies = [ "anchor-spl", "borsh 1.7.0", "litesvm", + "solana-address 2.6.1", "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -2649,6 +2487,7 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -2659,7 +2498,7 @@ dependencies = [ "pinocchio 0.10.2", "pinocchio-log", "pinocchio-pubkey", - "pinocchio-system", + "pinocchio-system 0.5.0", "solana-instruction 3.2.0", "solana-keypair", "solana-native-token 3.0.0", @@ -2717,10 +2556,23 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06810dac15a4ef83d3dabdb4f2f22fb39c9adff669cd2781da4f716510a647c" dependencies = [ - "solana-account-view", + "solana-account-view 1.0.0", "solana-address 2.6.1", "solana-define-syscall 4.0.1", - "solana-instruction-view", + "solana-instruction-view 1.0.0", + "solana-program-error 3.0.1", +] + +[[package]] +name = "pinocchio" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cababcb62a2e739c7078ae16eda02789a6e3edd21bbdded864e85c38a7914d" +dependencies = [ + "solana-account-view 2.0.0", + "solana-address 2.6.1", + "solana-define-syscall 5.1.0", + "solana-instruction-view 2.1.0", "solana-program-error 3.0.1", ] @@ -2765,6 +2617,40 @@ dependencies = [ "solana-address 2.6.1", ] +[[package]] +name = "pinocchio-system" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ae2ee67b42e2ac797dd88312da473c895660e270d8093e0ba2749b885b5778" +dependencies = [ + "pinocchio 0.11.2", + "solana-address 2.6.1", +] + +[[package]] +name = "pinocchio-token" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825f59c8348e5c2d3fd56432ef927f5819542b3b05fae4f5b6869801113e775e" +dependencies = [ + "solana-account-view 2.0.0", + "solana-address 2.6.1", + "solana-instruction-view 2.1.0", + "solana-program-error 3.0.1", +] + +[[package]] +name = "pinocchio-token-2022" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57dbab5718ab6ae446b2217c47598fabd29173a09bd32b05344f031739db92b8" +dependencies = [ + "solana-account-view 2.0.0", + "solana-address 2.6.1", + "solana-instruction-view 2.1.0", + "solana-program-error 3.0.1", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -2802,7 +2688,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "toml 0.5.11", + "toml", ] [[package]] @@ -2832,6 +2718,7 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -2903,7 +2790,7 @@ version = "0.1.0" dependencies = [ "litesvm", "pinocchio 0.10.2", - "pinocchio-system", + "pinocchio-system 0.5.0", "solana-instruction 3.2.0", "solana-keypair", "solana-native-token 3.0.0", @@ -2923,6 +2810,7 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -3082,7 +2970,7 @@ dependencies = [ "litesvm", "pinocchio 0.10.2", "pinocchio-log", - "pinocchio-system", + "pinocchio-system 0.5.0", "solana-instruction 3.2.0", "solana-keypair", "solana-native-token 3.0.0", @@ -3150,11 +3038,11 @@ name = "rent-example" version = "0.1.0" dependencies = [ "anchor-lang", - "borsh 1.7.0", "litesvm", "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -3164,7 +3052,7 @@ dependencies = [ "litesvm", "pinocchio 0.10.2", "pinocchio-log", - "pinocchio-system", + "pinocchio-system 0.5.0", "solana-instruction 3.2.0", "solana-keypair", "solana-native-token 3.0.0", @@ -3322,15 +3210,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_with" version = "3.21.0" @@ -3377,17 +3256,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", -] - [[package]] name = "sha2-const-stable" version = "0.1.0" @@ -3499,6 +3367,16 @@ dependencies = [ "solana-program-error 3.0.1", ] +[[package]] +name = "solana-account-view" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc141b940560430425ebaadb7645496c45f6a10fad9911d719bd03eab7f4d422" +dependencies = [ + "solana-address 2.6.1", + "solana-program-error 3.0.1", +] + [[package]] name = "solana-address" version = "1.1.0" @@ -4257,12 +4135,24 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60147e4d0a4620013df40bf30a86dd299203ff12fcb8b593cd51014fce0875d8" dependencies = [ - "solana-account-view", + "solana-account-view 1.0.0", "solana-address 2.6.1", "solana-define-syscall 4.0.1", "solana-program-error 3.0.1", ] +[[package]] +name = "solana-instruction-view" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab7a27d0c4214b9f7389c3dd00b68c93093a67f1dcc5b7893aebe299bbcbb47" +dependencies = [ + "solana-account-view 2.0.0", + "solana-address 2.6.1", + "solana-define-syscall 5.1.0", + "solana-program-error 3.0.1", +] + [[package]] name = "solana-instructions-sysvar" version = "2.2.2" @@ -4298,19 +4188,6 @@ dependencies = [ "solana-sysvar-id 3.1.0", ] -[[package]] -name = "solana-invoke" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4065031f5c7dd29ef5f5003c1a353011eeabbafa6c5a5033da0cedbfca824b94" -dependencies = [ - "solana-account-info 3.1.1", - "solana-define-syscall 3.0.0", - "solana-instruction 3.2.0", - "solana-program-entrypoint 3.1.1", - "solana-stable-layout 3.0.1", -] - [[package]] name = "solana-keccak-hasher" version = "2.2.1" @@ -4437,7 +4314,6 @@ dependencies = [ "solana-instruction 3.2.0", "solana-pubkey 3.0.0", "solana-sdk-ids 3.1.0", - "solana-system-interface 2.0.0", ] [[package]] @@ -4880,6 +4756,27 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "solana-program-log" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd48ddef444e6db138fb11bdb9ab8117bb830a78cef051a453454de5613039b" +dependencies = [ + "solana-define-syscall 5.1.0", + "solana-program-log-macro", +] + +[[package]] +name = "solana-program-log-macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b234f83b71ba6b5d3236b69ba42ccb639907cb724bf58e94ce8398b51ac87c" +dependencies = [ + "quote", + "regex", + "syn 2.0.118", +] + [[package]] name = "solana-program-memory" version = "2.3.1" @@ -6391,30 +6288,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -6431,9 +6304,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap", - "toml_datetime 1.1.1+spec-1.1.0", + "toml_datetime", "toml_parser", - "winnow 1.0.3", + "winnow", ] [[package]] @@ -6442,15 +6315,9 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.3", + "winnow", ] -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - [[package]] name = "transfer-sol" version = "0.1.0" @@ -6460,6 +6327,7 @@ dependencies = [ "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -6468,7 +6336,7 @@ version = "0.1.0" dependencies = [ "litesvm", "pinocchio 0.10.2", - "pinocchio-system", + "pinocchio-system 0.5.0", "solana-instruction 3.2.0", "solana-keypair", "solana-native-token 3.0.0", @@ -6570,9 +6438,12 @@ dependencies = [ "litesvm", "mock-swap-router", "solana-account 3.4.0", + "solana-address 2.6.1", + "solana-clock 3.1.1", "solana-keypair", "solana-kite", "solana-signer", + "wincode 0.5.5", ] [[package]] @@ -6729,12 +6600,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - [[package]] name = "winnow" version = "1.0.3" diff --git a/README.md b/README.md index a5ce485d1..f25073ec6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ _Solana program examples ('smart contracts') in Anchor, Quasar, Pinocchio, native Rust, and sBPF assembly. Focused on financial software ('DeFi'), plus the basics, tokens, Token Extensions, state compression, and more._ -Working, tested, up-to-date examples of common Solana programs (what other chains call smart contracts), maintained by [Quicknode](https://www.quicknode.com/chains/solana). Current as of July 2026 (see [CHANGELOG.md](./CHANGELOG.md)): every example builds and passes CI on **Anchor 1.1**, the current multi-file program layout (one file per instruction handler, account type, etc), and [LiteSVM](https://github.com/LiteSVM/litesvm) tests rather than the older `solana-test-validator` / web3.js stack. +Working, tested, up-to-date examples of common Solana programs (what other chains call smart contracts), maintained by [Quicknode](https://www.quicknode.com/chains/solana). Current as of August 2026 (see [CHANGELOG.md](./CHANGELOG.md)): every example builds and passes CI on **Anchor 2.0.0-rc.1**, the current multi-file program layout (one file per instruction handler, account type, etc), and [LiteSVM](https://github.com/LiteSVM/litesvm) tests rather than the older `solana-test-validator` / web3.js stack. [![Anchor](../../actions/workflows/anchor.yml/badge.svg)](../../actions/workflows/anchor.yml) [![Quasar](../../actions/workflows/quasar.yml/badge.svg)](../../actions/workflows/quasar.yml) [![Pinocchio](../../actions/workflows/pinocchio.yml/badge.svg)](../../actions/workflows/pinocchio.yml) [![Native](../../actions/workflows/native.yml/badge.svg)](../../actions/workflows/native.yml) [![ASM](../../actions/workflows/solana-asm.yml/badge.svg)](../../actions/workflows/solana-asm.yml) diff --git a/basics/account-data/anchor/programs/anchor-program-example/Cargo.toml b/basics/account-data/anchor/programs/anchor-program-example/Cargo.toml index 1ca762589..a533e1862 100644 --- a/basics/account-data/anchor/programs/anchor-program-example/Cargo.toml +++ b/basics/account-data/anchor/programs/anchor-program-example/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/account-data/anchor/programs/anchor-program-example/src/instructions/create.rs b/basics/account-data/anchor/programs/anchor-program-example/src/instructions/create.rs index 535efc853..0a3cd20c8 100644 --- a/basics/account-data/anchor/programs/anchor-program-example/src/instructions/create.rs +++ b/basics/account-data/anchor/programs/anchor-program-example/src/instructions/create.rs @@ -2,21 +2,21 @@ use crate::state::AddressInfo; use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct CreateAddressInfoAccountConstraints<'info> { +pub struct CreateAddressInfoAccountConstraints { #[account(mut)] - payer: Signer<'info>, + pub payer: Signer, #[account( init, payer = payer, space = AddressInfo::DISCRIMINATOR.len() + AddressInfo::INIT_SPACE, )] - address_info: Account<'info, AddressInfo>, - system_program: Program<'info, System>, + pub address_info: BorshAccount, + pub system_program: Program, } pub fn handle_create_address_info( - context: Context, + context: &mut Context, name: String, house_number: u8, street: String, diff --git a/basics/account-data/anchor/programs/anchor-program-example/src/lib.rs b/basics/account-data/anchor/programs/anchor-program-example/src/lib.rs index f5fd99dad..765a584cb 100644 --- a/basics/account-data/anchor/programs/anchor-program-example/src/lib.rs +++ b/basics/account-data/anchor/programs/anchor-program-example/src/lib.rs @@ -11,7 +11,7 @@ pub mod account_data_anchor_program { use super::*; pub fn create_address_info( - context: Context, + context: &mut Context, name: String, house_number: u8, street: String, diff --git a/basics/account-data/anchor/programs/anchor-program-example/src/state/address_info.rs b/basics/account-data/anchor/programs/anchor-program-example/src/state/address_info.rs index 1142f354d..ffdfff9c4 100644 --- a/basics/account-data/anchor/programs/anchor-program-example/src/state/address_info.rs +++ b/basics/account-data/anchor/programs/anchor-program-example/src/state/address_info.rs @@ -1,6 +1,8 @@ use anchor_lang::prelude::*; -#[account] +// `borsh` because the struct holds `String`s: v2's default `#[account]` backing +// is zero-copy and needs a `Pod` (fixed-layout) type. +#[account(borsh)] #[derive(InitSpace)] // automatically calculate the space required for the struct pub struct AddressInfo { #[max_len(50)] // set a max length for the string diff --git a/basics/account-data/anchor/programs/anchor-program-example/tests/test_account_data.rs b/basics/account-data/anchor/programs/anchor-program-example/tests/test_account_data.rs index 567a816a5..910fb4488 100644 --- a/basics/account-data/anchor/programs/anchor-program-example/tests/test_account_data.rs +++ b/basics/account-data/anchor/programs/anchor-program-example/tests/test_account_data.rs @@ -1,7 +1,6 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, system_program}, - AnchorSerialize, InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, InstructionData, ToAccountMetas, }, borsh::BorshDeserialize, litesvm::LiteSVM, @@ -42,7 +41,7 @@ fn test_create_address_info() { account_data_anchor_program::accounts::CreateAddressInfoAccountConstraints { payer: payer.pubkey(), address_info: address_info_keypair.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/basics/checking-accounts/anchor/programs/anchor-program-example/Cargo.toml b/basics/checking-accounts/anchor/programs/anchor-program-example/Cargo.toml index 30bbe293a..d598743ff 100644 --- a/basics/checking-accounts/anchor/programs/anchor-program-example/Cargo.toml +++ b/basics/checking-accounts/anchor/programs/anchor-program-example/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/checking-accounts/anchor/programs/anchor-program-example/src/lib.rs b/basics/checking-accounts/anchor/programs/anchor-program-example/src/lib.rs index 5cd90a851..2998c5eb9 100644 --- a/basics/checking-accounts/anchor/programs/anchor-program-example/src/lib.rs +++ b/basics/checking-accounts/anchor/programs/anchor-program-example/src/lib.rs @@ -6,7 +6,9 @@ declare_id!("ECWPhR3rJbaPfyNFgphnjxSEexbTArc7vxD8fnW6tgKw"); pub mod checking_account_program { use super::*; - pub fn check_accounts(_context: Context) -> Result<()> { + pub fn check_accounts( + _context: &mut Context, + ) -> Result<()> { Ok(()) } } @@ -14,17 +16,17 @@ pub mod checking_account_program { // Account validation in Anchor is done using the types and constraints specified in the #[derive(Accounts)] structs // This is a simple example and does not include all possible constraints and types #[derive(Accounts)] -pub struct CheckingAccountsAccountConstraints<'info> { - payer: Signer<'info>, // checks account is signer +pub struct CheckingAccountsAccountConstraints { + pub payer: Signer, // checks account is signer /// CHECK: No checks performed, example of an unchecked account #[account(mut)] - account_to_create: UncheckedAccount<'info>, + pub account_to_create: UncheckedAccount, /// CHECK: Perform owner check using constraint #[account( mut, owner = id() )] - account_to_change: UncheckedAccount<'info>, - system_program: Program<'info, System>, // checks account is executable, and is the system program + pub account_to_change: UncheckedAccount, + pub system_program: Program, // checks account is executable, and is the system program } diff --git a/basics/checking-accounts/anchor/programs/anchor-program-example/tests/test_checking_accounts.rs b/basics/checking-accounts/anchor/programs/anchor-program-example/tests/test_checking_accounts.rs index 15cb44851..88118fbc0 100644 --- a/basics/checking-accounts/anchor/programs/anchor-program-example/tests/test_checking_accounts.rs +++ b/basics/checking-accounts/anchor/programs/anchor-program-example/tests/test_checking_accounts.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, system_instruction, system_program}, - InstructionData, ToAccountMetas, + solana_program::{instruction::Instruction, system_instruction}, + system_program, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, @@ -47,7 +47,7 @@ fn test_check_accounts() { payer: payer.pubkey(), account_to_create: account_to_create.pubkey(), account_to_change: account_to_change.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/basics/close-account/anchor/programs/close-account/Cargo.toml b/basics/close-account/anchor/programs/close-account/Cargo.toml index fe78b018f..f1a78ee1f 100644 --- a/basics/close-account/anchor/programs/close-account/Cargo.toml +++ b/basics/close-account/anchor/programs/close-account/Cargo.toml @@ -20,7 +20,16 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. Both end up in the graph, and `Address`'s wincode impls +# then belong to the version the `#[account(borsh)]` derive is not using, so an +# `Address` field fails to satisfy `SchemaRead`/`SchemaWrite`. 2.6.1 is the last +# release on the 0.5 line. +solana-address = ">=2.6, <2.7" [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/close-account/anchor/programs/close-account/src/instructions/close_user.rs b/basics/close-account/anchor/programs/close-account/src/instructions/close_user.rs index 6606fe22a..b43bcd379 100644 --- a/basics/close-account/anchor/programs/close-account/src/instructions/close_user.rs +++ b/basics/close-account/anchor/programs/close-account/src/instructions/close_user.rs @@ -2,22 +2,22 @@ use crate::state::*; use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct CloseUserAccountConstraints<'info> { +pub struct CloseUserAccountConstraints { #[account(mut)] - pub user: Signer<'info>, + pub user: Signer, #[account( mut, seeds = [ b"USER", - user.key().as_ref(), + user.address().as_ref(), ], bump = user_account.bump, close = user, // close account and return lamports to user )] - pub user_account: Account<'info, User>, + pub user_account: BorshAccount, } -pub fn handle_close_user(_context: Context) -> Result<()> { +pub fn handle_close_user(_context: &mut Context) -> Result<()> { Ok(()) } diff --git a/basics/close-account/anchor/programs/close-account/src/instructions/create_user.rs b/basics/close-account/anchor/programs/close-account/src/instructions/create_user.rs index a64ebd79e..e8ac87e2e 100644 --- a/basics/close-account/anchor/programs/close-account/src/instructions/create_user.rs +++ b/basics/close-account/anchor/programs/close-account/src/instructions/create_user.rs @@ -2,9 +2,9 @@ use crate::state::*; use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct CreateUserAccountConstraints<'info> { +pub struct CreateUserAccountConstraints { #[account(mut)] - pub user: Signer<'info>, + pub user: Signer, #[account( init, @@ -12,21 +12,21 @@ pub struct CreateUserAccountConstraints<'info> { space = User::DISCRIMINATOR.len() + User::INIT_SPACE, seeds = [ b"USER", - user.key().as_ref(), + user.address().as_ref(), ], bump )] - pub user_account: Account<'info, User>, - pub system_program: Program<'info, System>, + pub user_account: BorshAccount, + pub system_program: Program, } pub fn handle_create_user( - context: Context, + context: &mut Context, name: String, ) -> Result<()> { *context.accounts.user_account = User { bump: context.bumps.user_account, - user: context.accounts.user.key(), + user: *context.accounts.user.address(), name, }; Ok(()) diff --git a/basics/close-account/anchor/programs/close-account/src/lib.rs b/basics/close-account/anchor/programs/close-account/src/lib.rs index 0819ae853..f8f0cdc1b 100644 --- a/basics/close-account/anchor/programs/close-account/src/lib.rs +++ b/basics/close-account/anchor/programs/close-account/src/lib.rs @@ -9,11 +9,14 @@ declare_id!("99TQtoDdQ5NS2v5Ppha93aqEmv3vV9VZVfHTP5rGST3c"); pub mod close_account_program { use super::*; - pub fn create_user(context: Context, name: String) -> Result<()> { + pub fn create_user( + context: &mut Context, + name: String, + ) -> Result<()> { create_user::handle_create_user(context, name) } - pub fn close_user(context: Context) -> Result<()> { + pub fn close_user(context: &mut Context) -> Result<()> { close_user::handle_close_user(context) } } diff --git a/basics/close-account/anchor/programs/close-account/src/state/user.rs b/basics/close-account/anchor/programs/close-account/src/state/user.rs index 6548d53dc..4adfbba2e 100644 --- a/basics/close-account/anchor/programs/close-account/src/state/user.rs +++ b/basics/close-account/anchor/programs/close-account/src/state/user.rs @@ -1,10 +1,12 @@ use anchor_lang::prelude::*; -#[account] +// `borsh` because the struct holds a `String`: v2's default `#[account]` +// backing is zero-copy and needs a `Pod` (fixed-layout) type. +#[account(borsh)] #[derive(InitSpace)] // automatically calculate the space required for the struct pub struct User { - pub bump: u8, // 1 byte - pub user: Pubkey, // 32 bytes + pub bump: u8, // 1 byte + pub user: Address, // 32 bytes #[max_len(50)] // set a max length for the string pub name: String, // 4 bytes + 50 bytes } diff --git a/basics/close-account/anchor/programs/close-account/tests/test_close_account.rs b/basics/close-account/anchor/programs/close-account/tests/test_close_account.rs index f7189df61..c12b11239 100644 --- a/basics/close-account/anchor/programs/close-account/tests/test_close_account.rs +++ b/basics/close-account/anchor/programs/close-account/tests/test_close_account.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, @@ -25,7 +25,7 @@ fn test_create_and_close_user() { // Derive the PDA for the user's account let (user_account_pda, _bump) = - Pubkey::find_program_address(&[b"USER", payer.pubkey().as_ref()], &program_id); + Address::find_program_address(&[b"USER", payer.pubkey().as_ref()], &program_id); // Create user let create_ix = Instruction::new_with_bytes( @@ -37,7 +37,7 @@ fn test_create_and_close_user() { close_account_program::accounts::CreateUserAccountConstraints { user: payer.pubkey(), user_account: user_account_pda, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/basics/counter/anchor/programs/counter_anchor/Cargo.toml b/basics/counter/anchor/programs/counter_anchor/Cargo.toml index 3bf40b435..b6e306811 100644 --- a/basics/counter/anchor/programs/counter_anchor/Cargo.toml +++ b/basics/counter/anchor/programs/counter_anchor/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/counter/anchor/programs/counter_anchor/src/instructions/increment.rs b/basics/counter/anchor/programs/counter_anchor/src/instructions/increment.rs index fa2a65946..8ed24a48d 100644 --- a/basics/counter/anchor/programs/counter_anchor/src/instructions/increment.rs +++ b/basics/counter/anchor/programs/counter_anchor/src/instructions/increment.rs @@ -3,12 +3,12 @@ use anchor_lang::prelude::*; use crate::{Counter, CounterError}; #[derive(Accounts)] -pub struct IncrementAccountConstraints<'info> { +pub struct IncrementAccountConstraints { #[account(mut)] - pub counter: Account<'info, Counter>, + pub counter: Account, } -pub fn handler(context: Context) -> Result<()> { +pub fn handler(context: &mut Context) -> Result<()> { context.accounts.counter.count = context .accounts .counter diff --git a/basics/counter/anchor/programs/counter_anchor/src/instructions/initialize_counter.rs b/basics/counter/anchor/programs/counter_anchor/src/instructions/initialize_counter.rs index fa17cfd55..43afcd257 100644 --- a/basics/counter/anchor/programs/counter_anchor/src/instructions/initialize_counter.rs +++ b/basics/counter/anchor/programs/counter_anchor/src/instructions/initialize_counter.rs @@ -3,19 +3,19 @@ use anchor_lang::prelude::*; use crate::Counter; #[derive(Accounts)] -pub struct InitializeCounterAccountConstraints<'info> { +pub struct InitializeCounterAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account( init, space = Counter::DISCRIMINATOR.len() + Counter::INIT_SPACE, payer = payer )] - pub counter: Account<'info, Counter>, - pub system_program: Program<'info, System>, + pub counter: Account, + pub system_program: Program, } -pub fn handler(_context: Context) -> Result<()> { +pub fn handler(_context: &mut Context) -> Result<()> { Ok(()) } diff --git a/basics/counter/anchor/programs/counter_anchor/src/lib.rs b/basics/counter/anchor/programs/counter_anchor/src/lib.rs index d6a48dfcd..2c44bbb34 100644 --- a/basics/counter/anchor/programs/counter_anchor/src/lib.rs +++ b/basics/counter/anchor/programs/counter_anchor/src/lib.rs @@ -9,11 +9,13 @@ declare_id!("BmDHboaj1kBUoinJKKSRqKfMeRKJqQqEbUj1VgzeQe4A"); pub mod counter_anchor { use super::*; - pub fn initialize_counter(context: Context) -> Result<()> { + pub fn initialize_counter( + context: &mut Context, + ) -> Result<()> { instructions::initialize_counter::handler(context) } - pub fn increment(context: Context) -> Result<()> { + pub fn increment(context: &mut Context) -> Result<()> { instructions::increment::handler(context) } } diff --git a/basics/counter/anchor/programs/counter_anchor/tests/test_counter.rs b/basics/counter/anchor/programs/counter_anchor/tests/test_counter.rs index 97a3d3c03..755fa0d48 100644 --- a/basics/counter/anchor/programs/counter_anchor/tests/test_counter.rs +++ b/basics/counter/anchor/programs/counter_anchor/tests/test_counter.rs @@ -1,7 +1,6 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, InstructionData, ToAccountMetas, }, borsh::BorshDeserialize, litesvm::LiteSVM, @@ -17,7 +16,7 @@ struct CounterAccount { count: u64, } -fn setup() -> (LiteSVM, anchor_lang::prelude::Pubkey, Keypair) { +fn setup() -> (LiteSVM, anchor_lang::prelude::Address, Keypair) { let program_id = counter_anchor::id(); let mut svm = LiteSVM::new(); let bytes = include_bytes!("../../../target/deploy/counter_anchor.so"); @@ -26,7 +25,7 @@ fn setup() -> (LiteSVM, anchor_lang::prelude::Pubkey, Keypair) { (svm, program_id, payer) } -fn fetch_counter(svm: &LiteSVM, counter_pubkey: &anchor_lang::prelude::Pubkey) -> u64 { +fn fetch_counter(svm: &LiteSVM, counter_pubkey: &anchor_lang::prelude::Address) -> u64 { let account = svm.get_account(counter_pubkey).unwrap(); let counter = CounterAccount::try_from_slice(&account.data).unwrap(); counter.count @@ -43,7 +42,7 @@ fn test_initialize_counter() { counter_anchor::accounts::InitializeCounterAccountConstraints { payer: payer.pubkey(), counter: counter_keypair.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -72,7 +71,7 @@ fn test_increment_counter() { counter_anchor::accounts::InitializeCounterAccountConstraints { payer: payer.pubkey(), counter: counter_keypair.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -111,7 +110,7 @@ fn test_increment_counter_again() { counter_anchor::accounts::InitializeCounterAccountConstraints { payer: payer.pubkey(), counter: counter_keypair.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/basics/create-account/anchor/programs/create-system-account/Cargo.toml b/basics/create-account/anchor/programs/create-system-account/Cargo.toml index 3dd5dabd7..a4b4b1636 100644 --- a/basics/create-account/anchor/programs/create-system-account/Cargo.toml +++ b/basics/create-account/anchor/programs/create-system-account/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/create-account/anchor/programs/create-system-account/src/lib.rs b/basics/create-account/anchor/programs/create-system-account/src/lib.rs index 8898ba274..bfe079fc9 100644 --- a/basics/create-account/anchor/programs/create-system-account/src/lib.rs +++ b/basics/create-account/anchor/programs/create-system-account/src/lib.rs @@ -8,28 +8,28 @@ pub mod create_system_account { use super::*; pub fn create_system_account( - context: Context, + context: &mut Context, ) -> Result<()> { msg!("Program invoked. Creating a system account..."); msg!( " New public key will be: {}", - &context.accounts.new_account.key().to_string() + context.accounts.new_account.address() ); // The minimum lamports for rent exemption - let lamports = (Rent::get()?).minimum_balance(0); + let lamports = Rent::get()?.try_minimum_balance(0)?; create_account( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), CreateAccount { - from: context.accounts.payer.to_account_info(), // From pubkey - to: context.accounts.new_account.to_account_info(), // To pubkey + from: context.accounts.payer.cpi_handle_mut(), // From pubkey + to: context.accounts.new_account.cpi_handle_mut(), // To pubkey }, ), - lamports, // Lamports - 0, // Space - &context.accounts.system_program.key(), // Owner Program + lamports, // Lamports + 0, // Space + context.accounts.system_program.address(), // Owner Program )?; msg!("Account created successfully."); @@ -38,10 +38,10 @@ pub mod create_system_account { } #[derive(Accounts)] -pub struct CreateSystemAccountAccountConstraints<'info> { +pub struct CreateSystemAccountAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account(mut)] - pub new_account: Signer<'info>, - pub system_program: Program<'info, System>, + pub new_account: Signer, + pub system_program: Program, } diff --git a/basics/create-account/anchor/programs/create-system-account/tests/test_create_account.rs b/basics/create-account/anchor/programs/create-system-account/tests/test_create_account.rs index 70fe47ec5..8264d06a6 100644 --- a/basics/create-account/anchor/programs/create-system-account/tests/test_create_account.rs +++ b/basics/create-account/anchor/programs/create-system-account/tests/test_create_account.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, rent::Rent, system_program}, - InstructionData, ToAccountMetas, + solana_program::{instruction::Instruction, rent::Rent}, + system_program, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, @@ -25,7 +25,7 @@ fn test_create_the_account() { create_system_account::accounts::CreateSystemAccountAccountConstraints { payer: payer.pubkey(), new_account: new_account.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/basics/cross-program-invocation/anchor/idls/lever.json b/basics/cross-program-invocation/anchor/idls/lever.json index 3ec862d79..a8ace89ce 100644 --- a/basics/cross-program-invocation/anchor/idls/lever.json +++ b/basics/cross-program-invocation/anchor/idls/lever.json @@ -9,7 +9,16 @@ "instructions": [ { "name": "initialize", - "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], + "discriminator": [ + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 + ], "accounts": [ { "name": "power", @@ -30,7 +39,16 @@ }, { "name": "switch_power", - "discriminator": [226, 238, 56, 172, 191, 45, 122, 87], + "discriminator": [ + 226, + 238, + 56, + 172, + 191, + 45, + 122, + 87 + ], "accounts": [ { "name": "power", @@ -48,7 +66,16 @@ "accounts": [ { "name": "PowerStatus", - "discriminator": [145, 147, 198, 35, 253, 101, 231, 26] + "discriminator": [ + 145, + 147, + 198, + 35, + 253, + 101, + 231, + 26 + ] } ], "types": [ diff --git a/basics/cross-program-invocation/anchor/programs/hand/Cargo.toml b/basics/cross-program-invocation/anchor/programs/hand/Cargo.toml index bfb4cecd4..2d7da4375 100644 --- a/basics/cross-program-invocation/anchor/programs/hand/Cargo.toml +++ b/basics/cross-program-invocation/anchor/programs/hand/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/cross-program-invocation/anchor/programs/hand/src/lib.rs b/basics/cross-program-invocation/anchor/programs/hand/src/lib.rs index 2efa0b06e..4dab21463 100644 --- a/basics/cross-program-invocation/anchor/programs/hand/src/lib.rs +++ b/basics/cross-program-invocation/anchor/programs/hand/src/lib.rs @@ -13,11 +13,14 @@ use lever::program::Lever; pub mod hand { use super::*; - pub fn pull_lever(context: Context, name: String) -> Result<()> { + pub fn pull_lever( + context: &mut Context, + name: String, + ) -> Result<()> { let cpi_ctx = CpiContext::new( - context.accounts.lever_program.key(), + context.accounts.lever_program.address(), SwitchPower { - power: context.accounts.power.to_account_info(), + power: context.accounts.power.cpi_handle_mut(), }, ); switch_power(cpi_ctx, name)?; @@ -26,8 +29,8 @@ pub mod hand { } #[derive(Accounts)] -pub struct PullLeverAccountConstraints<'info> { +pub struct PullLeverAccountConstraints { #[account(mut)] - pub power: Account<'info, PowerStatus>, - pub lever_program: Program<'info, Lever>, + pub power: BorshAccount, + pub lever_program: Program, } diff --git a/basics/cross-program-invocation/anchor/programs/hand/tests/test_hand.rs b/basics/cross-program-invocation/anchor/programs/hand/tests/test_hand.rs index e9c7c64ac..b6c81b4f2 100644 --- a/basics/cross-program-invocation/anchor/programs/hand/tests/test_hand.rs +++ b/basics/cross-program-invocation/anchor/programs/hand/tests/test_hand.rs @@ -1,10 +1,7 @@ use { anchor_lang::{ - solana_program::{ - instruction::{AccountMeta, Instruction}, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::{AccountMeta, Instruction}, + system_program, Address, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, @@ -15,7 +12,7 @@ use { /// PowerStatus account layout: 8-byte discriminator + 1-byte bool + 7 bytes padding. /// Account space is 8 + 8 = 16 bytes, so read the raw bytes instead of using BorshDeserialize /// to avoid "Not all bytes read" errors from the padding. -fn read_power_is_on(svm: &LiteSVM, pubkey: &anchor_lang::prelude::Pubkey) -> bool { +fn read_power_is_on(svm: &LiteSVM, pubkey: &anchor_lang::Address) -> bool { let account = svm.get_account(pubkey).unwrap(); // Skip 8-byte discriminator, read 1 byte for bool account.data[8] != 0 @@ -24,9 +21,9 @@ fn read_power_is_on(svm: &LiteSVM, pubkey: &anchor_lang::prelude::Pubkey) -> boo /// Build the lever program's `initialize` instruction manually. /// Discriminator from IDL: [175, 175, 109, 31, 13, 152, 155, 237] fn build_lever_initialize_ix( - lever_program_id: anchor_lang::prelude::Pubkey, - power: anchor_lang::prelude::Pubkey, - user: anchor_lang::prelude::Pubkey, + lever_program_id: anchor_lang::Address, + power: anchor_lang::Address, + user: anchor_lang::Address, ) -> Instruction { let discriminator: [u8; 8] = [175, 175, 109, 31, 13, 152, 155, 237]; Instruction { @@ -34,7 +31,7 @@ fn build_lever_initialize_ix( accounts: vec![ AccountMeta::new(power, true), AccountMeta::new(user, true), - AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(system_program::ID, false), ], data: discriminator.to_vec(), } diff --git a/basics/cross-program-invocation/anchor/programs/lever/Cargo.toml b/basics/cross-program-invocation/anchor/programs/lever/Cargo.toml index 19826a09a..265c9d12f 100644 --- a/basics/cross-program-invocation/anchor/programs/lever/Cargo.toml +++ b/basics/cross-program-invocation/anchor/programs/lever/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/cross-program-invocation/anchor/programs/lever/src/instructions/initialize.rs b/basics/cross-program-invocation/anchor/programs/lever/src/instructions/initialize.rs index 025fc5b7d..576b6d34d 100644 --- a/basics/cross-program-invocation/anchor/programs/lever/src/instructions/initialize.rs +++ b/basics/cross-program-invocation/anchor/programs/lever/src/instructions/initialize.rs @@ -3,14 +3,14 @@ use anchor_lang::prelude::*; use crate::PowerStatus; #[derive(Accounts)] -pub struct InitializeLeverAccountConstraints<'info> { +pub struct InitializeLeverAccountConstraints { #[account(init, payer = user, space = PowerStatus::DISCRIMINATOR.len() + PowerStatus::INIT_SPACE)] - pub power: Account<'info, PowerStatus>, + pub power: BorshAccount, #[account(mut)] - pub user: Signer<'info>, - pub system_program: Program<'info, System>, + pub user: Signer, + pub system_program: Program, } -pub fn handler(_context: Context) -> Result<()> { +pub fn handler(_context: &mut Context) -> Result<()> { Ok(()) } diff --git a/basics/cross-program-invocation/anchor/programs/lever/src/instructions/switch_power.rs b/basics/cross-program-invocation/anchor/programs/lever/src/instructions/switch_power.rs index 55674ab63..14572e5ab 100644 --- a/basics/cross-program-invocation/anchor/programs/lever/src/instructions/switch_power.rs +++ b/basics/cross-program-invocation/anchor/programs/lever/src/instructions/switch_power.rs @@ -3,12 +3,15 @@ use anchor_lang::prelude::*; use crate::PowerStatus; #[derive(Accounts)] -pub struct SetPowerStatusAccountConstraints<'info> { +pub struct SetPowerStatusAccountConstraints { #[account(mut)] - pub power: Account<'info, PowerStatus>, + pub power: BorshAccount, } -pub fn handler(context: Context, name: String) -> Result<()> { +pub fn handler( + context: &mut Context, + name: String, +) -> Result<()> { let power = &mut context.accounts.power; power.is_on = !power.is_on; diff --git a/basics/cross-program-invocation/anchor/programs/lever/src/lib.rs b/basics/cross-program-invocation/anchor/programs/lever/src/lib.rs index fa90458d9..14b414c20 100644 --- a/basics/cross-program-invocation/anchor/programs/lever/src/lib.rs +++ b/basics/cross-program-invocation/anchor/programs/lever/src/lib.rs @@ -9,19 +9,25 @@ declare_id!("E64FVeubGC4NPNF2UBJYX4AkrVowf74fRJD9q6YhwstN"); pub mod lever { use super::*; - pub fn initialize(context: Context) -> Result<()> { + pub fn initialize(context: &mut Context) -> Result<()> { instructions::initialize::handler(context) } pub fn switch_power( - context: Context, + context: &mut Context, name: String, ) -> Result<()> { instructions::switch_power::handler(context, name) } } -#[account] +// `borsh` rather than v2's zero-copy default. A zero-copy `PowerStatus` would +// have to store `is_on` as `PodBool` (bytemuck rejects `bool`, since only +// `0x00`/`0x01` are valid bit patterns), and the generated IDL renders +// `PodBool` as a plain `bool` alias, so `declare_program!` in the `hand` +// program would regenerate a struct that is no longer `Pod`. Borsh keeps the +// field a real `bool` on both sides of the CPI. +#[account(borsh)] #[derive(InitSpace)] pub struct PowerStatus { pub is_on: bool, diff --git a/basics/cross-program-invocation/anchor/programs/lever/tests/test_lever.rs b/basics/cross-program-invocation/anchor/programs/lever/tests/test_lever.rs index 47d32a638..87e8c002b 100644 --- a/basics/cross-program-invocation/anchor/programs/lever/tests/test_lever.rs +++ b/basics/cross-program-invocation/anchor/programs/lever/tests/test_lever.rs @@ -1,7 +1,6 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, @@ -11,7 +10,7 @@ use { /// PowerStatus account layout: 8-byte discriminator + 1-byte bool + 7 bytes padding. /// Account space is 8 + 8 = 16 bytes, so read raw bytes to avoid "Not all bytes read" errors. -fn read_power_is_on(svm: &LiteSVM, pubkey: &anchor_lang::prelude::Pubkey) -> bool { +fn read_power_is_on(svm: &LiteSVM, pubkey: &anchor_lang::prelude::Address) -> bool { let account = svm.get_account(pubkey).unwrap(); account.data[8] != 0 } @@ -32,7 +31,7 @@ fn test_initialize_lever() { lever::accounts::InitializeLeverAccountConstraints { power: power_keypair.pubkey(), user: payer.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -68,7 +67,7 @@ fn test_switch_power() { lever::accounts::InitializeLeverAccountConstraints { power: power_keypair.pubkey(), user: payer.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/basics/favorites/anchor/programs/favorites/Cargo.toml b/basics/favorites/anchor/programs/favorites/Cargo.toml index 229771829..0ec0e8e58 100644 --- a/basics/favorites/anchor/programs/favorites/Cargo.toml +++ b/basics/favorites/anchor/programs/favorites/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = {version = "1.1.2", features = ["init-if-needed"]} +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/favorites/anchor/programs/favorites/src/lib.rs b/basics/favorites/anchor/programs/favorites/src/lib.rs index 6f4fabc8c..794740738 100644 --- a/basics/favorites/anchor/programs/favorites/src/lib.rs +++ b/basics/favorites/anchor/programs/favorites/src/lib.rs @@ -10,23 +10,23 @@ pub mod favorites { // Our instruction handler! It sets the user's favorite number and color pub fn set_favorites( - context: Context, + context: &mut Context, number: u64, color: String, hobbies: Vec, ) -> Result<()> { msg!("Greetings from {}", context.program_id); - let user_public_key = context.accounts.user.key(); + let user_public_key = context.accounts.user.address(); msg!( "User {user_public_key}'s favorite number is {number}, favorite color is: {color}, and their hobbies are {hobbies:?}", ); - context.accounts.favorites.set_inner(Favorites { + *context.accounts.favorites = Favorites { number, color, hobbies, bump: context.bumps.favorites, - }); + }; Ok(()) } @@ -34,7 +34,9 @@ pub mod favorites { } // What we will put inside the Favorites PDA -#[account] +// `borsh` because the struct holds a `String` and a `Vec`: v2's default +// `#[account]` backing is zero-copy and needs a `Pod` (fixed-layout) type. +#[account(borsh)] #[derive(InitSpace)] pub struct Favorites { pub number: u64, @@ -51,18 +53,18 @@ pub struct Favorites { } // When people call the set_favorites instruction, they will need to provide the accounts that will be modifed. This keeps Solana fast! #[derive(Accounts)] -pub struct SetFavoritesAccountConstraints<'info> { +pub struct SetFavoritesAccountConstraints { #[account(mut)] - pub user: Signer<'info>, + pub user: Signer, #[account( init_if_needed, payer = user, space = Favorites::DISCRIMINATOR.len() + Favorites::INIT_SPACE, - seeds=[b"favorites", user.key().as_ref()], + seeds=[b"favorites", user.address().as_ref()], bump )] - pub favorites: Account<'info, Favorites>, + pub favorites: BorshAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } diff --git a/basics/favorites/anchor/programs/favorites/tests/test_favorites.rs b/basics/favorites/anchor/programs/favorites/tests/test_favorites.rs index 6f217c0df..2f5543adc 100644 --- a/basics/favorites/anchor/programs/favorites/tests/test_favorites.rs +++ b/basics/favorites/anchor/programs/favorites/tests/test_favorites.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_kite::{create_wallet, send_transaction_from_instructions}, @@ -17,7 +17,7 @@ struct FavoritesData { /// Manually deserialize the Favorites account data, skipping the 8-byte discriminator. /// We can't use BorshDeserialize on the full account because init_if_needed allocates /// more space than the serialized data occupies (padding for max_len strings/vecs). -fn read_favorites(svm: &LiteSVM, pda: &Pubkey) -> FavoritesData { +fn read_favorites(svm: &LiteSVM, pda: &Address) -> FavoritesData { let account = svm.get_account(pda).unwrap(); let data = &account.data[8..]; // skip discriminator let mut offset = 0; @@ -33,13 +33,11 @@ fn read_favorites(svm: &LiteSVM, pda: &Pubkey) -> FavoritesData { offset += color_len; // Vec hobbies (4-byte vec length + each string) - let hobbies_count = - u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize; + let hobbies_count = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize; offset += 4; let mut hobbies = Vec::with_capacity(hobbies_count); for _ in 0..hobbies_count { - let hobby_len = - u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize; + let hobby_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize; offset += 4; let hobby = String::from_utf8(data[offset..offset + hobby_len].to_vec()).unwrap(); offset += hobby_len; @@ -53,7 +51,7 @@ fn read_favorites(svm: &LiteSVM, pda: &Pubkey) -> FavoritesData { } } -fn setup() -> (LiteSVM, Pubkey, solana_keypair::Keypair) { +fn setup() -> (LiteSVM, Address, solana_keypair::Keypair) { let program_id = favorites::id(); let mut svm = LiteSVM::new(); let bytes = include_bytes!("../../../target/deploy/favorites.so"); @@ -62,8 +60,8 @@ fn setup() -> (LiteSVM, Pubkey, solana_keypair::Keypair) { (svm, program_id, payer) } -fn favorites_pda(program_id: &Pubkey, user: &Pubkey) -> Pubkey { - Pubkey::find_program_address(&[b"favorites", user.as_ref()], program_id).0 +fn favorites_pda(program_id: &Address, user: &Address) -> Address { + Address::find_program_address(&[b"favorites", user.as_ref()], program_id).0 } #[test] @@ -86,7 +84,7 @@ fn test_set_favorites() { favorites::accounts::SetFavoritesAccountConstraints { user: payer.pubkey(), favorites: pda, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -121,7 +119,7 @@ fn test_update_favorites() { favorites::accounts::SetFavoritesAccountConstraints { user: payer.pubkey(), favorites: pda, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -146,7 +144,7 @@ fn test_update_favorites() { favorites::accounts::SetFavoritesAccountConstraints { user: payer.pubkey(), favorites: pda, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/basics/hello-solana/anchor/programs/hello-solana/Cargo.toml b/basics/hello-solana/anchor/programs/hello-solana/Cargo.toml index a5c764e32..e96bdc106 100644 --- a/basics/hello-solana/anchor/programs/hello-solana/Cargo.toml +++ b/basics/hello-solana/anchor/programs/hello-solana/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/hello-solana/anchor/programs/hello-solana/src/lib.rs b/basics/hello-solana/anchor/programs/hello-solana/src/lib.rs index 54fc73ecb..253ff5abd 100644 --- a/basics/hello-solana/anchor/programs/hello-solana/src/lib.rs +++ b/basics/hello-solana/anchor/programs/hello-solana/src/lib.rs @@ -6,7 +6,7 @@ declare_id!("2phbC62wekpw95XuBk4i1KX4uA8zBUWmYbiTMhicSuBV"); pub mod hello_solana { use super::*; - pub fn hello(_context: Context) -> Result<()> { + pub fn hello(_context: &mut Context) -> Result<()> { msg!("Hello, Solana!"); msg!("Our program's Program ID: {}", &id()); diff --git a/basics/pda-rent-payer/anchor/programs/anchor-program-example/Cargo.toml b/basics/pda-rent-payer/anchor/programs/anchor-program-example/Cargo.toml index 2eba08465..740790332 100644 --- a/basics/pda-rent-payer/anchor/programs/anchor-program-example/Cargo.toml +++ b/basics/pda-rent-payer/anchor/programs/anchor-program-example/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/instructions/create_new_account.rs b/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/instructions/create_new_account.rs index 80a2e84c8..41523ba71 100644 --- a/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/instructions/create_new_account.rs +++ b/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/instructions/create_new_account.rs @@ -2,9 +2,9 @@ use anchor_lang::prelude::*; use anchor_lang::system_program::{create_account, CreateAccount}; #[derive(Accounts)] -pub struct CreateNewAccountAccountConstraints<'info> { +pub struct CreateNewAccountAccountConstraints { #[account(mut)] - new_account: Signer<'info>, + new_account: Signer, #[account( mut, @@ -13,32 +13,32 @@ pub struct CreateNewAccountAccountConstraints<'info> { ], bump, )] - rent_vault: SystemAccount<'info>, - system_program: Program<'info, System>, + rent_vault: SystemAccount, + system_program: Program, } pub fn handle_create_new_account( - context: Context, + context: &mut Context, ) -> Result<()> { // PDA signer seeds let signer_seeds: &[&[&[u8]]] = &[&[b"rent_vault", &[context.bumps.rent_vault]]]; // The minimum lamports for rent exemption - let lamports = (Rent::get()?).minimum_balance(0); + let lamports = Rent::get()?.try_minimum_balance(0)?; // Create the new account, transferring lamports from the rent vault to the new account create_account( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), CreateAccount { - from: context.accounts.rent_vault.to_account_info(), // From pubkey - to: context.accounts.new_account.to_account_info(), // To pubkey + from: context.accounts.rent_vault.cpi_handle_mut(), // From pubkey + to: context.accounts.new_account.cpi_handle_mut(), // To pubkey }, ) .with_signer(signer_seeds), - lamports, // Lamports - 0, // Space - &context.accounts.system_program.key(), // Owner Program + lamports, // Lamports + 0, // Space + context.accounts.system_program.address(), // Owner Program )?; Ok(()) } diff --git a/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/instructions/init_rent_vault.rs b/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/instructions/init_rent_vault.rs index a15c49b42..6c78c39d3 100644 --- a/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/instructions/init_rent_vault.rs +++ b/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/instructions/init_rent_vault.rs @@ -2,9 +2,9 @@ use anchor_lang::prelude::*; use anchor_lang::system_program::{transfer, Transfer}; #[derive(Accounts)] -pub struct InitRentVaultAccountConstraints<'info> { +pub struct InitRentVaultAccountConstraints { #[account(mut)] - payer: Signer<'info>, + payer: Signer, #[account( mut, @@ -13,22 +13,22 @@ pub struct InitRentVaultAccountConstraints<'info> { ], bump, )] - rent_vault: SystemAccount<'info>, - system_program: Program<'info, System>, + rent_vault: SystemAccount, + system_program: Program, } // When lamports are transferred to a new address (without and existing account), // An account owned by the system program is created by default pub fn handle_init_rent_vault( - context: Context, + context: &mut Context, fund_lamports: u64, ) -> Result<()> { transfer( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), Transfer { - from: context.accounts.payer.to_account_info(), - to: context.accounts.rent_vault.to_account_info(), + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.rent_vault.cpi_handle_mut(), }, ), fund_lamports, diff --git a/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/lib.rs b/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/lib.rs index c3f3b686c..c1a13a99f 100644 --- a/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/lib.rs +++ b/basics/pda-rent-payer/anchor/programs/anchor-program-example/src/lib.rs @@ -9,13 +9,15 @@ pub mod pda_rent_payer { use super::*; pub fn init_rent_vault( - context: Context, + context: &mut Context, fund_lamports: u64, ) -> Result<()> { init_rent_vault::handle_init_rent_vault(context, fund_lamports) } - pub fn create_new_account(context: Context) -> Result<()> { + pub fn create_new_account( + context: &mut Context, + ) -> Result<()> { create_new_account::handle_create_new_account(context) } } diff --git a/basics/pda-rent-payer/anchor/programs/anchor-program-example/tests/test_pda_rent_payer.rs b/basics/pda-rent-payer/anchor/programs/anchor-program-example/tests/test_pda_rent_payer.rs index 650d71864..463207020 100644 --- a/basics/pda-rent-payer/anchor/programs/anchor-program-example/tests/test_pda_rent_payer.rs +++ b/basics/pda-rent-payer/anchor/programs/anchor-program-example/tests/test_pda_rent_payer.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, @@ -23,7 +23,7 @@ fn test_init_rent_vault() { let (mut svm, payer) = setup(); let program_id = pda_rent_payer::id(); - let (rent_vault_pda, _bump) = Pubkey::find_program_address(&[b"rent_vault"], &program_id); + let (rent_vault_pda, _bump) = Address::find_program_address(&[b"rent_vault"], &program_id); // Fund the rent vault with 1 SOL let fund_amount: u64 = 1_000_000_000; @@ -36,7 +36,7 @@ fn test_init_rent_vault() { pda_rent_payer::accounts::InitRentVaultAccountConstraints { payer: payer.pubkey(), rent_vault: rent_vault_pda, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -58,7 +58,7 @@ fn test_create_new_account_from_rent_vault() { let (mut svm, payer) = setup(); let program_id = pda_rent_payer::id(); - let (rent_vault_pda, _bump) = Pubkey::find_program_address(&[b"rent_vault"], &program_id); + let (rent_vault_pda, _bump) = Address::find_program_address(&[b"rent_vault"], &program_id); // Fund the rent vault with 1 SOL let fund_amount: u64 = 1_000_000_000; @@ -71,7 +71,7 @@ fn test_create_new_account_from_rent_vault() { pda_rent_payer::accounts::InitRentVaultAccountConstraints { payer: payer.pubkey(), rent_vault: rent_vault_pda, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -88,7 +88,7 @@ fn test_create_new_account_from_rent_vault() { pda_rent_payer::accounts::CreateNewAccountAccountConstraints { new_account: new_account.pubkey(), rent_vault: rent_vault_pda, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/basics/processing-instructions/anchor/programs/processing-instructions/Cargo.toml b/basics/processing-instructions/anchor/programs/processing-instructions/Cargo.toml index 21eb08981..4bba91bde 100644 --- a/basics/processing-instructions/anchor/programs/processing-instructions/Cargo.toml +++ b/basics/processing-instructions/anchor/programs/processing-instructions/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/processing-instructions/anchor/programs/processing-instructions/src/lib.rs b/basics/processing-instructions/anchor/programs/processing-instructions/src/lib.rs index 1cc3a11f4..a9c94b8c2 100644 --- a/basics/processing-instructions/anchor/programs/processing-instructions/src/lib.rs +++ b/basics/processing-instructions/anchor/programs/processing-instructions/src/lib.rs @@ -9,7 +9,7 @@ pub mod processing_instructions { // With Anchor, we just put instruction data in the function signature! // pub fn go_to_park( - _context: Context, + _context: &mut Context, name: String, height: u32, ) -> Result<()> { diff --git a/basics/program-derived-addresses/anchor/programs/anchor-program-example/Cargo.toml b/basics/program-derived-addresses/anchor/programs/anchor-program-example/Cargo.toml index 6ad63ff3e..69b670b4c 100644 --- a/basics/program-derived-addresses/anchor/programs/anchor-program-example/Cargo.toml +++ b/basics/program-derived-addresses/anchor/programs/anchor-program-example/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/instructions/create.rs b/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/instructions/create.rs index 2278e1d13..64c59e769 100644 --- a/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/instructions/create.rs +++ b/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/instructions/create.rs @@ -2,9 +2,9 @@ use crate::state::PageVisits; use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct CreatePageVisitsAccountConstraints<'info> { +pub struct CreatePageVisitsAccountConstraints { #[account(mut)] - payer: Signer<'info>, + pub payer: Signer, #[account( init, @@ -12,20 +12,21 @@ pub struct CreatePageVisitsAccountConstraints<'info> { payer = payer, seeds = [ PageVisits::SEED_PREFIX, - payer.key().as_ref(), + payer.address().as_ref(), ], bump, )] - page_visits: Account<'info, PageVisits>, - system_program: Program<'info, System>, + pub page_visits: Account, + pub system_program: Program, } pub fn handle_create_page_visits( - context: Context, + context: &mut Context, ) -> Result<()> { *context.accounts.page_visits = PageVisits { page_visits: 0, bump: context.bumps.page_visits, + _padding: [0; 3], }; Ok(()) diff --git a/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/instructions/increment.rs b/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/instructions/increment.rs index 206ef7311..bfceb1469 100644 --- a/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/instructions/increment.rs +++ b/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/instructions/increment.rs @@ -2,21 +2,21 @@ use crate::{state::PageVisits, PageVisitsError}; use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct IncrementPageVisitsAccountConstraints<'info> { - user: SystemAccount<'info>, +pub struct IncrementPageVisitsAccountConstraints { + pub user: SystemAccount, #[account( mut, seeds = [ PageVisits::SEED_PREFIX, - user.key().as_ref(), + user.address().as_ref(), ], bump = page_visits.bump, )] - page_visits: Account<'info, PageVisits>, + pub page_visits: Account, } pub fn handle_increment_page_visits( - context: Context, + context: &mut Context, ) -> Result<()> { let page_visits = &mut context.accounts.page_visits; page_visits.page_visits = page_visits diff --git a/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/lib.rs b/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/lib.rs index 40a9f3bf9..30f0a14ce 100644 --- a/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/lib.rs +++ b/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/lib.rs @@ -11,12 +11,14 @@ declare_id!("oCCQRZyAbVxujyd8m57MPmDzZDmy2FoKW4ULS7KofCE"); pub mod program_derived_addresses_program { use super::*; - pub fn create_page_visits(context: Context) -> Result<()> { + pub fn create_page_visits( + context: &mut Context, + ) -> Result<()> { create::handle_create_page_visits(context) } pub fn increment_page_visits( - context: Context, + context: &mut Context, ) -> Result<()> { increment::handle_increment_page_visits(context) } diff --git a/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/state/page_visits.rs b/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/state/page_visits.rs index edb1fd715..e82a29388 100644 --- a/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/state/page_visits.rs +++ b/basics/program-derived-addresses/anchor/programs/anchor-program-example/src/state/page_visits.rs @@ -5,6 +5,9 @@ use anchor_lang::prelude::*; pub struct PageVisits { pub page_visits: u32, pub bump: u8, + // v2's `#[account]` is zero-copy, so the struct has to be Pod, and Pod + // rejects implicit padding. u32 + u8 leaves three bytes, so name them. + pub _padding: [u8; 3], } impl PageVisits { diff --git a/basics/program-derived-addresses/anchor/programs/anchor-program-example/tests/test_program_derived_addresses.rs b/basics/program-derived-addresses/anchor/programs/anchor-program-example/tests/test_program_derived_addresses.rs index 1ef8645bb..96d5b028a 100644 --- a/basics/program-derived-addresses/anchor/programs/anchor-program-example/tests/test_program_derived_addresses.rs +++ b/basics/program-derived-addresses/anchor/programs/anchor-program-example/tests/test_program_derived_addresses.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, borsh::BorshDeserialize, litesvm::LiteSVM, @@ -18,10 +18,14 @@ fn setup() -> (LiteSVM, solana_keypair::Keypair) { (svm, payer) } +/// Mirrors the on-chain layout. v2's `#[account]` is zero-copy, so the struct +/// carries explicit padding out to its alignment: `try_from_slice` rejects +/// trailing bytes, so the test type has to spell the padding out too. #[derive(BorshDeserialize)] struct PageVisits { page_visits: u32, bump: u8, + _padding: [u8; 3], } #[test] @@ -31,7 +35,7 @@ fn test_create_and_increment_page_visits() { // Derive PDA let (page_visits_pda, _bump) = - Pubkey::find_program_address(&[b"page_visits", payer.pubkey().as_ref()], &program_id); + Address::find_program_address(&[b"page_visits", payer.pubkey().as_ref()], &program_id); // Create page visits account let create_ix = Instruction::new_with_bytes( @@ -40,7 +44,7 @@ fn test_create_and_increment_page_visits() { program_derived_addresses_program::accounts::CreatePageVisitsAccountConstraints { payer: payer.pubkey(), page_visits: page_visits_pda, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/basics/pyth/anchor/README.md b/basics/pyth/anchor/README.md index 2b579deeb..a82eceb9a 100644 --- a/basics/pyth/anchor/README.md +++ b/basics/pyth/anchor/README.md @@ -5,17 +5,11 @@ Read a [Pyth](https://pyth.network/) price feed account and log price, confidenc See also: [Pyth overview](../README.md) and the [repository catalog](../../../README.md). > [!NOTE] -> **The official `pyth-solana-receiver-sdk` is not Anchor 1.0 compatible (as of June 2026), so this example vendors the `PriceUpdateV2` account type instead of importing it.** +> **This example vendors the `PriceUpdateV2` account type rather than importing `pyth-solana-receiver-sdk`.** > -> The latest `pyth-solana-receiver-sdk` (1.2.0) builds against `anchor-lang` 0.32 and pulls `pythnet-sdk` (2.3.1), which still derives **borsh 0.10** on `PriceFeedMessage`. Anchor 0.32's `AnchorSerialize`/`AnchorDeserialize` derives require **borsh 1.x**, so `pyth-solana-receiver-sdk`'s own `PriceUpdateV2` fails to compile: +> The SDK's current release (2.0.0, checked August 2026) builds against `anchor-lang` 1.0.2, and this repository is on 2.0.0-rc.1, whose account wrappers are a different set of types. Importing the SDK's `PriceUpdateV2` would pull a second `anchor-lang` into the graph. > -> ``` -> error[E0277]: the trait bound `pythnet_sdk::messages::PriceFeedMessage: BorshSerialize` is not satisfied -> ``` -> -> No published `pyth-solana-receiver-sdk` targets `anchor-lang` 1.0 (which this repo standardizes on), and no `pythnet-sdk` release has migrated to borsh 1.x - so the dependency can't simply be upgraded. Tracked upstream at [pyth-network/pyth-crosschain#3756](https://github.com/pyth-network/pyth-crosschain/issues/3756). -> -> As a workaround, `programs/pythexample/src/lib.rs` mirrors the onchain `PriceUpdateV2` layout locally (same fields, same 8-byte discriminator, owned by the Pyth Receiver program) so accounts written by Pyth deserialize unchanged. Replace the vendored type with the SDK import once an Anchor 1.0 / borsh 1.x compatible release ships. +> `programs/pythexample/src/lib.rs` mirrors the onchain layout instead: same fields in the same order, same 8-byte discriminator, owned by the Pyth Receiver program, so accounts written by Pyth deserialize unchanged. Import the SDK type once a release targeting `anchor-lang` 2.x ships. ## Major concepts diff --git a/basics/pyth/anchor/programs/pythexample/Cargo.toml b/basics/pyth/anchor/programs/pythexample/Cargo.toml index d7e244f8b..040ca02fe 100644 --- a/basics/pyth/anchor/programs/pythexample/Cargo.toml +++ b/basics/pyth/anchor/programs/pythexample/Cargo.toml @@ -21,7 +21,14 @@ custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6, which splits `Address`'s wincode impls across two +# incompatible trait versions. 2.6.1 is the last release on the 0.5 line. +solana-address = ">=2.6, <2.7" [dev-dependencies] # Self-dependency with no-entrypoint: host test builds otherwise export a @@ -34,6 +41,7 @@ litesvm = "0.13.1" solana-signer = "3.0.0" solana-keypair = "3.0.1" solana-account = "3.0.0" +solana-clock = "3.0.1" borsh = "1.6.1" sha2 = "0.10" solana-kite = "0.4.0" diff --git a/basics/pyth/anchor/programs/pythexample/src/lib.rs b/basics/pyth/anchor/programs/pythexample/src/lib.rs index 013b6882e..fe2911fc6 100644 --- a/basics/pyth/anchor/programs/pythexample/src/lib.rs +++ b/basics/pyth/anchor/programs/pythexample/src/lib.rs @@ -3,7 +3,8 @@ use anchor_lang::prelude::*; declare_id!("GUkjQmrLPFXXNK1bFLKt8XQi6g3TjxcHVspbjDoHvMG2"); /// The Pyth Receiver program that owns `PriceUpdateV2` accounts on devnet/mainnet. -pub const PYTH_RECEIVER_PROGRAM_ID: Pubkey = pubkey!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ"); +pub const PYTH_RECEIVER_PROGRAM_ID: Address = + anchor_lang::address!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ"); /// Maximum allowed age of a price update before it is rejected as stale. /// Pyth's `publish_time` is a unix timestamp in seconds, so the age check @@ -24,7 +25,7 @@ pub enum PythExampleError { pub mod anchor_test { use super::*; - pub fn read_price(context: Context) -> Result<()> { + pub fn read_price(context: &mut Context) -> Result<()> { let price_update = &context.accounts.price_update; // Reject stale prices: a price that stopped updating is wrong. @@ -50,39 +51,25 @@ pub mod anchor_test { } #[derive(Accounts)] -pub struct ReadPriceAccountConstraints<'info> { - pub price_update: Account<'info, PriceUpdateV2>, +pub struct ReadPriceAccountConstraints { + pub price_update: BorshAccount, } // --------------------------------------------------------------------------- // Pyth `PriceUpdateV2` account, vendored from `pyth-solana-receiver-sdk`. // -// The official `pyth-solana-receiver-sdk` is NOT Anchor 1.0 compatible (as of -// June 2026), so this example mirrors the `PriceUpdateV2` account type locally -// instead of importing it. +// The SDK's current release (2.0.0, checked August 2026) builds against +// `anchor-lang` 1.0.2, and this repository is on 2.0.0-rc.1, whose account +// wrappers are a different set of types. Importing the SDK's `PriceUpdateV2` +// would pull a second `anchor-lang` into the graph. // -// Details: the latest `pyth-solana-receiver-sdk` (1.2.0) builds against -// `anchor-lang` 0.32 and pulls `pythnet-sdk` (2.3.1), which still derives -// borsh 0.10 on `PriceFeedMessage`. Anchor 0.32's `AnchorSerialize` / -// `AnchorDeserialize` derives require borsh 1.x, so the SDK's own -// `PriceUpdateV2` fails to compile: -// -// error[E0277]: the trait bound -// `pythnet_sdk::messages::PriceFeedMessage: BorshSerialize` is not satisfied -// -// No published `pyth-solana-receiver-sdk` targets `anchor-lang` 1.0 (which this -// repo standardizes on) and no `pythnet-sdk` release has migrated to borsh 1.x, -// so the dependency can't simply be upgraded. Tracked upstream at -// https://github.com/pyth-network/pyth-crosschain/issues/3756 -// -// The fields, order, and 8-byte -// discriminator below match the onchain account exactly, and it is owned by -// the Pyth Receiver program (see the `Owner` impl), so accounts written by Pyth -// deserialize unchanged. Replace this with the SDK type once an Anchor 1.0 / -// borsh 1.x compatible `pyth-solana-receiver-sdk` release ships. +// The fields, order, and 8-byte discriminator below match the onchain account +// exactly, and it is owned by the Pyth Receiver program (see the `Owner` impl), +// so accounts written by Pyth deserialize unchanged. Import the SDK type once a +// release targeting `anchor-lang` 2.x ships. // --------------------------------------------------------------------------- -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, wincode::SchemaRead, wincode::SchemaWrite)] pub enum VerificationLevel { /// Partially verified: only `num_signatures` of the Wormhole guardians /// were checked against the price update. @@ -91,7 +78,7 @@ pub enum VerificationLevel { Full, } -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, wincode::SchemaRead, wincode::SchemaWrite)] pub struct PriceFeedMessage { pub feed_id: [u8; 32], pub price: i64, @@ -103,9 +90,9 @@ pub struct PriceFeedMessage { pub ema_conf: u64, } -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, wincode::SchemaRead, wincode::SchemaWrite)] pub struct PriceUpdateV2 { - pub write_authority: Pubkey, + pub write_authority: Address, pub verification_level: VerificationLevel, pub price_message: PriceFeedMessage, pub posted_slot: u64, @@ -118,38 +105,5 @@ impl anchor_lang::Discriminator for PriceUpdateV2 { // The account is created and owned by the Pyth Receiver program. impl anchor_lang::Owner for PriceUpdateV2 { - fn owner() -> Pubkey { - PYTH_RECEIVER_PROGRAM_ID - } -} - -impl anchor_lang::AccountSerialize for PriceUpdateV2 { - fn try_serialize(&self, writer: &mut W) -> Result<()> { - writer - .write_all(::DISCRIMINATOR) - .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?; - AnchorSerialize::serialize(self, writer) - .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?; - Ok(()) - } -} - -impl anchor_lang::AccountDeserialize for PriceUpdateV2 { - fn try_deserialize(buf: &mut &[u8]) -> Result { - let disc = ::DISCRIMINATOR; - if buf.len() < disc.len() { - return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into()); - } - if &buf[..disc.len()] != disc { - return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into()); - } - Self::try_deserialize_unchecked(buf) - } - - fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result { - let disc = ::DISCRIMINATOR; - let mut data: &[u8] = &buf[disc.len()..]; - AnchorDeserialize::deserialize(&mut data) - .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into()) - } + const OWNER: Address = PYTH_RECEIVER_PROGRAM_ID; } diff --git a/basics/pyth/anchor/programs/pythexample/tests/test_pyth.rs b/basics/pyth/anchor/programs/pythexample/tests/test_pyth.rs index 5071866a4..5929d9538 100644 --- a/basics/pyth/anchor/programs/pythexample/tests/test_pyth.rs +++ b/basics/pyth/anchor/programs/pythexample/tests/test_pyth.rs @@ -1,10 +1,11 @@ use { anchor_lang::{ - solana_program::{clock::Clock, instruction::Instruction}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, Address, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, pythexample::MAXIMUM_PRICE_AGE_SECONDS, + // LiteSVM's get_sysvar wants the host-side Clock, not pinocchio's. + solana_clock::Clock, solana_keypair::Keypair, solana_kite::{create_wallet, send_transaction_from_instructions}, solana_signer::Signer, @@ -14,14 +15,12 @@ use { const MOCK_PUBLISH_TIME: i64 = 1_700_000_000; /// Pyth Receiver program ID (rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ) -fn pyth_receiver_program_id() -> anchor_lang::solana_program::pubkey::Pubkey { +fn pyth_receiver_program_id() -> anchor_lang::Address { pythexample::PYTH_RECEIVER_PROGRAM_ID } /// Build mock PriceUpdateV2 account data with Anchor discriminator. -fn build_mock_price_update_account( - write_authority: &anchor_lang::solana_program::pubkey::Pubkey, -) -> Vec { +fn build_mock_price_update_account(write_authority: &anchor_lang::Address) -> Vec { // Discriminator: sha256("account:PriceUpdateV2")[..8] let discriminator: [u8; 8] = [34, 241, 35, 99, 157, 126, 244, 205]; @@ -83,7 +82,7 @@ fn set_clock_to_price_age(svm: &mut LiteSVM, age_seconds: i64) { } fn setup_with_price_account( - owner: anchor_lang::solana_program::pubkey::Pubkey, + owner: anchor_lang::Address, ) -> (LiteSVM, solana_keypair::Keypair, Keypair) { let program_id = pythexample::id(); let mut svm = LiteSVM::new(); @@ -111,9 +110,10 @@ fn setup_with_price_account( (svm, payer, price_update_key) } -fn read_price_instruction(price_update: anchor_lang::solana_program::pubkey::Pubkey) -> Instruction { +fn read_price_instruction(price_update: anchor_lang::Address) -> Instruction { let ix_data = pythexample::instruction::ReadPrice {}.data(); - let accounts = pythexample::accounts::ReadPriceAccountConstraints { price_update }.to_account_metas(None); + let accounts = + pythexample::accounts::ReadPriceAccountConstraints { price_update }.to_account_metas(None); Instruction::new_with_bytes(pythexample::id(), &ix_data, accounts) } diff --git a/basics/realloc/anchor/programs/anchor-realloc/Cargo.toml b/basics/realloc/anchor/programs/anchor-realloc/Cargo.toml index cc14c040b..225ed55ca 100644 --- a/basics/realloc/anchor/programs/anchor-realloc/Cargo.toml +++ b/basics/realloc/anchor/programs/anchor-realloc/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/realloc/anchor/programs/anchor-realloc/src/instructions/initialize.rs b/basics/realloc/anchor/programs/anchor-realloc/src/instructions/initialize.rs index dc45b857c..eb09532d6 100644 --- a/basics/realloc/anchor/programs/anchor-realloc/src/instructions/initialize.rs +++ b/basics/realloc/anchor/programs/anchor-realloc/src/instructions/initialize.rs @@ -1,23 +1,28 @@ +// v2's `#[derive(Accounts)]` binds the `#[instruction(...)]` args in more +// than one generated item, and only the one evaluating the constraints below +// reads them, so the binding looks unused to rustc even though `space` uses it. +#![allow(unused_variables)] + use anchor_lang::prelude::*; use crate::Message; #[derive(Accounts)] #[instruction(input: String)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account( init, payer = payer, space = Message::required_space(input.len()), )] - pub message_account: Account<'info, Message>, - pub system_program: Program<'info, System>, + pub message_account: BorshAccount, + pub system_program: Program, } -pub fn handler(context: Context, input: String) -> Result<()> { +pub fn handler(context: &mut Context, input: String) -> Result<()> { context.accounts.message_account.message = input; Ok(()) } diff --git a/basics/realloc/anchor/programs/anchor-realloc/src/instructions/update.rs b/basics/realloc/anchor/programs/anchor-realloc/src/instructions/update.rs index cd1032c5f..2e1327de3 100644 --- a/basics/realloc/anchor/programs/anchor-realloc/src/instructions/update.rs +++ b/basics/realloc/anchor/programs/anchor-realloc/src/instructions/update.rs @@ -1,24 +1,29 @@ +// v2's `#[derive(Accounts)]` binds the `#[instruction(...)]` args in more +// than one generated item, and only the one evaluating the constraints below +// reads them, so the binding looks unused to rustc even though `space` uses it. +#![allow(unused_variables)] + use anchor_lang::prelude::*; use crate::Message; #[derive(Accounts)] #[instruction(input: String)] -pub struct UpdateAccountConstraints<'info> { +pub struct UpdateAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account( mut, realloc = Message::required_space(input.len()), - realloc::payer = payer, - realloc::zero = true, + realloc_payer = payer, + realloc_zero = true, )] - pub message_account: Account<'info, Message>, - pub system_program: Program<'info, System>, + pub message_account: BorshAccount, + pub system_program: Program, } -pub fn handler(context: Context, input: String) -> Result<()> { +pub fn handler(context: &mut Context, input: String) -> Result<()> { context.accounts.message_account.message = input; Ok(()) } diff --git a/basics/realloc/anchor/programs/anchor-realloc/src/lib.rs b/basics/realloc/anchor/programs/anchor-realloc/src/lib.rs index f51e65d69..f8f150e0d 100644 --- a/basics/realloc/anchor/programs/anchor-realloc/src/lib.rs +++ b/basics/realloc/anchor/programs/anchor-realloc/src/lib.rs @@ -9,11 +9,14 @@ declare_id!("Fod47xKXjdHVQDzkFPBvfdWLm8gEAV4iMSXkfUzCHiSD"); pub mod anchor_realloc { use super::*; - pub fn initialize(context: Context, input: String) -> Result<()> { + pub fn initialize( + context: &mut Context, + input: String, + ) -> Result<()> { instructions::initialize::handler(context, input) } - pub fn update(context: Context, input: String) -> Result<()> { + pub fn update(context: &mut Context, input: String) -> Result<()> { instructions::update::handler(context, input) } } @@ -24,7 +27,7 @@ pub mod anchor_realloc { // `InitSpace` + `#[max_len(N)]` would force a fixed upper bound, defeating // the point of the example. Instead, `required_space` computes the exact // layout (discriminator + length prefix + bytes) for init/realloc. -#[account] +#[account(borsh)] pub struct Message { pub message: String, } diff --git a/basics/realloc/anchor/programs/anchor-realloc/tests/test_realloc.rs b/basics/realloc/anchor/programs/anchor-realloc/tests/test_realloc.rs index fecf1c6c1..cd6dca049 100644 --- a/basics/realloc/anchor/programs/anchor-realloc/tests/test_realloc.rs +++ b/basics/realloc/anchor/programs/anchor-realloc/tests/test_realloc.rs @@ -1,7 +1,6 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, InstructionData, ToAccountMetas, }, borsh::BorshDeserialize, litesvm::LiteSVM, @@ -16,7 +15,7 @@ struct MessageAccount { message: String, } -fn fetch_message(svm: &LiteSVM, pubkey: &anchor_lang::prelude::Pubkey) -> String { +fn fetch_message(svm: &LiteSVM, pubkey: &anchor_lang::prelude::Address) -> String { let account = svm.get_account(pubkey).unwrap(); let data = MessageAccount::try_from_slice(&account.data).unwrap(); data.message @@ -41,7 +40,7 @@ fn test_initialize() { anchor_realloc::accounts::InitializeAccountConstraints { payer: payer.pubkey(), message_account: message_keypair.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -82,7 +81,7 @@ fn test_update_grows() { anchor_realloc::accounts::InitializeAccountConstraints { payer: payer.pubkey(), message_account: message_keypair.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -105,7 +104,7 @@ fn test_update_grows() { anchor_realloc::accounts::UpdateAccountConstraints { payer: payer.pubkey(), message_account: message_keypair.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -139,7 +138,7 @@ fn test_update_shrinks() { anchor_realloc::accounts::InitializeAccountConstraints { payer: payer.pubkey(), message_account: message_keypair.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -162,7 +161,7 @@ fn test_update_shrinks() { anchor_realloc::accounts::UpdateAccountConstraints { payer: payer.pubkey(), message_account: message_keypair.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/basics/rent/anchor/programs/rent-example/Cargo.toml b/basics/rent/anchor/programs/rent-example/Cargo.toml index d63f77e9b..f67f2e2c1 100644 --- a/basics/rent/anchor/programs/rent-example/Cargo.toml +++ b/basics/rent/anchor/programs/rent-example/Cargo.toml @@ -20,13 +20,15 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" solana-signer = "3.0.0" solana-keypair = "3.0.1" -borsh = "1.6.1" solana-kite = "0.4.0" [lints.rust] diff --git a/basics/rent/anchor/programs/rent-example/src/lib.rs b/basics/rent/anchor/programs/rent-example/src/lib.rs index e2d8a840b..b97c4b08f 100644 --- a/basics/rent/anchor/programs/rent-example/src/lib.rs +++ b/basics/rent/anchor/programs/rent-example/src/lib.rs @@ -8,35 +8,37 @@ pub mod rent_example { use super::*; pub fn create_system_account( - context: Context, + context: &mut Context, address_data: AddressData, ) -> Result<()> { msg!("Program invoked. Creating a system account..."); msg!( " New public key will be: {}", - &context.accounts.new_account.key().to_string() + context.accounts.new_account.address() ); // Determine the necessary minimum rent by calculating the account's size // - // borsh 1.x: try_to_vec() removed, use borsh::to_vec() instead - let account_span = anchor_lang::prelude::borsh::to_vec(&address_data)?.len(); - let lamports_required = (Rent::get()?).minimum_balance(account_span); + // v2 encodes instruction data with wincode rather than borsh. `BorshConfig` + // is wincode's borsh-compatible wire format (fixed u32 little-endian length + // prefixes), so the span matches the bytes borsh would have produced. + let account_span = address_data.serialized_span()?; + let lamports_required = Rent::get()?.try_minimum_balance(account_span)?; - msg!("Account span: {}", &account_span); - msg!("Lamports required: {}", &lamports_required); + msg!("Account span: {}", account_span); + msg!("Lamports required: {}", lamports_required); system_program::create_account( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), system_program::CreateAccount { - from: context.accounts.payer.to_account_info(), - to: context.accounts.new_account.to_account_info(), + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.new_account.cpi_handle_mut(), }, ), lamports_required, account_span as u64, - &context.accounts.system_program.key(), + context.accounts.system_program.address(), )?; msg!("Account created successfully."); @@ -45,16 +47,25 @@ pub mod rent_example { } #[derive(Accounts)] -pub struct CreateSystemAccountAccountConstraints<'info> { +pub struct CreateSystemAccountAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account(mut)] - pub new_account: Signer<'info>, - pub system_program: Program<'info, System>, + pub new_account: Signer, + pub system_program: Program, } -#[derive(AnchorSerialize, AnchorDeserialize, Debug)] +#[derive(Clone, Debug, IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub struct AddressData { - name: String, - address: String, + pub name: String, + pub address: String, +} + +impl AddressData { + /// Bytes this struct occupies on the wire, which is what the new account + /// has to be sized (and therefore rent-funded) for. + fn serialized_span(&self) -> Result { + >::size_of(self) + .map_err(|_| ProgramError::InvalidInstructionData) + } } diff --git a/basics/rent/anchor/programs/rent-example/tests/test_rent.rs b/basics/rent/anchor/programs/rent-example/tests/test_rent.rs index 2451d0dcb..376f1ebc2 100644 --- a/basics/rent/anchor/programs/rent-example/tests/test_rent.rs +++ b/basics/rent/anchor/programs/rent-example/tests/test_rent.rs @@ -1,35 +1,13 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, InstructionData, ToAccountMetas, }, - borsh::BorshDeserialize, litesvm::LiteSVM, solana_keypair::Keypair, solana_kite::{create_wallet, send_transaction_from_instructions}, solana_signer::Signer, }; -/// Build borsh-serialized AddressData bytes (fields are private in the crate). -fn build_address_data_borsh(name: &str, address: &str) -> Vec { - let mut data = Vec::new(); - data.extend_from_slice(&(name.len() as u32).to_le_bytes()); - data.extend_from_slice(name.as_bytes()); - data.extend_from_slice(&(address.len() as u32).to_le_bytes()); - data.extend_from_slice(address.as_bytes()); - data -} - -/// Construct the full instruction data with discriminator + AddressData. -/// Deserialize the borsh bytes into AddressData via the crate's BorshDeserialize impl, -/// then use InstructionData to get the final bytes. -fn build_create_system_account_ix_data(name: &str, address: &str) -> Vec { - let address_data_bytes = build_address_data_borsh(name, address); - let address_data = - rent_example::AddressData::deserialize(&mut address_data_bytes.as_slice()).unwrap(); - rent_example::instruction::CreateSystemAccount { address_data }.data() -} - #[test] fn test_create_system_account() { let program_id = rent_example::id(); @@ -43,7 +21,13 @@ fn test_create_system_account() { let name = "Marcus"; let address = "123 Main St. San Francisco, CA"; - let ix_data = build_create_system_account_ix_data(name, address); + let ix_data = rent_example::instruction::CreateSystemAccount { + address_data: rent_example::AddressData { + name: name.to_string(), + address: address.to_string(), + }, + } + .data(); let instruction = Instruction::new_with_bytes( program_id, @@ -51,7 +35,7 @@ fn test_create_system_account() { rent_example::accounts::CreateSystemAccountAccountConstraints { payer: payer.pubkey(), new_account: new_account.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -65,7 +49,7 @@ fn test_create_system_account() { .unwrap(); // Verify the account was created with the correct size - // Borsh serialized AddressData: 4 + 6 ("Marcus") + 4 + 30 = 44 bytes + // Serialized AddressData: 4 + 6 ("Marcus") + 4 + 30 = 44 bytes let expected_size = 4 + name.len() + 4 + address.len(); let account = svm.get_account(&new_account.pubkey()).unwrap(); assert_eq!(account.data.len(), expected_size); diff --git a/basics/repository-layout/anchor/programs/carnival/Cargo.toml b/basics/repository-layout/anchor/programs/carnival/Cargo.toml index ff02c6a78..80bc8bd5b 100644 --- a/basics/repository-layout/anchor/programs/carnival/Cargo.toml +++ b/basics/repository-layout/anchor/programs/carnival/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/repository-layout/anchor/programs/carnival/src/instructions/eat_food.rs b/basics/repository-layout/anchor/programs/carnival/src/instructions/eat_food.rs index 2e3a62a06..555c6623a 100644 --- a/basics/repository-layout/anchor/programs/carnival/src/instructions/eat_food.rs +++ b/basics/repository-layout/anchor/programs/carnival/src/instructions/eat_food.rs @@ -34,5 +34,5 @@ pub fn eat_food(ix: EatFoodInstructionData) -> Result<()> { } } - Err(ProgramError::InvalidInstructionData.into()) + Err(ProgramError::InvalidInstructionData) } diff --git a/basics/repository-layout/anchor/programs/carnival/src/instructions/get_on_ride.rs b/basics/repository-layout/anchor/programs/carnival/src/instructions/get_on_ride.rs index 0427f0c36..43fb2f10b 100644 --- a/basics/repository-layout/anchor/programs/carnival/src/instructions/get_on_ride.rs +++ b/basics/repository-layout/anchor/programs/carnival/src/instructions/get_on_ride.rs @@ -50,5 +50,5 @@ pub fn get_on_ride(ix: GetOnRideInstructionData) -> Result<()> { } } - Err(ProgramError::InvalidInstructionData.into()) + Err(ProgramError::InvalidInstructionData) } diff --git a/basics/repository-layout/anchor/programs/carnival/src/instructions/play_game.rs b/basics/repository-layout/anchor/programs/carnival/src/instructions/play_game.rs index 4867f98e8..5613fc778 100644 --- a/basics/repository-layout/anchor/programs/carnival/src/instructions/play_game.rs +++ b/basics/repository-layout/anchor/programs/carnival/src/instructions/play_game.rs @@ -40,5 +40,5 @@ pub fn play_game(ix: PlayGameInstructionData) -> Result<()> { } } - Err(ProgramError::InvalidInstructionData.into()) + Err(ProgramError::InvalidInstructionData) } diff --git a/basics/repository-layout/anchor/programs/carnival/src/lib.rs b/basics/repository-layout/anchor/programs/carnival/src/lib.rs index 9d0bea35f..4a542c28a 100644 --- a/basics/repository-layout/anchor/programs/carnival/src/lib.rs +++ b/basics/repository-layout/anchor/programs/carnival/src/lib.rs @@ -15,7 +15,7 @@ pub mod carnival { use super::*; pub fn go_on_ride( - _context: Context, + _context: &mut Context, name: String, height: u32, ticket_count: u32, @@ -30,7 +30,7 @@ pub mod carnival { } pub fn play_game( - _context: Context, + _context: &mut Context, name: String, ticket_count: u32, game_name: String, @@ -43,7 +43,7 @@ pub mod carnival { } pub fn eat_food( - _context: Context, + _context: &mut Context, name: String, ticket_count: u32, food_stand_name: String, @@ -57,7 +57,7 @@ pub mod carnival { } #[derive(Accounts)] -pub struct CarnivalAccountConstraints<'info> { +pub struct CarnivalAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, } diff --git a/basics/transfer-sol/anchor/programs/transfer-sol/Cargo.toml b/basics/transfer-sol/anchor/programs/transfer-sol/Cargo.toml index cf282ac60..f162a6257 100644 --- a/basics/transfer-sol/anchor/programs/transfer-sol/Cargo.toml +++ b/basics/transfer-sol/anchor/programs/transfer-sol/Cargo.toml @@ -20,7 +20,10 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/basics/transfer-sol/anchor/programs/transfer-sol/src/instructions/transfer_sol_with_cpi.rs b/basics/transfer-sol/anchor/programs/transfer-sol/src/instructions/transfer_sol_with_cpi.rs index 4aa9ed0a9..84d600933 100644 --- a/basics/transfer-sol/anchor/programs/transfer-sol/src/instructions/transfer_sol_with_cpi.rs +++ b/basics/transfer-sol/anchor/programs/transfer-sol/src/instructions/transfer_sol_with_cpi.rs @@ -2,21 +2,24 @@ use anchor_lang::prelude::*; use anchor_lang::system_program; #[derive(Accounts)] -pub struct TransferSolWithCpiAccountConstraints<'info> { +pub struct TransferSolWithCpiAccountConstraints { #[account(mut)] - payer: Signer<'info>, + pub payer: Signer, #[account(mut)] - recipient: SystemAccount<'info>, - system_program: Program<'info, System>, + pub recipient: SystemAccount, + pub system_program: Program, } -pub fn handler(context: Context, amount: u64) -> Result<()> { +pub fn handler( + context: &mut Context, + amount: u64, +) -> Result<()> { system_program::transfer( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), system_program::Transfer { - from: context.accounts.payer.to_account_info(), - to: context.accounts.recipient.to_account_info(), + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.recipient.cpi_handle_mut(), }, ), amount, diff --git a/basics/transfer-sol/anchor/programs/transfer-sol/src/instructions/transfer_sol_with_program.rs b/basics/transfer-sol/anchor/programs/transfer-sol/src/instructions/transfer_sol_with_program.rs index 03d98b73b..2274f7138 100644 --- a/basics/transfer-sol/anchor/programs/transfer-sol/src/instructions/transfer_sol_with_program.rs +++ b/basics/transfer-sol/anchor/programs/transfer-sol/src/instructions/transfer_sol_with_program.rs @@ -9,36 +9,40 @@ pub enum TransferSolError { } #[derive(Accounts)] -pub struct TransferSolWithProgramAccountConstraints<'info> { +pub struct TransferSolWithProgramAccountConstraints { /// CHECK: Use owner constraint to check account is owned by our program #[account( mut, owner = crate::ID // value of declare_id!() )] - payer: UncheckedAccount<'info>, + pub payer: UncheckedAccount, #[account(mut)] - recipient: SystemAccount<'info>, + pub recipient: SystemAccount, } // Directly modifying lamports is only possible if the program is the owner of the account pub fn handler( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { let payer = &context.accounts.payer; let recipient = &context.accounts.recipient; let new_payer_lamports = payer - .lamports() + .get_lamports() .checked_sub(amount) .ok_or(TransferSolError::InsufficientFunds)?; let new_recipient_lamports = recipient - .lamports() + .get_lamports() .checked_add(amount) .ok_or(TransferSolError::AmountOverflow)?; - **payer.try_borrow_mut_lamports()? = new_payer_lamports; - **recipient.try_borrow_mut_lamports()? = new_recipient_lamports; + // `AccountView` is `Copy`, and a copy still points at the same backing + // buffer, so `set_lamports` writes through to the real account. + let mut payer_view = *payer.account(); + let mut recipient_view = *recipient.account(); + payer_view.set_lamports(new_payer_lamports); + recipient_view.set_lamports(new_recipient_lamports); Ok(()) } diff --git a/basics/transfer-sol/anchor/programs/transfer-sol/src/lib.rs b/basics/transfer-sol/anchor/programs/transfer-sol/src/lib.rs index 49f40cb3a..6c72a9ecc 100644 --- a/basics/transfer-sol/anchor/programs/transfer-sol/src/lib.rs +++ b/basics/transfer-sol/anchor/programs/transfer-sol/src/lib.rs @@ -10,14 +10,14 @@ pub mod transfer_sol { use super::*; pub fn transfer_sol_with_cpi( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { instructions::transfer_sol_with_cpi::handler(context, amount) } pub fn transfer_sol_with_program( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { instructions::transfer_sol_with_program::handler(context, amount) diff --git a/basics/transfer-sol/anchor/programs/transfer-sol/tests/test_transfer_sol.rs b/basics/transfer-sol/anchor/programs/transfer-sol/tests/test_transfer_sol.rs index 3afb86614..7847d442d 100644 --- a/basics/transfer-sol/anchor/programs/transfer-sol/tests/test_transfer_sol.rs +++ b/basics/transfer-sol/anchor/programs/transfer-sol/tests/test_transfer_sol.rs @@ -1,7 +1,6 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, @@ -30,7 +29,7 @@ fn test_transfer_sol_with_cpi() { transfer_sol::accounts::TransferSolWithCpiAccountConstraints { payer: payer.pubkey(), recipient: recipient.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/compression/cnft-burn/anchor/programs/cnft-burn/Cargo.toml b/compression/cnft-burn/anchor/programs/cnft-burn/Cargo.toml index e46e7e802..ff89b4f26 100644 --- a/compression/cnft-burn/anchor/programs/cnft-burn/Cargo.toml +++ b/compression/cnft-burn/anchor/programs/cnft-burn/Cargo.toml @@ -20,11 +20,21 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +# `borsh` adds the BorshSerialize/BorshDeserialize impls for `Address` that the +# vendored Bubblegum types need; pinocchio re-exports this same type. +solana-address = { version = ">=2.6, <2.7", features = ["borsh"] } # mpl-bubblegum and spl-account-compression removed: they depend on solana-program 2.x # which is incompatible with Anchor 1.0's solana 3.x types. CPI calls are built manually # using raw invoke() with hardcoded program IDs and discriminators. -borsh = "1" +borsh = { version = "1", features = ["derive"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/compression/cnft-burn/anchor/programs/cnft-burn/src/instructions/burn_cnft.rs b/compression/cnft-burn/anchor/programs/cnft-burn/src/instructions/burn_cnft.rs index 595a81da9..42592eab8 100644 --- a/compression/cnft-burn/anchor/programs/cnft-burn/src/instructions/burn_cnft.rs +++ b/compression/cnft-burn/anchor/programs/cnft-burn/src/instructions/burn_cnft.rs @@ -5,7 +5,7 @@ use anchor_lang::solana_program::{ }; use borsh::BorshSerialize; -use crate::{MPL_BUBBLEGUM_ID, SPLCompression}; +use crate::{SPLCompression, MPL_BUBBLEGUM_ID}; /// Burn instruction discriminator from mpl-bubblegum const BURN_DISCRIMINATOR: [u8; 8] = [116, 110, 29, 56, 107, 219, 42, 93]; @@ -21,42 +21,46 @@ struct BurnArgs { } #[derive(Accounts)] -pub struct BurnCnftAccountConstraints<'info> { +pub struct BurnCnftAccountConstraints { #[account(mut)] - pub leaf_owner: Signer<'info>, + pub leaf_owner: Signer, #[account(mut)] #[account( - seeds = [merkle_tree.key().as_ref()], + seeds = [merkle_tree.address().as_ref()], bump, - seeds::program = bubblegum_program.key() + seeds::program = bubblegum_program.address() )] /// CHECK: This account is modified in the downstream program - pub tree_authority: UncheckedAccount<'info>, + pub tree_authority: UncheckedAccount, #[account(mut)] /// CHECK: Written by the Bubblegum/Account Compression CPI (the burn /// replaces the leaf and updates the tree root); validated downstream /// by those programs. - pub merkle_tree: UncheckedAccount<'info>, + pub merkle_tree: UncheckedAccount, /// CHECK: This account is neither written to nor read from. - pub log_wrapper: UncheckedAccount<'info>, - pub compression_program: Program<'info, SPLCompression>, + pub log_wrapper: UncheckedAccount, + pub compression_program: Program, // Pin the bubblegum program account to the known mpl-bubblegum id. Without // this constraint the caller could pass any account and a malicious one // could short-circuit the CPI in unexpected ways. /// CHECK: address constrained to the mpl-bubblegum program id. #[account(address = MPL_BUBBLEGUM_ID)] - pub bubblegum_program: UncheckedAccount<'info>, - pub system_program: Program<'info, System>, + pub bubblegum_program: UncheckedAccount, + pub system_program: Program, } -pub fn handle_burn_cnft<'info>( - context: Context<'info, BurnCnftAccountConstraints<'info>>, +pub fn handle_burn_cnft( + context: &mut Context, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, ) -> Result<()> { + // `remaining_accounts()` walks the input cursor and returns an owned vec, + // so take it once up front and use the local everywhere below. + let proof_accounts = context.remaining_accounts()?; + // Build instruction data: discriminator + borsh-serialized args let args = BurnArgs { root, @@ -66,39 +70,43 @@ pub fn handle_burn_cnft<'info>( index, }; let mut data = BURN_DISCRIMINATOR.to_vec(); - args.serialize(&mut data)?; + args.serialize(&mut data) + .map_err(|_| ProgramError::InvalidInstructionData)?; // Build account metas matching mpl-bubblegum Burn instruction layout - let mut accounts = Vec::with_capacity(7 + context.remaining_accounts.len()); + let mut accounts = Vec::with_capacity(7 + proof_accounts.len()); accounts.push(AccountMeta::new_readonly( - context.accounts.tree_authority.key(), + *context.accounts.tree_authority.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.leaf_owner.key(), + *context.accounts.leaf_owner.address(), true, )); // leaf_delegate = leaf_owner, not a signer in this call accounts.push(AccountMeta::new_readonly( - context.accounts.leaf_owner.key(), + *context.accounts.leaf_owner.address(), + false, + )); + accounts.push(AccountMeta::new( + *context.accounts.merkle_tree.address(), false, )); - accounts.push(AccountMeta::new(context.accounts.merkle_tree.key(), false)); accounts.push(AccountMeta::new_readonly( - context.accounts.log_wrapper.key(), + *context.accounts.log_wrapper.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.compression_program.key(), + *context.accounts.compression_program.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.system_program.key(), + *context.accounts.system_program.address(), false, )); // Append remaining accounts (proof nodes) - for acc in context.remaining_accounts.iter() { - accounts.push(AccountMeta::new_readonly(acc.key(), false)); + for acc in proof_accounts.iter() { + accounts.push(AccountMeta::new_readonly(*acc.address(), false)); } let instruction = Instruction { @@ -107,18 +115,23 @@ pub fn handle_burn_cnft<'info>( data, }; - // Gather all account infos for the CPI - let mut account_infos = vec![ - context.accounts.bubblegum_program.to_account_info(), - context.accounts.tree_authority.to_account_info(), - context.accounts.leaf_owner.to_account_info(), - context.accounts.merkle_tree.to_account_info(), - context.accounts.log_wrapper.to_account_info(), - context.accounts.compression_program.to_account_info(), - context.accounts.system_program.to_account_info(), + // Account handles have to line up positionally with the instruction's + // account metas: v2's `invoke` matches each meta to the next handle in + // order, so the program account is not listed and an account that fills + // two slots supplies two handles. + let mut account_infos: Vec = vec![ + context.accounts.tree_authority.cpi_handle_mut().into(), + // leaf_owner also fills the leaf_delegate slot; both metas are + // read-only, so read-only handles satisfy them + context.accounts.leaf_owner.cpi_handle(), + context.accounts.leaf_owner.cpi_handle(), + context.accounts.merkle_tree.cpi_handle_mut().into(), + context.accounts.log_wrapper.cpi_handle(), + context.accounts.compression_program.cpi_handle(), + context.accounts.system_program.cpi_handle(), ]; - for acc in context.remaining_accounts.iter() { - account_infos.push(acc.to_account_info()); + for acc in proof_accounts.iter() { + account_infos.push(CpiHandle::readonly(acc)); } invoke(&instruction, &account_infos)?; diff --git a/compression/cnft-burn/anchor/programs/cnft-burn/src/lib.rs b/compression/cnft-burn/anchor/programs/cnft-burn/src/lib.rs index 7bd40193c..529f0673e 100644 --- a/compression/cnft-burn/anchor/programs/cnft-burn/src/lib.rs +++ b/compression/cnft-burn/anchor/programs/cnft-burn/src/lib.rs @@ -11,17 +11,17 @@ use instructions::*; declare_id!("C6qxH8n6mZxrrbtMtYWYSp8JR8vkQ55X1o4EBg7twnMv"); /// mpl-bubblegum program ID -pub const MPL_BUBBLEGUM_ID: Pubkey = pubkey!("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY"); +pub const MPL_BUBBLEGUM_ID: Address = pubkey!("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY"); /// SPL Account Compression program ID -pub const SPL_ACCOUNT_COMPRESSION_ID: Pubkey = +pub const SPL_ACCOUNT_COMPRESSION_ID: Address = pubkey!("cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK"); #[derive(Clone)] pub struct SPLCompression; impl anchor_lang::Id for SPLCompression { - fn id() -> Pubkey { + fn id() -> Address { SPL_ACCOUNT_COMPRESSION_ID } } @@ -30,14 +30,21 @@ impl anchor_lang::Id for SPLCompression { pub mod cnft_burn { use super::*; - pub fn burn_cnft<'info>( - context: Context<'info, BurnCnftAccountConstraints<'info>>, + pub fn burn_cnft( + context: &mut Context, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, ) -> Result<()> { - instructions::burn_cnft::handle_burn_cnft(context, root, data_hash, creator_hash, nonce, index) + instructions::burn_cnft::handle_burn_cnft( + context, + root, + data_hash, + creator_hash, + nonce, + index, + ) } } diff --git a/compression/cnft-vault/anchor/README.md b/compression/cnft-vault/anchor/README.md index 0bf600222..26096f2d4 100644 --- a/compression/cnft-vault/anchor/README.md +++ b/compression/cnft-vault/anchor/README.md @@ -6,7 +6,7 @@ The program keeps a PDA-owned vault. You send cNFTs to the vault, then the vault ## Authority model -Deposits are plain Bubblegum transfers to the **vault PDA** (seeds `["cNFT-vault"]`); no program instruction runs on deposit. Because of that, withdraw authorization is per-vault, not per-deposit: `initialize_vault` creates the vault PDA as a `Vault` state account and stores the signer as its **authority**. Both withdraw handlers require that stored authority as a `Signer` (`has_one = authority`) and reject any other signer with `VaultError::InvalidWithdrawAuthority` before the Bubblegum CPI runs. The same PDA doubles as the Bubblegum leaf owner and signs the transfer CPIs via `invoke_signed`. +Deposits are plain Bubblegum transfers to the **vault PDA** (seeds `["cNFT-vault"]`); no program instruction runs on deposit. Because of that, withdraw authorization is per-vault, not per-deposit: `initialize_vault` creates the vault PDA as a `Vault` state account and stores the signer as its **authority**. Both withdraw handlers require that stored authority as a `Signer` (`address = vault.authority`) and reject any other signer with `VaultError::InvalidWithdrawAuthority` before the Bubblegum CPI runs. The same PDA doubles as the Bubblegum leaf owner and signs the transfer CPIs via `invoke_signed`. Three handlers: diff --git a/compression/cnft-vault/anchor/programs/cnft-vault/Cargo.toml b/compression/cnft-vault/anchor/programs/cnft-vault/Cargo.toml index 4e54a8190..56ccfe2a9 100644 --- a/compression/cnft-vault/anchor/programs/cnft-vault/Cargo.toml +++ b/compression/cnft-vault/anchor/programs/cnft-vault/Cargo.toml @@ -20,11 +20,21 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +# `borsh` adds the BorshSerialize/BorshDeserialize impls for `Address` that the +# vendored Bubblegum types need; pinocchio re-exports this same type. +solana-address = { version = ">=2.6, <2.7", features = ["borsh"] } # mpl-bubblegum and spl-account-compression removed: they depend on solana-program 2.x # which is incompatible with Anchor 1.0's solana 3.x types. CPI calls are built manually # using raw invoke_signed() with hardcoded program IDs and discriminators. -borsh = "1" +borsh = { version = "1", features = ["derive"] } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/initialize_vault.rs b/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/initialize_vault.rs index 58cf3389b..8b57fea9a 100644 --- a/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/initialize_vault.rs +++ b/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/initialize_vault.rs @@ -3,9 +3,9 @@ use anchor_lang::prelude::*; use crate::state::{Vault, VAULT_SEED}; #[derive(Accounts)] -pub struct InitializeVaultAccountConstraints<'info> { +pub struct InitializeVaultAccountConstraints { #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, #[account( init, @@ -14,14 +14,14 @@ pub struct InitializeVaultAccountConstraints<'info> { seeds = [VAULT_SEED], bump, )] - pub vault: Account<'info, Vault>, + pub vault: BorshAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } -pub fn handler(context: Context) -> Result<()> { +pub fn handler(context: &mut Context) -> Result<()> { let vault = &mut context.accounts.vault; - vault.authority = context.accounts.authority.key(); + vault.authority = *context.accounts.authority.address(); vault.bump = context.bumps.vault; Ok(()) } diff --git a/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/withdraw_cnft.rs b/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/withdraw_cnft.rs index 79dfaed97..9040244f8 100644 --- a/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/withdraw_cnft.rs +++ b/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/withdraw_cnft.rs @@ -6,51 +6,49 @@ use crate::state::{Vault, VAULT_SEED}; use crate::{build_transfer_instruction, SPLCompression, TransferArgs, MPL_BUBBLEGUM_ID}; #[derive(Accounts)] -pub struct WithdrawCnftAccountConstraints<'info> { +pub struct WithdrawCnftAccountConstraints { /// The stored vault authority. Only this signer may withdraw. - pub authority: Signer<'info>, + #[account(address = vault.authority @ VaultError::InvalidWithdrawAuthority)] + pub authority: Signer, // The vault PDA owns the cNFTs (as Bubblegum leaf owner) and signs the // transfer CPI via invoke_signed. - #[account( - seeds = [VAULT_SEED], - bump = vault.bump, - has_one = authority @ VaultError::InvalidWithdrawAuthority, - )] - pub vault: Account<'info, Vault>, + #[account(seeds = [VAULT_SEED], + bump = vault.bump)] + pub vault: BorshAccount, #[account(mut)] #[account( - seeds = [merkle_tree.key().as_ref()], + seeds = [merkle_tree.address().as_ref()], bump, - seeds::program = bubblegum_program.key() + seeds::program = bubblegum_program.address() )] /// CHECK: This account is modified in the downstream program - pub tree_authority: UncheckedAccount<'info>, + pub tree_authority: UncheckedAccount, /// CHECK: This account is neither written to nor read from. - pub new_leaf_owner: UncheckedAccount<'info>, + pub new_leaf_owner: UncheckedAccount, #[account(mut)] /// CHECK: This account is modified in the downstream program - pub merkle_tree: UncheckedAccount<'info>, + pub merkle_tree: UncheckedAccount, /// CHECK: This account is neither written to nor read from. - pub log_wrapper: UncheckedAccount<'info>, + pub log_wrapper: UncheckedAccount, - pub compression_program: Program<'info, SPLCompression>, + pub compression_program: Program, // Pin the bubblegum program account to the known mpl-bubblegum id. Without // this constraint the caller could pass any account to the CPI. /// CHECK: address constrained to the mpl-bubblegum program id. #[account(address = MPL_BUBBLEGUM_ID)] - pub bubblegum_program: UncheckedAccount<'info>, + pub bubblegum_program: UncheckedAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } -pub fn handler<'info>( - context: Context<'info, WithdrawCnftAccountConstraints<'info>>, +pub fn handler( + context: &mut Context, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], @@ -60,24 +58,30 @@ pub fn handler<'info>( msg!( "attempting to send nft {} from tree {}", index, - context.accounts.merkle_tree.key() + context.accounts.merkle_tree.address() ); - let proof_metas: Vec = context - .remaining_accounts + // `remaining_accounts()` returns an owned vec; take it once so the proof + // views stay alive for the CPI below. + let proof_accounts = context.remaining_accounts()?; + + // Read the bump before the CPI handles take a mutable borrow of `vault`. + let vault_bump = context.accounts.vault.bump; + + let proof_metas: Vec = proof_accounts .iter() - .map(|acc| AccountMeta::new_readonly(acc.key(), false)) + .map(|acc| AccountMeta::new_readonly(*acc.address(), false)) .collect(); let instruction = build_transfer_instruction( - context.accounts.tree_authority.key(), - context.accounts.vault.key(), - context.accounts.vault.key(), - context.accounts.new_leaf_owner.key(), - context.accounts.merkle_tree.key(), - context.accounts.log_wrapper.key(), - context.accounts.compression_program.key(), - context.accounts.system_program.key(), + *context.accounts.tree_authority.address(), + *context.accounts.vault.address(), + *context.accounts.vault.address(), + *context.accounts.new_leaf_owner.address(), + *context.accounts.merkle_tree.address(), + *context.accounts.log_wrapper.address(), + *context.accounts.compression_program.address(), + *context.accounts.system_program.address(), &proof_metas, TransferArgs { root, @@ -88,25 +92,34 @@ pub fn handler<'info>( }, )?; - // Gather all account infos for the CPI - let mut account_infos = vec![ - context.accounts.bubblegum_program.to_account_info(), - context.accounts.tree_authority.to_account_info(), - context.accounts.vault.to_account_info(), - context.accounts.new_leaf_owner.to_account_info(), - context.accounts.merkle_tree.to_account_info(), - context.accounts.log_wrapper.to_account_info(), - context.accounts.compression_program.to_account_info(), - context.accounts.system_program.to_account_info(), + // `vault` signs the transfer, so its data borrow has to be handed back to + // the runtime before the CPI. It is read-only here and nothing reads it + // afterwards, so there is no reacquire. + context.accounts.vault.release_borrow()?; + + // Account handles have to line up positionally with the instruction's + // account metas: v2's `invoke` matches each meta to the next handle in + // order, so the program account is not listed and an account that fills + // two slots supplies two handles. + let mut account_infos: Vec = vec![ + context.accounts.tree_authority.cpi_handle_mut().into(), + // leaf_owner and leaf_delegate are both the vault PDA + context.accounts.vault.cpi_handle(), + context.accounts.vault.cpi_handle(), + context.accounts.new_leaf_owner.cpi_handle(), + context.accounts.merkle_tree.cpi_handle_mut().into(), + context.accounts.log_wrapper.cpi_handle(), + context.accounts.compression_program.cpi_handle(), + context.accounts.system_program.cpi_handle(), ]; - for acc in context.remaining_accounts.iter() { - account_infos.push(acc.to_account_info()); + for acc in proof_accounts.iter() { + account_infos.push(CpiHandle::readonly(acc)); } invoke_signed( &instruction, &account_infos, - &[&[VAULT_SEED, &[context.accounts.vault.bump]]], + &[&[VAULT_SEED, &[vault_bump]]], )?; Ok(()) diff --git a/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/withdraw_two_cnfts.rs b/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/withdraw_two_cnfts.rs index fadae6e01..18a82d112 100644 --- a/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/withdraw_two_cnfts.rs +++ b/compression/cnft-vault/anchor/programs/cnft-vault/src/instructions/withdraw_two_cnfts.rs @@ -6,68 +6,66 @@ use crate::state::{Vault, VAULT_SEED}; use crate::{build_transfer_instruction, SPLCompression, TransferArgs, MPL_BUBBLEGUM_ID}; #[derive(Accounts)] -pub struct WithdrawTwoCnftsAccountConstraints<'info> { +pub struct WithdrawTwoCnftsAccountConstraints { /// The stored vault authority. Only this signer may withdraw. - pub authority: Signer<'info>, + #[account(address = vault.authority @ VaultError::InvalidWithdrawAuthority)] + pub authority: Signer, // The vault PDA owns the cNFTs (as Bubblegum leaf owner) and signs both // transfer CPIs via invoke_signed. - #[account( - seeds = [VAULT_SEED], - bump = vault.bump, - has_one = authority @ VaultError::InvalidWithdrawAuthority, - )] - pub vault: Account<'info, Vault>, + #[account(seeds = [VAULT_SEED], + bump = vault.bump)] + pub vault: BorshAccount, #[account(mut)] #[account( - seeds = [merkle_tree1.key().as_ref()], + seeds = [merkle_tree1.address().as_ref()], bump, - seeds::program = bubblegum_program.key() + seeds::program = bubblegum_program.address() )] /// CHECK: This account is modified in the downstream program - pub tree_authority1: UncheckedAccount<'info>, + pub tree_authority1: UncheckedAccount, /// CHECK: This account is neither written to nor read from. - pub new_leaf_owner1: UncheckedAccount<'info>, + pub new_leaf_owner1: UncheckedAccount, #[account(mut)] /// CHECK: This account is modified in the downstream program - pub merkle_tree1: UncheckedAccount<'info>, + pub merkle_tree1: UncheckedAccount, #[account(mut)] #[account( - seeds = [merkle_tree2.key().as_ref()], + seeds = [merkle_tree2.address().as_ref()], bump, - seeds::program = bubblegum_program.key() + seeds::program = bubblegum_program.address() )] /// CHECK: This account is modified in the downstream program - pub tree_authority2: UncheckedAccount<'info>, + pub tree_authority2: UncheckedAccount, /// CHECK: This account is neither written to nor read from. - pub new_leaf_owner2: UncheckedAccount<'info>, + pub new_leaf_owner2: UncheckedAccount, #[account(mut)] /// CHECK: This account is modified in the downstream program - pub merkle_tree2: UncheckedAccount<'info>, + pub merkle_tree2: UncheckedAccount, /// CHECK: This account is neither written to nor read from. - pub log_wrapper: UncheckedAccount<'info>, + pub log_wrapper: UncheckedAccount, - pub compression_program: Program<'info, SPLCompression>, + pub compression_program: Program, // Pin the bubblegum program account to the known mpl-bubblegum id. Without // this constraint the caller could pass any account to the two CPI calls. /// CHECK: address constrained to the mpl-bubblegum program id. #[account(address = MPL_BUBBLEGUM_ID)] - pub bubblegum_program: UncheckedAccount<'info>, + pub bubblegum_program: UncheckedAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } #[allow(clippy::too_many_arguments)] -pub fn handler<'info>( - context: Context<'info, WithdrawTwoCnftsAccountConstraints<'info>>, +pub fn handler( + context: &mut Context, root1: [u8; 32], data_hash1: [u8; 32], creator_hash1: [u8; 32], @@ -81,8 +79,8 @@ pub fn handler<'info>( index2: u32, proof_2_length: u8, ) -> Result<()> { - let merkle_tree1 = context.accounts.merkle_tree1.key(); - let merkle_tree2 = context.accounts.merkle_tree2.key(); + let merkle_tree1 = context.accounts.merkle_tree1.address(); + let merkle_tree2 = context.accounts.merkle_tree2.address(); msg!( "attempting to send nfts from trees {} and {}", merkle_tree1, @@ -92,41 +90,52 @@ pub fn handler<'info>( // The proof lengths are client-supplied: bounds-check them against the // accounts actually provided before slicing, so adversarial input gets a // clean named error instead of a panic. + // `remaining_accounts()` returns an owned vec; take it once so the proof + // views stay alive for both CPIs below. + let proof_accounts = context.remaining_accounts()?; + let proof_1_length = proof_1_length as usize; let proof_2_length = proof_2_length as usize; require!( proof_1_length .checked_add(proof_2_length) - .is_some_and(|total| total == context.remaining_accounts.len()), + .is_some_and(|total| total == proof_accounts.len()), VaultError::ProofLengthMismatch ); - let signer_seeds: &[&[u8]] = &[VAULT_SEED, &[context.accounts.vault.bump]]; + // Read the bump before the CPI handles take a mutable borrow of `vault`. + let vault_bump = context.accounts.vault.bump; + let signer_seeds: &[&[u8]] = &[VAULT_SEED, &[vault_bump]]; // Split remaining accounts into proof1 and proof2 - let (proof1_accounts, proof2_accounts) = context.remaining_accounts.split_at(proof_1_length); + let (proof1_accounts, proof2_accounts) = proof_accounts.split_at(proof_1_length); let proof1_metas: Vec = proof1_accounts .iter() - .map(|acc| AccountMeta::new_readonly(acc.key(), false)) + .map(|acc| AccountMeta::new_readonly(*acc.address(), false)) .collect(); let proof2_metas: Vec = proof2_accounts .iter() - .map(|acc| AccountMeta::new_readonly(acc.key(), false)) + .map(|acc| AccountMeta::new_readonly(*acc.address(), false)) .collect(); + // `vault` signs both transfers, so its data borrow has to be handed back to + // the runtime before the CPIs. It is read-only here and nothing reads it + // afterwards, so there is no reacquire. + context.accounts.vault.release_borrow()?; + // Withdraw cNFT#1 msg!("withdrawing cNFT#1"); let instruction1 = build_transfer_instruction( - context.accounts.tree_authority1.key(), - context.accounts.vault.key(), - context.accounts.vault.key(), - context.accounts.new_leaf_owner1.key(), - context.accounts.merkle_tree1.key(), - context.accounts.log_wrapper.key(), - context.accounts.compression_program.key(), - context.accounts.system_program.key(), + *context.accounts.tree_authority1.address(), + *context.accounts.vault.address(), + *context.accounts.vault.address(), + *context.accounts.new_leaf_owner1.address(), + *context.accounts.merkle_tree1.address(), + *context.accounts.log_wrapper.address(), + *context.accounts.compression_program.address(), + *context.accounts.system_program.address(), &proof1_metas, TransferArgs { root: root1, @@ -137,18 +146,21 @@ pub fn handler<'info>( }, )?; - let mut account_infos1 = vec![ - context.accounts.bubblegum_program.to_account_info(), - context.accounts.tree_authority1.to_account_info(), - context.accounts.vault.to_account_info(), - context.accounts.new_leaf_owner1.to_account_info(), - context.accounts.merkle_tree1.to_account_info(), - context.accounts.log_wrapper.to_account_info(), - context.accounts.compression_program.to_account_info(), - context.accounts.system_program.to_account_info(), + // Handles line up positionally with the instruction's metas: the program + // account is not listed, and the vault fills both the leaf_owner and + // leaf_delegate slots. + let mut account_infos1: Vec = vec![ + context.accounts.tree_authority1.cpi_handle_mut().into(), + context.accounts.vault.cpi_handle(), + context.accounts.vault.cpi_handle(), + context.accounts.new_leaf_owner1.cpi_handle(), + context.accounts.merkle_tree1.cpi_handle_mut().into(), + context.accounts.log_wrapper.cpi_handle(), + context.accounts.compression_program.cpi_handle(), + context.accounts.system_program.cpi_handle(), ]; for acc in proof1_accounts.iter() { - account_infos1.push(acc.to_account_info()); + account_infos1.push(CpiHandle::readonly(acc)); } invoke_signed(&instruction1, &account_infos1, &[signer_seeds])?; @@ -156,14 +168,14 @@ pub fn handler<'info>( // Withdraw cNFT#2 msg!("withdrawing cNFT#2"); let instruction2 = build_transfer_instruction( - context.accounts.tree_authority2.key(), - context.accounts.vault.key(), - context.accounts.vault.key(), - context.accounts.new_leaf_owner2.key(), - context.accounts.merkle_tree2.key(), - context.accounts.log_wrapper.key(), - context.accounts.compression_program.key(), - context.accounts.system_program.key(), + *context.accounts.tree_authority2.address(), + *context.accounts.vault.address(), + *context.accounts.vault.address(), + *context.accounts.new_leaf_owner2.address(), + *context.accounts.merkle_tree2.address(), + *context.accounts.log_wrapper.address(), + *context.accounts.compression_program.address(), + *context.accounts.system_program.address(), &proof2_metas, TransferArgs { root: root2, @@ -174,18 +186,21 @@ pub fn handler<'info>( }, )?; - let mut account_infos2 = vec![ - context.accounts.bubblegum_program.to_account_info(), - context.accounts.tree_authority2.to_account_info(), - context.accounts.vault.to_account_info(), - context.accounts.new_leaf_owner2.to_account_info(), - context.accounts.merkle_tree2.to_account_info(), - context.accounts.log_wrapper.to_account_info(), - context.accounts.compression_program.to_account_info(), - context.accounts.system_program.to_account_info(), + // Handles line up positionally with the instruction's metas: the program + // account is not listed, and the vault fills both the leaf_owner and + // leaf_delegate slots. + let mut account_infos2: Vec = vec![ + context.accounts.tree_authority2.cpi_handle_mut().into(), + context.accounts.vault.cpi_handle(), + context.accounts.vault.cpi_handle(), + context.accounts.new_leaf_owner2.cpi_handle(), + context.accounts.merkle_tree2.cpi_handle_mut().into(), + context.accounts.log_wrapper.cpi_handle(), + context.accounts.compression_program.cpi_handle(), + context.accounts.system_program.cpi_handle(), ]; for acc in proof2_accounts.iter() { - account_infos2.push(acc.to_account_info()); + account_infos2.push(CpiHandle::readonly(acc)); } invoke_signed(&instruction2, &account_infos2, &[signer_seeds])?; diff --git a/compression/cnft-vault/anchor/programs/cnft-vault/src/lib.rs b/compression/cnft-vault/anchor/programs/cnft-vault/src/lib.rs index f86065d06..0763d752c 100644 --- a/compression/cnft-vault/anchor/programs/cnft-vault/src/lib.rs +++ b/compression/cnft-vault/anchor/programs/cnft-vault/src/lib.rs @@ -12,13 +12,13 @@ use instructions::*; declare_id!("Fd4iwpPWaCU8BNwGQGtvvrcvG4Tfizq3RgLm8YLBJX6D"); /// mpl-bubblegum program ID (BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY) -const MPL_BUBBLEGUM_ID: Pubkey = Pubkey::new_from_array([ +const MPL_BUBBLEGUM_ID: Address = Address::new_from_array([ 0x98, 0x8b, 0x80, 0xeb, 0x79, 0x35, 0x28, 0x69, 0xb2, 0x24, 0x74, 0x5f, 0x59, 0xdd, 0xbf, 0x8a, 0x26, 0x58, 0xca, 0x13, 0xdc, 0x68, 0x81, 0x21, 0x26, 0x35, 0x1c, 0xae, 0x07, 0xc1, 0xa5, 0xa5, ]); /// SPL Account Compression program ID (cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK) -const SPL_ACCOUNT_COMPRESSION_ID: Pubkey = Pubkey::new_from_array([ +const SPL_ACCOUNT_COMPRESSION_ID: Address = Address::new_from_array([ 0x09, 0x2a, 0x13, 0xee, 0x95, 0xc4, 0x1c, 0xba, 0x08, 0xa6, 0x7f, 0x5a, 0xc6, 0x7e, 0x8d, 0xf7, 0xe1, 0xda, 0x11, 0x62, 0x5e, 0x1d, 0x64, 0x13, 0x7f, 0x8f, 0x4f, 0x23, 0x83, 0x03, 0x7f, 0x14, ]); @@ -40,23 +40,23 @@ pub struct TransferArgs { pub struct SPLCompression; impl anchor_lang::Id for SPLCompression { - fn id() -> Pubkey { + fn id() -> Address { SPL_ACCOUNT_COMPRESSION_ID } } /// Build a mpl-bubblegum Transfer instruction from pubkeys and args. -/// This avoids using mpl-bubblegum's CPI wrapper which requires solana-program 2.x AccountInfo. +/// This avoids using mpl-bubblegum's CPI wrapper which requires solana-program 2.x AccountView. #[allow(clippy::too_many_arguments)] pub fn build_transfer_instruction( - tree_config: Pubkey, - leaf_owner: Pubkey, - leaf_delegate: Pubkey, - new_leaf_owner: Pubkey, - merkle_tree: Pubkey, - log_wrapper: Pubkey, - compression_program: Pubkey, - system_program: Pubkey, + tree_config: Address, + leaf_owner: Address, + leaf_delegate: Address, + new_leaf_owner: Address, + merkle_tree: Address, + log_wrapper: Address, + compression_program: Address, + system_program: Address, remaining_accounts: &[AccountMeta], args: TransferArgs, ) -> Result { @@ -74,7 +74,8 @@ pub fn build_transfer_instruction( accounts.extend_from_slice(remaining_accounts); let mut data = TRANSFER_DISCRIMINATOR.to_vec(); - args.serialize(&mut data)?; + args.serialize(&mut data) + .map_err(|_| ProgramError::InvalidInstructionData)?; Ok(Instruction { program_id: MPL_BUBBLEGUM_ID, @@ -87,12 +88,14 @@ pub fn build_transfer_instruction( pub mod cnft_vault { use super::*; - pub fn initialize_vault(context: Context) -> Result<()> { + pub fn initialize_vault( + context: &mut Context, + ) -> Result<()> { instructions::initialize_vault::handler(context) } - pub fn withdraw_cnft<'info>( - context: Context<'info, WithdrawCnftAccountConstraints<'info>>, + pub fn withdraw_cnft( + context: &mut Context, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], @@ -103,8 +106,8 @@ pub mod cnft_vault { } #[allow(clippy::too_many_arguments)] - pub fn withdraw_two_cnfts<'info>( - context: Context<'info, WithdrawTwoCnftsAccountConstraints<'info>>, + pub fn withdraw_two_cnfts( + context: &mut Context, root1: [u8; 32], data_hash1: [u8; 32], creator_hash1: [u8; 32], diff --git a/compression/cnft-vault/anchor/programs/cnft-vault/src/state/vault.rs b/compression/cnft-vault/anchor/programs/cnft-vault/src/state/vault.rs index 5ce304df8..4413f5bd3 100644 --- a/compression/cnft-vault/anchor/programs/cnft-vault/src/state/vault.rs +++ b/compression/cnft-vault/anchor/programs/cnft-vault/src/state/vault.rs @@ -5,10 +5,10 @@ use anchor_lang::prelude::*; pub const VAULT_SEED: &[u8] = b"cNFT-vault"; #[derive(InitSpace)] -#[account] +#[account(borsh)] pub struct Vault { /// The only signer allowed to withdraw cNFTs from the vault. - pub authority: Pubkey, + pub authority: Address, pub bump: u8, } diff --git a/compression/cnft-vault/anchor/programs/cnft-vault/tests/test_vault.rs b/compression/cnft-vault/anchor/programs/cnft-vault/tests/test_vault.rs index 9e0df7d91..44183400a 100644 --- a/compression/cnft-vault/anchor/programs/cnft-vault/tests/test_vault.rs +++ b/compression/cnft-vault/anchor/programs/cnft-vault/tests/test_vault.rs @@ -268,7 +268,10 @@ fn assert_custom_error( expected: VaultError, ) { let failed = result.expect_err("transaction should fail"); - let expected_code = u32::from(expected); + // v2's `#[error_code]` makes the enum `#[repr(u32)]` and only generates + // `From for anchor_lang::Error`, so the on-wire custom code is + // the discriminant plus the default 6000 offset. + let expected_code = expected as u32 + 6000; let error_text = format!("{:?}", failed.err); assert!( error_text.contains(&format!("Custom({expected_code})")), @@ -476,11 +479,7 @@ fn create_tree_with_vault_cnft(context: &mut VaultTestContext) -> TreeWithVaultC let proof = [empty_node(0), empty_node(1), empty_node(2)]; // Read the current root from the onchain tree account. - let tree_data = context - .svm - .get_account(&merkle_tree.pubkey()) - .unwrap() - .data; + let tree_data = context.svm.get_account(&merkle_tree.pubkey()).unwrap().data; let root = read_current_root(&tree_data); TreeWithVaultCnft { @@ -608,12 +607,8 @@ fn test_withdraw_cnft_by_authority() { let recipient = Keypair::new(); let authority = context.authority.insecure_clone(); - let withdraw_ix = build_withdraw_cnft_instruction( - &context, - authority.pubkey(), - &tree, - recipient.pubkey(), - ); + let withdraw_ix = + build_withdraw_cnft_instruction(&context, authority.pubkey(), &tree, recipient.pubkey()); // The stored authority signs, so the withdraw succeeds (the vault PDA // signs the Bubblegum CPI via invoke_signed inside the program). @@ -692,12 +687,8 @@ fn test_withdraw_two_cnfts_by_authority() { // Both trees' roots moved, so both cNFTs left the vault: replaying the // single-tree withdraw against either tree with the cached roots fails. - let replay1 = build_withdraw_cnft_instruction( - &context, - authority.pubkey(), - &tree1, - recipient.pubkey(), - ); + let replay1 = + build_withdraw_cnft_instruction(&context, authority.pubkey(), &tree1, recipient.pubkey()); let replay = send(&mut context.svm, vec![replay1], &authority, &[&authority]); assert!( replay.is_err(), diff --git a/compression/cutils/anchor/programs/cutils/Cargo.toml b/compression/cutils/anchor/programs/cutils/Cargo.toml index 2b24395ee..ce870ee74 100644 --- a/compression/cutils/anchor/programs/cutils/Cargo.toml +++ b/compression/cutils/anchor/programs/cutils/Cargo.toml @@ -20,12 +20,22 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +# `borsh` adds the BorshSerialize/BorshDeserialize impls for `Address` that the +# vendored Bubblegum types need; pinocchio re-exports this same type. +solana-address = { version = ">=2.6, <2.7", features = ["borsh"] } # mpl-bubblegum and spl-account-compression removed: they depend on solana-program 2.x # which is incompatible with Anchor 1.0's solana 3.x types. CPI calls are built manually # using raw invoke() with hardcoded program IDs and discriminators. Bubblegum types # (MetadataArgs, LeafSchema, etc.) are re-implemented in bubblegum_types.rs. -borsh = "1" +borsh = { version = "1", features = ["derive"] } sha3 = "0.10" [lints.rust] diff --git a/compression/cutils/anchor/programs/cutils/src/bubblegum_types.rs b/compression/cutils/anchor/programs/cutils/src/bubblegum_types.rs index 3f91dcef7..176ec4c65 100644 --- a/compression/cutils/anchor/programs/cutils/src/bubblegum_types.rs +++ b/compression/cutils/anchor/programs/cutils/src/bubblegum_types.rs @@ -1,4 +1,4 @@ -/// Re-implementation of mpl-bubblegum types using borsh 1.x and Anchor 1.0's Pubkey. +/// Re-implementation of mpl-bubblegum types using borsh 1.x and Anchor 1.0's Address. /// /// mpl-bubblegum 2.1.1 depends on solana-program 2.x which is incompatible with /// Anchor 1.0's solana 3.x types. These types are borsh-compatible reproductions @@ -9,7 +9,7 @@ use borsh::BorshSerialize; /// Mirrors mpl_bubblegum::types::Creator #[derive(BorshSerialize, Clone, Debug)] pub struct Creator { - pub address: Pubkey, + pub address: Address, pub verified: bool, pub share: u8, } @@ -18,7 +18,7 @@ pub struct Creator { #[derive(BorshSerialize, Clone, Debug)] pub struct Collection { pub verified: bool, - pub key: Pubkey, + pub key: Address, } /// Mirrors mpl_bubblegum::types::TokenProgramVersion @@ -82,9 +82,9 @@ pub struct MintToCollectionV1InstructionArgs { /// Compute the leaf hash for a V1 LeafSchema, matching mpl_bubblegum::types::LeafSchema::hash(). /// Uses keccak256 over the version byte and all fields. pub fn leaf_schema_v1_hash( - id: &Pubkey, - owner: &Pubkey, - delegate: &Pubkey, + id: &Address, + owner: &Address, + delegate: &Address, nonce: u64, data_hash: &[u8; 32], creator_hash: &[u8; 32], @@ -102,14 +102,14 @@ pub fn leaf_schema_v1_hash( } /// Compute the asset id from tree and nonce, matching mpl_bubblegum::utils::get_asset_id(). -pub fn get_asset_id(tree: &Pubkey, nonce: u64) -> Pubkey { +pub fn get_asset_id(tree: &Address, nonce: u64) -> Address { // mpl-bubblegum program ID - let bubblegum_id = Pubkey::new_from_array([ + let bubblegum_id = Address::new_from_array([ 0x98, 0x8b, 0x80, 0xeb, 0x79, 0x35, 0x28, 0x69, 0xb2, 0x24, 0x74, 0x5f, 0x59, 0xdd, 0xbf, 0x8a, 0x26, 0x58, 0xca, 0x13, 0xdc, 0x68, 0x81, 0x21, 0x26, 0x35, 0x1c, 0xae, 0x07, 0xc1, 0xa5, 0xa5, ]); - Pubkey::find_program_address( + Address::find_program_address( &[b"asset", tree.as_ref(), &nonce.to_le_bytes()], &bubblegum_id, ) diff --git a/compression/cutils/anchor/programs/cutils/src/instructions/mint.rs b/compression/cutils/anchor/programs/cutils/src/instructions/mint.rs index 795bb9caa..ae2364c72 100644 --- a/compression/cutils/anchor/programs/cutils/src/instructions/mint.rs +++ b/compression/cutils/anchor/programs/cutils/src/instructions/mint.rs @@ -11,62 +11,62 @@ use borsh::BorshSerialize; #[derive(Accounts)] #[instruction(params: MintParams)] -pub struct MintAccountConstraints<'info> { - pub payer: Signer<'info>, +pub struct MintAccountConstraints { + pub payer: Signer, #[account( mut, - seeds = [merkle_tree.key().as_ref()], - seeds::program = bubblegum_program.key(), + seeds = [merkle_tree.address().as_ref()], + seeds::program = bubblegum_program.address(), bump, )] /// CHECK: This account is modified in the downstream program - pub tree_authority: UncheckedAccount<'info>, + pub tree_authority: UncheckedAccount, /// CHECK: This account is neither written to nor read from. - pub leaf_owner: UncheckedAccount<'info>, + pub leaf_owner: UncheckedAccount, /// CHECK: This account is neither written to nor read from. - pub leaf_delegate: UncheckedAccount<'info>, + pub leaf_delegate: UncheckedAccount, #[account(mut)] /// CHECK: Written by the Bubblegum/Account Compression CPI (the mint /// appends a leaf and updates the tree root); validated downstream. - pub merkle_tree: UncheckedAccount<'info>, + pub merkle_tree: UncheckedAccount, - pub tree_delegate: Signer<'info>, + pub tree_delegate: Signer, - pub collection_authority: Signer<'info>, + pub collection_authority: Signer, /// CHECK: Optional collection authority record PDA. /// If there is no collection authority record PDA then /// this must be the Bubblegum program address. - pub collection_authority_record_pda: UncheckedAccount<'info>, + pub collection_authority_record_pda: UncheckedAccount, /// CHECK: This account is checked in the instruction - pub collection_mint: UncheckedAccount<'info>, + pub collection_mint: UncheckedAccount, #[account(mut)] /// CHECK: This account is checked in the instruction - pub collection_metadata: UncheckedAccount<'info>, + pub collection_metadata: UncheckedAccount, /// CHECK: This account is checked in the instruction - pub edition_account: UncheckedAccount<'info>, + pub edition_account: UncheckedAccount, /// CHECK: This is just used as a signing PDA. - pub bubblegum_signer: UncheckedAccount<'info>, + pub bubblegum_signer: UncheckedAccount, /// CHECK: This account is neither written to nor read from. - pub log_wrapper: UncheckedAccount<'info>, - pub compression_program: Program<'info, SPLCompression>, + pub log_wrapper: UncheckedAccount, + pub compression_program: Program, /// CHECK: This account is neither written to nor read from. - pub token_metadata_program: UncheckedAccount<'info>, + pub token_metadata_program: UncheckedAccount, /// CHECK: This account is neither written to nor read from. - pub bubblegum_program: UncheckedAccount<'info>, - pub system_program: Program<'info, System>, + pub bubblegum_program: UncheckedAccount, + pub system_program: Program, } -#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +#[derive(Clone, IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub struct MintParams { uri: String, } @@ -74,8 +74,8 @@ pub struct MintParams { // `with_capacity` + push is intentional here: it documents the exact 16-account // MintToCollectionV1 layout in CPI order, so allow clippy's vec_init_then_push. #[allow(clippy::vec_init_then_push)] -pub fn handle_mint<'info>( - context: Context<'info, MintAccountConstraints<'info>>, +pub fn handle_mint( + context: &mut Context, params: MintParams, ) -> Result<()> { // Build MintToCollectionV1 instruction data @@ -85,7 +85,7 @@ pub fn handle_mint<'info>( symbol: "BURG".to_string(), uri: params.uri, creators: vec![Creator { - address: context.accounts.collection_authority.key(), + address: *context.accounts.collection_authority.address(), verified: false, share: 100, }], @@ -96,7 +96,7 @@ pub fn handle_mint<'info>( uses: None, collection: Some(Collection { verified: false, - key: context.accounts.collection_mint.key(), + key: *context.accounts.collection_mint.address(), }), token_program_version: TokenProgramVersion::Original, token_standard: Some(TokenStandard::NonFungible), @@ -104,70 +104,74 @@ pub fn handle_mint<'info>( }; let mut data = MINT_TO_COLLECTION_V1_DISCRIMINATOR.to_vec(); - args.serialize(&mut data)?; + args.serialize(&mut data) + .map_err(|_| ProgramError::InvalidInstructionData)?; // Build account metas matching MintToCollectionV1 instruction layout let mut accounts = Vec::with_capacity(16); accounts.push(AccountMeta::new( - context.accounts.tree_authority.key(), + *context.accounts.tree_authority.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.leaf_owner.key(), + *context.accounts.leaf_owner.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.leaf_delegate.key(), + *context.accounts.leaf_delegate.address(), + false, + )); + accounts.push(AccountMeta::new( + *context.accounts.merkle_tree.address(), false, )); - accounts.push(AccountMeta::new(context.accounts.merkle_tree.key(), false)); accounts.push(AccountMeta::new_readonly( - context.accounts.payer.key(), + *context.accounts.payer.address(), true, )); accounts.push(AccountMeta::new_readonly( - context.accounts.tree_delegate.key(), + *context.accounts.tree_delegate.address(), true, )); accounts.push(AccountMeta::new_readonly( - context.accounts.collection_authority.key(), + *context.accounts.collection_authority.address(), true, )); // collection_authority_record_pda - pass as-is accounts.push(AccountMeta::new_readonly( - context.accounts.collection_authority_record_pda.key(), + *context.accounts.collection_authority_record_pda.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.collection_mint.key(), + *context.accounts.collection_mint.address(), false, )); accounts.push(AccountMeta::new( - context.accounts.collection_metadata.key(), + *context.accounts.collection_metadata.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.edition_account.key(), + *context.accounts.edition_account.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.bubblegum_signer.key(), + *context.accounts.bubblegum_signer.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.log_wrapper.key(), + *context.accounts.log_wrapper.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.compression_program.key(), + *context.accounts.compression_program.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.token_metadata_program.key(), + *context.accounts.token_metadata_program.address(), false, )); accounts.push(AccountMeta::new_readonly( - context.accounts.system_program.key(), + *context.accounts.system_program.address(), false, )); @@ -177,30 +181,29 @@ pub fn handle_mint<'info>( data, }; - // Gather all account infos for the CPI - let account_infos = vec![ - context.accounts.bubblegum_program.to_account_info(), - context.accounts.tree_authority.to_account_info(), - context.accounts.leaf_owner.to_account_info(), - context.accounts.leaf_delegate.to_account_info(), - context.accounts.merkle_tree.to_account_info(), - context.accounts.payer.to_account_info(), - context.accounts.tree_delegate.to_account_info(), - context.accounts.collection_authority.to_account_info(), - context - .accounts - .collection_authority_record_pda - .to_account_info(), - context.accounts.collection_mint.to_account_info(), - context.accounts.collection_metadata.to_account_info(), - context.accounts.edition_account.to_account_info(), - context.accounts.bubblegum_signer.to_account_info(), - context.accounts.log_wrapper.to_account_info(), - context.accounts.compression_program.to_account_info(), - context.accounts.token_metadata_program.to_account_info(), - context.accounts.system_program.to_account_info(), + // Account handles have to line up positionally with the instruction's + // account metas above: v2's `invoke` matches each meta to the next handle + // in order, and the program account is not listed. + let account_infos: Vec = vec![ + context.accounts.tree_authority.cpi_handle_mut().into(), + context.accounts.leaf_owner.cpi_handle(), + context.accounts.leaf_delegate.cpi_handle(), + context.accounts.merkle_tree.cpi_handle_mut().into(), + context.accounts.payer.cpi_handle(), + context.accounts.tree_delegate.cpi_handle(), + context.accounts.collection_authority.cpi_handle(), + context.accounts.collection_authority_record_pda.cpi_handle(), + context.accounts.collection_mint.cpi_handle(), + context.accounts.collection_metadata.cpi_handle_mut().into(), + context.accounts.edition_account.cpi_handle(), + context.accounts.bubblegum_signer.cpi_handle(), + context.accounts.log_wrapper.cpi_handle(), + context.accounts.compression_program.cpi_handle(), + context.accounts.token_metadata_program.cpi_handle(), + context.accounts.system_program.cpi_handle(), ]; + invoke(&instruction, &account_infos)?; Ok(()) diff --git a/compression/cutils/anchor/programs/cutils/src/instructions/verify.rs b/compression/cutils/anchor/programs/cutils/src/instructions/verify.rs index 60d72b51a..323d8f529 100644 --- a/compression/cutils/anchor/programs/cutils/src/instructions/verify.rs +++ b/compression/cutils/anchor/programs/cutils/src/instructions/verify.rs @@ -4,20 +4,20 @@ use anchor_lang::solana_program::instruction::{AccountMeta, Instruction}; #[derive(Accounts)] #[instruction(params: VerifyParams)] -pub struct VerifyAccountConstraints<'info> { - pub leaf_owner: Signer<'info>, +pub struct VerifyAccountConstraints { + pub leaf_owner: Signer, /// CHECK: This account is neither written to nor read from. - pub leaf_delegate: UncheckedAccount<'info>, + pub leaf_delegate: UncheckedAccount, /// CHECK: Read by the SPL Account Compression verify_leaf CPI, which /// validates the proof against this tree's stored root. - pub merkle_tree: UncheckedAccount<'info>, + pub merkle_tree: UncheckedAccount, - pub compression_program: Program<'info, SPLCompression>, + pub compression_program: Program, } -#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +#[derive(Clone, IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub struct VerifyParams { root: [u8; 32], data_hash: [u8; 32], @@ -31,15 +31,20 @@ pub struct VerifyParams { /// at runtime burns compute for no benefit. const VERIFY_LEAF_DISCRIMINATOR: [u8; 8] = [124, 220, 22, 223, 104, 10, 250, 224]; -pub fn handle_verify<'info>( - context: Context<'info, VerifyAccountConstraints<'info>>, +pub fn handle_verify( + context: &mut Context, params: &VerifyParams, ) -> Result<()> { - let asset_id = get_asset_id(&context.accounts.merkle_tree.key(), params.nonce); + // `remaining_accounts()` walks the input cursor and hands back an owned + // vec, so take it once up front: the mutable borrow of `context` ends here + // and the proof accounts stay alive for the CPI below. + let proof_accounts = context.remaining_accounts()?; + + let asset_id = get_asset_id(&context.accounts.merkle_tree.address(), params.nonce); let leaf_hash = leaf_schema_v1_hash( &asset_id, - &context.accounts.leaf_owner.key(), - &context.accounts.leaf_delegate.key(), + &context.accounts.leaf_owner.address(), + &context.accounts.leaf_delegate.address(), params.nonce, ¶ms.data_hash, ¶ms.creator_hash, @@ -49,11 +54,11 @@ pub fn handle_verify<'info>( // depends on solana-program 2.x which is incompatible with Anchor 1.0's solana 3.x // types. Once a compatible version is available, replace this with the CPI wrapper. let mut accounts = vec![AccountMeta::new_readonly( - context.accounts.merkle_tree.key(), + *context.accounts.merkle_tree.address(), false, )]; - for acc in context.remaining_accounts.iter() { - accounts.push(AccountMeta::new_readonly(acc.key(), false)); + for acc in proof_accounts.iter() { + accounts.push(AccountMeta::new_readonly(*acc.address(), false)); } let mut data = VERIFY_LEAF_DISCRIMINATOR.to_vec(); @@ -61,14 +66,16 @@ pub fn handle_verify<'info>( data.extend_from_slice(&leaf_hash); data.extend_from_slice(¶ms.index.to_le_bytes()); - let mut account_infos = vec![context.accounts.merkle_tree.to_account_info()]; - for acc in context.remaining_accounts.iter() { - account_infos.push(acc.to_account_info()); + // `verify_leaf` only reads, so every handle is readonly; the proof nodes + // arrive as bare `AccountView`s and are wrapped directly. + let mut account_infos = vec![context.accounts.merkle_tree.cpi_handle()]; + for acc in proof_accounts.iter() { + account_infos.push(CpiHandle::readonly(acc)); } anchor_lang::solana_program::program::invoke( &Instruction { - program_id: context.accounts.compression_program.key(), + program_id: *context.accounts.compression_program.address(), accounts, data, }, diff --git a/compression/cutils/anchor/programs/cutils/src/lib.rs b/compression/cutils/anchor/programs/cutils/src/lib.rs index dde598ed5..2ae22c66a 100644 --- a/compression/cutils/anchor/programs/cutils/src/lib.rs +++ b/compression/cutils/anchor/programs/cutils/src/lib.rs @@ -10,13 +10,13 @@ pub mod bubblegum_types; use anchor_lang::prelude::*; /// SPL Account Compression program ID (cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK) -const SPL_ACCOUNT_COMPRESSION_ID: Pubkey = Pubkey::new_from_array([ +const SPL_ACCOUNT_COMPRESSION_ID: Address = Address::new_from_array([ 0x09, 0x2a, 0x13, 0xee, 0x95, 0xc4, 0x1c, 0xba, 0x08, 0xa6, 0x7f, 0x5a, 0xc6, 0x7e, 0x8d, 0xf7, 0xe1, 0xda, 0x11, 0x62, 0x5e, 0x1d, 0x64, 0x13, 0x7f, 0x8f, 0x4f, 0x23, 0x83, 0x03, 0x7f, 0x14, ]); /// mpl-bubblegum program ID (BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY) -const MPL_BUBBLEGUM_ID: Pubkey = Pubkey::new_from_array([ +const MPL_BUBBLEGUM_ID: Address = Address::new_from_array([ 0x98, 0x8b, 0x80, 0xeb, 0x79, 0x35, 0x28, 0x69, 0xb2, 0x24, 0x74, 0x5f, 0x59, 0xdd, 0xbf, 0x8a, 0x26, 0x58, 0xca, 0x13, 0xdc, 0x68, 0x81, 0x21, 0x26, 0x35, 0x1c, 0xae, 0x07, 0xc1, 0xa5, 0xa5, ]); @@ -25,7 +25,7 @@ const MPL_BUBBLEGUM_ID: Pubkey = Pubkey::new_from_array([ pub struct SPLCompression; impl anchor_lang::Id for SPLCompression { - fn id() -> Pubkey { + fn id() -> Address { SPL_ACCOUNT_COMPRESSION_ID } } @@ -36,15 +36,12 @@ declare_id!("BuFyrgRYzg2nPhqYrxZ7d9uYUs4VXtxH71U8EcoAfTQZ"); pub mod cutils { use super::*; - pub fn mint<'info>( - context: Context<'info, MintAccountConstraints<'info>>, - params: MintParams, - ) -> Result<()> { + pub fn mint(context: &mut Context, params: MintParams) -> Result<()> { instructions::mint::handle_mint(context, params) } - pub fn verify<'info>( - context: Context<'info, VerifyAccountConstraints<'info>>, + pub fn verify( + context: &mut Context, params: VerifyParams, ) -> Result<()> { instructions::verify::handle_verify(context, ¶ms) diff --git a/docs/anchor-v2-migration.md b/docs/anchor-v2-migration.md new file mode 100644 index 000000000..daf856155 --- /dev/null +++ b/docs/anchor-v2-migration.md @@ -0,0 +1,534 @@ +# Porting an Anchor program from v1 to v2.0.0-rc.1 + +Anchor v2 is a ground-up rewrite, not a version bump. The crate is `no_std` and +built on pinocchio, the account model is static-scoped, borsh is replaced by +wincode, and accounts are zero-copy by default. This is the list of every +difference that came up porting the examples in this repository, in rough order +of how often it bites. + +The single most important one is **[borrows held across CPIs](#borrows-held-across-cpis-are-what-tests-catch)**: +it is the only rule here that the compiler will not catch for you. + +## Manifest Changes Every Crate Needs + +- `anchor-lang` and `anchor-spl` both move from `1.1.2` to `2.0.0-rc.1`. +- `wincode = { version = "0.5", features = ["derive"] }` is new. +- `features = ["init-if-needed"]` goes away. There is no such feature, and the + constraint is always available. +- `"anchor-spl/idl-build"` comes out of the `idl-build` feature list, because + anchor-spl has no `idl-build` feature. +- anchor-spl's features are `guardrails` (on by default) and `metadata`. The + Token Extensions modules are unconditional. + +Every program crate needs `wincode` as a **direct** dependency: the `#[program]` +macro expands to `wincode` paths for instruction-data (de)serialization. + +Programs that put an `Address` in serialized state also need: + +```toml +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls belong +# to the version the account derives are not using, and every `SchemaRead` / +# `SchemaWrite` bound fails. 2.6.1 is the last release on the 0.5 line. +solana-address = ">=2.6, <2.7" +``` + +Note the range: a bare `<2.7` lets cargo satisfy the requirement by reusing an +older 1.x already in the graph, which does not fix anything. + +## Renamed Accessors and Handler Signatures + +- `Context` becomes `&mut Context`, and the user `'info` lifetime is gone: + `Context<'info, T<'info>>` is `Context`. +- `Signer<'info>` and `Program<'info, T>` are `Signer` and `Program`. + `Interface<'info, TokenInterface>` is the one wrapper that keeps a lifetime, + as `Interface<'static, TokenInterface>`. +- `Pubkey` is `Address`. `AccountInfo<'info>` is `AccountView`, or + `UncheckedAccount` inside an `Accounts` struct. +- `.key()` and `.key` become `.address()`, which returns `&Address`. + Dereference where a value is wanted. +- `.to_account_info()` becomes `.cpi_handle()` or `.cpi_handle_mut()`. +- `ctx.remaining_accounts` is now the method `ctx.remaining_accounts()?`. It is + fallible, takes `&mut self`, and returns an owned `Vec`, so call + it before anything borrows `ctx.accounts`. +- `account.owner` is `account.owner()`. `Rent::minimum_balance` is + `Rent::try_minimum_balance`. +- `account.data.borrow()` is `account.account().try_borrow()?`. +- `set_inner(X { .. })` is `*ctx.accounts.foo = X { .. }`. +- `account.reload()?` is `account.revalidate_after_cpi()?`. Zero-copy reads are + already live, so nothing is reloaded; the call re-runs the schema checks a CPI + could have invalidated. +- `Account::::try_from(view)` is `AnchorAccount::load(view)`, or `load_mut` + (which is unsafe) to write back. +- `.exit(program_id)` is `.exit()`. +- `error!(MyError::X)` is just `MyError::X`. The macro is compat-only. `?` + converts, and a tail position needs `.into()`. +- `#[instruction(discriminator = X)]` is `#[discrim = X]`, and it takes a + literal. +- `#[account(zero)]` is `#[account(zeroed)]`, and `#[account(zero_copy(unsafe))]` + is plain `#[account]`, which is already zero-copy. +- `AccountLoader<'info, T>` with `load()` is `Account`, which derefs straight + to `T`. +- `#[account(has_one = x)]` on the owning account is `#[account(address = + owner.x)]` on the **sibling** field. + +`has_one` is deprecated rather than removed, but this repository's `rust.yml` runs +`cargo clippy -- -D warnings`, so every remaining use is a **hard CI failure**. +The check moves off the owning account and onto the sibling it names: + +```rust +// v1: on `offer` +#[account(mut, close = maker, has_one = maker)] +pub offer: BorshAccount, + +// v2: the constraint lives on `maker` +#[account(mut, address = offer.maker)] +pub maker: SystemAccount, +``` + +Dropping the `'info` lifetime from handler signatures is mechanical, but do not +strip `<'a>` from free functions that genuinely use it: a helper returning +`[&'a [u8]; 4]` still needs its parameter. + +## Account State Chooses Between Zero-Copy and Borsh + +v2's `#[account]` is **zero-copy** and requires a `Pod` layout: no implicit +padding, no `bool` (use `PodBool`), no `String` or `Vec`. + +- State holding `String` / `Vec` / enums → `#[account(borsh)]` + `BorshAccount`. +- Fixed-layout state can stay zero-copy but must carry **explicit padding** + (`u32` + `u8` leaves three bytes, so name them). +- `#[derive(InitSpace)]` and `#[max_len(N)]` work as before on borsh accounts; + Pod accounts should size with `core::mem::size_of`. + +**Default to `#[account(borsh)]` when porting.** v1 accounts were borsh-encoded, +so it reproduces the onchain layout byte-for-byte and existing clients and +tests keep working. Only keep zero-copy where the program deliberately wants it +(a large slab, say), because converting one to borsh defeats its purpose. + +v2 has no `Account::try_from(&AccountView)`. To load a borsh account out of +`remaining_accounts` **for writing**, use `AnchorAccount::load_mut`, which is +`unsafe` because the caller has to guarantee no other live `&mut` to the same +data; `exit()` then writes it back: + +```rust +let mut order = unsafe { BorshAccount::::load_mut(*view) }?; +order.filled_quantity += fill; +order.exit()?; +``` + +To read one without taking ownership of the write path, check the discriminator +and decode the payload yourself: + +```rust +let data = account.try_borrow()?; +let disc_len = ::DISCRIMINATOR.len(); +require!( + data.len() > disc_len + && &data[..disc_len] == ::DISCRIMINATOR, + MyError::BadAccount +); +let mut payload = &data[disc_len..]; +>::get(&mut payload) + .map_err(|_| MyError::BadAccount)? +``` + +`Owner` is a const in v2 (`const OWNER: Address`), which makes a foreign-owned +account (a vendored Pyth or Bubblegum type, say) straightforward to declare. + +## Borrows Held Across CPIs Are What Tests Catch + +**This is the rule tests catch and the compiler does not.** + +v2's typed CPI handles make aliasing a compile error: passing one account into +both a writable and a read-only CPI slot will not build. + +For **two read-only slots** there is nothing to work around: `cpi_handle()` +takes `&self`, so calling it twice on the same field is fine. Prefer it: on a +data account the wrapper's own `cpi_handle()` also relaxes the runtime borrow +check, which a hand-built handle does not. + +When a read-only slot has to coexist with a writable one, copying the +`AccountView` (which is `Copy`) satisfies the compiler. That is correct **only +for accounts that hold no data borrow** (`Signer`, `UncheckedAccount`, +`Program`), or for a data account whose borrow has already been released: + +```rust +let payer_view = *ctx.accounts.payer.account(); +// ... CpiHandle::readonly(&payer_view) in the read-only slots +``` + +On a still-borrowed data account the copy passes the compiler and then fails at +runtime with `AccountBorrowFailed`, because `CpiHandle::readonly` keeps the +borrow check on. Use `into_readonly()` instead: `CpiHandleMut` is `Copy`, and +erasing it carries the wrapper's relaxed borrow flag across. + +```rust +// one account filling a writable slot and a read-only one +let mint_handle = ctx.accounts.mint_account.cpi_handle_mut(); +let authority_handle = mint_handle.into_readonly(); +MintTo { mint: mint_handle, to: ..., authority: authority_handle } +``` + +Take the writable handle **last**: it borrows the field mutably for the rest of +the scope, so any `msg!` or `.address()` on the same account has to come first. + +## An Init Constraint Cannot Name the Account Being Initialized + +`mint::authority = mint_account` on `mint_account` itself, a PDA that is its +own mint authority, is rejected at macro-expansion time: an SPL `init` +constraint has to name a *sibling* field. The same goes for +`token::authority = `. + +Where that idiom is the point of the example, build the account by hand +(`create_account` + `initialize_mint2` / `initialize_account3`) rather than +adding a second field for the same address, which would then trip v2's +duplicate-mutable-account check. `initialize_mint2` and `initialize_account3` +take the authority as an address, so nothing is lost. + +For a **data** account (`Account`, `BorshAccount`, `InterfaceAccount`) that +copy is not enough. The account holds a live borrow on its buffer, and the +runtime rejects the CPI's own borrow with `AccountBorrowFailed`. Release it +across the call instead: + +```rust +ctx.accounts.offer.release_borrow()?; +let offer_view = *ctx.accounts.offer.account(); +// ... CPI signed by `offer` ... +ctx.accounts.offer.reacquire_borrow_mut()?; +``` + +`reacquire_borrow_mut` re-runs the load-time owner and discriminator checks, +because a CPI in the release window could have mutated either. + +Three ways this goes wrong: + +1. **Dereferencing after release panics** (`account borrow released (closed)`). + That includes the derive's own use of the account after the handler returns: + `associated_token::authority = event`, `address = event.x` and friends all + deref it. So the reacquire has to happen before the handler ends. +2. **Release and reacquire must be on the same branch.** Releasing inside + `if fee > 0` and reacquiring unconditionally re-borrows an account you still + hold. +3. **A read-only account cannot be reacquired.** There is no read-only + reacquire. If the derive references the account after the handler, it has to + be declared `mut`. + +Also: asking a read-only account for a writable handle panics, so read +`*account.address()` rather than `account.cpi_handle_mut().address()`. + +**On a `Box`ed account, call `to_cpi_handle_mut()` / `to_cpi_handle()`, not +`cpi_handle_mut()` / `cpi_handle()`.** `Box`'s `AnchorAccount` impl supplies +only `account()`, so `cpi_handle_mut()` falls through to the default, which +builds a handle *without* releasing the wrapper's data borrow, so the CPI is then +rejected with `AccountBorrowFailed`. `Box`'s `ToCpiHandleMut` impl does forward +to the inner type, which is where the release lives. It compiles either way, +so this only shows up in a test. + +Not every failure of this kind needs a release: a handler that only *reads* a +**read-only** account can take a second shared borrow (`account().try_borrow()`) +where `try_borrow_mut` on a copied view would be rejected. + +A **`mut`** data account is different. Loading one sets pinocchio's exclusive +sentinel (`borrow_state == 0`), so *any* `try_borrow()` on it fails. The +wrapper itself reads through `borrow_unchecked`. Three ways out, in order of +preference: + +1. **For a Token Extensions Program extension, use anchor-spl's accessor.** + `TokenInterfaceAccountExtensions::get_extension::()` on an + `InterfaceAccount` or `InterfaceAccount` parses the TLV + through the borrow the wrapper already holds, and checks the account is + owned by the Token Extensions Program on the way. It is bounded on `Pod`, so + it covers every + fixed-size extension but not a variable-length one like `TokenMetadata`. +2. **Drop the `mut`** if the account is not actually written. A mint passed to + `transfer_checked_with_fee` is read-only: the withheld fee accrues on the + destination token account. +3. **Declare the field `UncheckedAccount` and load the typed wrapper by hand.** + `AnchorAccount::load` is safe, runs exactly the validation the derive would + have run, and registers a *shared* borrow rather than an exclusive one, so + an ordinary `try_borrow()` still has room: + + ```rust + /// CHECK: loaded and validated as an `InterfaceAccount` below. + #[account(mut)] + pub mint_account: UncheckedAccount, + ... + let mint = InterfaceAccount::::load(*ctx.accounts.mint_account.account())?; + let buffer = mint.account().try_borrow()?; + ``` + + Keep the `mut`: it is what marks the account writable in the IDL, which the + client needs for the CPI that writes to it. `cpi_handle_mut()` on an + `UncheckedAccount` checks the runtime writable flag, not the wrapper's own + mutability. Drop the loaded wrapper before the CPI so its shared borrow is + released. This is what `token-extensions/metadata` does to reach + `TokenMetadata`, which `get_extension` cannot. + +`unsafe { account.account().borrow_unchecked() }`, reading through the +exclusive borrow you already hold, is what `get_extension` does internally, and +it is sound whenever the instruction holds that borrow for its whole duration +and hands out no second one. It is nonetheless not needed anywhere in this +repository's Anchor programs, and it should stay that way: one of the three +options above has covered every case so far. + +## Where `unsafe` Is Still Unavoidable + +Three Anchor programs read the `LastRestartSlot` sysvar, which pinocchio has no +typed accessor for. Call **`pinocchio::sysvars::get_sysvar(&mut buf, &id, 0)`** +rather than the `sol_get_sysvar` syscall directly: it is a safe wrapper, and +offchain it is a no-op that leaves the buffer zeroed, so IDL and client builds +read "the cluster has never restarted" without a `cfg` split of their own. + +After that, two kinds of `unsafe` are left in the Anchor programs, and neither +has a safe equivalent in v2: + +- **`transfer-hook/*/anchor/…/entrypoint.rs`**: `pub unsafe extern "C" fn + entrypoint` is the loader ABI. These programs claim the entrypoint themselves + to remap an SPL interface discriminator, so the `unsafe` the `#[program]` + macro would have hidden is written out. +- **`order-book/…/place_order.rs`**: `AnchorAccount::load_mut` is an `unsafe fn` + on the trait, and it is the only way to get a writable typed wrapper for an + account arriving through `remaining_accounts`. `BorshAccount`'s implementation + borrows through the checked `try_borrow_mut`, so a duplicate fails with + `AccountBorrowFailed` rather than aliasing. + +Any lint that bans `unsafe` repository-wide has to exempt those, plus the +Quasar and pinocchio programs, where raw zero-copy access is the point of the +example rather than an escape hatch. + +## Discriminators, and Programs That Implement an Interface + +By default a handler still dispatches on `sha256("global:")[..8]`, so +existing clients and tests keep working. What changed is the override: + +- `#[interface(...)]` and the `interface-instructions` feature are **gone**. +- `#[discrim = N]` on an executable `#[program]` takes a **single byte**, and + it is all-or-nothing: if one handler has it, every handler needs one. +- `#[program(interface, program_id = ID)]` accepts arbitrary discriminator + bytes, but it declares an interface for *other* programs to CPI into. It + generates a CPI client and no dispatch, so the crate builds to a ~900-byte + object with no `entrypoint` symbol and fails to load with + `ProgramLoad("Entrypoint out of bounds")`. + +That leaves no direct way to write a program that answers to a foreign +eight-byte discriminator, an SPL transfer hook's `Execute`, say. The +transfer-hook examples here handle it by taking the entrypoint over: the crate +sets `default = ["no-entrypoint"]`, which makes anchor export its dispatch as +`__anchor_dispatch` instead of claiming `entrypoint`, and `src/entrypoint.rs` +claims `entrypoint` itself, swaps the interface discriminator for the matching +handler's, and delegates. The payload behind the discriminator is unchanged, so +nothing else has to be replicated. With `no-entrypoint` set, the crate also has +to invoke `pinocchio::default_allocator!()` and +`pinocchio::default_panic_handler!()` itself: anchor only emits those on the +path where it owns the entrypoint. + +## Duplicate Mutable Accounts Are Rejected + +v2 rejects an account that appears in more than one declared slot when any of +those slots is mutable, with `ConstraintDuplicateMutableAccount`, custom error +2040. `#[account(unsafe(dup))]` opts a slot out; it implies `mut`, so it +replaces the `mut` rather than joining it. + +The catch: the walker flags **both** indices of a duplicate, so marking only +the second one still leaves the first intersecting the mutable mask. Every slot +that can legitimately alias needs the constraint, including a `payer`. + +Why the rule exists: v1 deserialized each `Account` into an owned copy and +serialized it back at the end of the instruction. Two slots over one account +meant two independent copies, and the second write-back silently clobbered the +first, the classic self-transfer bug, where debiting one copy and crediting +the other and writing both leaves the balance higher than it started. v2 +accounts are zero-copy views into the runtime's buffer, so two mutable wrappers +over one account would be two `&mut` to the same bytes. Hence the loader +rejects rather than warns. + +The `unsafe` is not decoration, so check two things before reaching for it: + +1. **That the accounts can actually alias.** If no caller ever passes the same + address twice, the constraint disables a live check for nothing. Leave it + off. `transfer-tokens`' `transfer` is this case: its test funds a fresh + `Keypair` as the recipient. +2. **That the aliasing slots hold no deserialized state.** `Signer`, + `SystemAccount` and `UncheckedAccount` carry no copy to write back, so + there is no lost update to worry about. The two `mint` instructions here are + this case: minting to yourself makes `mint_authority` and `recipient` the + same `Signer`, and the duplication comes from the caller rather than the + program, so refusing it would make an ordinary mint fail with 2040. Two + aliasing `Account` slots are what the check is *for*, and the fix there + is to restructure the accounts so only one of them exists. + +## anchor-spl Moved Its Types and Dropped Its Features + +`Mint` moved from `anchor_spl::token` to `anchor_spl::mint`, and the namespaced +constraints (`mint::decimals`, `token::authority`, …) expand to paths rooted at +those modules, so `mint` / `token` must be nameable in the file that uses them. + +SPL account fields are behind accessors now that the structs are Pod: +`.amount()`, `.decimals()`, `.supply()`, `.mint()`, `.owner()`. + +Init constraints resolve a **sibling account field**, not a pubkey expression or +a field read off another account: `mint::authority = payer`, not +`mint::authority = payer.address()`; `associated_token::mint = mint_to_raise`, +not `= fundraiser.mint_to_raise`. + +CPI structs dropped their `token_program_id` / `program_id` slots. The program +comes from the `CpiContext`. `create_metadata_accounts_v3` takes four arguments +(the signer flag moved into the accounts struct, and the optional `rent` account +is gone). + +There are **no `extensions::*` constraints**, for init or validation. To create a +mint with a Token Extensions Program extension, do it by hand, which is what +this repository's +`non-transferable` example always did: + +```rust +let mint_size = ExtensionType::try_calculate_account_len::(&[ + ExtensionType::MintCloseAuthority, +])?; +// create_account -> _initialize -> initialize_mint2, in that order +``` + +Extension initialization must come before `InitializeMint2`. + +## v2-Only Primitives Worth Reaching For + +These have no v1 equivalent, so a mechanical port never produces them, but they +are often the right answer when a straight translation gets ugly: + +- `Slab`: zero-copy header with a dynamic item tail, for ledgers, + order books and event logs. `Account` is `Slab` underneath. +- `PodVec`: fixed-capacity vec with a `u16` length, stored inline, so + variable-length data without leaving Pod. +- `PodU64`, `PodI128`, `PodBool` and friends: alignment-1 integer wrappers, so a + `#[repr(C)]` struct packs with no padding. +- `#[pod_wrapper]`: safe enum-to-Pod conversion that validates the discriminant + on equality and conversion. +- `Nested`: share one `#[derive(Accounts)]` validation block across + instructions. +- `#[event(bytemuck)]`: fixed-size events, a discriminator plus one memcpy. + Plain `#[event]` defaults to wincode, which is borsh-wire-compatible. + +If a program was zero-copy in v1 and the port is fighting Pod's rules, the +answer is usually one of the first four rather than moving it to borsh. + +### Feature flags + +- `guardrails` (default on): runtime safety nets. Dropping it saves ~300 bytes + and 1–2 CU per account, at the cost of diagnostic panics on misuse. +- `const-rent` (default off): folds `Rent::get()` to a compile-time constant, + ~85 CU per `create_account`. Burns the rent formula into the binary, so a + formula change (SIMD-0194) needs a rebuild. +- `compat` (default off): restores v1-shaped `error!`, `err!`, `pubkey!`, + `debug!`. Useful mid-port; `debug!` heap-allocates through `alloc::format!`. + +### Tooling + +- `anchor debugger`: TUI stepping through SBF instructions per test. +- `anchor test --profile`: per-test register-trace flamegraphs under + `target/anchor-v2-profile/`. +- `anchor-v2-testing`: wraps LiteSVM with optional register-trace capture. + +## Hand-Built CPIs Take Handles, Not Account Infos + +An example that builds its own `Instruction`, because the callee's crate is +not on a compatible Solana version, hands `invoke` a slice of `CpiHandle` +rather than a slice of `AccountInfo`. Four rules, all of them enforced at +runtime rather than by the compiler: + +- **The handles are positional.** v2 walks the instruction's account metas and + binds each one to the next handle in the slice. A handle that does not match + the meta at its position fails the whole call with `InvalidArgument`, so the + list has to mirror the metas exactly. +- **The program account is not a handle.** v1 code habitually pushed the callee + program's `AccountInfo` into the infos vec; in v2 that extra leading entry is + what breaks the positional match. +- **An account filling two meta slots supplies two handles.** Bubblegum's + `Transfer` names the same PDA as both `leaf_owner` and `leaf_delegate`, so the + handle appears twice. Read-only handles make this trivial: `cpi_handle()` + takes `&self`. +- **Writability has to be at least as strong as the meta.** A writable meta + needs a writable handle; a read-only meta accepts either. Going the other way, + `cpi_handle_mut()` on an account not declared `mut` panics outright with + *"cpi_handle_mut called on a read-only account"*, which surfaces as + `ProgramFailedToComplete` and a `src/traits.rs` log line. + +Mixing the two handle kinds in one vec means converting element-wise, as in +`vec![a.cpi_handle_mut().into(), b.cpi_handle()]`, since a trailing +`.map(CpiHandle::from)` forces every element to be a `CpiHandleMut`. + +A `BorshAccount` that signs such a CPI still needs `release_borrow()` first, per +[Borrows held across CPIs](#borrows-held-across-cpis-are-what-tests-catch). When the account is read-only and +nothing reads it after the CPI, there is no reacquire, because `reacquire_borrow_mut` +asserts the account was loaded mutably. + +Vendored types that derive `BorshSerialize` over `Address` fields need the +`borsh` feature on `solana-address`; `Address` is pinocchio's re-export of that +crate's type and carries no borsh impls by default. Their `serialize` returns +`io::Error`, which does not convert into `ProgramError`, so map it: + +```rust +args.serialize(&mut data) + .map_err(|_| ProgramError::InvalidInstructionData)?; +``` + +## Seeds, Sysvars and Cross-Program Types + +`seeds` takes a byte array directly and binds it itself: `id.to_le_bytes()`, +not `id.to_le_bytes().as_ref()`, which produces a temporary that dies before the +derive uses it. + +pinocchio ships only the `Clock` and `Rent` sysvars. Anything else (this +repository needs `LastRestartSlot`) has to be declared locally and read through +the +`sol_get_sysvar` syscall (`solana-define-syscall`). The Quasar variants of these +same examples carry the identical workaround. + +A sibling program's account is an `UncheckedAccount` validated by a constraint: +v2 only generates a `program` marker module from `declare_program!`, not from +`#[program]` in a dependency crate. + +Ambiguity to watch for: the prelude exports an `Event` trait, so a state struct +named `Event` that reaches the crate root via glob re-export becomes ambiguous. +Import it explicitly (`use crate::state::Event;`). + +## Tests Assert Numeric Error Codes + +The test-side surface barely changed, but three things move: + +- `anchor_lang::solana_program` has no `system_program` submodule. The real + module is at the crate root and exposes `ID`, not `id()`. +- `solana_program::pubkey::Pubkey` only exists under the `compat` feature; + `anchor_lang::Address` is the same 32-byte type. +- `anchor_lang::prelude::Clock` is pinocchio's onchain type. LiteSVM's + `get_sysvar` / `set_sysvar` want the host-side `solana_clock::Clock`. +- `#[error_code]` no longer generates `From for u32`. It makes the enum + `#[repr(u32)]` and generates only `From for anchor_lang::Error`, so a + test asserting on the wire code writes `my_error as u32 + 6000` (6000 being + the default offset, overridable with `#[error_code(offset = ...)]`). + +Tests that decode account bytes with borsh keep working, because +`BorshConfig` makes wincode's wire format byte-identical, but a Pod account +that grew explicit padding needs that padding mirrored in the test's decode +struct, since `try_from_slice` rejects trailing bytes. + +## A Workspace With More Than One Program Needs Per-Crate Builds + +`cargo-build-sbf` at an anchor workspace root builds every member in one +invocation, and cargo unifies features across them. A program that depends on a +sibling with `features = ["cpi"]`, which implies `no-entrypoint`, therefore +makes that sibling's *own* `.so` build with `no-entrypoint` too, and in v2 that +exports its dispatch as `__anchor_dispatch` rather than `entrypoint`. The +result loads with `ProgramLoad("Entrypoint out of bounds")`. + +`anchor build` builds each program separately and is unaffected. Anything else +driving `cargo-build-sbf` has to do the same. + +## Toolchain Versions the Port Depends On + +CI installs the CLI with `avm install 2.0.0-rc.1`; it is a pre-release, so it +has to be named explicitly rather than resolved as latest. A v2 CLI cannot build +v1 programs, so that pin can only move once every example is ported. + +`anchor idl build` compiles the test targets too, so the `.so` has to exist +first, so run `cargo-build-sbf` before regenerating an IDL. diff --git a/finance/betting-market/anchor/programs/betting-market/Cargo.toml b/finance/betting-market/anchor/programs/betting-market/Cargo.toml index a30d1ac15..9acadab10 100644 --- a/finance/betting-market/anchor/programs/betting-market/Cargo.toml +++ b/finance/betting-market/anchor/programs/betting-market/Cargo.toml @@ -14,15 +14,23 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] # init-if-needed: place_bet and the lazy User index create accounts only on first use. -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] # no-entrypoint: solana-kite pulls these SPL program crates into the host test diff --git a/finance/betting-market/anchor/programs/betting-market/src/instructions/add_outcome.rs b/finance/betting-market/anchor/programs/betting-market/src/instructions/add_outcome.rs index 0095688cd..aa6e4b653 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/instructions/add_outcome.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/instructions/add_outcome.rs @@ -1,41 +1,43 @@ use anchor_lang::prelude::*; -use crate::{error::BettingError, Config, Event, EventStatus, Outcome}; +use crate::state::Event; + +use crate::{error::BettingError, Config, EventStatus, Outcome}; pub const MAX_LABEL_LEN: usize = 64; #[derive(Accounts)] -pub struct AddOutcomeAccountConstraints<'info> { - #[account(mut)] - pub admin: Signer<'info>, +pub struct AddOutcomeAccountConstraints { + #[account(mut, address = config.admin @ BettingError::Unauthorized)] + pub admin: Signer, - #[account( - seeds = [b"config"], - bump = config.bump, - has_one = admin @ BettingError::Unauthorized, - )] - pub config: Account<'info, Config>, + #[account(seeds = [b"config"], + bump = config.bump)] + pub config: BorshAccount, #[account( mut, - seeds = [b"event", event.event_id.to_le_bytes().as_ref()], + seeds = [b"event", event.event_id.to_le_bytes()], bump = event.bump, )] - pub event: Account<'info, Event>, + pub event: BorshAccount, #[account( init, payer = admin, space = Outcome::DISCRIMINATOR.len() + Outcome::INIT_SPACE, - seeds = [b"outcome", event.key().as_ref(), &[event.outcome_count]], + seeds = [b"outcome", event.address().as_ref(), &[event.outcome_count]], bump )] - pub outcome: Account<'info, Outcome>, + pub outcome: BorshAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } -pub fn handle_add_outcome(context: Context, label: String) -> Result<()> { +pub fn handle_add_outcome( + context: &mut Context, + label: String, +) -> Result<()> { require!(label.len() <= MAX_LABEL_LEN, BettingError::LabelTooLong); require!( context.accounts.event.status == EventStatus::Open, @@ -49,14 +51,14 @@ pub fn handle_add_outcome(context: Context, label: ); let index = context.accounts.event.outcome_count; - context.accounts.outcome.set_inner(Outcome { - event: context.accounts.event.key(), + *context.accounts.outcome = Outcome { + event: *context.accounts.event.address(), index, label, total_amount: 0, bet_count: 0, bump: context.bumps.outcome, - }); + }; context.accounts.event.outcome_count += 1; Ok(()) diff --git a/finance/betting-market/anchor/programs/betting-market/src/instructions/cancel_event.rs b/finance/betting-market/anchor/programs/betting-market/src/instructions/cancel_event.rs index 2bec20497..fbd31f33a 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/instructions/cancel_event.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/instructions/cancel_event.rs @@ -1,29 +1,29 @@ use anchor_lang::prelude::*; -use crate::{error::BettingError, Config, Event, EventStatus}; +use crate::state::Event; + +use crate::{error::BettingError, Config, EventStatus}; // Abandon an event that can't be resolved (e.g. the real-world result is void). // Bettors then reclaim their exact stakes via `claim_refund`; no fee is taken. #[derive(Accounts)] -pub struct CancelEventAccountConstraints<'info> { - pub admin: Signer<'info>, +pub struct CancelEventAccountConstraints { + #[account(address = config.admin @ BettingError::Unauthorized)] + pub admin: Signer, - #[account( - seeds = [b"config"], - bump = config.bump, - has_one = admin @ BettingError::Unauthorized, - )] - pub config: Account<'info, Config>, + #[account(seeds = [b"config"], + bump = config.bump)] + pub config: BorshAccount, #[account( mut, - seeds = [b"event", event.event_id.to_le_bytes().as_ref()], + seeds = [b"event", event.event_id.to_le_bytes()], bump = event.bump, )] - pub event: Account<'info, Event>, + pub event: BorshAccount, } -pub fn handle_cancel_event(context: Context) -> Result<()> { +pub fn handle_cancel_event(context: &mut Context) -> Result<()> { require!( context.accounts.event.status == EventStatus::Open, BettingError::EventNotOpen diff --git a/finance/betting-market/anchor/programs/betting-market/src/instructions/claim_refund.rs b/finance/betting-market/anchor/programs/betting-market/src/instructions/claim_refund.rs index c922d9cc6..45b4ee352 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/instructions/claim_refund.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/instructions/claim_refund.rs @@ -1,42 +1,48 @@ use anchor_lang::prelude::*; + +use crate::state::Event; +use anchor_spl::mint; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; -use crate::{error::BettingError, Bet, Event, EventStatus, User}; +use crate::{error::BettingError, Bet, EventStatus, User}; use super::transfer_tokens_from_vault; #[derive(Accounts)] -pub struct ClaimRefundAccountConstraints<'info> { - #[account(mut)] - pub bettor: Signer<'info>, +pub struct ClaimRefundAccountConstraints { + #[account(mut, address = bet.bettor)] + pub bettor: Signer, #[account(mint::token_program = token_program)] - pub token_mint: InterfaceAccount<'info, Mint>, + pub token_mint: InterfaceAccount, + // `mut` so the borrow released for the vault CPI below can be reacquired: + // v2 has no read-only reacquire, and the derive dereferences `event` again + // when it checks the constraints that name it. #[account( - seeds = [b"event", event.event_id.to_le_bytes().as_ref()], + mut, + seeds = [b"event", event.event_id.to_le_bytes()], bump = event.bump, + address = bet.event, )] - pub event: Account<'info, Event>, + pub event: BorshAccount, // Closing the Bet ends the position: the rent goes back to the bettor and // a second refund fails because the account no longer exists. #[account( mut, close = bettor, - has_one = bettor, - has_one = event, - seeds = [b"bet", bet.outcome.as_ref(), bettor.key().as_ref()], + seeds = [b"bet", bet.outcome.as_ref(), bettor.address().as_ref()], bump = bet.bump, )] - pub bet: Account<'info, Bet>, + pub bet: BorshAccount, #[account( mut, - seeds = [b"user", bettor.key().as_ref()], + seeds = [b"user", bettor.address().as_ref()], bump = user.bump, )] - pub user: Account<'info, User>, + pub user: BorshAccount, #[account( mut, @@ -44,7 +50,7 @@ pub struct ClaimRefundAccountConstraints<'info> { associated_token::authority = bettor, associated_token::token_program = token_program, )] - pub bettor_token_account: InterfaceAccount<'info, TokenAccount>, + pub bettor_token_account: InterfaceAccount, #[account( mut, @@ -52,12 +58,12 @@ pub struct ClaimRefundAccountConstraints<'info> { associated_token::authority = event, associated_token::token_program = token_program, )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } -pub fn handle_claim_refund(context: Context) -> Result<()> { +pub fn handle_claim_refund(context: &mut Context) -> Result<()> { require!( context.accounts.event.status == EventStatus::Cancelled, BettingError::EventNotCancelled @@ -68,21 +74,29 @@ pub fn handle_claim_refund(context: Context) -> R // The position is over, so drop the Bet from the bettor's index before the // transfer (effects before interactions); the Bet account itself closes // when the instruction finishes. - let bet_key = context.accounts.bet.key(); + let bet_key = context.accounts.bet.address(); context.accounts.user.remove_bet(&bet_key)?; let event_id = context.accounts.event.event_id; let event_bump = context.accounts.event.bump; + // `event` signs the transfer below. Release its borrow across + // the CPI: the runtime rejects a CPI that borrows an account we hold. + context.accounts.event.release_borrow()?; + let event_view = *context.accounts.event.account(); + transfer_tokens_from_vault( - &context.accounts.vault, - &context.accounts.bettor_token_account, + &mut context.accounts.vault, + &mut context.accounts.bettor_token_account, stake, &context.accounts.token_mint, - &context.accounts.event.to_account_info(), + event_view, &context.accounts.token_program, event_id, event_bump, )?; + // Take the borrow back before the derive's exit path touches `event` again. + context.accounts.event.reacquire_borrow_mut()?; + Ok(()) } diff --git a/finance/betting-market/anchor/programs/betting-market/src/instructions/claim_winnings.rs b/finance/betting-market/anchor/programs/betting-market/src/instructions/claim_winnings.rs index 912945d0d..2406aaab6 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/instructions/claim_winnings.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/instructions/claim_winnings.rs @@ -1,42 +1,48 @@ use anchor_lang::prelude::*; + +use crate::state::Event; +use anchor_spl::mint; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; -use crate::{error::BettingError, Bet, Event, EventStatus, User}; +use crate::{error::BettingError, Bet, EventStatus, User}; use super::transfer_tokens_from_vault; #[derive(Accounts)] -pub struct ClaimWinningsAccountConstraints<'info> { - #[account(mut)] - pub bettor: Signer<'info>, +pub struct ClaimWinningsAccountConstraints { + #[account(mut, address = bet.bettor)] + pub bettor: Signer, #[account(mint::token_program = token_program)] - pub token_mint: InterfaceAccount<'info, Mint>, + pub token_mint: InterfaceAccount, + // `mut` so the borrow released for the vault CPI below can be reacquired: + // v2 has no read-only reacquire, and the derive dereferences `event` again + // when it checks the constraints that name it. #[account( - seeds = [b"event", event.event_id.to_le_bytes().as_ref()], + mut, + seeds = [b"event", event.event_id.to_le_bytes()], bump = event.bump, + address = bet.event, )] - pub event: Account<'info, Event>, + pub event: BorshAccount, // Closing the Bet ends the position: the rent goes back to the bettor and // a second claim fails because the account no longer exists. #[account( mut, close = bettor, - has_one = bettor, - has_one = event, - seeds = [b"bet", bet.outcome.as_ref(), bettor.key().as_ref()], + seeds = [b"bet", bet.outcome.as_ref(), bettor.address().as_ref()], bump = bet.bump, )] - pub bet: Account<'info, Bet>, + pub bet: BorshAccount, #[account( mut, - seeds = [b"user", bettor.key().as_ref()], + seeds = [b"user", bettor.address().as_ref()], bump = user.bump, )] - pub user: Account<'info, User>, + pub user: BorshAccount, #[account( mut, @@ -44,7 +50,7 @@ pub struct ClaimWinningsAccountConstraints<'info> { associated_token::authority = bettor, associated_token::token_program = token_program, )] - pub bettor_token_account: InterfaceAccount<'info, TokenAccount>, + pub bettor_token_account: InterfaceAccount, #[account( mut, @@ -52,12 +58,12 @@ pub struct ClaimWinningsAccountConstraints<'info> { associated_token::authority = event, associated_token::token_program = token_program, )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } -pub fn handle_claim_winnings(context: Context) -> Result<()> { +pub fn handle_claim_winnings(context: &mut Context) -> Result<()> { require!( context.accounts.event.status == EventStatus::Settled, BettingError::EventNotSettled @@ -93,21 +99,29 @@ pub fn handle_claim_winnings(context: Context) // The position is over, so drop the Bet from the bettor's index before the // transfer (effects before interactions); the Bet account itself closes // when the instruction finishes. - let bet_key = context.accounts.bet.key(); + let bet_key = context.accounts.bet.address(); context.accounts.user.remove_bet(&bet_key)?; let event_id = context.accounts.event.event_id; let event_bump = context.accounts.event.bump; + // `event` signs the transfer below. Release its borrow across + // the CPI: the runtime rejects a CPI that borrows an account we hold. + context.accounts.event.release_borrow()?; + let event_view = *context.accounts.event.account(); + transfer_tokens_from_vault( - &context.accounts.vault, - &context.accounts.bettor_token_account, + &mut context.accounts.vault, + &mut context.accounts.bettor_token_account, payout, &context.accounts.token_mint, - &context.accounts.event.to_account_info(), + event_view, &context.accounts.token_program, event_id, event_bump, )?; + // Take the borrow back before the derive's exit path touches `event` again. + context.accounts.event.reacquire_borrow_mut()?; + Ok(()) } diff --git a/finance/betting-market/anchor/programs/betting-market/src/instructions/close_losing_bet.rs b/finance/betting-market/anchor/programs/betting-market/src/instructions/close_losing_bet.rs index ed533dddc..5fcb37b68 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/instructions/close_losing_bet.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/instructions/close_losing_bet.rs @@ -1,41 +1,44 @@ use anchor_lang::prelude::*; -use crate::{error::BettingError, Bet, Event, EventStatus, User}; +use crate::state::Event; + +use crate::{error::BettingError, Bet, EventStatus, User}; // A losing bet pays nothing, but it still occupies a slot in the bettor's // User index and holds rent. Closing it frees the slot (so the bettor can // open a new position) and returns the rent. Winning bets must go through // claim_winnings instead, which also pays out the stake and winnings. #[derive(Accounts)] -pub struct CloseLosingBetAccountConstraints<'info> { - #[account(mut)] - pub bettor: Signer<'info>, +pub struct CloseLosingBetAccountConstraints { + #[account(mut, address = bet.bettor)] + pub bettor: Signer, #[account( - seeds = [b"event", event.event_id.to_le_bytes().as_ref()], + seeds = [b"event", event.event_id.to_le_bytes()], bump = event.bump, + address = bet.event, )] - pub event: Account<'info, Event>, + pub event: BorshAccount, #[account( mut, close = bettor, - has_one = bettor, - has_one = event, - seeds = [b"bet", bet.outcome.as_ref(), bettor.key().as_ref()], + seeds = [b"bet", bet.outcome.as_ref(), bettor.address().as_ref()], bump = bet.bump, )] - pub bet: Account<'info, Bet>, + pub bet: BorshAccount, #[account( mut, - seeds = [b"user", bettor.key().as_ref()], + seeds = [b"user", bettor.address().as_ref()], bump = user.bump, )] - pub user: Account<'info, User>, + pub user: BorshAccount, } -pub fn handle_close_losing_bet(context: Context) -> Result<()> { +pub fn handle_close_losing_bet( + context: &mut Context, +) -> Result<()> { require!( context.accounts.event.status == EventStatus::Settled, BettingError::EventNotSettled @@ -45,7 +48,7 @@ pub fn handle_close_losing_bet(context: Context { +pub struct InitializeConfigAccountConstraints { #[account(mut)] - pub admin: Signer<'info>, + pub admin: Signer, #[account(mint::token_program = token_program)] - pub token_mint: InterfaceAccount<'info, Mint>, + pub token_mint: InterfaceAccount, #[account( init, @@ -20,26 +21,26 @@ pub struct InitializeConfigAccountConstraints<'info> { seeds = [b"config"], bump )] - pub config: Account<'info, Config>, + pub config: BorshAccount, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } pub fn handle_initialize_config( - context: Context, + context: &mut Context, fee_bps: u16, - fee_recipient: Pubkey, + fee_recipient: Address, ) -> Result<()> { require!(fee_bps <= MAX_FEE_BPS, BettingError::FeeTooHigh); - context.accounts.config.set_inner(Config { - admin: context.accounts.admin.key(), - token_mint: context.accounts.token_mint.key(), + *context.accounts.config = Config { + admin: *context.accounts.admin.address(), + token_mint: *context.accounts.token_mint.address(), fee_recipient, fee_bps, event_count: 0, bump: context.bumps.config, - }); + }; Ok(()) } diff --git a/finance/betting-market/anchor/programs/betting-market/src/instructions/initialize_event.rs b/finance/betting-market/anchor/programs/betting-market/src/instructions/initialize_event.rs index 1e90e885a..427bbb861 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/instructions/initialize_event.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/instructions/initialize_event.rs @@ -1,39 +1,38 @@ use anchor_lang::prelude::*; + +use crate::state::Event; +use anchor_spl::mint; use anchor_spl::{ associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface}, }; -use crate::{error::BettingError, Config, Event, EventStatus}; +use crate::{error::BettingError, Config, EventStatus}; pub const MAX_DESCRIPTION_LEN: usize = 200; #[derive(Accounts)] #[instruction(event_id: u64)] -pub struct InitializeEventAccountConstraints<'info> { - #[account(mut)] - pub admin: Signer<'info>, +pub struct InitializeEventAccountConstraints { + #[account(mut, address = config.admin @ BettingError::Unauthorized)] + pub admin: Signer, - #[account( - mut, + #[account(mut, seeds = [b"config"], - bump = config.bump, - has_one = admin @ BettingError::Unauthorized, - has_one = token_mint, - )] - pub config: Account<'info, Config>, + bump = config.bump)] + pub config: BorshAccount, - #[account(mint::token_program = token_program)] - pub token_mint: InterfaceAccount<'info, Mint>, + #[account(mint::token_program = token_program, address = config.token_mint)] + pub token_mint: InterfaceAccount, #[account( init, payer = admin, space = Event::DISCRIMINATOR.len() + Event::INIT_SPACE, - seeds = [b"event", event_id.to_le_bytes().as_ref()], + seeds = [b"event", event_id.to_le_bytes()], bump )] - pub event: Account<'info, Event>, + pub event: BorshAccount, // The single pool for the whole market: an ATA owned by the Event PDA. #[account( @@ -43,15 +42,15 @@ pub struct InitializeEventAccountConstraints<'info> { associated_token::authority = event, associated_token::token_program = token_program )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } pub fn handle_initialize_event( - context: Context, + context: &mut Context, event_id: u64, description: String, ) -> Result<()> { @@ -60,7 +59,7 @@ pub fn handle_initialize_event( BettingError::DescriptionTooLong ); - context.accounts.event.set_inner(Event { + *context.accounts.event = Event { event_id, description, outcome_count: 0, @@ -71,7 +70,7 @@ pub fn handle_initialize_event( winning_pool: 0, distributable_losing_pool: 0, bump: context.bumps.event, - }); + }; context.accounts.config.event_count += 1; Ok(()) diff --git a/finance/betting-market/anchor/programs/betting-market/src/instructions/mod.rs b/finance/betting-market/anchor/programs/betting-market/src/instructions/mod.rs index 29a4a92a8..9056cbadf 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/instructions/mod.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/instructions/mod.rs @@ -3,8 +3,8 @@ pub mod cancel_event; pub mod claim_refund; pub mod claim_winnings; pub mod close_losing_bet; -pub mod initialize_event; pub mod initialize_config; +pub mod initialize_event; pub mod place_bet; pub mod settle_event; pub mod shared; @@ -14,8 +14,8 @@ pub use cancel_event::*; pub use claim_refund::*; pub use claim_winnings::*; pub use close_losing_bet::*; -pub use initialize_event::*; pub use initialize_config::*; +pub use initialize_event::*; pub use place_bet::*; pub use settle_event::*; pub use shared::*; diff --git a/finance/betting-market/anchor/programs/betting-market/src/instructions/place_bet.rs b/finance/betting-market/anchor/programs/betting-market/src/instructions/place_bet.rs index b7043e6f0..2af5be0c4 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/instructions/place_bet.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/instructions/place_bet.rs @@ -1,44 +1,42 @@ use anchor_lang::prelude::*; + +use crate::state::Event; +use anchor_spl::mint; use anchor_spl::{ associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface}, }; -use crate::{ - error::BettingError, Bet, Config, Event, EventStatus, Outcome, User, MAX_BETS_PER_USER, -}; +use crate::{error::BettingError, Bet, Config, EventStatus, Outcome, User, MAX_BETS_PER_USER}; use super::transfer_tokens_to_vault; #[derive(Accounts)] -pub struct PlaceBetAccountConstraints<'info> { +pub struct PlaceBetAccountConstraints { #[account(mut)] - pub bettor: Signer<'info>, + pub bettor: Signer, - #[account( - seeds = [b"config"], - bump = config.bump, - has_one = token_mint, - )] - pub config: Account<'info, Config>, + #[account(seeds = [b"config"], + bump = config.bump)] + pub config: BorshAccount, - #[account(mint::token_program = token_program)] - pub token_mint: Box>, + #[account(mint::token_program = token_program, address = config.token_mint)] + pub token_mint: Box>, #[account( mut, - seeds = [b"event", event.event_id.to_le_bytes().as_ref()], + seeds = [b"event", event.event_id.to_le_bytes()], bump = event.bump, + address = outcome.event, )] - pub event: Box>, + pub event: Box>, #[account( mut, - has_one = event, - seeds = [b"outcome", event.key().as_ref(), &[outcome.index]], + seeds = [b"outcome", event.address().as_ref(), &[outcome.index]], bump = outcome.bump, )] - pub outcome: Box>, + pub outcome: Box>, #[account( mut, @@ -46,7 +44,7 @@ pub struct PlaceBetAccountConstraints<'info> { associated_token::authority = bettor, associated_token::token_program = token_program, )] - pub bettor_token_account: Box>, + pub bettor_token_account: Box>, #[account( mut, @@ -54,32 +52,35 @@ pub struct PlaceBetAccountConstraints<'info> { associated_token::authority = event, associated_token::token_program = token_program, )] - pub vault: Box>, + pub vault: Box>, #[account( init_if_needed, payer = bettor, space = Bet::DISCRIMINATOR.len() + Bet::INIT_SPACE, - seeds = [b"bet", outcome.key().as_ref(), bettor.key().as_ref()], + seeds = [b"bet", outcome.address().as_ref(), bettor.address().as_ref()], bump )] - pub bet: Box>, + pub bet: Box>, #[account( init_if_needed, payer = bettor, space = User::DISCRIMINATOR.len() + User::INIT_SPACE, - seeds = [b"user", bettor.key().as_ref()], + seeds = [b"user", bettor.address().as_ref()], bump )] - pub user: Box>, + pub user: Box>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } -pub fn handle_place_bet(context: Context, amount: u64) -> Result<()> { +pub fn handle_place_bet( + context: &mut Context, + amount: u64, +) -> Result<()> { require!(amount > 0, BettingError::ZeroAmount); require!( context.accounts.event.status == EventStatus::Open, @@ -87,19 +88,19 @@ pub fn handle_place_bet(context: Context, amount: u6 ); transfer_tokens_to_vault( - &context.accounts.bettor_token_account, - &context.accounts.vault, + &mut context.accounts.bettor_token_account, + &mut context.accounts.vault, amount, &context.accounts.token_mint, &context.accounts.bettor, &context.accounts.token_program, )?; - let bettor_key = context.accounts.bettor.key(); - let event_key = context.accounts.event.key(); - let outcome_key = context.accounts.outcome.key(); + let bettor_key = *context.accounts.bettor.address(); + let event_key = *context.accounts.event.address(); + let outcome_key = *context.accounts.outcome.address(); let outcome_index = context.accounts.outcome.index; - let bet_key = context.accounts.bet.key(); + let bet_key = *context.accounts.bet.address(); let bet_bump = context.bumps.bet; let user_bump = context.bumps.user; @@ -138,7 +139,7 @@ pub fn handle_place_bet(context: Context, amount: u6 .ok_or(BettingError::MathOverflow)?; let user = &mut context.accounts.user; - if user.authority == Pubkey::default() { + if user.authority == Address::default() { user.authority = bettor_key; user.bump = user_bump; } diff --git a/finance/betting-market/anchor/programs/betting-market/src/instructions/settle_event.rs b/finance/betting-market/anchor/programs/betting-market/src/instructions/settle_event.rs index debfae8e7..42a392445 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/instructions/settle_event.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/instructions/settle_event.rs @@ -1,10 +1,13 @@ use anchor_lang::prelude::*; + +use crate::state::Event; +use anchor_spl::mint; use anchor_spl::{ associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface}, }; -use crate::{error::BettingError, Config, Event, EventStatus, Outcome}; +use crate::{error::BettingError, Config, EventStatus, Outcome}; use super::transfer_tokens_from_vault; @@ -12,35 +15,30 @@ const BPS_DENOMINATOR: u128 = 10_000; #[derive(Accounts)] #[instruction(winning_outcome_index: u8)] -pub struct SettleEventAccountConstraints<'info> { - #[account(mut)] - pub admin: Signer<'info>, +pub struct SettleEventAccountConstraints { + #[account(mut, address = config.admin @ BettingError::Unauthorized)] + pub admin: Signer, - #[account( - seeds = [b"config"], - bump = config.bump, - has_one = admin @ BettingError::Unauthorized, - has_one = token_mint, - has_one = fee_recipient, - )] - pub config: Account<'info, Config>, + #[account(seeds = [b"config"], + bump = config.bump)] + pub config: BorshAccount, - #[account(mint::token_program = token_program)] - pub token_mint: InterfaceAccount<'info, Mint>, + #[account(mint::token_program = token_program, address = config.token_mint)] + pub token_mint: InterfaceAccount, #[account( mut, - seeds = [b"event", event.event_id.to_le_bytes().as_ref()], + seeds = [b"event", event.event_id.to_le_bytes()], bump = event.bump, + address = winning_outcome.event, )] - pub event: Account<'info, Event>, + pub event: BorshAccount, #[account( - has_one = event, - seeds = [b"outcome", event.key().as_ref(), &[winning_outcome_index]], + seeds = [b"outcome", event.address().as_ref(), &[winning_outcome_index]], bump = winning_outcome.bump, )] - pub winning_outcome: Account<'info, Outcome>, + pub winning_outcome: BorshAccount, #[account( mut, @@ -48,10 +46,11 @@ pub struct SettleEventAccountConstraints<'info> { associated_token::authority = event, associated_token::token_program = token_program, )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, - /// CHECK: validated against config.fee_recipient by the `has_one` above. - pub fee_recipient: UncheckedAccount<'info>, + /// CHECK: validated against config.fee_recipient by the `address` constraint. + #[account(address = config.fee_recipient)] + pub fee_recipient: UncheckedAccount, #[account( init_if_needed, @@ -60,15 +59,15 @@ pub struct SettleEventAccountConstraints<'info> { associated_token::authority = fee_recipient, associated_token::token_program = token_program, )] - pub fee_recipient_token_account: InterfaceAccount<'info, TokenAccount>, + pub fee_recipient_token_account: InterfaceAccount, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } pub fn handle_settle_event( - context: Context, + context: &mut Context, winning_outcome_index: u8, ) -> Result<()> { require!( @@ -86,22 +85,32 @@ pub fn handle_settle_event( // Winners always get their own stake back; the fee is only ever charged on // the losing side, so a winner can never receive less than they staked. - let fee = (losing_pool as u128 * context.accounts.event.fee_bps as u128 / BPS_DENOMINATOR) as u64; + let fee = + (losing_pool as u128 * context.accounts.event.fee_bps as u128 / BPS_DENOMINATOR) as u64; let distributable_losing_pool = losing_pool - fee; if fee > 0 { let event_id = context.accounts.event.event_id; let event_bump = context.accounts.event.bump; + // `event` signs the transfer below. Release its borrow across the CPI: + // the runtime rejects a CPI that borrows an account we still hold. + context.accounts.event.release_borrow()?; + let event_view = *context.accounts.event.account(); + transfer_tokens_from_vault( - &context.accounts.vault, - &context.accounts.fee_recipient_token_account, + &mut context.accounts.vault, + &mut context.accounts.fee_recipient_token_account, fee, &context.accounts.token_mint, - &context.accounts.event.to_account_info(), + event_view, &context.accounts.token_program, event_id, event_bump, )?; + + // Take the borrow back before writing the settled state through it. + // Only released on this branch, so only reacquired here. + context.accounts.event.reacquire_borrow_mut()?; } let event = &mut context.accounts.event; diff --git a/finance/betting-market/anchor/programs/betting-market/src/instructions/shared.rs b/finance/betting-market/anchor/programs/betting-market/src/instructions/shared.rs index 8a6668912..0a64d113e 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/instructions/shared.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/instructions/shared.rs @@ -1,38 +1,41 @@ use anchor_lang::prelude::*; +use crate::state::Event; + use anchor_spl::token_interface::{ transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked, }; // Move tokens from a wallet-owned account into the vault. The authority is a // plain Signer (the bettor), so no PDA seeds are needed. -pub fn transfer_tokens_to_vault<'info>( - from: &InterfaceAccount<'info, TokenAccount>, - to: &InterfaceAccount<'info, TokenAccount>, +pub fn transfer_tokens_to_vault( + from: &mut InterfaceAccount, + to: &mut InterfaceAccount, amount: u64, - mint: &InterfaceAccount<'info, Mint>, - authority: &Signer<'info>, - token_program: &Interface<'info, TokenInterface>, + mint: &InterfaceAccount, + authority: &Signer, + token_program: &Interface<'static, TokenInterface>, ) -> Result<()> { + let decimals = mint.decimals(); let transfer_accounts = TransferChecked { - from: from.to_account_info(), - mint: mint.to_account_info(), - to: to.to_account_info(), - authority: authority.to_account_info(), + from: from.cpi_handle_mut(), + mint: mint.cpi_handle(), + to: to.cpi_handle_mut(), + authority: authority.cpi_handle(), }; - let cpi_context = CpiContext::new(token_program.key(), transfer_accounts); - transfer_checked(cpi_context, amount, mint.decimals) + let cpi_context = CpiContext::new(token_program.address(), transfer_accounts); + transfer_checked(cpi_context, amount, decimals) } // Move tokens out of the vault, signed by the Event PDA. The event vault's // authority is the Event account, so the program signs with the event's seeds. -pub fn transfer_tokens_from_vault<'info>( - vault: &InterfaceAccount<'info, TokenAccount>, - to: &InterfaceAccount<'info, TokenAccount>, +pub fn transfer_tokens_from_vault( + vault: &mut InterfaceAccount, + to: &mut InterfaceAccount, amount: u64, - mint: &InterfaceAccount<'info, Mint>, - event: &AccountInfo<'info>, - token_program: &Interface<'info, TokenInterface>, + mint: &InterfaceAccount, + event: AccountView, + token_program: &Interface<'static, TokenInterface>, event_id: u64, event_bump: u8, ) -> Result<()> { @@ -40,16 +43,14 @@ pub fn transfer_tokens_from_vault<'info>( let seeds = &[b"event".as_ref(), event_id_bytes.as_ref(), &[event_bump]]; let signer_seeds = [&seeds[..]]; + let decimals = mint.decimals(); let transfer_accounts = TransferChecked { - from: vault.to_account_info(), - mint: mint.to_account_info(), - to: to.to_account_info(), - authority: event.clone(), + from: vault.cpi_handle_mut(), + mint: mint.cpi_handle(), + to: to.cpi_handle_mut(), + authority: CpiHandle::readonly(&event), }; - let cpi_context = CpiContext::new_with_signer( - token_program.key(), - transfer_accounts, - &signer_seeds, - ); - transfer_checked(cpi_context, amount, mint.decimals) + let cpi_context = + CpiContext::new_with_signer(token_program.address(), transfer_accounts, &signer_seeds); + transfer_checked(cpi_context, amount, decimals) } diff --git a/finance/betting-market/anchor/programs/betting-market/src/lib.rs b/finance/betting-market/anchor/programs/betting-market/src/lib.rs index 731546e4c..bbd58f343 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/lib.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/lib.rs @@ -16,16 +16,16 @@ pub mod betting_market { // One-time setup: the signer becomes the admin and fixes the stake token and // the settlement fee (basis points) for every market in this deployment. pub fn initialize_config( - context: Context, + context: &mut Context, fee_bps: u16, - fee_recipient: Pubkey, + fee_recipient: Address, ) -> Result<()> { instructions::initialize_config::handle_initialize_config(context, fee_bps, fee_recipient) } // Admin opens a new market and creates its pool vault. pub fn initialize_event( - context: Context, + context: &mut Context, event_id: u64, description: String, ) -> Result<()> { @@ -33,41 +33,47 @@ pub mod betting_market { } // Admin adds a possible result. Only allowed before betting starts. - pub fn add_outcome(context: Context, label: String) -> Result<()> { + pub fn add_outcome( + context: &mut Context, + label: String, + ) -> Result<()> { instructions::add_outcome::handle_add_outcome(context, label) } // A bettor stakes tokens on one outcome. The stake joins the event's pool. - pub fn place_bet(context: Context, amount: u64) -> Result<()> { + pub fn place_bet(context: &mut Context, amount: u64) -> Result<()> { instructions::place_bet::handle_place_bet(context, amount) } // Admin resolves the market: takes the fee from the losing pool and records // the figures winners need to claim their share. - pub fn settle_event(context: Context, winning_outcome_index: u8) -> Result<()> { + pub fn settle_event( + context: &mut Context, + winning_outcome_index: u8, + ) -> Result<()> { instructions::settle_event::handle_settle_event(context, winning_outcome_index) } // A winner withdraws their stake plus their pro-rata share of the losing // pool. The Bet account closes and leaves the bettor's User index. - pub fn claim_winnings(context: Context) -> Result<()> { + pub fn claim_winnings(context: &mut Context) -> Result<()> { instructions::claim_winnings::handle_claim_winnings(context) } // A loser closes their worthless bet after settlement, reclaiming the // Bet account's rent and freeing the slot in their User index. - pub fn close_losing_bet(context: Context) -> Result<()> { + pub fn close_losing_bet(context: &mut Context) -> Result<()> { instructions::close_losing_bet::handle_close_losing_bet(context) } // Admin voids an unresolved market so bettors can be made whole. - pub fn cancel_event(context: Context) -> Result<()> { + pub fn cancel_event(context: &mut Context) -> Result<()> { instructions::cancel_event::handle_cancel_event(context) } // After a cancellation, a bettor reclaims their exact stake. The Bet // account closes and leaves the bettor's User index. - pub fn claim_refund(context: Context) -> Result<()> { + pub fn claim_refund(context: &mut Context) -> Result<()> { instructions::claim_refund::handle_claim_refund(context) } } diff --git a/finance/betting-market/anchor/programs/betting-market/src/state/bet.rs b/finance/betting-market/anchor/programs/betting-market/src/state/bet.rs index 65330d78f..1b19414cd 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/state/bet.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/state/bet.rs @@ -5,12 +5,12 @@ use anchor_lang::prelude::*; // one Bet per (outcome, bettor). The account lives only while the position is // open: it closes (rent back to the bettor) on claim_winnings, claim_refund, // or close_losing_bet, which is also what prevents double claims. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Bet { - pub bettor: Pubkey, - pub event: Pubkey, - pub outcome: Pubkey, + pub bettor: Address, + pub event: Address, + pub outcome: Address, pub outcome_index: u8, pub amount: u64, pub bump: u8, diff --git a/finance/betting-market/anchor/programs/betting-market/src/state/config.rs b/finance/betting-market/anchor/programs/betting-market/src/state/config.rs index 03cbcfb18..a417c5528 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/state/config.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/state/config.rs @@ -3,12 +3,12 @@ use anchor_lang::prelude::*; // The global, single Config account. Its `admin` is the only key allowed to // create events, add outcomes, settle, and cancel. `token_mint` fixes the one // asset every market in this deployment accepts as a stake. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Config { - pub admin: Pubkey, - pub token_mint: Pubkey, - pub fee_recipient: Pubkey, + pub admin: Address, + pub token_mint: Address, + pub fee_recipient: Address, // Protocol fee, in basis points, taken from the losing pool at settlement. pub fee_bps: u16, pub event_count: u64, diff --git a/finance/betting-market/anchor/programs/betting-market/src/state/event.rs b/finance/betting-market/anchor/programs/betting-market/src/state/event.rs index b88e89180..068d086ec 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/state/event.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/state/event.rs @@ -1,6 +1,6 @@ use anchor_lang::prelude::*; -#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)] +#[derive(Clone, PartialEq, Eq, InitSpace, IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub enum EventStatus { // Accepting bets. Open, @@ -13,7 +13,7 @@ pub enum EventStatus { // One betting market. All stakes across every outcome live in a single vault // token account whose authority is this Event PDA, so the program signs payouts // with the event's seeds. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Event { pub event_id: u64, diff --git a/finance/betting-market/anchor/programs/betting-market/src/state/outcome.rs b/finance/betting-market/anchor/programs/betting-market/src/state/outcome.rs index de817e540..51f18d204 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/state/outcome.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/state/outcome.rs @@ -3,10 +3,10 @@ use anchor_lang::prelude::*; // One possible result of an event (e.g. "Yes", "Team A wins"). `total_amount` // is this outcome's share of the pool and is the denominator for pro-rata // payouts when this outcome wins. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Outcome { - pub event: Pubkey, + pub event: Address, pub index: u8, #[max_len(64)] pub label: String, diff --git a/finance/betting-market/anchor/programs/betting-market/src/state/user.rs b/finance/betting-market/anchor/programs/betting-market/src/state/user.rs index 44e41b0f8..afa3cd3f4 100644 --- a/finance/betting-market/anchor/programs/betting-market/src/state/user.rs +++ b/finance/betting-market/anchor/programs/betting-market/src/state/user.rs @@ -14,19 +14,19 @@ pub const MAX_BETS_PER_USER: usize = 32; // authoritative stake state lives in the Bet accounts; this is a convenience // index. Entries are added by place_bet and removed whenever the Bet account // closes. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct User { - pub authority: Pubkey, + pub authority: Address, #[max_len(MAX_BETS_PER_USER)] - pub bets: Vec, + pub bets: Vec
, pub bump: u8, } impl User { // Drop a closed Bet's entry from the index. Order is not meaningful, so a // swap_remove (move the last entry into the gap) is the cheapest removal. - pub fn remove_bet(&mut self, bet_key: &Pubkey) -> Result<()> { + pub fn remove_bet(&mut self, bet_key: &Address) -> Result<()> { let index = self .bets .iter() diff --git a/finance/betting-market/anchor/programs/betting-market/tests/test_betting_market.rs b/finance/betting-market/anchor/programs/betting-market/tests/test_betting_market.rs index 8390c93f0..16282804f 100644 --- a/finance/betting-market/anchor/programs/betting-market/tests/test_betting_market.rs +++ b/finance/betting-market/anchor/programs/betting-market/tests/test_betting_market.rs @@ -1,14 +1,15 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - AccountDeserialize, InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, AccountDeserialize, Address, + InstructionData, ToAccountMetas, }, betting_market::{User, MAX_BETS_PER_USER}, litesvm::LiteSVM, solana_keypair::Keypair, solana_kite::{ create_associated_token_account, create_token_mint, create_wallet, - get_token_account_balance, mint_tokens_to_token_account, send_transaction_from_instructions, + get_token_account_balance, mint_tokens_to_token_account, + send_transaction_from_instructions, }, solana_signer::Signer, }; @@ -16,53 +17,60 @@ use { const DECIMALS: u8 = 6; const FEE_BPS: u16 = 200; // 2% -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn ata_program_id() -> Pubkey { +fn ata_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - Pubkey::find_program_address( +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + Address::find_program_address( &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &ata_program_id(), ) .0 } -fn config_pda() -> Pubkey { - Pubkey::find_program_address(&[b"config"], &betting_market::id()).0 +fn config_pda() -> Address { + Address::find_program_address(&[b"config"], &betting_market::id()).0 } -fn event_pda(event_id: u64) -> Pubkey { - Pubkey::find_program_address(&[b"event", &event_id.to_le_bytes()], &betting_market::id()).0 +fn event_pda(event_id: u64) -> Address { + Address::find_program_address(&[b"event", &event_id.to_le_bytes()], &betting_market::id()).0 } -fn outcome_pda(event: &Pubkey, index: u8) -> Pubkey { - Pubkey::find_program_address(&[b"outcome", event.as_ref(), &[index]], &betting_market::id()).0 +fn outcome_pda(event: &Address, index: u8) -> Address { + Address::find_program_address( + &[b"outcome", event.as_ref(), &[index]], + &betting_market::id(), + ) + .0 } -fn bet_pda(outcome: &Pubkey, bettor: &Pubkey) -> Pubkey { - Pubkey::find_program_address(&[b"bet", outcome.as_ref(), bettor.as_ref()], &betting_market::id()) - .0 +fn bet_pda(outcome: &Address, bettor: &Address) -> Address { + Address::find_program_address( + &[b"bet", outcome.as_ref(), bettor.as_ref()], + &betting_market::id(), + ) + .0 } -fn user_pda(bettor: &Pubkey) -> Pubkey { - Pubkey::find_program_address(&[b"user", bettor.as_ref()], &betting_market::id()).0 +fn user_pda(bettor: &Address) -> Address { + Address::find_program_address(&[b"user", bettor.as_ref()], &betting_market::id()).0 } struct Market { svm: LiteSVM, admin: Keypair, - mint: Pubkey, + mint: Address, fee_recipient: Keypair, - fee_recipient_ata: Pubkey, + fee_recipient_ata: Address, } // Spin up the SVM with the program loaded, an admin wallet, the stake-token mint @@ -70,7 +78,8 @@ struct Market { fn setup() -> Market { let mut svm = LiteSVM::new(); let program_bytes = include_bytes!("../../../target/deploy/betting_market.so"); - svm.add_program(betting_market::id(), program_bytes).unwrap(); + svm.add_program(betting_market::id(), program_bytes) + .unwrap(); let admin = create_wallet(&mut svm, 100_000_000_000).unwrap(); let mint = create_token_mint(&mut svm, &admin, DECIMALS, None).unwrap(); @@ -89,7 +98,7 @@ fn setup() -> Market { } // Create a funded bettor with a token ATA holding `amount` of the stake token. -fn create_bettor(market: &mut Market, amount: u64) -> (Keypair, Pubkey) { +fn create_bettor(market: &mut Market, amount: u64) -> (Keypair, Address) { let bettor = create_wallet(&mut market.svm, 10_000_000_000).unwrap(); let ata = create_associated_token_account( &mut market.svm, @@ -103,7 +112,7 @@ fn create_bettor(market: &mut Market, amount: u64) -> (Keypair, Pubkey) { (bettor, ata) } -fn initialize_config_ix(admin: Pubkey, mint: Pubkey, fee_recipient: Pubkey) -> Instruction { +fn initialize_config_ix(admin: Address, mint: Address, fee_recipient: Address) -> Instruction { Instruction::new_with_bytes( betting_market::id(), &betting_market::instruction::InitializeConfig { @@ -116,13 +125,18 @@ fn initialize_config_ix(admin: Pubkey, mint: Pubkey, fee_recipient: Pubkey) -> I token_mint: mint, config: config_pda(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) } -fn initialize_event_ix(admin: Pubkey, mint: Pubkey, event_id: u64, description: &str) -> Instruction { +fn initialize_event_ix( + admin: Address, + mint: Address, + event_id: u64, + description: &str, +) -> Instruction { let event = event_pda(event_id); Instruction::new_with_bytes( betting_market::id(), @@ -139,13 +153,13 @@ fn initialize_event_ix(admin: Pubkey, mint: Pubkey, event_id: u64, description: vault: derive_ata(&event, &mint), associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) } -fn add_outcome_ix(admin: Pubkey, event_id: u64, index: u8, label: &str) -> Instruction { +fn add_outcome_ix(admin: Address, event_id: u64, index: u8, label: &str) -> Instruction { let event = event_pda(event_id); Instruction::new_with_bytes( betting_market::id(), @@ -158,16 +172,16 @@ fn add_outcome_ix(admin: Pubkey, event_id: u64, index: u8, label: &str) -> Instr config: config_pda(), event, outcome: outcome_pda(&event, index), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) } fn place_bet_ix( - mint: Pubkey, - bettor: &Pubkey, - bettor_ata: &Pubkey, + mint: Address, + bettor: &Address, + bettor_ata: &Address, event_id: u64, outcome_index: u8, amount: u64, @@ -189,17 +203,17 @@ fn place_bet_ix( user: user_pda(bettor), associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) } fn settle_event_ix( - admin: Pubkey, - mint: Pubkey, - fee_recipient: Pubkey, - fee_recipient_ata: Pubkey, + admin: Address, + mint: Address, + fee_recipient: Address, + fee_recipient_ata: Address, event_id: u64, winning_outcome_index: u8, ) -> Instruction { @@ -221,16 +235,16 @@ fn settle_event_ix( fee_recipient_token_account: fee_recipient_ata, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) } fn claim_winnings_ix( - mint: Pubkey, - bettor: &Pubkey, - bettor_ata: &Pubkey, + mint: Address, + bettor: &Address, + bettor_ata: &Address, event_id: u64, outcome_index: u8, ) -> Instruction { @@ -253,7 +267,7 @@ fn claim_winnings_ix( ) } -fn cancel_event_ix(admin: Pubkey, event_id: u64) -> Instruction { +fn cancel_event_ix(admin: Address, event_id: u64) -> Instruction { Instruction::new_with_bytes( betting_market::id(), &betting_market::instruction::CancelEvent {}.data(), @@ -267,9 +281,9 @@ fn cancel_event_ix(admin: Pubkey, event_id: u64) -> Instruction { } fn claim_refund_ix( - mint: Pubkey, - bettor: &Pubkey, - bettor_ata: &Pubkey, + mint: Address, + bettor: &Address, + bettor_ata: &Address, event_id: u64, outcome_index: u8, ) -> Instruction { @@ -292,7 +306,7 @@ fn claim_refund_ix( ) } -fn close_losing_bet_ix(bettor: &Pubkey, event_id: u64, outcome_index: u8) -> Instruction { +fn close_losing_bet_ix(bettor: &Address, event_id: u64, outcome_index: u8) -> Instruction { let event = event_pda(event_id); let outcome = outcome_pda(&event, outcome_index); Instruction::new_with_bytes( @@ -310,7 +324,7 @@ fn close_losing_bet_ix(bettor: &Pubkey, event_id: u64, outcome_index: u8) -> Ins // Decode a User account so tests can assert exactly which Bet addresses the // per-wallet index currently holds. -fn read_user_bets(market: &Market, bettor: &Pubkey) -> Vec { +fn read_user_bets(market: &Market, bettor: &Address) -> Vec
{ let account = market.svm.get_account(&user_pda(bettor)).unwrap(); User::try_deserialize(&mut account.data.as_slice()) .unwrap() @@ -359,21 +373,42 @@ fn test_full_lifecycle() { send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &alice.pubkey(), &alice_ata, event_id, 0, 100)], + vec![place_bet_ix( + mint, + &alice.pubkey(), + &alice_ata, + event_id, + 0, + 100, + )], &[&alice], &alice.pubkey(), ) .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &bob.pubkey(), &bob_ata, event_id, 0, 300)], + vec![place_bet_ix( + mint, + &bob.pubkey(), + &bob_ata, + event_id, + 0, + 300, + )], &[&bob], &bob.pubkey(), ) .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &carol.pubkey(), &carol_ata, event_id, 1, 200)], + vec![place_bet_ix( + mint, + &carol.pubkey(), + &carol_ata, + event_id, + 1, + 200, + )], &[&carol], &carol.pubkey(), ) @@ -384,7 +419,10 @@ fn test_full_lifecycle() { assert_eq!(get_token_account_balance(&market.svm, &vault).unwrap(), 600); assert_eq!( read_user_bets(&market, &alice.pubkey()), - vec![bet_pda(&outcome_pda(&event_pda(event_id), 0), &alice.pubkey())] + vec![bet_pda( + &outcome_pda(&event_pda(event_id), 0), + &alice.pubkey() + )] ); // Settle to "Yes" (index 0). Losing pool 200, fee = 2% = 4, distributable = 196. @@ -392,7 +430,14 @@ fn test_full_lifecycle() { let fee_recipient_ata = market.fee_recipient_ata; send_transaction_from_instructions( &mut market.svm, - vec![settle_event_ix(admin, mint, fee_recipient, fee_recipient_ata, event_id, 0)], + vec![settle_event_ix( + admin, + mint, + fee_recipient, + fee_recipient_ata, + event_id, + 0, + )], &[&market.admin], &admin, ) @@ -405,14 +450,26 @@ fn test_full_lifecycle() { // Alice: 100 + 100*196/400 = 149. Bob: 300 + 300*196/400 = 447. send_transaction_from_instructions( &mut market.svm, - vec![claim_winnings_ix(mint, &alice.pubkey(), &alice_ata, event_id, 0)], + vec![claim_winnings_ix( + mint, + &alice.pubkey(), + &alice_ata, + event_id, + 0, + )], &[&alice], &alice.pubkey(), ) .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![claim_winnings_ix(mint, &bob.pubkey(), &bob_ata, event_id, 0)], + vec![claim_winnings_ix( + mint, + &bob.pubkey(), + &bob_ata, + event_id, + 0, + )], &[&bob], &bob.pubkey(), ) @@ -436,11 +493,20 @@ fn test_full_lifecycle() { // Carol bet the losing outcome, so she has nothing to claim. let carol_claim = send_transaction_from_instructions( &mut market.svm, - vec![claim_winnings_ix(mint, &carol.pubkey(), &carol_ata, event_id, 1)], + vec![claim_winnings_ix( + mint, + &carol.pubkey(), + &carol_ata, + event_id, + 1, + )], &[&carol], &carol.pubkey(), ); - assert!(carol_claim.is_err(), "loser must not be able to claim winnings"); + assert!( + carol_claim.is_err(), + "loser must not be able to claim winnings" + ); // Her losing position stays in the index until she closes it. let carol_bet = bet_pda(&outcome_pda(&event_pda(event_id), 1), &carol.pubkey()); @@ -464,7 +530,12 @@ fn test_only_admin_can_initialize_event() { let mallory = create_wallet(&mut market.svm, 10_000_000_000).unwrap(); let result = send_transaction_from_instructions( &mut market.svm, - vec![initialize_event_ix(mallory.pubkey(), mint, 7, "Unauthorized event")], + vec![initialize_event_ix( + mallory.pubkey(), + mint, + 7, + "Unauthorized event", + )], &[&mallory], &mallory.pubkey(), ); @@ -496,14 +567,28 @@ fn test_cannot_bet_after_settle() { .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &alice.pubkey(), &alice_ata, event_id, 0, 100)], + vec![place_bet_ix( + mint, + &alice.pubkey(), + &alice_ata, + event_id, + 0, + 100, + )], &[&alice], &alice.pubkey(), ) .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![settle_event_ix(admin, mint, fee_recipient, fee_recipient_ata, event_id, 0)], + vec![settle_event_ix( + admin, + mint, + fee_recipient, + fee_recipient_ata, + event_id, + 0, + )], &[&market.admin], &admin, ) @@ -511,7 +596,14 @@ fn test_cannot_bet_after_settle() { let late_bet = send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &bob.pubkey(), &bob_ata, event_id, 1, 100)], + vec![place_bet_ix( + mint, + &bob.pubkey(), + &bob_ata, + event_id, + 1, + 100, + )], &[&bob], &bob.pubkey(), ); @@ -543,21 +635,42 @@ fn test_double_claim_fails() { .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &alice.pubkey(), &alice_ata, event_id, 0, 100)], + vec![place_bet_ix( + mint, + &alice.pubkey(), + &alice_ata, + event_id, + 0, + 100, + )], &[&alice], &alice.pubkey(), ) .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &carol.pubkey(), &carol_ata, event_id, 1, 100)], + vec![place_bet_ix( + mint, + &carol.pubkey(), + &carol_ata, + event_id, + 1, + 100, + )], &[&carol], &carol.pubkey(), ) .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![settle_event_ix(admin, mint, fee_recipient, fee_recipient_ata, event_id, 0)], + vec![settle_event_ix( + admin, + mint, + fee_recipient, + fee_recipient_ata, + event_id, + 0, + )], &[&market.admin], &admin, ) @@ -565,7 +678,13 @@ fn test_double_claim_fails() { send_transaction_from_instructions( &mut market.svm, - vec![claim_winnings_ix(mint, &alice.pubkey(), &alice_ata, event_id, 0)], + vec![claim_winnings_ix( + mint, + &alice.pubkey(), + &alice_ata, + event_id, + 0, + )], &[&alice], &alice.pubkey(), ) @@ -574,7 +693,13 @@ fn test_double_claim_fails() { market.svm.expire_blockhash(); let second = send_transaction_from_instructions( &mut market.svm, - vec![claim_winnings_ix(mint, &alice.pubkey(), &alice_ata, event_id, 0)], + vec![claim_winnings_ix( + mint, + &alice.pubkey(), + &alice_ata, + event_id, + 0, + )], &[&alice], &alice.pubkey(), ); @@ -606,7 +731,14 @@ fn test_settle_outcome_without_bets_fails() { // Everyone bets Horse A; Horse B has no bets. send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &alice.pubkey(), &alice_ata, event_id, 0, 100)], + vec![place_bet_ix( + mint, + &alice.pubkey(), + &alice_ata, + event_id, + 0, + 100, + )], &[&alice], &alice.pubkey(), ) @@ -614,11 +746,21 @@ fn test_settle_outcome_without_bets_fails() { let result = send_transaction_from_instructions( &mut market.svm, - vec![settle_event_ix(admin, mint, fee_recipient, fee_recipient_ata, event_id, 1)], + vec![settle_event_ix( + admin, + mint, + fee_recipient, + fee_recipient_ata, + event_id, + 1, + )], &[&market.admin], &admin, ); - assert!(result.is_err(), "settling to an outcome with no bets must fail"); + assert!( + result.is_err(), + "settling to an outcome with no bets must fail" + ); } #[test] @@ -644,14 +786,28 @@ fn test_cancel_and_refund() { .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &alice.pubkey(), &alice_ata, event_id, 0, 250)], + vec![place_bet_ix( + mint, + &alice.pubkey(), + &alice_ata, + event_id, + 0, + 250, + )], &[&alice], &alice.pubkey(), ) .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &carol.pubkey(), &carol_ata, event_id, 1, 750)], + vec![place_bet_ix( + mint, + &carol.pubkey(), + &carol_ata, + event_id, + 1, + 750, + )], &[&carol], &carol.pubkey(), ) @@ -670,7 +826,13 @@ fn test_cancel_and_refund() { send_transaction_from_instructions( &mut market.svm, - vec![claim_refund_ix(mint, &alice.pubkey(), &alice_ata, event_id, 0)], + vec![claim_refund_ix( + mint, + &alice.pubkey(), + &alice_ata, + event_id, + 0, + )], &[&alice], &alice.pubkey(), ) @@ -681,15 +843,27 @@ fn test_cancel_and_refund() { send_transaction_from_instructions( &mut market.svm, - vec![claim_refund_ix(mint, &carol.pubkey(), &carol_ata, event_id, 1)], + vec![claim_refund_ix( + mint, + &carol.pubkey(), + &carol_ata, + event_id, + 1, + )], &[&carol], &carol.pubkey(), ) .unwrap(); // Both bettors made whole; no fee on a cancelled event. - assert_eq!(get_token_account_balance(&market.svm, &alice_ata).unwrap(), 1_000); - assert_eq!(get_token_account_balance(&market.svm, &carol_ata).unwrap(), 1_000); + assert_eq!( + get_token_account_balance(&market.svm, &alice_ata).unwrap(), + 1_000 + ); + assert_eq!( + get_token_account_balance(&market.svm, &carol_ata).unwrap(), + 1_000 + ); let vault = derive_ata(&event_pda(event_id), &mint); assert_eq!(get_token_account_balance(&market.svm, &vault).unwrap(), 0); } @@ -719,14 +893,28 @@ fn test_close_losing_bet_only_after_settle_and_only_for_losers() { .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &alice.pubkey(), &alice_ata, event_id, 0, 100)], + vec![place_bet_ix( + mint, + &alice.pubkey(), + &alice_ata, + event_id, + 0, + 100, + )], &[&alice], &alice.pubkey(), ) .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &carol.pubkey(), &carol_ata, event_id, 1, 100)], + vec![place_bet_ix( + mint, + &carol.pubkey(), + &carol_ata, + event_id, + 1, + 100, + )], &[&carol], &carol.pubkey(), ) @@ -739,11 +927,21 @@ fn test_close_losing_bet_only_after_settle_and_only_for_losers() { &[&carol], &carol.pubkey(), ); - assert!(premature_close.is_err(), "closing before settlement must fail"); + assert!( + premature_close.is_err(), + "closing before settlement must fail" + ); send_transaction_from_instructions( &mut market.svm, - vec![settle_event_ix(admin, mint, fee_recipient, fee_recipient_ata, event_id, 0)], + vec![settle_event_ix( + admin, + mint, + fee_recipient, + fee_recipient_ata, + event_id, + 0, + )], &[&market.admin], &admin, ) @@ -756,7 +954,10 @@ fn test_close_losing_bet_only_after_settle_and_only_for_losers() { &[&alice], &alice.pubkey(), ); - assert!(winner_close.is_err(), "a winning bet must not be closed as losing"); + assert!( + winner_close.is_err(), + "a winning bet must not be closed as losing" + ); let alice_bet = bet_pda(&outcome_pda(&event_pda(event_id), 0), &alice.pubkey()); assert_eq!(read_user_bets(&market, &alice.pubkey()), vec![alice_bet]); @@ -793,7 +994,12 @@ fn test_closing_a_bet_frees_a_slot_for_a_new_bet() { send_transaction_from_instructions( &mut market.svm, - vec![initialize_event_ix(admin, mint, full_event_id, "Wide field")], + vec![initialize_event_ix( + admin, + mint, + full_event_id, + "Wide field", + )], &[&market.admin], &admin, ) @@ -801,7 +1007,12 @@ fn test_closing_a_bet_frees_a_slot_for_a_new_bet() { for index in 0..outcome_count { send_transaction_from_instructions( &mut market.svm, - vec![add_outcome_ix(admin, full_event_id, index, &format!("Runner {index}"))], + vec![add_outcome_ix( + admin, + full_event_id, + index, + &format!("Runner {index}"), + )], &[&market.admin], &admin, ) @@ -823,13 +1034,23 @@ fn test_closing_a_bet_frees_a_slot_for_a_new_bet() { for index in 0..MAX_BETS_PER_USER as u8 { send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &alice.pubkey(), &alice_ata, full_event_id, index, STAKE)], + vec![place_bet_ix( + mint, + &alice.pubkey(), + &alice_ata, + full_event_id, + index, + STAKE, + )], &[&alice], &alice.pubkey(), ) .unwrap(); } - assert_eq!(read_user_bets(&market, &alice.pubkey()).len(), MAX_BETS_PER_USER); + assert_eq!( + read_user_bets(&market, &alice.pubkey()).len(), + MAX_BETS_PER_USER + ); // With the index full, any new position is rejected - on this event or another. let one_too_many = send_transaction_from_instructions( @@ -845,14 +1066,27 @@ fn test_closing_a_bet_frees_a_slot_for_a_new_bet() { &[&alice], &alice.pubkey(), ); - assert!(one_too_many.is_err(), "a full index must reject a new position"); + assert!( + one_too_many.is_err(), + "a full index must reject a new position" + ); let other_market_bet = send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &alice.pubkey(), &alice_ata, second_event_id, 0, STAKE)], + vec![place_bet_ix( + mint, + &alice.pubkey(), + &alice_ata, + second_event_id, + 0, + STAKE, + )], &[&alice], &alice.pubkey(), ); - assert!(other_market_bet.is_err(), "a full index must reject bets on any market"); + assert!( + other_market_bet.is_err(), + "a full index must reject bets on any market" + ); // Unwind one position: cancel the event and refund the first bet. send_transaction_from_instructions( @@ -864,7 +1098,13 @@ fn test_closing_a_bet_frees_a_slot_for_a_new_bet() { .unwrap(); send_transaction_from_instructions( &mut market.svm, - vec![claim_refund_ix(mint, &alice.pubkey(), &alice_ata, full_event_id, 0)], + vec![claim_refund_ix( + mint, + &alice.pubkey(), + &alice_ata, + full_event_id, + 0, + )], &[&alice], &alice.pubkey(), ) @@ -882,13 +1122,26 @@ fn test_closing_a_bet_frees_a_slot_for_a_new_bet() { market.svm.expire_blockhash(); send_transaction_from_instructions( &mut market.svm, - vec![place_bet_ix(mint, &alice.pubkey(), &alice_ata, second_event_id, 0, STAKE)], + vec![place_bet_ix( + mint, + &alice.pubkey(), + &alice_ata, + second_event_id, + 0, + STAKE, + )], &[&alice], &alice.pubkey(), ) .unwrap(); let final_bets = read_user_bets(&market, &alice.pubkey()); assert_eq!(final_bets.len(), MAX_BETS_PER_USER); - let new_bet = bet_pda(&outcome_pda(&event_pda(second_event_id), 0), &alice.pubkey()); - assert!(final_bets.contains(&new_bet), "the new position must appear in the index"); + let new_bet = bet_pda( + &outcome_pda(&event_pda(second_event_id), 0), + &alice.pubkey(), + ); + assert!( + final_bets.contains(&new_bet), + "the new position must appear in the index" + ); } diff --git a/finance/escrow/anchor/README.md b/finance/escrow/anchor/README.md index e88da57c5..20fd380d5 100644 --- a/finance/escrow/anchor/README.md +++ b/finance/escrow/anchor/README.md @@ -17,7 +17,7 @@ The maker pays the rent for the offer account and the vault, and every path that A maker opens an offer with `make_offer`, passing the `id`, `token_a_offered_amount`, and `token_b_wanted_amount`. The maker signs and pays all rent. The handler creates the offer PDA and the vault, creates the maker's token-B associated token account if needed (paid by the maker, so the eventual taker never funds a maker-owned account), moves the offered token A into the vault with `transfer_checked`, and records the offer state. -A taker settles the offer with `take_offer`. The taker signs. Anchor's constraints bind every account to the stored offer state (`has_one` on the maker and both mints, associated-token constraints on the vault and all token accounts, and the PDA seeds on the offer itself). The handler sends the wanted token B from the taker to the maker, releases the vault's token A to the taker signed by the offer PDA, and closes both the vault and the offer account back to the maker, who paid their rent. The taker's own token-A account is created on the fly if needed, paid by the taker. +A taker settles the offer with `take_offer`. The taker signs. Anchor's constraints bind every account to the stored offer state (`address = offer.maker` on the maker and `address = offer.token_mint_a` / `address = offer.token_mint_b` on the mints, associated-token constraints on the vault and all token accounts, and the PDA seeds on the offer itself). The handler sends the wanted token B from the taker to the maker, releases the vault's token A to the taker signed by the offer PDA, and closes both the vault and the offer account back to the maker, who paid their rent. The taker's own token-A account is created on the fly if needed, paid by the taker. A maker abandons an offer with `cancel_offer`. Only the maker can call it; without it, an unwanted offer would lock the maker's tokens in the vault forever. The handler returns the vault's token A to the maker and closes the vault and offer accounts, refunding both rents to the maker. diff --git a/finance/escrow/anchor/programs/escrow/Cargo.toml b/finance/escrow/anchor/programs/escrow/Cargo.toml index 47a3a726b..71fe7fd8a 100644 --- a/finance/escrow/anchor/programs/escrow/Cargo.toml +++ b/finance/escrow/anchor/programs/escrow/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"]} -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/finance/escrow/anchor/programs/escrow/src/instructions/cancel_offer.rs b/finance/escrow/anchor/programs/escrow/src/instructions/cancel_offer.rs index c2cd0fb9b..e62dbe7e8 100644 --- a/finance/escrow/anchor/programs/escrow/src/instructions/cancel_offer.rs +++ b/finance/escrow/anchor/programs/escrow/src/instructions/cancel_offer.rs @@ -14,11 +14,12 @@ use super::{close_token_account, transfer_tokens}; // account's rent unclaimed). The maker signs, the vault tokens flow back to // the maker, and both the vault and the offer accounts are closed. #[derive(Accounts)] -pub struct CancelOfferAccountConstraints<'info> { - #[account(mut)] - pub maker: Signer<'info>, +pub struct CancelOfferAccountConstraints { + #[account(mut, address = offer.maker)] + pub maker: Signer, - pub token_mint_a: InterfaceAccount<'info, Mint>, + #[account(address = offer.token_mint_a)] + pub token_mint_a: InterfaceAccount, #[account( mut, @@ -26,17 +27,15 @@ pub struct CancelOfferAccountConstraints<'info> { associated_token::authority = maker, associated_token::token_program = token_program, )] - pub maker_token_account_a: InterfaceAccount<'info, TokenAccount>, + pub maker_token_account_a: InterfaceAccount, #[account( mut, close = maker, - has_one = maker, - has_one = token_mint_a, - seeds = [b"offer", maker.key().as_ref(), offer.id.to_le_bytes().as_ref()], + seeds = [b"offer", maker.address().as_ref(), offer.id.to_le_bytes()], bump = offer.bump, )] - pub offer: Account<'info, Offer>, + pub offer: BorshAccount, #[account( mut, @@ -44,35 +43,44 @@ pub struct CancelOfferAccountConstraints<'info> { associated_token::authority = offer, associated_token::token_program = token_program, )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } -pub fn handle_cancel_offer(context: Context) -> Result<()> { - let maker_key = context.accounts.maker.key(); +pub fn handle_cancel_offer(context: &mut Context) -> Result<()> { + let maker_key = context.accounts.maker.address(); let id_bytes = context.accounts.offer.id.to_le_bytes(); let bump = [context.accounts.offer.bump]; let offer_seeds: &[&[u8]] = &[b"offer", maker_key.as_ref(), id_bytes.as_ref(), &bump]; + // Read the balance before taking the mutable borrow of the vault. + let vault_amount = context.accounts.vault.amount(); + + // `offer` signs both CPIs below. It is a data account, so it holds a live + // borrow on its buffer; the runtime rejects a CPI that borrows it again. + // Release the borrow for the duration of the CPIs and take it back after. + context.accounts.offer.release_borrow()?; + let offer_view = *context.accounts.offer.account(); + // Move all tokens back from the vault to the maker. transfer_tokens( - &context.accounts.vault, - &context.accounts.maker_token_account_a, - &context.accounts.vault.amount, + &mut context.accounts.vault, + &mut context.accounts.maker_token_account_a, + &vault_amount, &context.accounts.token_mint_a, - &context.accounts.offer.to_account_info(), + offer_view, &context.accounts.token_program, Some(offer_seeds), )?; // Close the vault, sending its rent lamports back to the maker. close_token_account( - &context.accounts.vault, - &context.accounts.maker.to_account_info(), - &context.accounts.offer.to_account_info(), + &mut context.accounts.vault, + *context.accounts.maker.account(), + offer_view, &context.accounts.token_program, Some(offer_seeds), )?; diff --git a/finance/escrow/anchor/programs/escrow/src/instructions/make_offer.rs b/finance/escrow/anchor/programs/escrow/src/instructions/make_offer.rs index cee614335..0566d66fe 100644 --- a/finance/escrow/anchor/programs/escrow/src/instructions/make_offer.rs +++ b/finance/escrow/anchor/programs/escrow/src/instructions/make_offer.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::mint; use anchor_spl::{ associated_token::AssociatedToken, @@ -12,15 +13,15 @@ use super::transfer_tokens; // See https://www.anchor-lang.com/docs/references/account-constraints#instruction-attribute #[derive(Accounts)] #[instruction(id: u64)] -pub struct MakeOfferAccountConstraints<'info> { +pub struct MakeOfferAccountConstraints { #[account(mut)] - pub maker: Signer<'info>, + pub maker: Signer, #[account(mint::token_program = token_program)] - pub token_mint_a: InterfaceAccount<'info, Mint>, + pub token_mint_a: InterfaceAccount, #[account(mint::token_program = token_program)] - pub token_mint_b: InterfaceAccount<'info, Mint>, + pub token_mint_b: InterfaceAccount, #[account( mut, @@ -28,7 +29,7 @@ pub struct MakeOfferAccountConstraints<'info> { associated_token::authority = maker, associated_token::token_program = token_program )] - pub maker_token_account_a: InterfaceAccount<'info, TokenAccount>, + pub maker_token_account_a: InterfaceAccount, // The maker's token-B ATA is initialized here, paid by the maker, so the // rent burden lives with the party who chose to open the offer (take_offer @@ -40,16 +41,16 @@ pub struct MakeOfferAccountConstraints<'info> { associated_token::authority = maker, associated_token::token_program = token_program )] - pub maker_token_account_b: InterfaceAccount<'info, TokenAccount>, + pub maker_token_account_b: InterfaceAccount, #[account( init, payer = maker, space = Offer::DISCRIMINATOR.len() + Offer::INIT_SPACE, - seeds = [b"offer", maker.key().as_ref(), id.to_le_bytes().as_ref()], + seeds = [b"offer", maker.address().as_ref(), id.to_le_bytes()], bump )] - pub offer: Account<'info, Offer>, + pub offer: BorshAccount, #[account( init, @@ -58,24 +59,24 @@ pub struct MakeOfferAccountConstraints<'info> { associated_token::authority = offer, associated_token::token_program = token_program )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } // Move the tokens from the maker's ATA to the vault pub fn handle_send_offered_tokens_to_vault( - context: &Context, + context: &mut Context, token_a_offered_amount: u64, ) -> Result<()> { transfer_tokens( - &context.accounts.maker_token_account_a, - &context.accounts.vault, + &mut context.accounts.maker_token_account_a, + &mut context.accounts.vault, &token_a_offered_amount, &context.accounts.token_mint_a, - &context.accounts.maker.to_account_info(), + *context.accounts.maker.account(), &context.accounts.token_program, None, ) @@ -83,17 +84,17 @@ pub fn handle_send_offered_tokens_to_vault( // Save the details of the offer to the offer account pub fn handle_save_offer( - context: Context, + context: &mut Context, id: u64, token_b_wanted_amount: u64, ) -> Result<()> { - context.accounts.offer.set_inner(Offer { + *context.accounts.offer = Offer { id, - maker: context.accounts.maker.key(), - token_mint_a: context.accounts.token_mint_a.key(), - token_mint_b: context.accounts.token_mint_b.key(), + maker: *context.accounts.maker.address(), + token_mint_a: *context.accounts.token_mint_a.address(), + token_mint_b: *context.accounts.token_mint_b.address(), token_b_wanted_amount, bump: context.bumps.offer, - }); + }; Ok(()) } diff --git a/finance/escrow/anchor/programs/escrow/src/instructions/shared.rs b/finance/escrow/anchor/programs/escrow/src/instructions/shared.rs index 349c107b3..3e2634f23 100644 --- a/finance/escrow/anchor/programs/escrow/src/instructions/shared.rs +++ b/finance/escrow/anchor/programs/escrow/src/instructions/shared.rs @@ -8,55 +8,56 @@ use anchor_spl::token_interface::{ // Transfer tokens from one token account to another. // When transferring out of a token account owned by a PDA, pass the PDA's // signer seeds via owning_pda_seeds; otherwise pass None. -pub fn transfer_tokens<'info>( - from: &InterfaceAccount<'info, TokenAccount>, - to: &InterfaceAccount<'info, TokenAccount>, +pub fn transfer_tokens( + from: &mut InterfaceAccount, + to: &mut InterfaceAccount, amount: &u64, - mint: &InterfaceAccount<'info, Mint>, - authority: &AccountInfo<'info>, - token_program: &Interface<'info, TokenInterface>, + mint: &InterfaceAccount, + authority: AccountView, + token_program: &Interface<'static, TokenInterface>, owning_pda_seeds: Option<&[&[u8]]>, ) -> Result<()> { + let decimals = mint.decimals(); let transfer_accounts = TransferChecked { - from: from.to_account_info(), - mint: mint.to_account_info(), - to: to.to_account_info(), - authority: authority.to_account_info(), + from: from.cpi_handle_mut(), + mint: mint.cpi_handle(), + to: to.cpi_handle_mut(), + authority: CpiHandle::readonly(&authority), }; let signer_seeds = owning_pda_seeds.map(|seeds| [seeds]); let cpi_context = match signer_seeds.as_ref() { Some(signer_seeds) => { - CpiContext::new_with_signer(token_program.key(), transfer_accounts, signer_seeds) + CpiContext::new_with_signer(token_program.address(), transfer_accounts, signer_seeds) } - None => CpiContext::new(token_program.key(), transfer_accounts), + None => CpiContext::new(token_program.address(), transfer_accounts), }; - transfer_checked(cpi_context, *amount, mint.decimals) + transfer_checked(cpi_context, *amount, decimals) } // Close a token account, sending its rent lamports to destination. // When the token account is owned by a PDA, pass the PDA's signer seeds via // owning_pda_seeds; otherwise pass None. -pub fn close_token_account<'info>( - token_account: &InterfaceAccount<'info, TokenAccount>, - destination: &AccountInfo<'info>, - authority: &AccountInfo<'info>, - token_program: &Interface<'info, TokenInterface>, +pub fn close_token_account( + token_account: &mut InterfaceAccount, + mut destination: AccountView, + authority: AccountView, + token_program: &Interface<'static, TokenInterface>, owning_pda_seeds: Option<&[&[u8]]>, ) -> Result<()> { let close_accounts = CloseAccount { - account: token_account.to_account_info(), - destination: destination.to_account_info(), - authority: authority.to_account_info(), + account: token_account.cpi_handle_mut(), + destination: CpiHandleMut::writable(&mut destination), + authority: CpiHandle::readonly(&authority), }; let signer_seeds = owning_pda_seeds.map(|seeds| [seeds]); let cpi_context = match signer_seeds.as_ref() { Some(signer_seeds) => { - CpiContext::new_with_signer(token_program.key(), close_accounts, signer_seeds) + CpiContext::new_with_signer(token_program.address(), close_accounts, signer_seeds) } - None => CpiContext::new(token_program.key(), close_accounts), + None => CpiContext::new(token_program.address(), close_accounts), }; close_account(cpi_context) diff --git a/finance/escrow/anchor/programs/escrow/src/instructions/take_offer.rs b/finance/escrow/anchor/programs/escrow/src/instructions/take_offer.rs index a8edc1a13..f1d6f58a2 100644 --- a/finance/escrow/anchor/programs/escrow/src/instructions/take_offer.rs +++ b/finance/escrow/anchor/programs/escrow/src/instructions/take_offer.rs @@ -10,16 +10,18 @@ use crate::Offer; use super::{close_token_account, transfer_tokens}; #[derive(Accounts)] -pub struct TakeOfferAccountConstraints<'info> { +pub struct TakeOfferAccountConstraints { #[account(mut)] - pub taker: Signer<'info>, + pub taker: Signer, - #[account(mut)] - pub maker: SystemAccount<'info>, + #[account(mut, address = offer.maker)] + pub maker: SystemAccount, - pub token_mint_a: InterfaceAccount<'info, Mint>, + #[account(address = offer.token_mint_a)] + pub token_mint_a: InterfaceAccount, - pub token_mint_b: InterfaceAccount<'info, Mint>, + #[account(address = offer.token_mint_b)] + pub token_mint_b: InterfaceAccount, #[account( init_if_needed, @@ -28,7 +30,7 @@ pub struct TakeOfferAccountConstraints<'info> { associated_token::authority = taker, associated_token::token_program = token_program, )] - pub taker_token_account_a: Box>, + pub taker_token_account_a: Box>, #[account( mut, @@ -36,7 +38,7 @@ pub struct TakeOfferAccountConstraints<'info> { associated_token::authority = taker, associated_token::token_program = token_program, )] - pub taker_token_account_b: Box>, + pub taker_token_account_b: Box>, // The maker's token-B ATA is initialized in make_offer, paid by the maker. #[account( @@ -45,18 +47,15 @@ pub struct TakeOfferAccountConstraints<'info> { associated_token::authority = maker, associated_token::token_program = token_program, )] - pub maker_token_account_b: Box>, + pub maker_token_account_b: Box>, #[account( mut, close = maker, - has_one = maker, - has_one = token_mint_a, - has_one = token_mint_b, - seeds = [b"offer", maker.key().as_ref(), offer.id.to_le_bytes().as_ref()], - bump = offer.bump + seeds = [b"offer", maker.address().as_ref(), offer.id.to_le_bytes()], + bump = offer.bump, )] - offer: Account<'info, Offer>, + offer: BorshAccount, #[account( mut, @@ -64,39 +63,51 @@ pub struct TakeOfferAccountConstraints<'info> { associated_token::authority = offer, associated_token::token_program = token_program, )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } pub fn handle_send_wanted_tokens_to_maker( - context: &Context, + context: &mut Context, ) -> Result<()> { + let wanted_amount = context.accounts.offer.token_b_wanted_amount; + let taker_view = *context.accounts.taker.account(); transfer_tokens( - &context.accounts.taker_token_account_b, - &context.accounts.maker_token_account_b, - &context.accounts.offer.token_b_wanted_amount, + &mut context.accounts.taker_token_account_b, + &mut context.accounts.maker_token_account_b, + &wanted_amount, &context.accounts.token_mint_b, - &context.accounts.taker.to_account_info(), + taker_view, &context.accounts.token_program, None, ) } -pub fn handle_withdraw_and_close_vault(context: Context) -> Result<()> { - let maker_key = context.accounts.maker.key(); +pub fn handle_withdraw_and_close_vault( + context: &mut Context, +) -> Result<()> { + let maker_key = context.accounts.maker.address(); let id_bytes = context.accounts.offer.id.to_le_bytes(); let bump = [context.accounts.offer.bump]; let offer_seeds: &[&[u8]] = &[b"offer", maker_key.as_ref(), id_bytes.as_ref(), &bump]; + // Read the balance before taking the mutable borrow of the vault. + let vault_amount = context.accounts.vault.amount(); + + // `offer` signs both CPIs below. It is a data account, so it holds a live + // borrow on its buffer; the runtime rejects a CPI that borrows it again. + context.accounts.offer.release_borrow()?; + let offer_view = *context.accounts.offer.account(); + transfer_tokens( - &context.accounts.vault, - &context.accounts.taker_token_account_a, - &context.accounts.vault.amount, + &mut context.accounts.vault, + &mut context.accounts.taker_token_account_a, + &vault_amount, &context.accounts.token_mint_a, - &context.accounts.offer.to_account_info(), + offer_view, &context.accounts.token_program, Some(offer_seeds), )?; @@ -104,10 +115,14 @@ pub fn handle_withdraw_and_close_vault(context: Context, + context: &mut Context, id: u64, token_a_offered_amount: u64, token_b_wanted_amount: u64, ) -> Result<()> { - instructions::make_offer::handle_send_offered_tokens_to_vault(&context, token_a_offered_amount)?; + instructions::make_offer::handle_send_offered_tokens_to_vault( + context, + token_a_offered_amount, + )?; instructions::make_offer::handle_save_offer(context, id, token_b_wanted_amount) } - pub fn take_offer(context: Context) -> Result<()> { - instructions::take_offer::handle_send_wanted_tokens_to_maker(&context)?; + pub fn take_offer(context: &mut Context) -> Result<()> { + instructions::take_offer::handle_send_wanted_tokens_to_maker(context)?; instructions::take_offer::handle_withdraw_and_close_vault(context) } @@ -32,7 +35,7 @@ pub mod escrow { // to the maker, and both the vault and offer accounts are closed (rent // refunded to the maker). Without this, abandoned offers would lock funds // forever. - pub fn cancel_offer(context: Context) -> Result<()> { + pub fn cancel_offer(context: &mut Context) -> Result<()> { instructions::cancel_offer::handle_cancel_offer(context) } } diff --git a/finance/escrow/anchor/programs/escrow/src/state/offer.rs b/finance/escrow/anchor/programs/escrow/src/state/offer.rs index 65f90af73..2848315b2 100644 --- a/finance/escrow/anchor/programs/escrow/src/state/offer.rs +++ b/finance/escrow/anchor/programs/escrow/src/state/offer.rs @@ -1,12 +1,12 @@ use anchor_lang::prelude::*; -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Offer { pub id: u64, - pub maker: Pubkey, - pub token_mint_a: Pubkey, - pub token_mint_b: Pubkey, + pub maker: Address, + pub token_mint_a: Address, + pub token_mint_b: Address, pub token_b_wanted_amount: u64, pub bump: u8, } diff --git a/finance/escrow/anchor/programs/escrow/tests/test_escrow.rs b/finance/escrow/anchor/programs/escrow/tests/test_escrow.rs index db586e808..837a099f8 100644 --- a/finance/escrow/anchor/programs/escrow/tests/test_escrow.rs +++ b/finance/escrow/anchor/programs/escrow/tests/test_escrow.rs @@ -1,42 +1,43 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, solana_kite::{ create_associated_token_account, create_token_mint, create_wallet, - get_token_account_balance, mint_tokens_to_token_account, send_transaction_from_instructions, + get_token_account_balance, mint_tokens_to_token_account, + send_transaction_from_instructions, }, solana_signer::Signer, }; -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn ata_program_id() -> Pubkey { +fn ata_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn lamports(svm: &LiteSVM, address: &Pubkey) -> u64 { +fn lamports(svm: &LiteSVM, address: &Address) -> u64 { svm.get_account(address).map(|a| a.lamports).unwrap_or(0) } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - let (ata, _bump) = Pubkey::find_program_address( +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + let (ata, _bump) = Address::find_program_address( &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &ata_program_id(), ); ata } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = escrow::id(); let mut svm = LiteSVM::new(); @@ -49,16 +50,16 @@ fn setup() -> (LiteSVM, Pubkey, Keypair) { struct EscrowSetup { svm: LiteSVM, - program_id: Pubkey, + program_id: Address, payer: Keypair, alice: Keypair, bob: Keypair, - mint_a: Pubkey, - mint_b: Pubkey, - alice_ata_a: Pubkey, - alice_ata_b: Pubkey, - bob_ata_a: Pubkey, - bob_ata_b: Pubkey, + mint_a: Address, + mint_b: Address, + alice_ata_a: Address, + alice_ata_b: Address, + bob_ata_a: Address, + bob_ata_b: Address, } fn full_setup() -> EscrowSetup { @@ -114,7 +115,7 @@ fn test_make_offer() { let token_b_wanted_amount: u64 = 1_000_000; // Derive offer PDA - let (offer_pda, _bump) = Pubkey::find_program_address( + let (offer_pda, _bump) = Address::find_program_address( &[ b"offer", es.alice.pubkey().as_ref(), @@ -144,7 +145,7 @@ fn test_make_offer() { vault, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -168,7 +169,7 @@ fn test_make_offer() { let data = &offer_data.data[8..]; // Skip 8-byte discriminator let stored_id = u64::from_le_bytes(data[0..8].try_into().unwrap()); assert_eq!(stored_id, offer_id); - let stored_maker = Pubkey::try_from(&data[8..40]).unwrap(); + let stored_maker = Address::try_from(&data[8..40]).unwrap(); assert_eq!(stored_maker, es.alice.pubkey()); } @@ -181,7 +182,7 @@ fn test_take_offer() { let token_b_wanted_amount: u64 = 1_000_000; // Derive offer PDA - let (offer_pda, _bump) = Pubkey::find_program_address( + let (offer_pda, _bump) = Address::find_program_address( &[ b"offer", es.alice.pubkey().as_ref(), @@ -218,7 +219,7 @@ fn test_take_offer() { vault, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -253,7 +254,7 @@ fn test_take_offer() { vault, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -313,7 +314,7 @@ fn test_cancel_offer() { let token_a_offered_amount: u64 = 500_000; let token_b_wanted_amount: u64 = 1_000_000; - let (offer_pda, _bump) = Pubkey::find_program_address( + let (offer_pda, _bump) = Address::find_program_address( &[ b"offer", es.alice.pubkey().as_ref(), @@ -346,7 +347,7 @@ fn test_cancel_offer() { vault, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -375,7 +376,7 @@ fn test_cancel_offer() { vault, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -417,7 +418,7 @@ fn test_cancel_offer_rejects_non_maker() { let token_a_offered_amount: u64 = 500_000; let token_b_wanted_amount: u64 = 1_000_000; - let (offer_pda, _bump) = Pubkey::find_program_address( + let (offer_pda, _bump) = Address::find_program_address( &[ b"offer", es.alice.pubkey().as_ref(), @@ -446,7 +447,7 @@ fn test_cancel_offer_rejects_non_maker() { vault, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -458,7 +459,7 @@ fn test_cancel_offer_rejects_non_maker() { ) .unwrap(); - // Bob tries to cancel Alice's offer - the has_one = maker / signer + seeds + // Bob tries to cancel Alice's offer - the address = offer.maker / signer + seeds // constraints should reject this. let bob_ata_a = create_associated_token_account(&mut es.svm, &es.bob.pubkey(), &es.mint_a, &es.payer) @@ -474,7 +475,7 @@ fn test_cancel_offer_rejects_non_maker() { vault, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/finance/lending/anchor/README.md b/finance/lending/anchor/README.md index 4fe766a76..ad81df63c 100644 --- a/finance/lending/anchor/README.md +++ b/finance/lending/anchor/README.md @@ -123,7 +123,7 @@ wall-clock time in zero slots, so `price_scaled` also rejects any price stamped at or before the `LastRestartSlot` sysvar's slot, pausing valuation until the publisher posts again. The feed PDA is seeded by `[b"price_feed", market, mint]` (scoped to a market, not to any individual) and only that market's `owner` may write it -(`set_price` checks `has_one = owner`). So prices can't be squatted, a reserve +(`set_price` checks `address = lending_market.owner`). So prices can't be squatted, a reserve trusts exactly its own market's feed for the mint, and isolated markets can price the same asset independently. @@ -171,7 +171,7 @@ refreshed in the same transaction, so a typical action transaction is ## Setup -- Rust and the Solana toolchain (`cargo-build-sbf`), Anchor 1.0.x, Solana 3.1.8. +- Rust and the Solana toolchain (`cargo-build-sbf`), Anchor 2.0.0-rc.1, Solana 3.1.8. - This program has no client/JavaScript code; tests are Rust + LiteSVM. ## Testing diff --git a/finance/lending/anchor/programs/lending/Cargo.toml b/finance/lending/anchor/programs/lending/Cargo.toml index 2e7be6417..a0c47fa08 100644 --- a/finance/lending/anchor/programs/lending/Cargo.toml +++ b/finance/lending/anchor/programs/lending/Cargo.toml @@ -14,19 +14,26 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] # init-if-needed: the obligation share vault and the test price feed are created lazily. -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = "1.1.2" -# For the LastRestartSlot sysvar (not re-exported by anchor-lang): the price -# feed rejects prices from before a cluster restart. Same major as the -# solana-sysvar anchor-lang itself uses, so only one copy is compiled in. -solana-sysvar = "3" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +# pinocchio ships only Clock and Rent, so LastRestartSlot is read through +# pinocchio's own `get_sysvar` wrapper (see src/last_restart.rs). +pinocchio = "0.11" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" @@ -34,6 +41,11 @@ solana-signer = "3.0.0" solana-keypair = "3.0.1" solana-kite = "0.4.0" borsh = "1.6.1" +# Test-side only: LiteSVM's get_sysvar/set_sysvar want the host-side sysvar +# types, not the pinocchio ones anchor-lang exposes on-chain. +solana-sysvar = "3" +solana-clock = "3" +solana-pubkey = "3" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/finance/lending/anchor/programs/lending/src/instructions/admin/collect_protocol_fees.rs b/finance/lending/anchor/programs/lending/src/instructions/admin/collect_protocol_fees.rs index 2e0b00618..ccab9bbea 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/admin/collect_protocol_fees.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/admin/collect_protocol_fees.rs @@ -10,13 +10,15 @@ use crate::state::{reserve_signer_seeds, LendingMarket, Reserve}; /// how the owner earns: `reserve_factor_bps` of every interest accrual is set /// aside in `accumulated_protocol_fees` (never credited to suppliers), and this /// handler pays it out, capped by the liquidity actually sitting in the vault. -pub fn handle_collect_protocol_fees(context: Context) -> Result<()> { +pub fn handle_collect_protocol_fees(context: &mut Context) -> Result<()> { context.accounts.reserve.require_refreshed()?; let reserve = &mut context.accounts.reserve; // Fees are a claim on liquidity; only what is currently un-borrowed can be paid // out right now. Any remainder stays owed until borrowers repay. - let amount = reserve.accumulated_protocol_fees.min(reserve.available_liquidity); + let amount = reserve + .accumulated_protocol_fees + .min(reserve.available_liquidity); require!(amount > 0, LendingError::NothingToCollect); reserve.accumulated_protocol_fees = reserve @@ -28,51 +30,57 @@ pub fn handle_collect_protocol_fees(context: Context) -> Re .checked_sub(amount) .ok_or(LendingError::MathOverflow)?; + // Copy the seed inputs out: `release_borrow` below needs `&mut reserve`. let bump = [reserve.bump]; - let seeds = reserve_signer_seeds(&reserve.lending_market, &reserve.liquidity_mint, &bump); + let lending_market = reserve.lending_market; + let liquidity_mint = reserve.liquidity_mint; + let decimals = reserve.liquidity_decimals; + let seeds = reserve_signer_seeds(&lending_market, &liquidity_mint, &bump); + // `reserve` signs this CPI. It is a data account holding a live borrow on + // its buffer, which the runtime would reject when the CPI borrows the same + // account, so hand the borrow back across the call. `release_borrow` + // flushes the pending writes, and `reacquire_borrow_mut` re-reads them. + context.accounts.reserve.release_borrow()?; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.liquidity_vault.to_account_info(), - mint: context.accounts.liquidity_mint.to_account_info(), - to: context.accounts.owner_liquidity.to_account_info(), - authority: reserve.to_account_info(), + from: context.accounts.liquidity_vault.cpi_handle_mut(), + mint: context.accounts.liquidity_mint.cpi_handle(), + to: context.accounts.owner_liquidity.cpi_handle_mut(), + authority: context.accounts.reserve.cpi_handle(), }, &[&seeds], ), amount, - reserve.liquidity_decimals, + decimals, )?; + context.accounts.reserve.reacquire_borrow_mut()?; Ok(()) } #[derive(Accounts)] -pub struct CollectProtocolFees<'info> { - // Identified by the reserve's `has_one = lending_market`; we only prove the +pub struct CollectProtocolFees { + // Identified by `address = reserve.lending_market`; we only prove the // signer owns it. - #[account(has_one = owner)] - pub lending_market: Account<'info, LendingMarket>, + #[account(address = reserve.lending_market)] + pub lending_market: BorshAccount, - #[account(mut)] - pub owner: Signer<'info>, + #[account(mut, address = lending_market.owner)] + pub owner: Signer, - #[account( - mut, - has_one = lending_market, - has_one = liquidity_mint, - has_one = liquidity_vault, - )] - pub reserve: Account<'info, Reserve>, + #[account(mut)] + pub reserve: BorshAccount, - pub liquidity_mint: InterfaceAccount<'info, Mint>, + #[account(address = reserve.liquidity_mint)] + pub liquidity_mint: InterfaceAccount, - #[account(mut)] - pub liquidity_vault: InterfaceAccount<'info, TokenAccount>, + #[account(mut, address = reserve.liquidity_vault)] + pub liquidity_vault: InterfaceAccount, #[account(mut)] - pub owner_liquidity: InterfaceAccount<'info, TokenAccount>, + pub owner_liquidity: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/admin/initialize_lending_market.rs b/finance/lending/anchor/programs/lending/src/instructions/admin/initialize_lending_market.rs index 2e6d478cc..0eff12db4 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/admin/initialize_lending_market.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/admin/initialize_lending_market.rs @@ -5,23 +5,23 @@ use crate::constants::LENDING_MARKET_SEED; use crate::state::LendingMarket; pub fn handle_initialize_lending_market( - context: Context, + context: &mut Context, market_id: u64, ) -> Result<()> { let market = &mut context.accounts.lending_market; market.market_id = market_id; - market.owner = context.accounts.owner.key(); - market.quote_currency_mint = context.accounts.quote_currency_mint.key(); + market.owner = *context.accounts.owner.address(); + market.quote_currency_mint = *context.accounts.quote_currency_mint.address(); market.bump = context.bumps.lending_market; Ok(()) } #[derive(Accounts)] #[instruction(market_id: u64)] -pub struct InitializeLendingMarket<'info> { +pub struct InitializeLendingMarket { // Seeded by `market_id` alone — the market is not identified by any // individual's address. `owner` is stored as a field and used only for - // authorization (`has_one = owner`) on admin instructions. + // authorization (`address = lending_market.owner`) on admin instructions. #[account( init, payer = owner, @@ -29,12 +29,12 @@ pub struct InitializeLendingMarket<'info> { seeds = [LENDING_MARKET_SEED, &market_id.to_le_bytes()], bump, )] - pub lending_market: Account<'info, LendingMarket>, + pub lending_market: BorshAccount, #[account(mut)] - pub owner: Signer<'info>, + pub owner: Signer, - pub quote_currency_mint: InterfaceAccount<'info, Mint>, + pub quote_currency_mint: InterfaceAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/admin/initialize_reserve.rs b/finance/lending/anchor/programs/lending/src/instructions/admin/initialize_reserve.rs index 96478950b..74976f2ae 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/admin/initialize_reserve.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/admin/initialize_reserve.rs @@ -1,4 +1,6 @@ use anchor_lang::prelude::*; +use anchor_spl::mint; +use anchor_spl::token; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use crate::constants::{ @@ -6,16 +8,19 @@ use crate::constants::{ }; use crate::state::{LendingMarket, PriceFeed, Reserve, ReserveConfig}; -pub fn handle_initialize_reserve(context: Context, config: ReserveConfig) -> Result<()> { +pub fn handle_initialize_reserve( + context: &mut Context, + config: ReserveConfig, +) -> Result<()> { config.validate()?; let reserve = &mut context.accounts.reserve; - reserve.lending_market = context.accounts.lending_market.key(); - reserve.liquidity_mint = context.accounts.liquidity_mint.key(); - reserve.liquidity_vault = context.accounts.liquidity_vault.key(); - reserve.share_mint = context.accounts.share_mint.key(); - reserve.price_feed = context.accounts.price_feed.key(); - reserve.liquidity_decimals = context.accounts.liquidity_mint.decimals; + reserve.lending_market = *context.accounts.lending_market.address(); + reserve.liquidity_mint = *context.accounts.liquidity_mint.address(); + reserve.liquidity_vault = *context.accounts.liquidity_vault.address(); + reserve.share_mint = *context.accounts.share_mint.address(); + reserve.price_feed = *context.accounts.price_feed.address(); + reserve.liquidity_decimals = context.accounts.liquidity_mint.decimals(); reserve.available_liquidity = 0; reserve.share_mint_supply = 0; reserve.borrowed_principal = 0; @@ -28,55 +33,56 @@ pub fn handle_initialize_reserve(context: Context, config: Re } #[derive(Accounts)] -pub struct InitializeReserve<'info> { +pub struct InitializeReserve { // The reserve PDA below is seeded by this market's address, so the market is // pinned by that seed; we only need to prove the signer owns it. - #[account(has_one = owner)] - pub lending_market: Account<'info, LendingMarket>, + pub lending_market: BorshAccount, - #[account(mut)] - pub owner: Signer<'info>, + #[account(mut, address = lending_market.owner)] + pub owner: Signer, #[account( init, payer = owner, space = Reserve::DISCRIMINATOR.len() + Reserve::INIT_SPACE, - seeds = [RESERVE_SEED, lending_market.key().as_ref(), liquidity_mint.key().as_ref()], + seeds = [RESERVE_SEED, lending_market.address().as_ref(), liquidity_mint.address().as_ref()], bump, )] - pub reserve: Account<'info, Reserve>, + pub reserve: BorshAccount, - pub liquidity_mint: InterfaceAccount<'info, Mint>, + pub liquidity_mint: InterfaceAccount, #[account( init, payer = owner, token::mint = liquidity_mint, token::authority = reserve, - seeds = [LIQUIDITY_VAULT_SEED, reserve.key().as_ref()], + token::token_program = token_program, + seeds = [LIQUIDITY_VAULT_SEED, reserve.address().as_ref()], bump, )] - pub liquidity_vault: InterfaceAccount<'info, TokenAccount>, + pub liquidity_vault: InterfaceAccount, #[account( init, payer = owner, - mint::decimals = liquidity_mint.decimals, + mint::decimals = liquidity_mint.decimals(), mint::authority = reserve, - seeds = [SHARE_MINT_SEED, reserve.key().as_ref()], + mint::token_program = token_program, + seeds = [SHARE_MINT_SEED, reserve.address().as_ref()], bump, )] - pub share_mint: InterfaceAccount<'info, Mint>, + pub share_mint: InterfaceAccount, // Bound by seeds to this market's feed for this mint — the reserve can only // trust the price its own market publishes. #[account( - seeds = [PRICE_FEED_SEED, lending_market.key().as_ref(), liquidity_mint.key().as_ref()], + seeds = [PRICE_FEED_SEED, lending_market.address().as_ref(), liquidity_mint.address().as_ref()], bump = price_feed.bump, )] - pub price_feed: Account<'info, PriceFeed>, + pub price_feed: BorshAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, - pub system_program: Program<'info, System>, + pub system_program: Program, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/admin/set_price.rs b/finance/lending/anchor/programs/lending/src/instructions/admin/set_price.rs index 4044c255b..7918e0193 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/admin/set_price.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/admin/set_price.rs @@ -12,13 +12,13 @@ use crate::state::{LendingMarket, PriceFeed}; /// requires the market's `owner` to sign, so a market's prices can only be set /// by that market and never squatted by an outsider. pub fn handle_set_price( - context: Context, + context: &mut Context, price_mantissa: i128, exponent: i32, ) -> Result<()> { let feed = &mut context.accounts.price_feed; - feed.market = context.accounts.lending_market.key(); - feed.mint = context.accounts.mint.key(); + feed.market = *context.accounts.lending_market.address(); + feed.mint = *context.accounts.mint.address(); feed.bump = context.bumps.price_feed; feed.price_mantissa = price_mantissa; feed.exponent = exponent; @@ -27,24 +27,23 @@ pub fn handle_set_price( } #[derive(Accounts)] -pub struct SetPrice<'info> { +pub struct SetPrice { // Only the market's owner may publish its prices. - #[account(has_one = owner)] - pub lending_market: Account<'info, LendingMarket>, + pub lending_market: BorshAccount, - #[account(mut)] - pub owner: Signer<'info>, + #[account(mut, address = lending_market.owner)] + pub owner: Signer, #[account( init_if_needed, payer = owner, space = PriceFeed::DISCRIMINATOR.len() + PriceFeed::INIT_SPACE, - seeds = [PRICE_FEED_SEED, lending_market.key().as_ref(), mint.key().as_ref()], + seeds = [PRICE_FEED_SEED, lending_market.address().as_ref(), mint.address().as_ref()], bump, )] - pub price_feed: Account<'info, PriceFeed>, + pub price_feed: BorshAccount, - pub mint: InterfaceAccount<'info, Mint>, + pub mint: InterfaceAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/admin/update_reserve_config.rs b/finance/lending/anchor/programs/lending/src/instructions/admin/update_reserve_config.rs index 06df1b1b0..ddcf53dd0 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/admin/update_reserve_config.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/admin/update_reserve_config.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; use crate::state::{LendingMarket, Reserve, ReserveConfig}; pub fn handle_update_reserve_config( - context: Context, + context: &mut Context, config: ReserveConfig, ) -> Result<()> { config.validate()?; @@ -12,17 +12,15 @@ pub fn handle_update_reserve_config( } #[derive(Accounts)] -pub struct UpdateReserveConfig<'info> { - // The market is identified by the reserve's `has_one = lending_market`; we +pub struct UpdateReserveConfig { + // The market is identified by `address = reserve.lending_market`; we // only need to prove the signer owns it, not re-derive its address. - #[account(has_one = owner)] - pub lending_market: Account<'info, LendingMarket>, + #[account(address = reserve.lending_market)] + pub lending_market: BorshAccount, - pub owner: Signer<'info>, + #[account(address = lending_market.owner)] + pub owner: Signer, - #[account( - mut, - has_one = lending_market, - )] - pub reserve: Account<'info, Reserve>, + #[account(mut)] + pub reserve: BorshAccount, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/borrow_obligation_liquidity.rs b/finance/lending/anchor/programs/lending/src/instructions/borrow_obligation_liquidity.rs index 723a29d1b..e3557b958 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/borrow_obligation_liquidity.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/borrow_obligation_liquidity.rs @@ -13,12 +13,12 @@ use crate::state::{reserve_signer_seeds, Obligation, PriceFeed, Reserve}; /// allowed-borrow value. The borrowed amount is recorded as scaled principal at /// the reserve's current index (rounded up) so it accrues interest going forward. pub fn handle_borrow_obligation_liquidity( - context: Context, + context: &mut Context, liquidity_amount: u64, ) -> Result<()> { require!(liquidity_amount > 0, LendingError::ZeroAmount); let slot = Clock::get()?.slot; - let reserve_key = context.accounts.reserve.key(); + let reserve_key = *context.accounts.reserve.address(); context.accounts.obligation.require_refreshed()?; context.accounts.reserve.require_refreshed()?; @@ -70,52 +70,61 @@ pub fn handle_borrow_obligation_liquidity( obligation.stale = true; } + // Copy the seed inputs out: `release_borrow` below needs `&mut reserve`. let reserve = &context.accounts.reserve; let bump = [reserve.bump]; - let seeds = reserve_signer_seeds(&reserve.lending_market, &reserve.liquidity_mint, &bump); + let lending_market = reserve.lending_market; + let liquidity_mint = reserve.liquidity_mint; + let seeds = reserve_signer_seeds(&lending_market, &liquidity_mint, &bump); + // `reserve` signs this CPI. It is a data account holding a live borrow on + // its buffer, which the runtime would reject when the CPI borrows the same + // account, so hand the borrow back across the call. `release_borrow` + // flushes the pending writes, and `reacquire_borrow_mut` re-reads them. + context.accounts.reserve.release_borrow()?; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.liquidity_vault.to_account_info(), - mint: context.accounts.liquidity_mint.to_account_info(), - to: context.accounts.user_liquidity.to_account_info(), - authority: reserve.to_account_info(), + from: context.accounts.liquidity_vault.cpi_handle_mut(), + mint: context.accounts.liquidity_mint.cpi_handle(), + to: context.accounts.user_liquidity.cpi_handle_mut(), + authority: context.accounts.reserve.cpi_handle(), }, &[&seeds], ), liquidity_amount, decimals, )?; + context.accounts.reserve.reacquire_borrow_mut()?; Ok(()) } #[derive(Accounts)] -pub struct BorrowObligationLiquidity<'info> { - #[account(mut, has_one = owner)] - pub obligation: Account<'info, Obligation>, +pub struct BorrowObligationLiquidity { + #[account(mut)] + pub obligation: BorshAccount, - pub owner: Signer<'info>, + #[account(address = obligation.owner)] + pub owner: Signer, #[account( mut, - has_one = liquidity_mint, - has_one = liquidity_vault, - has_one = price_feed, constraint = reserve.lending_market == obligation.lending_market @ LendingError::MarketMismatch, )] - pub reserve: Account<'info, Reserve>, + pub reserve: BorshAccount, - pub price_feed: Account<'info, PriceFeed>, + #[account(address = reserve.price_feed)] + pub price_feed: BorshAccount, - pub liquidity_mint: InterfaceAccount<'info, Mint>, + #[account(address = reserve.liquidity_mint)] + pub liquidity_mint: InterfaceAccount, - #[account(mut)] - pub liquidity_vault: InterfaceAccount<'info, TokenAccount>, + #[account(mut, address = reserve.liquidity_vault)] + pub liquidity_vault: InterfaceAccount, #[account(mut)] - pub user_liquidity: InterfaceAccount<'info, TokenAccount>, + pub user_liquidity: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/deposit_obligation_collateral.rs b/finance/lending/anchor/programs/lending/src/instructions/deposit_obligation_collateral.rs index a33e8bfac..60856ea74 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/deposit_obligation_collateral.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/deposit_obligation_collateral.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::token_interface::{ transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked, }; @@ -12,14 +13,14 @@ use crate::state::{Obligation, Reserve}; /// adding collateral only improves health — but the obligation is marked stale /// so its cached values are recomputed before the next health-dependent action. pub fn handle_deposit_obligation_collateral( - context: Context, + context: &mut Context, share_amount: u64, ) -> Result<()> { require!(share_amount > 0, LendingError::ZeroAmount); - let reserve_key = context.accounts.reserve.key(); + let reserve_key = context.accounts.reserve.address(); let obligation = &mut context.accounts.obligation; - let index = obligation.upsert_collateral(reserve_key)?; + let index = obligation.upsert_collateral(*reserve_key)?; obligation.deposits[index].deposited_shares = obligation.deposits[index] .deposited_shares .checked_add(share_amount) @@ -28,51 +29,52 @@ pub fn handle_deposit_obligation_collateral( transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.user_share.to_account_info(), - mint: context.accounts.share_mint.to_account_info(), - to: context.accounts.obligation_share_vault.to_account_info(), - authority: context.accounts.owner.to_account_info(), + from: context.accounts.user_share.cpi_handle_mut(), + mint: context.accounts.share_mint.cpi_handle(), + to: context.accounts.obligation_share_vault.cpi_handle_mut(), + authority: context.accounts.owner.cpi_handle(), }, ), share_amount, - context.accounts.share_mint.decimals, + context.accounts.share_mint.decimals(), )?; Ok(()) } #[derive(Accounts)] -pub struct DepositObligationCollateral<'info> { - #[account(mut, has_one = owner)] - pub obligation: Account<'info, Obligation>, - +pub struct DepositObligationCollateral { #[account(mut)] - pub owner: Signer<'info>, + pub obligation: BorshAccount, + + #[account(mut, address = obligation.owner)] + pub owner: Signer, #[account( - has_one = share_mint, constraint = reserve.lending_market == obligation.lending_market @ LendingError::MarketMismatch, )] - pub reserve: Account<'info, Reserve>, + pub reserve: BorshAccount, - pub share_mint: InterfaceAccount<'info, Mint>, + #[account(address = reserve.share_mint)] + pub share_mint: InterfaceAccount, #[account( init_if_needed, payer = owner, token::mint = share_mint, token::authority = obligation, - seeds = [OBLIGATION_SHARE_VAULT_SEED, reserve.key().as_ref(), obligation.key().as_ref()], + token::token_program = token_program, + seeds = [OBLIGATION_SHARE_VAULT_SEED, reserve.address().as_ref(), obligation.address().as_ref()], bump, )] - pub obligation_share_vault: InterfaceAccount<'info, TokenAccount>, + pub obligation_share_vault: InterfaceAccount, #[account(mut)] - pub user_share: InterfaceAccount<'info, TokenAccount>, + pub user_share: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, - pub system_program: Program<'info, System>, + pub system_program: Program, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/deposit_reserve_liquidity.rs b/finance/lending/anchor/programs/lending/src/instructions/deposit_reserve_liquidity.rs index 3425497ca..e932fc157 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/deposit_reserve_liquidity.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/deposit_reserve_liquidity.rs @@ -12,7 +12,7 @@ use crate::state::{reserve_signer_seeds, Reserve}; /// `liquidity_amount * share_supply / total_liquidity`, floored so the protocol /// keeps any rounding dust. pub fn handle_deposit_reserve_liquidity( - context: Context, + context: &mut Context, liquidity_amount: u64, ) -> Result<()> { require!(liquidity_amount > 0, LendingError::ZeroAmount); @@ -23,7 +23,11 @@ pub fn handle_deposit_reserve_liquidity( let share_amount = if share_supply == 0 { liquidity_amount as u128 } else { - mul_div_floor(liquidity_amount as u128, share_supply, reserve.total_liquidity()?)? + mul_div_floor( + liquidity_amount as u128, + share_supply, + reserve.total_liquidity()?, + )? }; require!(share_amount > 0, LendingError::DepositTooSmall); let share_amount = u64::try_from(share_amount).map_err(|_| LendingError::MathOverflow)?; @@ -40,61 +44,66 @@ pub fn handle_deposit_reserve_liquidity( transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.user_liquidity.to_account_info(), - mint: context.accounts.liquidity_mint.to_account_info(), - to: context.accounts.liquidity_vault.to_account_info(), - authority: context.accounts.owner.to_account_info(), + from: context.accounts.user_liquidity.cpi_handle_mut(), + mint: context.accounts.liquidity_mint.cpi_handle(), + to: context.accounts.liquidity_vault.cpi_handle_mut(), + authority: context.accounts.owner.cpi_handle(), }, ), liquidity_amount, reserve.liquidity_decimals, )?; + // Copy the seed inputs out: `release_borrow` below needs `&mut reserve`. let bump = [reserve.bump]; - let seeds = reserve_signer_seeds(&reserve.lending_market, &reserve.liquidity_mint, &bump); + let lending_market = reserve.lending_market; + let liquidity_mint = reserve.liquidity_mint; + let seeds = reserve_signer_seeds(&lending_market, &liquidity_mint, &bump); + // `reserve` signs this CPI. It is a data account holding a live borrow on + // its buffer, which the runtime would reject when the CPI borrows the same + // account, so hand the borrow back across the call. `release_borrow` + // flushes the pending writes, and `reacquire_borrow_mut` re-reads them. + context.accounts.reserve.release_borrow()?; mint_to( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), MintTo { - mint: context.accounts.share_mint.to_account_info(), - to: context.accounts.user_share.to_account_info(), - authority: reserve.to_account_info(), + mint: context.accounts.share_mint.cpi_handle_mut(), + to: context.accounts.user_share.cpi_handle_mut(), + authority: context.accounts.reserve.cpi_handle(), }, &[&seeds], ), share_amount, )?; + context.accounts.reserve.reacquire_borrow_mut()?; Ok(()) } #[derive(Accounts)] -pub struct DepositReserveLiquidity<'info> { - #[account( - mut, - has_one = liquidity_mint, - has_one = liquidity_vault, - has_one = share_mint, - )] - pub reserve: Account<'info, Reserve>, +pub struct DepositReserveLiquidity { + #[account(mut)] + pub reserve: BorshAccount, - pub liquidity_mint: InterfaceAccount<'info, Mint>, + #[account(address = reserve.liquidity_mint)] + pub liquidity_mint: InterfaceAccount, - #[account(mut)] - pub liquidity_vault: InterfaceAccount<'info, TokenAccount>, + #[account(mut, address = reserve.liquidity_vault)] + pub liquidity_vault: InterfaceAccount, - #[account(mut)] - pub share_mint: InterfaceAccount<'info, Mint>, + #[account(mut, address = reserve.share_mint)] + pub share_mint: InterfaceAccount, #[account(mut)] - pub user_liquidity: InterfaceAccount<'info, TokenAccount>, + pub user_liquidity: InterfaceAccount, #[account(mut)] - pub user_share: InterfaceAccount<'info, TokenAccount>, + pub user_share: InterfaceAccount, - pub owner: Signer<'info>, + pub owner: Signer, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/initialize_obligation.rs b/finance/lending/anchor/programs/lending/src/instructions/initialize_obligation.rs index c7a761354..b2c285645 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/initialize_obligation.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/initialize_obligation.rs @@ -3,10 +3,10 @@ use anchor_lang::prelude::*; use crate::constants::OBLIGATION_SEED; use crate::state::{LendingMarket, Obligation}; -pub fn handle_initialize_obligation(context: Context) -> Result<()> { +pub fn handle_initialize_obligation(context: &mut Context) -> Result<()> { let obligation = &mut context.accounts.obligation; - obligation.lending_market = context.accounts.lending_market.key(); - obligation.owner = context.accounts.owner.key(); + obligation.lending_market = *context.accounts.lending_market.address(); + obligation.owner = *context.accounts.owner.address(); obligation.last_update_slot = Clock::get()?.slot; // Stale until the first refresh; an empty obligation has nothing to value yet. obligation.stale = true; @@ -21,20 +21,20 @@ pub fn handle_initialize_obligation(context: Context) -> R } #[derive(Accounts)] -pub struct InitializeObligation<'info> { - pub lending_market: Account<'info, LendingMarket>, +pub struct InitializeObligation { + pub lending_market: BorshAccount, #[account( init, payer = owner, space = Obligation::DISCRIMINATOR.len() + Obligation::INIT_SPACE, - seeds = [OBLIGATION_SEED, lending_market.key().as_ref(), owner.key().as_ref()], + seeds = [OBLIGATION_SEED, lending_market.address().as_ref(), owner.address().as_ref()], bump, )] - pub obligation: Account<'info, Obligation>, + pub obligation: BorshAccount, #[account(mut)] - pub owner: Signer<'info>, + pub owner: Signer, - pub system_program: Program<'info, System>, + pub system_program: Program, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/liquidate_obligation.rs b/finance/lending/anchor/programs/lending/src/instructions/liquidate_obligation.rs index bb24f1369..da1ab81d3 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/liquidate_obligation.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/liquidate_obligation.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::token_interface::{ transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked, }; @@ -25,7 +26,7 @@ use crate::state::{Obligation, PriceFeed, Reserve}; /// it is only possible while unhealthy and is economically pointless, matching /// how Solend and Kamino behave. pub fn handle_liquidate_obligation( - context: Context, + context: &mut Context, liquidity_amount: u64, ) -> Result<()> { require!(liquidity_amount > 0, LendingError::ZeroAmount); @@ -47,8 +48,8 @@ pub fn handle_liquidate_obligation( let repay_price = context.accounts.repay_price_feed.price_scaled(slot)?; let collateral_price = context.accounts.collateral_price_feed.price_scaled(slot)?; - let borrow_index = obligation.find_borrow(repay_reserve.key())?; - let collateral_index = obligation.find_collateral(collateral_reserve.key())?; + let borrow_index = obligation.find_borrow(*repay_reserve.address())?; + let collateral_index = obligation.find_collateral(*collateral_reserve.address())?; let borrowed_principal = obligation.borrows[borrow_index].borrowed_principal; let deposited_shares = obligation.deposits[collateral_index].deposited_shares; @@ -61,7 +62,8 @@ pub fn handle_liquidate_obligation( repay_reserve.config.close_factor_bps as u128, BPS_DENOMINATOR, )?; - let repay = liquidity_amount.min(u64::try_from(max_repay).map_err(|_| LendingError::MathOverflow)?); + let repay = + liquidity_amount.min(u64::try_from(max_repay).map_err(|_| LendingError::MathOverflow)?); require!(repay > 0, LendingError::ZeroAmount); // Collateral to seize: value of the repayment plus the bonus, converted into @@ -99,8 +101,8 @@ pub fn handle_liquidate_obligation( LendingError::LiquidationTooLarge ); - let scaled_removed = - mul_div_floor(repay as u128, FIXED_POINT_SCALE, accumulation_factor)?.min(borrowed_principal); + let scaled_removed = mul_div_floor(repay as u128, FIXED_POINT_SCALE, accumulation_factor)? + .min(borrowed_principal); // Effects: repay side. { @@ -135,14 +137,19 @@ pub fn handle_liquidate_obligation( }; // Interactions: liquidator repays, then receives the seized share tokens. + // + // These accounts are `Box`ed, and `Box`'s `AnchorAccount` impl does not + // override `cpi_handle_mut`, so that call would build a handle without + // releasing the wrapper's data borrow and the CPI would be rejected with + // `AccountBorrowFailed`. `to_cpi_handle_mut` forwards to the inner type. transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.liquidator_repay_source.to_account_info(), - mint: context.accounts.repay_liquidity_mint.to_account_info(), - to: context.accounts.repay_liquidity_vault.to_account_info(), - authority: context.accounts.liquidator.to_account_info(), + from: context.accounts.liquidator_repay_source.to_cpi_handle_mut(), + mint: context.accounts.repay_liquidity_mint.to_cpi_handle(), + to: context.accounts.repay_liquidity_vault.to_cpi_handle_mut(), + authority: context.accounts.liquidator.cpi_handle(), }, ), repay, @@ -150,21 +157,38 @@ pub fn handle_liquidate_obligation( )?; let bump = [obligation_bump]; - let seeds: [&[u8]; 4] = [OBLIGATION_SEED, lending_market.as_ref(), owner.as_ref(), &bump]; + let seeds: [&[u8]; 4] = [ + OBLIGATION_SEED, + lending_market.as_ref(), + owner.as_ref(), + &bump, + ]; + // `obligation` signs this CPI. It is a data account holding a live borrow on + // its buffer, which the runtime would reject when the CPI borrows the same + // account, so hand the borrow back across the call. `release_borrow` + // flushes the pending writes, and `reacquire_borrow_mut` re-reads them. + context.accounts.obligation.release_borrow()?; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.obligation_collateral_vault.to_account_info(), - mint: context.accounts.collateral_share_mint.to_account_info(), - to: context.accounts.liquidator_collateral_dest.to_account_info(), - authority: context.accounts.obligation.to_account_info(), + from: context + .accounts + .obligation_collateral_vault + .to_cpi_handle_mut(), + mint: context.accounts.collateral_share_mint.to_cpi_handle(), + to: context + .accounts + .liquidator_collateral_dest + .to_cpi_handle_mut(), + authority: context.accounts.obligation.cpi_handle(), }, &[&seeds], ), seize_shares, - context.accounts.collateral_share_mint.decimals, + context.accounts.collateral_share_mint.decimals(), )?; + context.accounts.obligation.reacquire_borrow_mut()?; Ok(()) } @@ -172,52 +196,52 @@ pub fn handle_liquidate_obligation( // Liquidation touches 13 accounts; every Account/InterfaceAccount is boxed so // account deserialization happens on the heap and stays within the BPF stack frame. #[derive(Accounts)] -pub struct LiquidateObligation<'info> { +pub struct LiquidateObligation { #[account(mut)] - pub obligation: Box>, + pub obligation: Box>, - pub liquidator: Signer<'info>, + pub liquidator: Signer, #[account( mut, constraint = repay_reserve.lending_market == obligation.lending_market @ LendingError::MarketMismatch, )] - pub repay_reserve: Box>, + pub repay_reserve: Box>, #[account( constraint = collateral_reserve.lending_market == obligation.lending_market @ LendingError::MarketMismatch, )] - pub collateral_reserve: Box>, + pub collateral_reserve: Box>, #[account(address = repay_reserve.price_feed)] - pub repay_price_feed: Box>, + pub repay_price_feed: Box>, #[account(address = collateral_reserve.price_feed)] - pub collateral_price_feed: Box>, + pub collateral_price_feed: Box>, #[account(address = repay_reserve.liquidity_mint)] - pub repay_liquidity_mint: Box>, + pub repay_liquidity_mint: Box>, #[account(address = collateral_reserve.share_mint)] - pub collateral_share_mint: Box>, + pub collateral_share_mint: Box>, #[account(mut, address = repay_reserve.liquidity_vault)] - pub repay_liquidity_vault: Box>, + pub repay_liquidity_vault: Box>, #[account( mut, - seeds = [OBLIGATION_SHARE_VAULT_SEED, collateral_reserve.key().as_ref(), obligation.key().as_ref()], + seeds = [OBLIGATION_SHARE_VAULT_SEED, collateral_reserve.address().as_ref(), obligation.address().as_ref()], bump, token::mint = collateral_share_mint, token::authority = obligation, )] - pub obligation_collateral_vault: Box>, + pub obligation_collateral_vault: Box>, #[account(mut)] - pub liquidator_repay_source: Box>, + pub liquidator_repay_source: Box>, #[account(mut)] - pub liquidator_collateral_dest: Box>, + pub liquidator_collateral_dest: Box>, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/redeem_reserve_collateral.rs b/finance/lending/anchor/programs/lending/src/instructions/redeem_reserve_collateral.rs index 459b56cf8..2416fc2ed 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/redeem_reserve_collateral.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/redeem_reserve_collateral.rs @@ -12,7 +12,7 @@ use crate::state::{reserve_signer_seeds, Reserve}; /// keeps any rounding dust. Capped by the reserve's available (un-borrowed) /// liquidity. pub fn handle_redeem_reserve_collateral( - context: Context, + context: &mut Context, share_amount: u64, ) -> Result<()> { require!(share_amount > 0, LendingError::ZeroAmount); @@ -26,7 +26,8 @@ pub fn handle_redeem_reserve_collateral( reserve.total_liquidity()?, share_supply, )?; - let liquidity_amount = u64::try_from(liquidity_amount).map_err(|_| LendingError::MathOverflow)?; + let liquidity_amount = + u64::try_from(liquidity_amount).map_err(|_| LendingError::MathOverflow)?; require!( liquidity_amount <= reserve.available_liquidity, LendingError::InsufficientReserveLiquidity @@ -43,61 +44,67 @@ pub fn handle_redeem_reserve_collateral( burn( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), Burn { - mint: context.accounts.share_mint.to_account_info(), - from: context.accounts.user_share.to_account_info(), - authority: context.accounts.owner.to_account_info(), + mint: context.accounts.share_mint.cpi_handle_mut(), + from: context.accounts.user_share.cpi_handle_mut(), + authority: context.accounts.owner.cpi_handle(), }, ), share_amount, )?; + // Copy the seed inputs out: `release_borrow` below needs `&mut reserve`. let bump = [reserve.bump]; - let seeds = reserve_signer_seeds(&reserve.lending_market, &reserve.liquidity_mint, &bump); + let lending_market = reserve.lending_market; + let liquidity_mint = reserve.liquidity_mint; + let decimals = reserve.liquidity_decimals; + let seeds = reserve_signer_seeds(&lending_market, &liquidity_mint, &bump); + // `reserve` signs this CPI. It is a data account holding a live borrow on + // its buffer, which the runtime would reject when the CPI borrows the same + // account, so hand the borrow back across the call. `release_borrow` + // flushes the pending writes, and `reacquire_borrow_mut` re-reads them. + context.accounts.reserve.release_borrow()?; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.liquidity_vault.to_account_info(), - mint: context.accounts.liquidity_mint.to_account_info(), - to: context.accounts.user_liquidity.to_account_info(), - authority: reserve.to_account_info(), + from: context.accounts.liquidity_vault.cpi_handle_mut(), + mint: context.accounts.liquidity_mint.cpi_handle(), + to: context.accounts.user_liquidity.cpi_handle_mut(), + authority: context.accounts.reserve.cpi_handle(), }, &[&seeds], ), liquidity_amount, - reserve.liquidity_decimals, + decimals, )?; + context.accounts.reserve.reacquire_borrow_mut()?; Ok(()) } #[derive(Accounts)] -pub struct RedeemReserveCollateral<'info> { - #[account( - mut, - has_one = liquidity_mint, - has_one = liquidity_vault, - has_one = share_mint, - )] - pub reserve: Account<'info, Reserve>, +pub struct RedeemReserveCollateral { + #[account(mut)] + pub reserve: BorshAccount, - pub liquidity_mint: InterfaceAccount<'info, Mint>, + #[account(address = reserve.liquidity_mint)] + pub liquidity_mint: InterfaceAccount, - #[account(mut)] - pub liquidity_vault: InterfaceAccount<'info, TokenAccount>, + #[account(mut, address = reserve.liquidity_vault)] + pub liquidity_vault: InterfaceAccount, - #[account(mut)] - pub share_mint: InterfaceAccount<'info, Mint>, + #[account(mut, address = reserve.share_mint)] + pub share_mint: InterfaceAccount, #[account(mut)] - pub user_liquidity: InterfaceAccount<'info, TokenAccount>, + pub user_liquidity: InterfaceAccount, #[account(mut)] - pub user_share: InterfaceAccount<'info, TokenAccount>, + pub user_share: InterfaceAccount, - pub owner: Signer<'info>, + pub owner: Signer, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/refresh_obligation.rs b/finance/lending/anchor/programs/lending/src/instructions/refresh_obligation.rs index 21460be55..3777a7a7d 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/refresh_obligation.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/refresh_obligation.rs @@ -15,11 +15,13 @@ use crate::state::{Obligation, PriceFeed, Reserve}; /// /// Collateral value is floored and debt value is ceiled, so health is always /// evaluated conservatively against the borrower. -pub fn handle_refresh_obligation(context: Context) -> Result<()> { +pub fn handle_refresh_obligation(context: &mut Context) -> Result<()> { let slot = Clock::get()?.slot; + // `remaining_accounts()` takes `&mut context`, so collect it before the + // obligation's own mutable borrow starts. It hands back an owned vec. + let accounts = context.remaining_accounts()?; let obligation = &mut context.accounts.obligation; let lending_market = obligation.lending_market; - let accounts = context.remaining_accounts; let mut cursor = 0usize; let mut deposited_value: u128 = 0; @@ -27,8 +29,13 @@ pub fn handle_refresh_obligation(context: Context) -> Result< let mut unhealthy_borrow_value: u128 = 0; for collateral in obligation.deposits.iter_mut() { - let (reserve, price_scaled) = - read_pair(accounts, &mut cursor, collateral.reserve, lending_market, slot)?; + let (reserve, price_scaled) = read_pair( + &accounts, + &mut cursor, + collateral.reserve, + lending_market, + slot, + )?; let liquidity = mul_div_floor( collateral.deposited_shares as u128, @@ -36,7 +43,12 @@ pub fn handle_refresh_obligation(context: Context) -> Result< (reserve.share_mint_supply as u128).max(1), )?; let liquidity = u64::try_from(liquidity).map_err(|_| LendingError::MathOverflow)?; - let value = market_value(liquidity, reserve.liquidity_decimals, price_scaled, Rounding::Down)?; + let value = market_value( + liquidity, + reserve.liquidity_decimals, + price_scaled, + Rounding::Down, + )?; collateral.market_value = value; deposited_value = deposited_value @@ -61,7 +73,7 @@ pub fn handle_refresh_obligation(context: Context) -> Result< let mut borrowed_value: u128 = 0; for borrow in obligation.borrows.iter_mut() { let (reserve, price_scaled) = - read_pair(accounts, &mut cursor, borrow.reserve, lending_market, slot)?; + read_pair(&accounts, &mut cursor, borrow.reserve, lending_market, slot)?; let debt = mul_div_ceil( borrow.borrowed_principal, @@ -95,16 +107,13 @@ pub fn handle_refresh_obligation(context: Context) -> Result< /// checking it matches the obligation's stored reserve, belongs to the /// obligation's lending market, and that both the reserve (refreshed this /// slot) and the price (fresh) are usable. -fn read_pair<'a, 'info>( - accounts: &'a [AccountInfo<'info>], +fn read_pair( + accounts: &[AccountView], cursor: &mut usize, - expected_reserve: Pubkey, - lending_market: Pubkey, + expected_reserve: Address, + lending_market: Address, slot: u64, -) -> Result<(Reserve, u128)> -where - 'a: 'info, -{ +) -> Result<(Reserve, u128)> { let reserve_info = accounts .get(*cursor) .ok_or(LendingError::InvalidObligationAccount)?; @@ -114,11 +123,22 @@ where *cursor += 2; require_keys_eq!( - reserve_info.key(), + *reserve_info.address(), expected_reserve, LendingError::InvalidObligationAccount ); - let reserve = Account::::try_from(reserve_info)?; + let reserve = { + let data = reserve_info.try_borrow()?; + let disc_len = ::DISCRIMINATOR.len(); + require!( + data.len() > disc_len + && &data[..disc_len] == ::DISCRIMINATOR, + LendingError::InvalidObligationAccount + ); + let mut payload = &data[disc_len..]; + >::get(&mut payload) + .map_err(|_| LendingError::InvalidObligationAccount)? + }; require_keys_eq!( reserve.lending_market, lending_market, @@ -127,18 +147,29 @@ where reserve.require_refreshed()?; require_keys_eq!( - price_info.key(), + *price_info.address(), reserve.price_feed, LendingError::InvalidObligationAccount ); - let price_feed = Account::::try_from(price_info)?; + let price_feed = { + let data = price_info.try_borrow()?; + let disc_len = ::DISCRIMINATOR.len(); + require!( + data.len() > disc_len + && &data[..disc_len] == ::DISCRIMINATOR, + LendingError::InvalidObligationAccount + ); + let mut payload = &data[disc_len..]; + >::get(&mut payload) + .map_err(|_| LendingError::InvalidObligationAccount)? + }; let price_scaled = price_feed.price_scaled(slot)?; - Ok((reserve.into_inner(), price_scaled)) + Ok((reserve, price_scaled)) } #[derive(Accounts)] -pub struct RefreshObligation<'info> { +pub struct RefreshObligation { #[account(mut)] - pub obligation: Account<'info, Obligation>, + pub obligation: BorshAccount, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/refresh_reserve.rs b/finance/lending/anchor/programs/lending/src/instructions/refresh_reserve.rs index 151f095ad..e2770d14a 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/refresh_reserve.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/refresh_reserve.rs @@ -5,12 +5,12 @@ use crate::state::Reserve; /// Accrue interest up to the current slot. Must run (as its own instruction in /// the same transaction) before any handler that reads the reserve's value, and /// before `refresh_obligation` for any reserve the obligation touches. -pub fn handle_refresh_reserve(context: Context) -> Result<()> { +pub fn handle_refresh_reserve(context: &mut Context) -> Result<()> { context.accounts.reserve.accrue_interest(Clock::get()?.slot) } #[derive(Accounts)] -pub struct RefreshReserve<'info> { +pub struct RefreshReserve { #[account(mut)] - pub reserve: Account<'info, Reserve>, + pub reserve: BorshAccount, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/repay_obligation_liquidity.rs b/finance/lending/anchor/programs/lending/src/instructions/repay_obligation_liquidity.rs index d71a4217b..522a77eb1 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/repay_obligation_liquidity.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/repay_obligation_liquidity.rs @@ -13,17 +13,17 @@ use crate::state::{Obligation, Reserve}; /// borrower rather than being forgiven by rounding. Anyone may repay on behalf /// of an obligation, so there is no owner check. pub fn handle_repay_obligation_liquidity( - context: Context, + context: &mut Context, liquidity_amount: u64, ) -> Result<()> { require!(liquidity_amount > 0, LendingError::ZeroAmount); - let reserve_key = context.accounts.reserve.key(); + let reserve_key = context.accounts.reserve.address(); context.accounts.reserve.require_refreshed()?; let index = context.accounts.reserve.borrow_accumulation_factor; let decimals = context.accounts.reserve.liquidity_decimals; - let borrow_index = context.accounts.obligation.find_borrow(reserve_key)?; + let borrow_index = context.accounts.obligation.find_borrow(*reserve_key)?; let borrowed_principal = context.accounts.obligation.borrows[borrow_index].borrowed_principal; let debt_now = mul_div_ceil(borrowed_principal, index, FIXED_POINT_SCALE)?; @@ -31,7 +31,8 @@ pub fn handle_repay_obligation_liquidity( let repay = liquidity_amount.min(debt_now); require!(repay > 0, LendingError::ZeroAmount); - let scaled_removed = mul_div_floor(repay as u128, FIXED_POINT_SCALE, index)?.min(borrowed_principal); + let scaled_removed = + mul_div_floor(repay as u128, FIXED_POINT_SCALE, index)?.min(borrowed_principal); { let reserve = &mut context.accounts.reserve; @@ -58,12 +59,12 @@ pub fn handle_repay_obligation_liquidity( transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.user_liquidity.to_account_info(), - mint: context.accounts.liquidity_mint.to_account_info(), - to: context.accounts.liquidity_vault.to_account_info(), - authority: context.accounts.repayer.to_account_info(), + from: context.accounts.user_liquidity.cpi_handle_mut(), + mint: context.accounts.liquidity_mint.cpi_handle(), + to: context.accounts.liquidity_vault.cpi_handle_mut(), + authority: context.accounts.repayer.cpi_handle(), }, ), repay, @@ -74,27 +75,26 @@ pub fn handle_repay_obligation_liquidity( } #[derive(Accounts)] -pub struct RepayObligationLiquidity<'info> { +pub struct RepayObligationLiquidity { #[account(mut)] - pub obligation: Account<'info, Obligation>, + pub obligation: BorshAccount, #[account( mut, - has_one = liquidity_mint, - has_one = liquidity_vault, constraint = reserve.lending_market == obligation.lending_market @ LendingError::MarketMismatch, )] - pub reserve: Account<'info, Reserve>, + pub reserve: BorshAccount, - pub liquidity_mint: InterfaceAccount<'info, Mint>, + #[account(address = reserve.liquidity_mint)] + pub liquidity_mint: InterfaceAccount, - #[account(mut)] - pub liquidity_vault: InterfaceAccount<'info, TokenAccount>, + #[account(mut, address = reserve.liquidity_vault)] + pub liquidity_vault: InterfaceAccount, #[account(mut)] - pub user_liquidity: InterfaceAccount<'info, TokenAccount>, + pub user_liquidity: InterfaceAccount, - pub repayer: Signer<'info>, + pub repayer: Signer, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/lending/anchor/programs/lending/src/instructions/withdraw_obligation_collateral.rs b/finance/lending/anchor/programs/lending/src/instructions/withdraw_obligation_collateral.rs index a38860b85..ec810d751 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/withdraw_obligation_collateral.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/withdraw_obligation_collateral.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::token_interface::{ transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked, }; @@ -13,7 +14,7 @@ use crate::state::{Obligation, PriceFeed, Reserve}; /// value is simulated and the withdraw is rejected if the existing debt would /// exceed it. pub fn handle_withdraw_obligation_collateral( - context: Context, + context: &mut Context, share_amount: u64, ) -> Result<()> { require!(share_amount > 0, LendingError::ZeroAmount); @@ -25,7 +26,7 @@ pub fn handle_withdraw_obligation_collateral( let price_scaled = context.accounts.price_feed.price_scaled(slot)?; let obligation = &mut context.accounts.obligation; - let index = obligation.find_collateral(reserve.key())?; + let index = obligation.find_collateral(*reserve.address())?; require!( obligation.deposits[index].deposited_shares >= share_amount, LendingError::WithdrawTooLarge @@ -41,7 +42,8 @@ pub fn handle_withdraw_obligation_collateral( reserve.total_liquidity()?, (reserve.share_mint_supply as u128).max(1), )?; - let removed_liquidity = u64::try_from(removed_liquidity).map_err(|_| LendingError::MathOverflow)?; + let removed_liquidity = + u64::try_from(removed_liquidity).map_err(|_| LendingError::MathOverflow)?; let removed_value = market_value( removed_liquidity, reserve.liquidity_decimals, @@ -83,53 +85,60 @@ pub fn handle_withdraw_obligation_collateral( owner.as_ref(), &bump, ]; + // `obligation` signs this CPI. It is a data account holding a live borrow on + // its buffer, which the runtime would reject when the CPI borrows the same + // account, so hand the borrow back across the call. `release_borrow` + // flushes the pending writes, and `reacquire_borrow_mut` re-reads them. + context.accounts.obligation.release_borrow()?; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.obligation_share_vault.to_account_info(), - mint: context.accounts.share_mint.to_account_info(), - to: context.accounts.user_share.to_account_info(), - authority: obligation.to_account_info(), + from: context.accounts.obligation_share_vault.cpi_handle_mut(), + mint: context.accounts.share_mint.cpi_handle(), + to: context.accounts.user_share.cpi_handle_mut(), + authority: context.accounts.obligation.cpi_handle(), }, &[&seeds], ), share_amount, - context.accounts.share_mint.decimals, + context.accounts.share_mint.decimals(), )?; + context.accounts.obligation.reacquire_borrow_mut()?; Ok(()) } #[derive(Accounts)] -pub struct WithdrawObligationCollateral<'info> { - #[account(mut, has_one = owner)] - pub obligation: Account<'info, Obligation>, +pub struct WithdrawObligationCollateral { + #[account(mut)] + pub obligation: BorshAccount, - pub owner: Signer<'info>, + #[account(address = obligation.owner)] + pub owner: Signer, #[account( - has_one = share_mint, - has_one = price_feed, constraint = reserve.lending_market == obligation.lending_market @ LendingError::MarketMismatch, )] - pub reserve: Account<'info, Reserve>, + pub reserve: BorshAccount, - pub price_feed: Account<'info, PriceFeed>, + #[account(address = reserve.price_feed)] + pub price_feed: BorshAccount, - pub share_mint: InterfaceAccount<'info, Mint>, + #[account(address = reserve.share_mint)] + pub share_mint: InterfaceAccount, #[account( mut, - seeds = [OBLIGATION_SHARE_VAULT_SEED, reserve.key().as_ref(), obligation.key().as_ref()], + seeds = [OBLIGATION_SHARE_VAULT_SEED, reserve.address().as_ref(), obligation.address().as_ref()], bump, token::mint = share_mint, token::authority = obligation, )] - pub obligation_share_vault: InterfaceAccount<'info, TokenAccount>, + pub obligation_share_vault: InterfaceAccount, #[account(mut)] - pub user_share: InterfaceAccount<'info, TokenAccount>, + pub user_share: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/lending/anchor/programs/lending/src/last_restart.rs b/finance/lending/anchor/programs/lending/src/last_restart.rs new file mode 100644 index 000000000..89c11f01c --- /dev/null +++ b/finance/lending/anchor/programs/lending/src/last_restart.rs @@ -0,0 +1,45 @@ +//! The LastRestartSlot sysvar: the slot of the most recent cluster restart, +//! or 0 if the cluster has never restarted (SIMD-0047). anchor-lang v2 is +//! built on pinocchio, which ships only the Clock and Rent sysvars, so this +//! program declares the 8-byte layout itself and reads it through +//! `pinocchio::sysvars::get_sysvar`, the same syscall wrapper pinocchio's own +//! sysvars use. (`solana-sysvar`'s `LastRestartSlot::get` is not usable here: +//! it is bound to that crate's `Sysvar` trait, not pinocchio's.) +//! +//! Why the program reads it: a halt stops the slot count but not the wall +//! clock, so after a restart an oracle price can look fresh in slots while +//! its value is hours old. `PriceFeed::price_scaled` rejects any price stamped +//! at or before the restart slot, so the market pauses valuation until the +//! publisher posts again. + +use anchor_lang::prelude::*; + +/// `SysvarLastRestartS1ot1111111111111111111111`, decoded at compile time. +const LAST_RESTART_SLOT_ID: Address = + anchor_lang::address!("SysvarLastRestartS1ot1111111111111111111111"); + +/// The sysvar's whole data: one little-endian u64. +#[repr(C)] +#[derive(Copy, Clone)] +pub struct LastRestartSlot { + pub last_restart_slot: [u8; 8], +} + +const _: () = assert!(core::mem::size_of::() == 8); + +impl LastRestartSlot { + /// Slot of the most recent cluster restart, 0 if there has never been one. + pub fn last_restart_slot(&self) -> u64 { + u64::from_le_bytes(self.last_restart_slot) + } + + pub fn get() -> Result { + // `pinocchio::sysvars::get_sysvar` is the safe wrapper over the + // `sol_get_sysvar` syscall. Off-chain (IDL builds, client compilation) + // it is a no-op that leaves the buffer zeroed, which reads as "the + // cluster has never restarted". + let mut last_restart_slot = [0u8; 8]; + pinocchio::sysvars::get_sysvar(&mut last_restart_slot, &LAST_RESTART_SLOT_ID, 0)?; + Ok(Self { last_restart_slot }) + } +} diff --git a/finance/lending/anchor/programs/lending/src/lib.rs b/finance/lending/anchor/programs/lending/src/lib.rs index 462b3be7f..2662ac1ab 100644 --- a/finance/lending/anchor/programs/lending/src/lib.rs +++ b/finance/lending/anchor/programs/lending/src/lib.rs @@ -2,8 +2,9 @@ use anchor_lang::prelude::*; pub mod constants; pub mod errors; -pub mod math; pub mod instructions; +pub mod last_restart; +pub mod math; pub mod state; use instructions::*; @@ -16,91 +17,94 @@ pub mod lending { use super::*; pub fn initialize_lending_market( - context: Context, + context: &mut Context, market_id: u64, ) -> Result<()> { instructions::handle_initialize_lending_market(context, market_id) } - pub fn initialize_reserve(context: Context, config: ReserveConfig) -> Result<()> { + pub fn initialize_reserve( + context: &mut Context, + config: ReserveConfig, + ) -> Result<()> { instructions::handle_initialize_reserve(context, config) } pub fn update_reserve_config( - context: Context, + context: &mut Context, config: ReserveConfig, ) -> Result<()> { instructions::handle_update_reserve_config(context, config) } - pub fn collect_protocol_fees(context: Context) -> Result<()> { + pub fn collect_protocol_fees(context: &mut Context) -> Result<()> { instructions::handle_collect_protocol_fees(context) } pub fn set_price( - context: Context, + context: &mut Context, price_mantissa: i128, exponent: i32, ) -> Result<()> { instructions::handle_set_price(context, price_mantissa, exponent) } - pub fn refresh_reserve(context: Context) -> Result<()> { + pub fn refresh_reserve(context: &mut Context) -> Result<()> { instructions::handle_refresh_reserve(context) } pub fn deposit_reserve_liquidity( - context: Context, + context: &mut Context, liquidity_amount: u64, ) -> Result<()> { instructions::handle_deposit_reserve_liquidity(context, liquidity_amount) } pub fn redeem_reserve_collateral( - context: Context, + context: &mut Context, share_amount: u64, ) -> Result<()> { instructions::handle_redeem_reserve_collateral(context, share_amount) } - pub fn initialize_obligation(context: Context) -> Result<()> { + pub fn initialize_obligation(context: &mut Context) -> Result<()> { instructions::handle_initialize_obligation(context) } - pub fn refresh_obligation(context: Context) -> Result<()> { + pub fn refresh_obligation(context: &mut Context) -> Result<()> { instructions::handle_refresh_obligation(context) } pub fn deposit_obligation_collateral( - context: Context, + context: &mut Context, share_amount: u64, ) -> Result<()> { instructions::handle_deposit_obligation_collateral(context, share_amount) } pub fn withdraw_obligation_collateral( - context: Context, + context: &mut Context, share_amount: u64, ) -> Result<()> { instructions::handle_withdraw_obligation_collateral(context, share_amount) } pub fn borrow_obligation_liquidity( - context: Context, + context: &mut Context, liquidity_amount: u64, ) -> Result<()> { instructions::handle_borrow_obligation_liquidity(context, liquidity_amount) } pub fn repay_obligation_liquidity( - context: Context, + context: &mut Context, liquidity_amount: u64, ) -> Result<()> { instructions::handle_repay_obligation_liquidity(context, liquidity_amount) } pub fn liquidate_obligation( - context: Context, + context: &mut Context, liquidity_amount: u64, ) -> Result<()> { instructions::handle_liquidate_obligation(context, liquidity_amount) diff --git a/finance/lending/anchor/programs/lending/src/state/lending_market.rs b/finance/lending/anchor/programs/lending/src/state/lending_market.rs index 387dff1d6..bc9872d4c 100644 --- a/finance/lending/anchor/programs/lending/src/state/lending_market.rs +++ b/finance/lending/anchor/programs/lending/src/state/lending_market.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; /// Top-level configuration shared by every reserve and obligation under it. /// The owner is the only account that may create reserves and change their config. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct LendingMarket { /// Index this market's PDA is derived from (`["lending_market", market_id]`). @@ -11,12 +11,12 @@ pub struct LendingMarket { /// Distinct markets (0, 1, 2 …) give independent, risk-isolated pools. pub market_id: u64, - pub owner: Pubkey, + pub owner: Address, /// The mint that obligation values are denominated in (for example USDC). /// Stored for reference; valuations come from each reserve's own price feed, /// which must report prices in this currency. - pub quote_currency_mint: Pubkey, + pub quote_currency_mint: Address, pub bump: u8, } diff --git a/finance/lending/anchor/programs/lending/src/state/obligation.rs b/finance/lending/anchor/programs/lending/src/state/obligation.rs index 20be5b92d..393853c83 100644 --- a/finance/lending/anchor/programs/lending/src/state/obligation.rs +++ b/finance/lending/anchor/programs/lending/src/state/obligation.rs @@ -6,12 +6,12 @@ use crate::errors::LendingError; /// A borrower's position in one lending market: the share-token collateral they /// have posted and the liquidity they have borrowed, plus the cached quote- /// currency valuations that `refresh_obligation` recomputes. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Obligation { - pub lending_market: Pubkey, + pub lending_market: Address, - pub owner: Pubkey, + pub owner: Address, pub last_update_slot: u64, @@ -42,16 +42,20 @@ pub struct Obligation { pub bump: u8, } -#[derive(InitSpace, Clone, Copy, AnchorSerialize, AnchorDeserialize, Debug, Default)] +#[derive( + InitSpace, Clone, Copy, Debug, Default, IdlType, wincode::SchemaRead, wincode::SchemaWrite, +)] pub struct ObligationCollateral { - pub reserve: Pubkey, + pub reserve: Address, pub deposited_shares: u64, pub market_value: u128, } -#[derive(InitSpace, Clone, Copy, AnchorSerialize, AnchorDeserialize, Debug, Default)] +#[derive( + InitSpace, Clone, Copy, Debug, Default, IdlType, wincode::SchemaRead, wincode::SchemaWrite, +)] pub struct ObligationLiquidity { - pub reserve: Pubkey, + pub reserve: Address, /// Borrowed principal, scaled by the reserve's index at borrow time so the /// live debt grows automatically as that index advances: /// `debt = borrowed_principal * reserve.borrow_accumulation_factor / FIXED_POINT_SCALE`. @@ -74,8 +78,12 @@ impl Obligation { /// Index of the collateral entry for `reserve`, creating an empty one if the /// obligation has room. Used when posting collateral. - pub fn upsert_collateral(&mut self, reserve: Pubkey) -> Result { - if let Some(index) = self.deposits.iter().position(|entry| entry.reserve == reserve) { + pub fn upsert_collateral(&mut self, reserve: Address) -> Result { + if let Some(index) = self + .deposits + .iter() + .position(|entry| entry.reserve == reserve) + { return Ok(index); } require!( @@ -92,8 +100,12 @@ impl Obligation { /// Index of the borrow entry for `reserve`, creating an empty one if the /// obligation has room. Used when borrowing. - pub fn upsert_borrow(&mut self, reserve: Pubkey) -> Result { - if let Some(index) = self.borrows.iter().position(|entry| entry.reserve == reserve) { + pub fn upsert_borrow(&mut self, reserve: Address) -> Result { + if let Some(index) = self + .borrows + .iter() + .position(|entry| entry.reserve == reserve) + { return Ok(index); } require!( @@ -108,14 +120,14 @@ impl Obligation { Ok(self.borrows.len() - 1) } - pub fn find_collateral(&self, reserve: Pubkey) -> Result { + pub fn find_collateral(&self, reserve: Address) -> Result { self.deposits .iter() .position(|entry| entry.reserve == reserve) .ok_or(LendingError::ReserveNotFound.into()) } - pub fn find_borrow(&self, reserve: Pubkey) -> Result { + pub fn find_borrow(&self, reserve: Address) -> Result { self.borrows .iter() .position(|entry| entry.reserve == reserve) diff --git a/finance/lending/anchor/programs/lending/src/state/price_feed.rs b/finance/lending/anchor/programs/lending/src/state/price_feed.rs index 6d7cc8b82..dd2747849 100644 --- a/finance/lending/anchor/programs/lending/src/state/price_feed.rs +++ b/finance/lending/anchor/programs/lending/src/state/price_feed.rs @@ -1,5 +1,4 @@ use anchor_lang::prelude::*; -use solana_sysvar::last_restart_slot::LastRestartSlot; use crate::constants::MAX_PRICE_STALENESS_SLOTS; use crate::errors::LendingError; @@ -19,13 +18,13 @@ use crate::math::price_mantissa_to_scaled; /// `set_price` handler writes it directly so LiteSVM tests are deterministic. /// A production read should also reject results whose confidence interval is /// too wide; this stand-in has no confidence field to check. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct PriceFeed { /// The lending market this feed serves; part of the PDA seeds. - pub market: Pubkey, + pub market: Address, - pub mint: Pubkey, + pub mint: Address, pub price_mantissa: i128, @@ -44,7 +43,10 @@ impl PriceFeed { let age = current_slot .checked_sub(self.last_updated_slot) .ok_or(LendingError::MathOverflow)?; - require!(age <= MAX_PRICE_STALENESS_SLOTS, LendingError::StalePriceFeed); + require!( + age <= MAX_PRICE_STALENESS_SLOTS, + LendingError::StalePriceFeed + ); // Restart handling. A cluster halt stops the slot count but not the // wall clock, so after a restart a feed can look fresh in slots while @@ -52,7 +54,7 @@ impl PriceFeed { // restart slot; the market then pauses valuation until the publisher // posts again, rather than lending against a pre-halt price. Zero // means the cluster has never restarted. - let last_restart_slot = LastRestartSlot::get()?.last_restart_slot; + let last_restart_slot = crate::last_restart::LastRestartSlot::get()?.last_restart_slot(); require!( last_restart_slot == 0 || self.last_updated_slot > last_restart_slot, LendingError::PricePredatesRestart diff --git a/finance/lending/anchor/programs/lending/src/state/reserve.rs b/finance/lending/anchor/programs/lending/src/state/reserve.rs index b510a3c1e..9ffc392e3 100644 --- a/finance/lending/anchor/programs/lending/src/state/reserve.rs +++ b/finance/lending/anchor/programs/lending/src/state/reserve.rs @@ -7,8 +7,8 @@ use crate::math::{mul_div_ceil, mul_div_floor}; /// Signer seeds for a reserve PDA, which is the authority over its liquidity /// vault and the mint authority of its share token. pub fn reserve_signer_seeds<'a>( - lending_market: &'a Pubkey, - liquidity_mint: &'a Pubkey, + lending_market: &'a Address, + liquidity_mint: &'a Address, bump: &'a [u8; 1], ) -> [&'a [u8]; 4] { [ @@ -23,22 +23,22 @@ pub fn reserve_signer_seeds<'a>( /// `liquidity_vault` and receive share tokens (`share_mint`); the share-to- /// liquidity exchange rate rises as borrowers pay interest. Borrowers draw /// `liquidity_mint` out against collateral held in their obligation. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Reserve { - pub lending_market: Pubkey, + pub lending_market: Address, - pub liquidity_mint: Pubkey, + pub liquidity_mint: Address, /// Program-owned token account holding the un-borrowed liquidity. Its /// authority is this reserve PDA. - pub liquidity_vault: Pubkey, + pub liquidity_vault: Address, /// Share-token mint. Supply equals `share_mint_supply`. Mint authority is /// this reserve PDA. - pub share_mint: Pubkey, + pub share_mint: Address, - pub price_feed: Pubkey, + pub price_feed: Address, pub liquidity_decimals: u8, @@ -77,7 +77,9 @@ pub struct Reserve { } /// Risk and interest-rate parameters. All ratios are basis points (10_000 = 100%). -#[derive(InitSpace, Clone, Copy, AnchorSerialize, AnchorDeserialize, Debug, Default)] +#[derive( + InitSpace, Clone, Copy, Debug, Default, IdlType, wincode::SchemaRead, wincode::SchemaWrite, +)] pub struct ReserveConfig { /// Fraction of deposited collateral value a borrower may borrow against. pub loan_to_value_bps: u16, @@ -178,7 +180,11 @@ impl Reserve { if gross == 0 { return Ok(0); } - mul_div_floor(self.current_borrowed_amount()? as u128, BPS_DENOMINATOR, gross) + mul_div_floor( + self.current_borrowed_amount()? as u128, + BPS_DENOMINATOR, + gross, + ) } /// Per-slot borrow rate (FIXED_POINT_SCALE-scaled) from the kinked curve: diff --git a/finance/lending/anchor/programs/lending/tests/common/mod.rs b/finance/lending/anchor/programs/lending/tests/common/mod.rs index dcf51deb7..2f1803ed4 100644 --- a/finance/lending/anchor/programs/lending/tests/common/mod.rs +++ b/finance/lending/anchor/programs/lending/tests/common/mod.rs @@ -7,11 +7,8 @@ //! instructions into the same transaction, exactly as a real client must. use anchor_lang::{ - solana_program::{ - instruction::{AccountMeta, Instruction}, - system_program, - }, - AccountDeserialize, InstructionData, ToAccountMetas, + solana_program::instruction::{AccountMeta, Instruction}, + system_program, AccountDeserialize, InstructionData, ToAccountMetas, }; use anchor_spl::token::ID as TOKEN_PROGRAM_ID; use litesvm::LiteSVM; @@ -28,7 +25,7 @@ use lending::constants::{ }; use lending::state::{Obligation, Reserve, ReserveConfig}; -pub use anchor_lang::prelude::Pubkey; +pub use anchor_lang::prelude::Address; /// A FIXED_POINT_SCALE-scaled price exponent: prices are passed as /// `mantissa * 10^-18`, matching a Switchboard On-Demand feed's 1e18 result. @@ -43,19 +40,19 @@ pub fn cents(amount: u64) -> i128 { (amount as i128) * 10_000_000_000_000_000 } -pub fn ata(owner: &Pubkey, mint: &Pubkey) -> Pubkey { - let ata_program: Pubkey = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" +pub fn ata(owner: &Address, mint: &Address) -> Address { + let ata_program: Address = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap(); - Pubkey::find_program_address( + Address::find_program_address( &[owner.as_ref(), TOKEN_PROGRAM_ID.as_ref(), mint.as_ref()], &ata_program, ) .0 } -fn pda(seeds: &[&[u8]]) -> Pubkey { - Pubkey::find_program_address(seeds, &lending::id()).0 +fn pda(seeds: &[&[u8]]) -> Address { + Address::find_program_address(seeds, &lending::id()).0 } /// Map kite's transaction result to a String so tests can assert on the program @@ -64,21 +61,40 @@ fn send( svm: &mut LiteSVM, instructions: Vec, signers: &[&Keypair], - payer: &Pubkey, + payer: &Address, ) -> Result<(), String> { send_transaction_from_instructions(svm, instructions, signers, payer) .map_err(|thrown| format!("{thrown:?}")) } +/// v2's `#[error_code]` does not log the variant name, so a failed transaction +/// carries only the numeric custom code: the enum discriminant plus anchor's +/// default 6000 offset. Assert on that rather than on a name that is no longer +/// in the logs. +pub const ANCHOR_ERROR_OFFSET: u32 = 6000; + +macro_rules! assert_program_error { + ($result:expr, $variant:path) => {{ + let message = $result.expect_err("transaction should have failed"); + let code = $variant as u32 + $crate::common::ANCHOR_ERROR_OFFSET; + assert!( + message.contains(&format!("Custom({code})")), + "expected {} (Custom({code})), got: {message}", + stringify!($variant), + ); + }}; +} +pub(crate) use assert_program_error; + /// Handle to one reserve and its associated PDAs. #[derive(Clone, Copy)] pub struct ReserveHandle { - pub mint: Pubkey, + pub mint: Address, pub decimals: u8, - pub reserve: Pubkey, - pub share_mint: Pubkey, - pub liquidity_vault: Pubkey, - pub price_feed: Pubkey, + pub reserve: Address, + pub share_mint: Address, + pub liquidity_vault: Address, + pub price_feed: Address, } pub struct Env { @@ -86,7 +102,7 @@ pub struct Env { /// Market owner; also the mint authority for every test mint and the price /// feed authority. pub owner: Keypair, - pub market: Pubkey, + pub market: Address, } impl Env { @@ -107,7 +123,7 @@ impl Env { lending_market: market, owner: owner.pubkey(), quote_currency_mint: quote_mint, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), data: lending::instruction::InitializeLendingMarket { market_id }.data(), @@ -118,12 +134,12 @@ impl Env { } pub fn current_slot(&self) -> u64 { - self.svm.get_sysvar::().slot + self.svm.get_sysvar::().slot } /// Create a second lending market owned by `market_owner`, for tests that /// exercise cross-market isolation. - pub fn init_market_for(&mut self, market_owner: &Keypair) -> Pubkey { + pub fn init_market_for(&mut self, market_owner: &Keypair) -> Address { let env_owner = self.owner.insecure_clone(); let quote_mint = create_token_mint(&mut self.svm, &env_owner, 6, None).unwrap(); // A distinct id from the env's market 0, since the id is the market's @@ -136,12 +152,18 @@ impl Env { lending_market: market, owner: market_owner.pubkey(), quote_currency_mint: quote_mint, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), data: lending::instruction::InitializeLendingMarket { market_id }.data(), }; - send(&mut self.svm, vec![instruction], &[market_owner], &market_owner.pubkey()).unwrap(); + send( + &mut self.svm, + vec![instruction], + &[market_owner], + &market_owner.pubkey(), + ) + .unwrap(); market } @@ -151,7 +173,7 @@ impl Env { pub fn add_reserve_to( &mut self, market_owner: &Keypair, - market: Pubkey, + market: Address, decimals: u8, price_mantissa: i128, config: ReserveConfig, @@ -176,12 +198,18 @@ impl Env { share_mint, price_feed, token_program: TOKEN_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), data: lending::instruction::InitializeReserve { config }.data(), }; - send(&mut self.svm, vec![instruction], &[market_owner], &market_owner.pubkey()).unwrap(); + send( + &mut self.svm, + vec![instruction], + &[market_owner], + &market_owner.pubkey(), + ) + .unwrap(); ReserveHandle { mint, @@ -203,19 +231,20 @@ impl Env { /// Simulate a cluster restart at `slot`: prices stamped at or before it /// must be rejected until the publisher posts again. pub fn set_last_restart_slot(&mut self, slot: u64) { - self.svm.set_sysvar(&solana_sysvar::last_restart_slot::LastRestartSlot { - last_restart_slot: slot, - }); + self.svm + .set_sysvar(&solana_sysvar::last_restart_slot::LastRestartSlot { + last_restart_slot: slot, + }); } /// The feed PDA the market owner writes for `mint`: seeded by the owner's /// key, so it is the feed `add_reserve` registers reserves against. /// The feed PDA for a given market and mint (seeds `["price_feed", market, mint]`). - pub fn price_feed_address(&self, market: Pubkey, mint: Pubkey) -> Pubkey { + pub fn price_feed_address(&self, market: Address, mint: Address) -> Address { pda(&[PRICE_FEED_SEED, market.as_ref(), mint.as_ref()]) } - pub fn set_price(&mut self, mint: Pubkey, price_mantissa: i128) { + pub fn set_price(&mut self, mint: Address, price_mantissa: i128) { let owner = self.owner.insecure_clone(); let market = self.market; self.set_price_for(&owner, market, mint, price_mantissa); @@ -225,8 +254,8 @@ impl Env { pub fn set_price_for( &mut self, owner: &Keypair, - market: Pubkey, - mint: Pubkey, + market: Address, + mint: Address, price_mantissa: i128, ) { let price_feed = self.price_feed_address(market, mint); @@ -237,7 +266,7 @@ impl Env { owner: owner.pubkey(), price_feed, mint, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), data: lending::instruction::SetPrice { @@ -284,7 +313,7 @@ impl Env { } /// Create the user's token account for a mint and mint `amount` into it. - pub fn fund(&mut self, user: &Keypair, mint: Pubkey, amount: u64) -> Pubkey { + pub fn fund(&mut self, user: &Keypair, mint: Address, amount: u64) -> Address { let owner = self.owner.insecure_clone(); let token_account = create_associated_token_account(&mut self.svm, &user.pubkey(), &mint, user).unwrap(); @@ -313,7 +342,7 @@ impl Env { user: &Keypair, handle: &ReserveHandle, amount: u64, - ) -> Result { + ) -> Result { let user_liquidity = ata(&user.pubkey(), &handle.mint); let user_share = create_associated_token_account( &mut self.svm, @@ -342,11 +371,16 @@ impl Env { .data(), }; let refresh = self.refresh_reserve_ix(handle); - send(&mut self.svm, vec![refresh, deposit], &[user], &user.pubkey())?; + send( + &mut self.svm, + vec![refresh, deposit], + &[user], + &user.pubkey(), + )?; Ok(user_share) } - pub fn supply(&mut self, user: &Keypair, handle: &ReserveHandle, amount: u64) -> Pubkey { + pub fn supply(&mut self, user: &Keypair, handle: &ReserveHandle, amount: u64) -> Address { self.try_supply(user, handle, amount).unwrap() } @@ -374,18 +408,27 @@ impl Env { data: lending::instruction::RedeemReserveCollateral { share_amount }.data(), }; let refresh = self.refresh_reserve_ix(handle); - send(&mut self.svm, vec![refresh, redeem], &[user], &user.pubkey()) + send( + &mut self.svm, + vec![refresh, redeem], + &[user], + &user.pubkey(), + ) } - pub fn initialize_obligation(&mut self, user: &Keypair) -> Pubkey { - let obligation = pda(&[OBLIGATION_SEED, self.market.as_ref(), user.pubkey().as_ref()]); + pub fn initialize_obligation(&mut self, user: &Keypair) -> Address { + let obligation = pda(&[ + OBLIGATION_SEED, + self.market.as_ref(), + user.pubkey().as_ref(), + ]); let instruction = Instruction { program_id: lending::id(), accounts: lending::accounts::InitializeObligation { lending_market: self.market, obligation, owner: user.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), data: lending::instruction::InitializeObligation {}.data(), @@ -394,7 +437,7 @@ impl Env { obligation } - pub fn obligation_share_vault(&self, handle: &ReserveHandle, obligation: Pubkey) -> Pubkey { + pub fn obligation_share_vault(&self, handle: &ReserveHandle, obligation: Address) -> Address { pda(&[ OBLIGATION_SHARE_VAULT_SEED, handle.reserve.as_ref(), @@ -405,7 +448,7 @@ impl Env { pub fn try_post_collateral( &mut self, user: &Keypair, - obligation: Pubkey, + obligation: Address, handle: &ReserveHandle, share_amount: u64, ) -> Result<(), String> { @@ -421,7 +464,7 @@ impl Env { obligation_share_vault: vault, user_share, token_program: TOKEN_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), data: lending::instruction::DepositObligationCollateral { share_amount }.data(), @@ -432,7 +475,7 @@ impl Env { pub fn post_collateral( &mut self, user: &Keypair, - obligation: Pubkey, + obligation: Address, handle: &ReserveHandle, share_amount: u64, ) { @@ -442,11 +485,12 @@ impl Env { fn refresh_obligation_ix( &self, - obligation: Pubkey, + obligation: Address, deposit_reserves: &[&ReserveHandle], borrow_reserves: &[&ReserveHandle], ) -> Instruction { - let mut accounts = lending::accounts::RefreshObligation { obligation }.to_account_metas(None); + let mut accounts = + lending::accounts::RefreshObligation { obligation }.to_account_metas(None); for handle in deposit_reserves.iter().chain(borrow_reserves.iter()) { accounts.push(AccountMeta::new_readonly(handle.reserve, false)); accounts.push(AccountMeta::new_readonly(handle.price_feed, false)); @@ -461,7 +505,7 @@ impl Env { /// All reserves an obligation touches must be refreshed before /// refresh_obligation; this collects the de-duplicated refresh instructions. fn refresh_all_ix(&self, reserves: &[&ReserveHandle]) -> Vec { - let mut seen: Vec = Vec::new(); + let mut seen: Vec
= Vec::new(); let mut instructions = Vec::new(); for handle in reserves { if !seen.contains(&handle.reserve) { @@ -481,7 +525,7 @@ impl Env { pub fn try_borrow( &mut self, user: &Keypair, - obligation: Pubkey, + obligation: Address, existing_deposits: &[&ReserveHandle], existing_borrows: &[&ReserveHandle], borrow: &ReserveHandle, @@ -492,7 +536,11 @@ impl Env { refresh_set.push(borrow); let mut instructions = self.refresh_all_ix(&refresh_set); - instructions.push(self.refresh_obligation_ix(obligation, existing_deposits, existing_borrows)); + instructions.push(self.refresh_obligation_ix( + obligation, + existing_deposits, + existing_borrows, + )); instructions.push(self.borrow_ix(user, obligation, borrow, amount)); send(&mut self.svm, instructions, &[user], &user.pubkey()) } @@ -500,7 +548,7 @@ impl Env { fn borrow_ix( &self, user: &Keypair, - obligation: Pubkey, + obligation: Address, borrow: &ReserveHandle, amount: u64, ) -> Instruction { @@ -530,7 +578,7 @@ impl Env { pub fn try_borrow_skip_obligation_refresh( &mut self, user: &Keypair, - obligation: Pubkey, + obligation: Address, all_reserves: &[&ReserveHandle], borrow: &ReserveHandle, amount: u64, @@ -543,7 +591,7 @@ impl Env { pub fn repay( &mut self, user: &Keypair, - obligation: Pubkey, + obligation: Address, borrow: &ReserveHandle, amount: u64, ) { @@ -575,7 +623,7 @@ impl Env { pub fn try_withdraw_collateral( &mut self, user: &Keypair, - obligation: Pubkey, + obligation: Address, deposit_reserves: &[&ReserveHandle], borrow_reserves: &[&ReserveHandle], collateral: &ReserveHandle, @@ -587,7 +635,11 @@ impl Env { all.extend_from_slice(borrow_reserves); let mut instructions = self.refresh_all_ix(&all); - instructions.push(self.refresh_obligation_ix(obligation, deposit_reserves, borrow_reserves)); + instructions.push(self.refresh_obligation_ix( + obligation, + deposit_reserves, + borrow_reserves, + )); instructions.push(Instruction { program_id: lending::id(), accounts: lending::accounts::WithdrawObligationCollateral { @@ -610,7 +662,7 @@ impl Env { pub fn try_liquidate( &mut self, liquidator: &Keypair, - obligation: Pubkey, + obligation: Address, deposit_reserves: &[&ReserveHandle], borrow_reserves: &[&ReserveHandle], repay: &ReserveHandle, @@ -635,7 +687,11 @@ impl Env { let mut all: Vec<&ReserveHandle> = deposit_reserves.to_vec(); all.extend_from_slice(borrow_reserves); let mut instructions = self.refresh_all_ix(&all); - instructions.push(self.refresh_obligation_ix(obligation, deposit_reserves, borrow_reserves)); + instructions.push(self.refresh_obligation_ix( + obligation, + deposit_reserves, + borrow_reserves, + )); instructions.push(Instruction { program_id: lending::id(), accounts: lending::accounts::LiquidateObligation { @@ -659,7 +715,12 @@ impl Env { } .data(), }); - send(&mut self.svm, instructions, &[liquidator], &liquidator.pubkey()) + send( + &mut self.svm, + instructions, + &[liquidator], + &liquidator.pubkey(), + ) } /// Send a lone `refresh_reserve` so accrued interest lands in the index. @@ -672,7 +733,7 @@ impl Env { pub fn refresh_obligation_only( &mut self, payer: &Keypair, - obligation: Pubkey, + obligation: Address, deposits: &[&ReserveHandle], borrows: &[&ReserveHandle], ) { @@ -686,7 +747,7 @@ impl Env { /// Market owner collects accrued protocol fees from a reserve to their own /// token account. Bundles `refresh_reserve` so fees are current. Returns the /// owner's fee-receiving token account. - pub fn collect_protocol_fees(&mut self, handle: &ReserveHandle) -> Pubkey { + pub fn collect_protocol_fees(&mut self, handle: &ReserveHandle) -> Address { let owner = self.owner.insecure_clone(); let owner_liquidity = ata(&owner.pubkey(), &handle.mint); if self.svm.get_account(&owner_liquidity).is_none() { @@ -708,7 +769,13 @@ impl Env { .to_account_metas(None), data: lending::instruction::CollectProtocolFees {}.data(), }; - send(&mut self.svm, vec![refresh, collect], &[&owner], &owner.pubkey()).unwrap(); + send( + &mut self.svm, + vec![refresh, collect], + &[&owner], + &owner.pubkey(), + ) + .unwrap(); owner_liquidity } @@ -719,12 +786,12 @@ impl Env { Reserve::try_deserialize(&mut account.data.as_slice()).unwrap() } - pub fn obligation(&self, obligation: Pubkey) -> Obligation { + pub fn obligation(&self, obligation: Address) -> Obligation { let account = self.svm.get_account(&obligation).unwrap(); Obligation::try_deserialize(&mut account.data.as_slice()).unwrap() } - pub fn token_balance(&self, token_account: Pubkey) -> u64 { + pub fn token_balance(&self, token_account: Address) -> u64 { get_token_account_balance(&self.svm, &token_account).unwrap() } } diff --git a/finance/lending/anchor/programs/lending/tests/test_borrow_repay.rs b/finance/lending/anchor/programs/lending/tests/test_borrow_repay.rs index 9d829e4aa..a31083604 100644 --- a/finance/lending/anchor/programs/lending/tests/test_borrow_repay.rs +++ b/finance/lending/anchor/programs/lending/tests/test_borrow_repay.rs @@ -1,5 +1,7 @@ mod common; +use lending::errors::LendingError; + use common::{ata, default_config, dollars, Env, ReserveHandle}; use solana_keypair::Keypair; use solana_signer::Signer; @@ -7,7 +9,13 @@ use solana_signer::Signer; /// One market with a collateral reserve and a separately-supplied borrow /// reserve, plus a borrower who has posted 1000 units of collateral (value /// $1000, so 75% LTV => $750 borrow power). Both tokens priced at $1, 6 decimals. -fn setup() -> (Env, ReserveHandle, ReserveHandle, Keypair, anchor_lang::prelude::Pubkey) { +fn setup() -> ( + Env, + ReserveHandle, + ReserveHandle, + Keypair, + solana_pubkey::Pubkey, +) { let mut env = Env::new(); let collateral = env.add_reserve(6, dollars(1), default_config()); let borrow = env.add_reserve(6, dollars(1), default_config()); @@ -32,19 +40,30 @@ fn borrow_up_to_max_ltv_then_one_more_fails() { let (mut env, collateral, borrow, borrower, obligation) = setup(); // $750 of borrow power, borrowing a $1 token => 750 units exactly. - env.try_borrow(&borrower, obligation, &[&collateral], &[], &borrow, 750_000_000) - .unwrap(); + env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[], + &borrow, + 750_000_000, + ) + .unwrap(); assert_eq!( env.token_balance(ata(&borrower.pubkey(), &borrow.mint)), 750_000_000 ); // One more unit exceeds the allowed borrow value. - let result = env.try_borrow(&borrower, obligation, &[&collateral], &[&borrow], &borrow, 1); - assert!( - result.unwrap_err().contains("BorrowTooLarge"), - "borrowing past the LTV limit must be rejected" + let result = env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[&borrow], + &borrow, + 1, ); + common::assert_program_error!(result, LendingError::BorrowTooLarge); } #[test] @@ -57,7 +76,7 @@ fn borrow_without_obligation_refresh_is_rejected() { &borrow, 100_000_000, ); - assert!(result.unwrap_err().contains("ObligationStale")); + common::assert_program_error!(result, LendingError::ObligationStale); } #[test] @@ -65,8 +84,15 @@ fn borrow_with_stale_price_feed_is_rejected() { let (mut env, collateral, borrow, borrower, obligation) = setup(); // Advance well past the staleness window without re-publishing prices. env.warp_slots(50); - let result = env.try_borrow(&borrower, obligation, &[&collateral], &[], &borrow, 100_000_000); - assert!(result.unwrap_err().contains("StalePriceFeed")); + let result = env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[], + &borrow, + 100_000_000, + ); + common::assert_program_error!(result, LendingError::StalePriceFeed); } /// A cluster restart passes hours of wall-clock time in zero slots, so a price @@ -83,11 +109,15 @@ fn borrow_with_price_from_before_a_restart_is_rejected() { env.warp_slots(5); env.set_last_restart_slot(restart_slot); - let result = env.try_borrow(&borrower, obligation, &[&collateral], &[], &borrow, 100_000_000); - assert!( - result.unwrap_err().contains("PricePredatesRestart"), - "a pre-restart price must be rejected even inside the staleness window" + let result = env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[], + &borrow, + 100_000_000, ); + common::assert_program_error!(result, LendingError::PricePredatesRestart); // Publishing after the restart reopens the market. Warp first: the retry is // otherwise byte-identical to the rejected borrow, so it would carry the @@ -96,15 +126,29 @@ fn borrow_with_price_from_before_a_restart_is_rejected() { env.warp_slots(1); env.set_price(collateral.mint, dollars(1)); env.set_price(borrow.mint, dollars(1)); - env.try_borrow(&borrower, obligation, &[&collateral], &[], &borrow, 100_000_000) - .expect("a freshly published price must be accepted after a restart"); + env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[], + &borrow, + 100_000_000, + ) + .expect("a freshly published price must be accepted after a restart"); } #[test] fn repay_reduces_debt_and_over_repay_clamps() { let (mut env, collateral, borrow, borrower, obligation) = setup(); - env.try_borrow(&borrower, obligation, &[&collateral], &[], &borrow, 500_000_000) - .unwrap(); + env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[], + &borrow, + 500_000_000, + ) + .unwrap(); assert_eq!(env.reserve(&borrow).borrowed_principal > 0, true); env.repay(&borrower, obligation, &borrow, 200_000_000); @@ -120,8 +164,15 @@ fn repay_reduces_debt_and_over_repay_clamps() { #[test] fn withdraw_blocked_while_borrowed_then_allowed_after_repay() { let (mut env, collateral, borrow, borrower, obligation) = setup(); - env.try_borrow(&borrower, obligation, &[&collateral], &[], &borrow, 750_000_000) - .unwrap(); + env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[], + &borrow, + 750_000_000, + ) + .unwrap(); // At the LTV limit, withdrawing any collateral would undercollateralize. let blocked = env.try_withdraw_collateral( @@ -132,7 +183,7 @@ fn withdraw_blocked_while_borrowed_then_allowed_after_repay() { &collateral, 100_000_000, ); - assert!(blocked.unwrap_err().contains("WithdrawTooLarge")); + common::assert_program_error!(blocked, LendingError::WithdrawTooLarge); // Repay everything, then the collateral is free to withdraw. env.repay(&borrower, obligation, &borrow, 750_000_000); diff --git a/finance/lending/anchor/programs/lending/tests/test_deposit_redeem.rs b/finance/lending/anchor/programs/lending/tests/test_deposit_redeem.rs index f52f3cf88..33c04ccef 100644 --- a/finance/lending/anchor/programs/lending/tests/test_deposit_redeem.rs +++ b/finance/lending/anchor/programs/lending/tests/test_deposit_redeem.rs @@ -32,8 +32,14 @@ fn raw_token_donation_does_not_inflate_exchange_rate() { // Attacker donates raw tokens straight into the reserve vault. available_liquidity // is the source of truth, so this must NOT change the share exchange rate. let owner = env.owner.insecure_clone(); - mint_tokens_to_token_account(&mut env.svm, &usdc.mint, &usdc.liquidity_vault, amount, &owner) - .unwrap(); + mint_tokens_to_token_account( + &mut env.svm, + &usdc.mint, + &usdc.liquidity_vault, + amount, + &owner, + ) + .unwrap(); let second = env.create_user(); env.fund(&second, usdc.mint, amount); diff --git a/finance/lending/anchor/programs/lending/tests/test_interest.rs b/finance/lending/anchor/programs/lending/tests/test_interest.rs index 15d6be350..e9ac96cf8 100644 --- a/finance/lending/anchor/programs/lending/tests/test_interest.rs +++ b/finance/lending/anchor/programs/lending/tests/test_interest.rs @@ -1,6 +1,6 @@ mod common; -use common::{default_config, dollars, ata, Env, SLOTS_PER_YEAR}; +use common::{ata, default_config, dollars, Env, SLOTS_PER_YEAR}; use lending::constants::FIXED_POINT_SCALE; use solana_signer::Signer; @@ -26,10 +26,20 @@ fn interest_accrues_on_borrows_over_time() { env.supply(&borrower, &collateral, 1_000_000_000); let obligation = env.initialize_obligation(&borrower); env.post_collateral(&borrower, obligation, &collateral, 1_000_000_000); - env.try_borrow(&borrower, obligation, &[&collateral], &[], &borrow, 500_000_000) - .unwrap(); - - assert_eq!(env.reserve(&borrow).borrow_accumulation_factor, FIXED_POINT_SCALE); + env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[], + &borrow, + 500_000_000, + ) + .unwrap(); + + assert_eq!( + env.reserve(&borrow).borrow_accumulation_factor, + FIXED_POINT_SCALE + ); // Let a tenth of a year pass, counted at the reserve's own slots-per-year // figure, then re-publish prices and refresh. @@ -81,8 +91,15 @@ fn protocol_fees_accrue_and_owner_can_collect() { env.supply(&borrower, &collateral, 1_000_000_000); let obligation = env.initialize_obligation(&borrower); env.post_collateral(&borrower, obligation, &collateral, 1_000_000_000); - env.try_borrow(&borrower, obligation, &[&collateral], &[], &borrow, 500_000_000) - .unwrap(); + env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[], + &borrow, + 500_000_000, + ) + .unwrap(); // No interest has accrued yet, so no fees. assert_eq!(env.reserve(&borrow).accumulated_protocol_fees, 0); @@ -96,7 +113,7 @@ fn protocol_fees_accrue_and_owner_can_collect() { assert!(fees > 0, "protocol fees should accrue once interest does"); let total_interest = reserve.current_borrowed_amount().unwrap() - 500_000_000; let expected_fee = total_interest / 10; // 1000 bps = 10% - // Allow a 1-unit rounding tolerance from flooring. + // Allow a 1-unit rounding tolerance from flooring. assert!( fees.abs_diff(expected_fee) <= 1, "fees {fees} should be ~10% of interest {total_interest}" diff --git a/finance/lending/anchor/programs/lending/tests/test_liquidation.rs b/finance/lending/anchor/programs/lending/tests/test_liquidation.rs index 2024369d0..2fc35216f 100644 --- a/finance/lending/anchor/programs/lending/tests/test_liquidation.rs +++ b/finance/lending/anchor/programs/lending/tests/test_liquidation.rs @@ -1,5 +1,7 @@ mod common; +use lending::errors::LendingError; + use common::{ata, cents, default_config, dollars, Env, ReserveHandle}; use solana_keypair::Keypair; use solana_signer::Signer; @@ -11,7 +13,7 @@ fn setup() -> ( ReserveHandle, ReserveHandle, Keypair, - anchor_lang::prelude::Pubkey, + solana_pubkey::Pubkey, Keypair, ) { let mut env = Env::new(); @@ -28,8 +30,15 @@ fn setup() -> ( env.supply(&borrower, &collateral, 1_000_000_000); let obligation = env.initialize_obligation(&borrower); env.post_collateral(&borrower, obligation, &collateral, 1_000_000_000); - env.try_borrow(&borrower, obligation, &[&collateral], &[], &borrow, 700_000_000) - .unwrap(); + env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[], + &borrow, + 700_000_000, + ) + .unwrap(); let liquidator = env.create_user(); env.fund(&liquidator, borrow.mint, 1_000_000_000); @@ -49,7 +58,7 @@ fn healthy_obligation_cannot_be_liquidated() { &collateral, 100_000_000, ); - assert!(result.unwrap_err().contains("ObligationHealthy")); + common::assert_program_error!(result, LendingError::ObligationHealthy); } #[test] @@ -96,7 +105,10 @@ fn unhealthy_obligation_liquidated_with_bonus_capped_by_close_factor() { // The borrower's debt and collateral both dropped. let obligation_state = env.obligation(obligation); - assert_eq!(obligation_state.deposits[0].deposited_shares, 1_000_000_000 - 459_375_000); + assert_eq!( + obligation_state.deposits[0].deposited_shares, + 1_000_000_000 - 459_375_000 + ); } /// A repayment whose seizure would exceed the posted collateral is rejected @@ -120,7 +132,7 @@ fn over_seizing_liquidation_rejected_smaller_succeeds() { &collateral, 350_000_000, ); - assert!(over_seize.unwrap_err().contains("LiquidationTooLarge")); + common::assert_program_error!(over_seize, LendingError::LiquidationTooLarge); // Repaying $50 seizes $52.50 of collateral = 525 units at $0.10 — fits. env.try_liquidate( @@ -134,5 +146,8 @@ fn over_seizing_liquidation_rejected_smaller_succeeds() { ) .unwrap(); let liquidator_collateral_account = ata(&liquidator.pubkey(), &collateral.share_mint); - assert_eq!(env.token_balance(liquidator_collateral_account), 525_000_000); + assert_eq!( + env.token_balance(liquidator_collateral_account), + 525_000_000 + ); } diff --git a/finance/lending/anchor/programs/lending/tests/test_reserve.rs b/finance/lending/anchor/programs/lending/tests/test_reserve.rs index ac4439091..396be7c80 100644 --- a/finance/lending/anchor/programs/lending/tests/test_reserve.rs +++ b/finance/lending/anchor/programs/lending/tests/test_reserve.rs @@ -1,5 +1,7 @@ mod common; +use lending::errors::LendingError; + use common::{default_config, Env, SLOTS_PER_YEAR}; use lending::constants::FIXED_POINT_SCALE; @@ -28,10 +30,7 @@ fn rejects_ltv_above_liquidation_threshold() { bad.loan_to_value_bps = 9_000; bad.liquidation_threshold_bps = 8_000; let result = env.try_update_config(&usdc, bad); - assert!( - result.unwrap_err().contains("InvalidConfig"), - "LTV above the liquidation threshold must be rejected" - ); + common::assert_program_error!(result, LendingError::InvalidConfig); } #[test] @@ -44,7 +43,7 @@ fn rejects_misordered_interest_rate_curve() { bad.optimal_borrow_rate_bps = 2_000; // optimal below min bad.max_borrow_rate_bps = 15_000; let result = env.try_update_config(&usdc, bad); - assert!(result.unwrap_err().contains("InvalidConfig")); + common::assert_program_error!(result, LendingError::InvalidConfig); } #[test] @@ -66,10 +65,7 @@ fn rejects_zero_slots_per_year() { let mut bad = default_config(); bad.slots_per_year = 0; let result = env.try_update_config(&usdc, bad); - assert!( - result.unwrap_err().contains("InvalidConfig"), - "a zero slots-per-year divisor must be rejected, not divided by" - ); + common::assert_program_error!(result, LendingError::InvalidConfig); } /// The rate fields are annual; what a borrower is charged per slot is the APR diff --git a/finance/lending/anchor/programs/lending/tests/test_rounding.rs b/finance/lending/anchor/programs/lending/tests/test_rounding.rs index 7cebb803c..223acae55 100644 --- a/finance/lending/anchor/programs/lending/tests/test_rounding.rs +++ b/finance/lending/anchor/programs/lending/tests/test_rounding.rs @@ -1,5 +1,7 @@ mod common; +use lending::errors::LendingError; + use common::{ata, default_config, dollars, Env}; use solana_signer::Signer; @@ -22,21 +24,27 @@ fn deposit_that_would_mint_zero_shares_is_rejected() { env.supply(&borrower, &collateral, 1_000_000_000); let obligation = env.initialize_obligation(&borrower); env.post_collateral(&borrower, obligation, &collateral, 1_000_000_000); - env.try_borrow(&borrower, obligation, &[&collateral], &[], &borrow, 500_000_000) - .unwrap(); + env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[], + &borrow, + 500_000_000, + ) + .unwrap(); // Accrue enough interest that total liquidity exceeds the share supply. env.warp_slots(7_884_000); env.refresh_reserve_only(&borrower, &borrow); - assert!(env.reserve(&borrow).borrow_accumulation_factor > lending::constants::FIXED_POINT_SCALE); + assert!( + env.reserve(&borrow).borrow_accumulation_factor > lending::constants::FIXED_POINT_SCALE + ); let dust_depositor = env.create_user(); env.fund(&dust_depositor, borrow.mint, 1); let result = env.try_supply(&dust_depositor, &borrow, 1); - assert!( - result.unwrap_err().contains("DepositTooSmall"), - "a 1-unit deposit into an appreciated pool mints zero shares and must be rejected" - ); + common::assert_program_error!(result, LendingError::DepositTooSmall); } #[test] @@ -74,8 +82,15 @@ fn withdraw_at_health_boundary_then_one_more_unit_fails() { env.post_collateral(&borrower, obligation, &collateral, 1_000_000_000); // Borrow $600 against $1000 collateral (75% LTV => $750 power). - env.try_borrow(&borrower, obligation, &[&collateral], &[], &borrow, 600_000_000) - .unwrap(); + env.try_borrow( + &borrower, + obligation, + &[&collateral], + &[], + &borrow, + 600_000_000, + ) + .unwrap(); // Withdrawing $200 of collateral lands exactly on the limit: new power // $750 - 0.75*$200 = $600 == debt. This must pass. @@ -102,5 +117,5 @@ fn withdraw_at_health_boundary_then_one_more_unit_fails() { &collateral, 1, ); - assert!(result.unwrap_err().contains("WithdrawTooLarge")); + common::assert_program_error!(result, LendingError::WithdrawTooLarge); } diff --git a/finance/lending/anchor/programs/lending/tests/test_security.rs b/finance/lending/anchor/programs/lending/tests/test_security.rs index d928d7e09..88be6eaab 100644 --- a/finance/lending/anchor/programs/lending/tests/test_security.rs +++ b/finance/lending/anchor/programs/lending/tests/test_security.rs @@ -1,8 +1,9 @@ mod common; +use lending::errors::LendingError; + use anchor_lang::{ - solana_program::{instruction::Instruction, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, InstructionData, ToAccountMetas, }; use common::{default_config, dollars, Env}; use solana_signer::Signer; @@ -30,10 +31,7 @@ fn cross_market_reserve_is_rejected() { // token movement. env.fund(&borrower, foreign_reserve.share_mint, 0); // create the share ATA let result = env.try_post_collateral(&borrower, obligation, &foreign_reserve, 1); - assert!( - result.unwrap_err().contains("MarketMismatch"), - "a reserve from another lending market must be rejected" - ); + common::assert_program_error!(result, LendingError::MarketMismatch); } /// A market's price feed can only be written by that market's owner: an @@ -47,7 +45,7 @@ fn non_owner_cannot_write_market_price_feed() { // The market's feed for this mint (seeds ["price_feed", market, mint]). let market_feed = env.price_feed_address(env.market, usdc.mint); - // The attacker passes the real market but signs as themself; `has_one = owner` + // The attacker passes the real market but signs as themself; `address = lending_market.owner` // on the market rejects them before any write. let instruction = Instruction { program_id: lending::id(), @@ -56,7 +54,7 @@ fn non_owner_cannot_write_market_price_feed() { owner: attacker.pubkey(), price_feed: market_feed, mint: usdc.mint, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), data: lending::instruction::SetPrice { diff --git a/finance/order-book/anchor/README.md b/finance/order-book/anchor/README.md index 867333433..67854b149 100644 --- a/finance/order-book/anchor/README.md +++ b/finance/order-book/anchor/README.md @@ -366,7 +366,7 @@ Alice's remaining 2-NVDAx [bid](https://www.investopedia.com/terms/b/bid.asp) st ### State / data accounts - `Market`: PDA yes, seeds `["market", base_mint, quote_mint]`, authority program, holds fee rate, tick size, min order size, base/quote mint pubkeys, vault pubkeys, order book pubkey, `authority` wallet (allowed to withdraw fees) -- `OrderBook`: PDA no (client-allocated keypair), seeds n/a: too large (~180 KB) for an `init`/CPI PDA, so created via `create_account` (which needs a signing key a PDA lacks); tied to its market via `has_one`; authority program, holds two critbit trees (bids highest-first, asks lowest-first, 1024 leaves each), `next_order_id` +- `OrderBook`: PDA no (client-allocated keypair), seeds n/a: too large (~180 KB) for an `init`/CPI PDA, so created via `create_account` (which needs a signing key a PDA lacks); tied to its market via `address = market.order_book`; authority program, holds two critbit trees (bids highest-first, asks lowest-first, 1024 leaves each), `next_order_id` - `Order`: PDA yes, seeds `["order", market, order_id.to_le_bytes()]`, authority program, holds owner, side, price, original_quantity, filled_quantity, status, timestamp - `MarketUser`: PDA yes, seeds `["market_user", market, owner]`, authority program, holds `unsettled_base`, `unsettled_quote`, `open_orders: Vec` (max 20) @@ -382,7 +382,7 @@ accounts created with `init` in `initialize_market.rs`; their Their addresses are computed by the caller (e.g. generated Keypairs in the tests) and then written to `market.base_vault` / `quote_vault` / `fee_vault` for the program to validate them on later calls via -`has_one = fee_vault` etc. +`address = market.fee_vault` etc. ### Leaf layout in the `OrderBook` slab @@ -596,7 +596,7 @@ pub fn place_order<'info>( **Accounts in (named):** -- `market` (mut, `has_one = fee_vault`) +- `market` (mut, with `fee_vault` bound by `address = market.fee_vault`) - `order_book` (mut, PDA seeds-checked) - `order` (PDA, **init**, seeds `["order", market, next_order_id.to_le_bytes()]`) @@ -821,7 +821,7 @@ double-withdraw. **Accounts in:** -- `market` (mut, `has_one = fee_vault`) +- `market` (mut, with `fee_vault` bound by `address = market.fee_vault`) - `fee_vault` (mut, boxed) - `authority_quote_account` (mut, boxed - destination) - `quote_mint` (boxed) @@ -1283,7 +1283,7 @@ From [`errors.rs`](programs/order-book/src/errors.rs): - `OrderNotCancellable`: `cancel_order` on a Filled or Cancelled order - `NumericalOverflow`: Any checked arithmetic returned `None` - `InvalidFeeBasisPoints`: `fee_basis_points > 10_000` at init -- `InvalidFeeVault`: `market.fee_vault` on the struct does not match the passed `fee_vault` (Anchor `has_one`) +- `InvalidFeeVault`: `market.fee_vault` on the struct does not match the passed `fee_vault` (the `address` constraint on `fee_vault`) - `MakerAccountMismatch`: Wrong number of maker accounts, wrong order, wrong market, or caller walked the book out of order - `MissingMakerAccounts`: `remaining_accounts.len()` not a multiple of 2 - `MakerOwnerMismatch`: Maker Order and MarketUser have different owners @@ -1342,10 +1342,10 @@ From [`errors.rs`](programs/order-book/src/errors.rs): stack and the Solana VM gives handlers a tight budget. Don't unbox these without testing the compute output size. -- **Discriminator + `has_one`.** Every state account carries an 8- - byte discriminator that Anchor checks. `Market` has - `has_one = fee_vault`, so the `place_order` handler can trust the - `fee_vault` account without re-checking its mint or authority. +- **Discriminator + `address`.** Every state account carries an 8- + byte discriminator that Anchor checks, and `fee_vault` carries + `address = market.fee_vault`, so the `place_order` handler can trust + the `fee_vault` account without re-checking its mint or authority. - **Book capacity check after matching.** The taker's remainder check happens at the end. A bid that clears enough asks to free @@ -1393,7 +1393,7 @@ run first. ### Prerequisites -- Anchor 1.0.0 +- Anchor 2.0.0-rc.1 - Solana CLI (`solana -V`) - Rust stable (pinned at the repo root) diff --git a/finance/order-book/anchor/programs/order-book/Cargo.toml b/finance/order-book/anchor/programs/order-book/Cargo.toml index e2aabd9aa..235b4946b 100644 --- a/finance/order-book/anchor/programs/order-book/Cargo.toml +++ b/finance/order-book/anchor/programs/order-book/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" # Used by the ported Openbook slab - `bytemuck::Pod` / `Zeroable` on every node # variant + `min_const_generics` so `[AnyNode; 1024]` can derive Pod without # hitting bytemuck's default-32 array cap. `static_assertions` keeps the slab diff --git a/finance/order-book/anchor/programs/order-book/src/instructions/admin/withdraw_fees.rs b/finance/order-book/anchor/programs/order-book/src/instructions/admin/withdraw_fees.rs index 2afc31834..89b6758ac 100644 --- a/finance/order-book/anchor/programs/order-book/src/instructions/admin/withdraw_fees.rs +++ b/finance/order-book/anchor/programs/order-book/src/instructions/admin/withdraw_fees.rs @@ -11,15 +11,15 @@ use crate::state::{Market, MARKET_SEED}; /// the fee vault. Transfers the current balance of the fee vault in full; /// a partial-withdraw flavour could take an amount parameter, left out here /// to keep the example focused. -pub fn handle_withdraw_fees(context: Context) -> Result<()> { +pub fn handle_withdraw_fees(context: &mut Context) -> Result<()> { let market = &context.accounts.market; require!( - context.accounts.authority.key() == market.authority, + *context.accounts.authority.address() == market.authority, ErrorCode::NotMarketAuthority ); - let fee_balance = context.accounts.fee_vault.amount; + let fee_balance = context.accounts.fee_vault.amount(); if fee_balance == 0 { // Nothing to do - exit quietly rather than failing, so this // instruction is safe to call on a cron/heartbeat even when there @@ -27,51 +27,56 @@ pub fn handle_withdraw_fees(context: Context) -> return Ok(()); } + // Copied out because `market` has to release its data borrow before it + // signs: the runtime would otherwise reject the CPI's own borrow of the + // same account with AccountBorrowFailed. let market_bump = [market.bump]; + let base_mint = market.base_mint; + let quote_mint = market.quote_mint; let signer_seeds: [&[u8]; 4] = [ MARKET_SEED, - market.base_mint.as_ref(), - market.quote_mint.as_ref(), + base_mint.as_ref(), + quote_mint.as_ref(), &market_bump, ]; let signer_seeds = &[&signer_seeds[..]]; + let quote_decimals = context.accounts.quote_mint.decimals(); + context.accounts.market.release_borrow()?; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.fee_vault.to_account_info(), - mint: context.accounts.quote_mint.to_account_info(), - to: context.accounts.authority_quote_account.to_account_info(), - authority: market.to_account_info(), + from: context.accounts.fee_vault.to_cpi_handle_mut(), + mint: context.accounts.quote_mint.to_cpi_handle(), + to: context.accounts.authority_quote_account.to_cpi_handle_mut(), + authority: context.accounts.market.cpi_handle(), }, signer_seeds, ), fee_balance, - context.accounts.quote_mint.decimals, + quote_decimals, )?; + context.accounts.market.reacquire_borrow_mut()?; Ok(()) } #[derive(Accounts)] -pub struct WithdrawFeesAccountConstraints<'info> { - #[account( - mut, - has_one = fee_vault @ ErrorCode::InvalidFeeVault, - )] - pub market: Account<'info, Market>, +pub struct WithdrawFeesAccountConstraints { + #[account(mut)] + pub market: BorshAccount, // Boxed to keep the struct under the BPF stack limit (see PlaceOrderAccountConstraints). - #[account(mut)] - pub fee_vault: Box>, + #[account(mut, address = market.fee_vault @ ErrorCode::InvalidFeeVault)] + pub fee_vault: Box>, #[account(mut)] - pub authority_quote_account: Box>, + pub authority_quote_account: Box>, - pub quote_mint: Box>, + pub quote_mint: Box>, - pub authority: Signer<'info>, + pub authority: Signer, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/order-book/anchor/programs/order-book/src/instructions/cancel_order.rs b/finance/order-book/anchor/programs/order-book/src/instructions/cancel_order.rs index e03064d12..3f09e6d89 100644 --- a/finance/order-book/anchor/programs/order-book/src/instructions/cancel_order.rs +++ b/finance/order-book/anchor/programs/order-book/src/instructions/cancel_order.rs @@ -2,15 +2,15 @@ use anchor_lang::prelude::*; use crate::errors::ErrorCode; use crate::state::{ - remaining_quantity, remove_open_order, Market, Order, OrderBook, OrderSide, OrderStatus, - MarketUser, ORDER_SEED, MARKET_USER_SEED, + remaining_quantity, remove_open_order, Market, MarketUser, Order, OrderBook, OrderSide, + OrderStatus, MARKET_USER_SEED, ORDER_SEED, }; -pub fn handle_cancel_order(context: Context) -> Result<()> { +pub fn handle_cancel_order(context: &mut Context) -> Result<()> { let order = &mut context.accounts.order; require!( - order.owner == context.accounts.owner.key(), + order.owner == *context.accounts.owner.address(), ErrorCode::Unauthorized ); @@ -35,7 +35,7 @@ pub fn handle_cancel_order(context: Context) -> R .checked_mul(context.accounts.market.quote_lot_size as u128) .ok_or(ErrorCode::NumericalOverflow)? .try_into() - .map_err(|_| error!(ErrorCode::NumericalOverflow))?; + .map_err(|_| ErrorCode::NumericalOverflow)?; market_user.unsettled_quote = market_user .unsettled_quote .checked_add(quote_amount) @@ -46,7 +46,7 @@ pub fn handle_cancel_order(context: Context) -> R .checked_mul(context.accounts.market.base_lot_size as u128) .ok_or(ErrorCode::NumericalOverflow)? .try_into() - .map_err(|_| error!(ErrorCode::NumericalOverflow))?; + .map_err(|_| ErrorCode::NumericalOverflow)?; market_user.unsettled_base = market_user .unsettled_base .checked_add(base_amount) @@ -58,7 +58,7 @@ pub fn handle_cancel_order(context: Context) -> R // Remove the leaf from the slab. The current cancel API doesn't tell us // which side the order is on without reading the Order PDA - which we // already have, so use it. - let mut order_book = context.accounts.order_book.load_mut()?; + let mut order_book = (&mut *context.accounts.order_book); let removed = order_book.remove_from(order.side, order.order_id).is_some(); require!(removed, ErrorCode::OrderNotFound); drop(order_book); @@ -72,27 +72,26 @@ pub fn handle_cancel_order(context: Context) -> R } #[derive(Accounts)] -pub struct CancelOrderAccountConstraints<'info> { - #[account(has_one = order_book @ ErrorCode::InvalidOrderBook)] - pub market: Account<'info, Market>, +pub struct CancelOrderAccountConstraints { + pub market: BorshAccount, - // Not a PDA (see initialize_market.rs); bound to `market` via has_one. - #[account(mut)] - pub order_book: AccountLoader<'info, OrderBook>, + // Not a PDA (see initialize_market.rs); bound to `market` via `address`. + #[account(mut, address = market.order_book @ ErrorCode::InvalidOrderBook)] + pub order_book: Account, #[account( mut, - seeds = [ORDER_SEED, market.key().as_ref(), order.order_id.to_le_bytes().as_ref()], + seeds = [ORDER_SEED, market.address().as_ref(), order.order_id.to_le_bytes()], bump = order.bump )] - pub order: Account<'info, Order>, + pub order: BorshAccount, #[account( mut, - seeds = [MARKET_USER_SEED, market.key().as_ref(), owner.key().as_ref()], + seeds = [MARKET_USER_SEED, market.address().as_ref(), owner.address().as_ref()], bump = market_user.bump )] - pub market_user: Account<'info, MarketUser>, + pub market_user: BorshAccount, - pub owner: Signer<'info>, + pub owner: Signer, } diff --git a/finance/order-book/anchor/programs/order-book/src/instructions/initialize_market.rs b/finance/order-book/anchor/programs/order-book/src/instructions/initialize_market.rs index 0361fc696..59e193a84 100644 --- a/finance/order-book/anchor/programs/order-book/src/instructions/initialize_market.rs +++ b/finance/order-book/anchor/programs/order-book/src/instructions/initialize_market.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use crate::errors::ErrorCode; @@ -9,7 +10,7 @@ use crate::state::{Market, OrderBook, MARKET_SEED}; const MAX_FEE_BASIS_POINTS: u16 = 10_000; pub fn handle_initialize_market( - context: Context, + context: &mut Context, fee_basis_points: u16, tick_size: u64, base_lot_size: u64, @@ -26,13 +27,13 @@ pub fn handle_initialize_market( ); let market = &mut context.accounts.market; - market.authority = context.accounts.authority.key(); - market.base_mint = context.accounts.base_mint.key(); - market.quote_mint = context.accounts.quote_mint.key(); - market.base_vault = context.accounts.base_vault.key(); - market.quote_vault = context.accounts.quote_vault.key(); - market.fee_vault = context.accounts.fee_vault.key(); - market.order_book = context.accounts.order_book.key(); + market.authority = *context.accounts.authority.address(); + market.base_mint = *context.accounts.base_mint.address(); + market.quote_mint = *context.accounts.quote_mint.address(); + market.base_vault = *context.accounts.base_vault.address(); + market.quote_vault = *context.accounts.quote_vault.address(); + market.fee_vault = *context.accounts.fee_vault.address(); + market.order_book = *context.accounts.order_book.address(); market.fee_basis_points = fee_basis_points; market.tick_size = tick_size; market.base_lot_size = base_lot_size; @@ -41,26 +42,26 @@ pub fn handle_initialize_market( market.is_active = true; market.bump = context.bumps.market; - // Zero-copy account: initialize the slab in place. `load_init` is the - // first-write path - every subsequent handler uses `load` / `load_mut`. - // The order book is not a PDA (see the comment on the `order_book` - // account below), so `bump` is unused and stored as 0. - let mut order_book = context.accounts.order_book.load_init()?; - order_book.initialize(context.accounts.market.key(), 0); + // Zero-copy account: v2's `Account` derefs straight to `T`, so the slab + // is written in place with no `load_init` / `load_mut` step. The order book + // is not a PDA (see the comment on the `order_book` account below), so + // `bump` is unused and stored as 0. + let market_address = *context.accounts.market.address(); + context.accounts.order_book.initialize(market_address, 0); Ok(()) } #[derive(Accounts)] -pub struct InitializeMarketAccountConstraints<'info> { +pub struct InitializeMarketAccountConstraints { #[account( init, payer = authority, space = Market::DISCRIMINATOR.len() + Market::INIT_SPACE, - seeds = [MARKET_SEED, base_mint.key().as_ref(), quote_mint.key().as_ref()], + seeds = [MARKET_SEED, base_mint.address().as_ref(), quote_mint.address().as_ref()], bump )] - pub market: Account<'info, Market>, + pub market: BorshAccount, // The order book is a zero-copy account (~180 KB: two 1024-slot critbit // slabs back to back). Solana's BPF runtime caps inner-CPI account @@ -69,7 +70,7 @@ pub struct InitializeMarketAccountConstraints<'info> { // instruction, sizing the account to ORDER_BOOK_ACCOUNT_SIZE, owned by // this program, and zero-initialized. // - // `#[account(zero)]` verifies the account is owned by this program + // `#[account(zeroed)]` verifies the account is owned by this program // and has its discriminator unset, which is exactly what a freshly // create_account-d account looks like. The handler then stamps the // discriminator + struct via `load_init()`. @@ -77,14 +78,14 @@ pub struct InitializeMarketAccountConstraints<'info> { // This is not a PDA. create_account requires the new account to sign // its own creation, and a PDA has no private key to sign with, so the // client must generate a real keypair for it. The program ties this - // account to its market via `has_one = order_book` on `market`, not via + // account to its market via `address = market.order_book`, not via // seeds. - #[account(zero)] - pub order_book: AccountLoader<'info, OrderBook>, + #[account(zeroed)] + pub order_book: Account, - pub base_mint: InterfaceAccount<'info, Mint>, + pub base_mint: InterfaceAccount, - pub quote_mint: InterfaceAccount<'info, Mint>, + pub quote_mint: InterfaceAccount, #[account( init, @@ -93,7 +94,7 @@ pub struct InitializeMarketAccountConstraints<'info> { token::authority = market, token::token_program = token_program )] - pub base_vault: InterfaceAccount<'info, TokenAccount>, + pub base_vault: InterfaceAccount, #[account( init, @@ -102,7 +103,7 @@ pub struct InitializeMarketAccountConstraints<'info> { token::authority = market, token::token_program = token_program )] - pub quote_vault: InterfaceAccount<'info, TokenAccount>, + pub quote_vault: InterfaceAccount, // Taker fees accumulate here (quote mint). Separate from quote_vault so // maker-owed balances and market-earned fees can't be confused. @@ -113,12 +114,12 @@ pub struct InitializeMarketAccountConstraints<'info> { token::authority = market, token::token_program = token_program )] - pub fee_vault: InterfaceAccount<'info, TokenAccount>, + pub fee_vault: InterfaceAccount, #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, - pub system_program: Program<'info, System>, + pub system_program: Program, } diff --git a/finance/order-book/anchor/programs/order-book/src/instructions/initialize_market_user.rs b/finance/order-book/anchor/programs/order-book/src/instructions/initialize_market_user.rs index 25b885c37..1f3972a6d 100644 --- a/finance/order-book/anchor/programs/order-book/src/instructions/initialize_market_user.rs +++ b/finance/order-book/anchor/programs/order-book/src/instructions/initialize_market_user.rs @@ -2,10 +2,12 @@ use anchor_lang::prelude::*; use crate::state::{Market, MarketUser, MARKET_USER_SEED}; -pub fn handle_initialize_market_user(context: Context) -> Result<()> { +pub fn handle_initialize_market_user( + context: &mut Context, +) -> Result<()> { let market_user = &mut context.accounts.market_user; - market_user.market = context.accounts.market.key(); - market_user.owner = context.accounts.owner.key(); + market_user.market = *context.accounts.market.address(); + market_user.owner = *context.accounts.owner.address(); market_user.unsettled_base = 0; market_user.unsettled_quote = 0; market_user.open_orders = Vec::new(); @@ -15,20 +17,20 @@ pub fn handle_initialize_market_user(context: Context { +pub struct InitializeMarketUserAccountConstraints { #[account( init, payer = owner, space = MarketUser::DISCRIMINATOR.len() + MarketUser::INIT_SPACE, - seeds = [MARKET_USER_SEED, market.key().as_ref(), owner.key().as_ref()], + seeds = [MARKET_USER_SEED, market.address().as_ref(), owner.address().as_ref()], bump )] - pub market_user: Account<'info, MarketUser>, + pub market_user: BorshAccount, - pub market: Account<'info, Market>, + pub market: BorshAccount, #[account(mut)] - pub owner: Signer<'info>, + pub owner: Signer, - pub system_program: Program<'info, System>, + pub system_program: Program, } diff --git a/finance/order-book/anchor/programs/order-book/src/instructions/mod.rs b/finance/order-book/anchor/programs/order-book/src/instructions/mod.rs index 1ed385342..5fc5f626c 100644 --- a/finance/order-book/anchor/programs/order-book/src/instructions/mod.rs +++ b/finance/order-book/anchor/programs/order-book/src/instructions/mod.rs @@ -1,13 +1,13 @@ pub mod admin; pub mod cancel_order; -pub mod initialize_market_user; pub mod initialize_market; +pub mod initialize_market_user; pub mod place_order; pub mod settle_funds; pub use admin::*; pub use cancel_order::*; -pub use initialize_market_user::*; pub use initialize_market::*; +pub use initialize_market_user::*; pub use place_order::*; pub use settle_funds::*; diff --git a/finance/order-book/anchor/programs/order-book/src/instructions/place_order.rs b/finance/order-book/anchor/programs/order-book/src/instructions/place_order.rs index a00ccfbfa..e65da4fb1 100644 --- a/finance/order-book/anchor/programs/order-book/src/instructions/place_order.rs +++ b/finance/order-book/anchor/programs/order-book/src/instructions/place_order.rs @@ -5,8 +5,8 @@ use anchor_spl::token_interface::{ use crate::errors::ErrorCode; use crate::state::{ - add_open_order, plan_fills, remove_open_order, Market, Order, OrderBook, OrderSide, - OrderStatus, MarketUser, MARKET_SEED, ORDER_SEED, MARKET_USER_SEED, + add_open_order, plan_fills, remove_open_order, Market, MarketUser, Order, OrderBook, OrderSide, + OrderStatus, MARKET_SEED, MARKET_USER_SEED, ORDER_SEED, }; // Mirror of MarketUser.open_orders max_len. Kept as a constant so the @@ -25,12 +25,22 @@ const BASIS_POINTS_DENOMINATOR: u128 = 10_000; // small. const ACCOUNTS_PER_MAKER: usize = 2; -pub fn handle_place_order<'info>( - context: Context<'info, PlaceOrderAccountConstraints<'info>>, +pub fn handle_place_order( + context: &mut Context, side: OrderSide, price: u64, quantity: u64, ) -> Result<()> { + // `remaining_accounts()` takes `&mut context` and returns an owned vec, so + // collect it before anything borrows `context.accounts`. + let maker_accounts = context.remaining_accounts()?; + + // `AccountView` is Copy, and a copy still points at the same + // account. v2's typed handles make the aliasing a compile error. + let quote_mint_view = *context.accounts.quote_mint.account(); + // Read the decimals up front: the CPI handles below borrow the mints. + let quote_decimals = context.accounts.quote_mint.decimals(); + let base_decimals = context.accounts.base_mint.decimals(); let market = &context.accounts.market; require!(market.is_active, ErrorCode::MarketPaused); @@ -61,39 +71,39 @@ pub fn handle_place_order<'info>( let (source_account, mint_account_info, decimals, transfer_amount, destination_vault) = match side { OrderSide::Bid => ( - context.accounts.user_quote_account.to_account_info(), - context.accounts.quote_mint.to_account_info(), - context.accounts.quote_mint.decimals, + context.accounts.user_quote_account.to_cpi_handle_mut(), + context.accounts.quote_mint.to_cpi_handle(), + quote_decimals, (price as u128) .checked_mul(quantity as u128) .ok_or(ErrorCode::NumericalOverflow)? .checked_mul(market.quote_lot_size as u128) .ok_or(ErrorCode::NumericalOverflow)? .try_into() - .map_err(|_| error!(ErrorCode::NumericalOverflow))?, - context.accounts.quote_vault.to_account_info(), + .map_err(|_| ErrorCode::NumericalOverflow)?, + context.accounts.quote_vault.to_cpi_handle_mut(), ), OrderSide::Ask => ( - context.accounts.user_base_account.to_account_info(), - context.accounts.base_mint.to_account_info(), - context.accounts.base_mint.decimals, + context.accounts.user_base_account.to_cpi_handle_mut(), + context.accounts.base_mint.to_cpi_handle(), + base_decimals, (quantity as u128) .checked_mul(market.base_lot_size as u128) .ok_or(ErrorCode::NumericalOverflow)? .try_into() - .map_err(|_| error!(ErrorCode::NumericalOverflow))?, - context.accounts.base_vault.to_account_info(), + .map_err(|_| ErrorCode::NumericalOverflow)?, + context.accounts.base_vault.to_cpi_handle_mut(), ), }; transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { from: source_account, mint: mint_account_info, to: destination_vault, - authority: context.accounts.owner.to_account_info(), + authority: context.accounts.owner.cpi_handle(), }, ), transfer_amount, @@ -108,20 +118,19 @@ pub fn handle_place_order<'info>( // transaction's remaining_accounts, in the same price-time-priority // order the book would walk. We plan fills against the resting tree, // then verify the caller's account list matches the plan, then apply. - let maker_accounts = &context.remaining_accounts; require!( maker_accounts.len() % ACCOUNTS_PER_MAKER == 0, ErrorCode::MissingMakerAccounts ); - let order_book_loader = &context.accounts.order_book; + let order_book_loader = &mut context.accounts.order_book; // Plan in an immutable load scope, copy the fills out, then drop the // borrow before we re-borrow for mutations. AccountLoader::load() is a // RefCell-based runtime borrow, so the loaded ref must not outlive the // plan we copy out of it. let (fills, taker_remaining) = { - let order_book = order_book_loader.load()?; + let order_book = (&*order_book_loader); plan_fills(&order_book, side, price, quantity) }; @@ -136,13 +145,24 @@ pub fn handle_place_order<'info>( // market. for (fill_index, fill) in fills.iter().enumerate() { let maker_order_info = &maker_accounts[fill_index * ACCOUNTS_PER_MAKER]; - let maker_order = Account::::try_from(maker_order_info)?; + let maker_order = { + let data = maker_order_info.try_borrow()?; + let disc_len = ::DISCRIMINATOR.len(); + require!( + data.len() > disc_len + && &data[..disc_len] == ::DISCRIMINATOR, + ErrorCode::MakerAccountMismatch + ); + let mut payload = &data[disc_len..]; + >::get(&mut payload) + .map_err(|_| ErrorCode::MakerAccountMismatch)? + }; require!( maker_order.order_id == fill.maker_order_id, ErrorCode::MakerAccountMismatch ); require!( - maker_order.market == market.key(), + maker_order.market == *market.address(), ErrorCode::MakerAccountMismatch ); } @@ -163,15 +183,26 @@ pub fn handle_place_order<'info>( let maker_order_info = &maker_accounts[fill_index * ACCOUNTS_PER_MAKER]; let maker_user_info = &maker_accounts[fill_index * ACCOUNTS_PER_MAKER + 1]; - let mut maker_order = Account::::try_from(maker_order_info)?; - let mut maker_market_user = Account::::try_from(maker_user_info)?; + // v2 has no `Account::try_from`. `AnchorAccount::load_mut` is the + // equivalent for a writable account reached through remaining_accounts, + // and it is the only route to one: the derive cannot type a variable + // number of accounts. + // + // SAFETY: the obligation is that no other `&mut` to the same data is + // live. The trait method is unsafe because `Slab` bypasses the runtime + // borrow check; `BorshAccount` does not, taking its guard through + // `try_borrow_mut`, so an account already borrowed elsewhere in this + // handler yields `AccountBorrowFailed` instead of aliasing. + let mut maker_order = unsafe { BorshAccount::::load_mut(*maker_order_info) }?; + let mut maker_market_user = + unsafe { BorshAccount::::load_mut(*maker_user_info) }?; require!( maker_order.owner == maker_market_user.owner, ErrorCode::MakerOwnerMismatch ); require!( - maker_market_user.market == market.key(), + maker_market_user.market == *market.address(), ErrorCode::MakerAccountMismatch ); @@ -199,7 +230,7 @@ pub fn handle_place_order<'info>( .checked_mul(market.quote_lot_size as u128) .ok_or(ErrorCode::NumericalOverflow)? .try_into() - .map_err(|_| error!(ErrorCode::NumericalOverflow))?; + .map_err(|_| ErrorCode::NumericalOverflow)?; // Ceiling division: round the fee in the protocol's favour. Flooring // would leak up to 1 minor unit of quote per fill to the maker, which @@ -212,7 +243,7 @@ pub fn handle_place_order<'info>( .checked_div(BASIS_POINTS_DENOMINATOR) .ok_or(ErrorCode::NumericalOverflow)? .try_into() - .map_err(|_| error!(ErrorCode::NumericalOverflow))?; + .map_err(|_| ErrorCode::NumericalOverflow)?; // Defensive invariant: fees are a fraction of gross, never more. // `fee_basis_points <= 10_000` is enforced at market init, so this @@ -235,7 +266,7 @@ pub fn handle_place_order<'info>( .checked_mul(market.base_lot_size as u128) .ok_or(ErrorCode::NumericalOverflow)? .try_into() - .map_err(|_| error!(ErrorCode::NumericalOverflow))?; + .map_err(|_| ErrorCode::NumericalOverflow)?; taker_base_received = taker_base_received .checked_add(base_from_fill) .ok_or(ErrorCode::NumericalOverflow)?; @@ -251,7 +282,7 @@ pub fn handle_place_order<'info>( .checked_mul(market.quote_lot_size as u128) .ok_or(ErrorCode::NumericalOverflow)? .try_into() - .map_err(|_| error!(ErrorCode::NumericalOverflow))?; + .map_err(|_| ErrorCode::NumericalOverflow)?; let rebate: u64 = locked_for_this_fill .checked_sub(gross_quote) .ok_or(ErrorCode::NumericalOverflow)?; @@ -265,7 +296,7 @@ pub fn handle_place_order<'info>( .checked_mul(market.base_lot_size as u128) .ok_or(ErrorCode::NumericalOverflow)? .try_into() - .map_err(|_| error!(ErrorCode::NumericalOverflow))?; + .map_err(|_| ErrorCode::NumericalOverflow)?; maker_market_user.unsettled_base = maker_market_user .unsettled_base .checked_add(base_from_fill) @@ -290,8 +321,7 @@ pub fn handle_place_order<'info>( .checked_add(fill.fill_quantity) .ok_or(ErrorCode::NumericalOverflow)?; - let maker_fully_filled = - maker_order.filled_quantity >= maker_order.original_quantity; + let maker_fully_filled = maker_order.filled_quantity >= maker_order.original_quantity; maker_order.status = if maker_fully_filled { OrderStatus::Filled } else { @@ -301,8 +331,8 @@ pub fn handle_place_order<'info>( remove_open_order(&mut maker_market_user, maker_order.order_id); } - maker_order.exit(context.program_id)?; - maker_market_user.exit(context.program_id)?; + maker_order.exit()?; + maker_market_user.exit()?; } // --------------------------------------------------------------- @@ -315,7 +345,7 @@ pub fn handle_place_order<'info>( }; { - let mut order_book = order_book_loader.load_mut()?; + let mut order_book = (&mut *order_book_loader); for fill in &fills { order_book.apply_fill_to_maker( maker_side, @@ -329,29 +359,36 @@ pub fn handle_place_order<'info>( // Move accumulated fee from quote_vault → fee_vault (one CPI signed // by the market PDA). if total_fee_quote > 0 { - let market_bump = [market.bump]; + // Copied out because `market` has to release its data borrow before it + // signs: the runtime would otherwise reject the CPI's own borrow of the + // same account with AccountBorrowFailed. + let market_bump = [context.accounts.market.bump]; + let base_mint = context.accounts.market.base_mint; + let quote_mint = context.accounts.market.quote_mint; let signer_seeds: [&[u8]; 4] = [ MARKET_SEED, - market.base_mint.as_ref(), - market.quote_mint.as_ref(), + base_mint.as_ref(), + quote_mint.as_ref(), &market_bump, ]; let signer_seeds = &[&signer_seeds[..]]; + context.accounts.market.release_borrow()?; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.quote_vault.to_account_info(), - mint: context.accounts.quote_mint.to_account_info(), - to: context.accounts.fee_vault.to_account_info(), - authority: market.to_account_info(), + from: context.accounts.quote_vault.to_cpi_handle_mut(), + mint: CpiHandle::readonly("e_mint_view), + to: context.accounts.fee_vault.to_cpi_handle_mut(), + authority: context.accounts.market.cpi_handle(), }, signer_seeds, ), total_fee_quote, - context.accounts.quote_mint.decimals, + quote_decimals, )?; + context.accounts.market.reacquire_borrow_mut()?; } // Apply taker accounting deltas in a single mutation. @@ -377,18 +414,15 @@ pub fn handle_place_order<'info>( // --------------------------------------------------------------- let timestamp = Clock::get()?.unix_timestamp; let order_id = { - let mut order_book = order_book_loader.load_mut()?; + let mut order_book = (&mut *order_book_loader); let id = order_book.allocate_order_id()?; if taker_remaining > 0 { - require!( - !order_book.is_side_full(side), - ErrorCode::OrderBookFull - ); + require!(!order_book.is_side_full(side), ErrorCode::OrderBookFull); order_book.place_resting( side, price, taker_remaining, - context.accounts.owner.key(), + *context.accounts.owner.address(), id, timestamp, )?; @@ -397,8 +431,8 @@ pub fn handle_place_order<'info>( }; let order = &mut context.accounts.order; - order.market = market.key(); - order.owner = context.accounts.owner.key(); + order.market = *context.accounts.market.address(); + order.owner = *context.accounts.owner.address(); order.order_id = order_id; order.side = side; order.price = price; @@ -429,32 +463,24 @@ pub fn handle_place_order<'info>( #[derive(Accounts)] #[instruction(side: OrderSide, price: u64, quantity: u64)] -pub struct PlaceOrderAccountConstraints<'info> { - // `has_one` ties every market-owned account on this struct to the +pub struct PlaceOrderAccountConstraints { + // `address` ties every market-owned account on this struct to the // addresses recorded on the Market PDA. Crucially, without - // has_one on base_vault / quote_vault / base_mint / quote_mint a caller + // address constraints on base_vault / quote_vault / base_mint / quote_mint a caller // could swap fee_vault in for quote_vault (same mint, same authority) // and steer the per-fill fee transfer to drain real fees instead of // routing them in. - #[account( - mut, - has_one = fee_vault @ ErrorCode::InvalidFeeVault, - has_one = base_vault @ ErrorCode::InvalidBaseVault, - has_one = quote_vault @ ErrorCode::InvalidQuoteVault, - has_one = base_mint @ ErrorCode::InvalidBaseMint, - has_one = quote_mint @ ErrorCode::InvalidQuoteMint, - has_one = order_book @ ErrorCode::InvalidOrderBook, - )] - pub market: Account<'info, Market>, + #[account(mut)] + pub market: BorshAccount, // Zero-copy: AccountLoader streams the slab in/out without paying // borsh (de)serialization on every instruction. See order_book.rs for // the layout. Not a PDA - the client created it directly via // system_program::create_account (see initialize_market.rs for why); - // `has_one = order_book` on `market` is what ties this specific account + // `address = market.order_book` is what ties this specific account // to this specific market. - #[account(mut)] - pub order_book: AccountLoader<'info, OrderBook>, + #[account(mut, address = market.order_book @ ErrorCode::InvalidOrderBook)] + pub order_book: Account, // The order PDA seed uses the book's `next_order_id` *before* this // instruction increments it - i.e. the id this new order will receive. @@ -465,47 +491,49 @@ pub struct PlaceOrderAccountConstraints<'info> { space = Order::DISCRIMINATOR.len() + Order::INIT_SPACE, seeds = [ ORDER_SEED, - market.key().as_ref(), - order_book.load()?.next_order_id.to_le_bytes().as_ref() + market.address().as_ref(), + (&*order_book).next_order_id.to_le_bytes() ], bump )] - pub order: Account<'info, Order>, + pub order: BorshAccount, #[account( mut, - seeds = [MARKET_USER_SEED, market.key().as_ref(), owner.key().as_ref()], + seeds = [MARKET_USER_SEED, market.address().as_ref(), owner.address().as_ref()], bump = market_user.bump )] - pub market_user: Account<'info, MarketUser>, + pub market_user: BorshAccount, // InterfaceAccount on the stack is ~1 KB each; with 7 of them this struct // blows the 4 KB stack-offset limit on BPF. Boxing moves each to the heap. - #[account(mut)] - pub base_vault: Box>, + #[account(mut, address = market.base_vault @ ErrorCode::InvalidBaseVault)] + pub base_vault: Box>, - #[account(mut)] - pub quote_vault: Box>, + #[account(mut, address = market.quote_vault @ ErrorCode::InvalidQuoteVault)] + pub quote_vault: Box>, - // Taker fees are routed here. Constrained via `has_one = fee_vault` on + // Taker fees are routed here. Constrained via `address = market.fee_vault` on // `market` above so the program can trust it without re-checking. - #[account(mut)] - pub fee_vault: Box>, + #[account(mut, address = market.fee_vault @ ErrorCode::InvalidFeeVault)] + pub fee_vault: Box>, #[account(mut)] - pub user_base_account: Box>, + pub user_base_account: Box>, #[account(mut)] - pub user_quote_account: Box>, + pub user_quote_account: Box>, - pub base_mint: Box>, + #[account(address = market.base_mint @ ErrorCode::InvalidBaseMint)] + pub base_mint: Box>, - pub quote_mint: Box>, + #[account(address = market.quote_mint @ ErrorCode::InvalidQuoteMint)] + pub quote_mint: Box>, #[account(mut)] - pub owner: Signer<'info>, + pub owner: Signer, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, - pub system_program: Program<'info, System>, + pub system_program: Program, } diff --git a/finance/order-book/anchor/programs/order-book/src/instructions/settle_funds.rs b/finance/order-book/anchor/programs/order-book/src/instructions/settle_funds.rs index 4a9f53ad0..2a9087284 100644 --- a/finance/order-book/anchor/programs/order-book/src/instructions/settle_funds.rs +++ b/finance/order-book/anchor/programs/order-book/src/instructions/settle_funds.rs @@ -6,9 +6,8 @@ use anchor_spl::token_interface::{ use crate::errors::ErrorCode; use crate::state::{Market, MarketUser, MARKET_SEED, MARKET_USER_SEED}; -pub fn handle_settle_funds(context: Context) -> Result<()> { +pub fn handle_settle_funds(context: &mut Context) -> Result<()> { let market_user = &mut context.accounts.market_user; - let market = &context.accounts.market; // Snapshot the amounts the user is owed, then zero the counters // BEFORE the token transfers. Checks-effects-interactions: even though @@ -23,96 +22,107 @@ pub fn handle_settle_funds(context: Context) -> R market_user.unsettled_quote = 0; // Seeds to sign as the market PDA (the authority of both vaults). Built - // once and reused for the two possible transfers. + // once and reused for the two possible transfers. The values are copied + // out because `market` has to release its data borrow before it signs. + let market = &context.accounts.market; let market_bump = [market.bump]; + let base_mint = market.base_mint; + let quote_mint = market.quote_mint; let signer_seeds: [&[u8]; 4] = [ MARKET_SEED, - market.base_mint.as_ref(), - market.quote_mint.as_ref(), + base_mint.as_ref(), + quote_mint.as_ref(), &market_bump, ]; let signer_seeds = &[&signer_seeds[..]]; + // Read the decimals before the CPI handles borrow the mints. + let base_decimals = context.accounts.base_mint.decimals(); + let quote_decimals = context.accounts.quote_mint.decimals(); + + // `market` signs both transfers. It is a data account holding a live borrow + // on its buffer, which the runtime would reject when the CPI borrows the + // same account, so hand the borrow back across the calls. + context.accounts.market.release_borrow()?; + if base_amount > 0 { transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.base_vault.to_account_info(), - mint: context.accounts.base_mint.to_account_info(), - to: context.accounts.user_base_account.to_account_info(), - authority: market.to_account_info(), + from: context.accounts.base_vault.to_cpi_handle_mut(), + mint: context.accounts.base_mint.to_cpi_handle(), + to: context.accounts.user_base_account.to_cpi_handle_mut(), + authority: context.accounts.market.cpi_handle(), }, signer_seeds, ), base_amount, - context.accounts.base_mint.decimals, + base_decimals, )?; } if quote_amount > 0 { transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.quote_vault.to_account_info(), - mint: context.accounts.quote_mint.to_account_info(), - to: context.accounts.user_quote_account.to_account_info(), - authority: market.to_account_info(), + from: context.accounts.quote_vault.to_cpi_handle_mut(), + mint: context.accounts.quote_mint.to_cpi_handle(), + to: context.accounts.user_quote_account.to_cpi_handle_mut(), + authority: context.accounts.market.cpi_handle(), }, signer_seeds, ), quote_amount, - context.accounts.quote_mint.decimals, + quote_decimals, )?; } + context.accounts.market.reacquire_borrow_mut()?; + Ok(()) } #[derive(Accounts)] -pub struct SettleFundsAccountConstraints<'info> { - // `has_one` constraints bind these vaults/mints to the addresses stored +pub struct SettleFundsAccountConstraints { + // `address` constraints bind these vaults/mints to the addresses stored // on the Market PDA at initialise_market time. Without them a caller // could substitute the fee_vault (same mint + same authority as // quote_vault) for `quote_vault` and drain accumulated taker fees, // since transfer_checked only verifies mint + authority on the source // account, not its identity. - #[account( - mut, - has_one = base_vault @ ErrorCode::InvalidBaseVault, - has_one = quote_vault @ ErrorCode::InvalidQuoteVault, - has_one = base_mint @ ErrorCode::InvalidBaseMint, - has_one = quote_mint @ ErrorCode::InvalidQuoteMint, - )] - pub market: Account<'info, Market>, + #[account(mut)] + pub market: BorshAccount, #[account( mut, - seeds = [MARKET_USER_SEED, market.key().as_ref(), owner.key().as_ref()], + seeds = [MARKET_USER_SEED, market.address().as_ref(), owner.address().as_ref()], bump = market_user.bump )] - pub market_user: Account<'info, MarketUser>, + pub market_user: BorshAccount, // Boxed for the same reason as in PlaceOrderAccountConstraints - // InterfaceAccount is too large to keep on the BPF stack in bulk. - #[account(mut)] - pub base_vault: Box>, + #[account(mut, address = market.base_vault @ ErrorCode::InvalidBaseVault)] + pub base_vault: Box>, - #[account(mut)] - pub quote_vault: Box>, + #[account(mut, address = market.quote_vault @ ErrorCode::InvalidQuoteVault)] + pub quote_vault: Box>, #[account(mut)] - pub user_base_account: Box>, + pub user_base_account: Box>, #[account(mut)] - pub user_quote_account: Box>, + pub user_quote_account: Box>, - pub base_mint: Box>, + #[account(address = market.base_mint @ ErrorCode::InvalidBaseMint)] + pub base_mint: Box>, - pub quote_mint: Box>, + #[account(address = market.quote_mint @ ErrorCode::InvalidQuoteMint)] + pub quote_mint: Box>, - pub owner: Signer<'info>, + pub owner: Signer, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/order-book/anchor/programs/order-book/src/lib.rs b/finance/order-book/anchor/programs/order-book/src/lib.rs index edb913309..0812df92e 100644 --- a/finance/order-book/anchor/programs/order-book/src/lib.rs +++ b/finance/order-book/anchor/programs/order-book/src/lib.rs @@ -16,7 +16,7 @@ pub mod order_book { /// the order book PDA, and the two PDA-authority vaults that hold locked /// funds while orders are open. pub fn initialize_market( - context: Context, + context: &mut Context, fee_basis_points: u16, tick_size: u64, base_lot_size: u64, @@ -35,7 +35,9 @@ pub mod order_book { /// Create a per-user, per-market account that tracks a user's open orders /// and unsettled balances. - pub fn initialize_market_user(context: Context) -> Result<()> { + pub fn initialize_market_user( + context: &mut Context, + ) -> Result<()> { instructions::initialize_market_user::handle_initialize_market_user(context) } @@ -50,8 +52,8 @@ pub mod order_book { /// `remaining_accounts`, in pairs of /// `(maker_order_pda, maker_user_account_pda)`, ordered by the /// book's price-time priority (i.e. best ask first for a taker bid). - pub fn place_order<'info>( - context: Context<'info, PlaceOrderAccountConstraints<'info>>, + pub fn place_order( + context: &mut Context, side: state::OrderSide, price: u64, quantity: u64, @@ -62,19 +64,19 @@ pub mod order_book { /// Cancel an open (or partially filled) order. Credits the remaining /// locked amount back to the owner's unsettled balance; the actual token /// transfer happens on settle_funds. - pub fn cancel_order(context: Context) -> Result<()> { + pub fn cancel_order(context: &mut Context) -> Result<()> { instructions::cancel_order::handle_cancel_order(context) } /// Move accumulated unsettled balances out of the market vault and into /// the user's token accounts. No-op if both balances are zero. - pub fn settle_funds(context: Context) -> Result<()> { + pub fn settle_funds(context: &mut Context) -> Result<()> { instructions::settle_funds::handle_settle_funds(context) } /// Drain the fee vault into the market authority's token account. /// Authority-gated - only the market's stored `authority` may call this. - pub fn withdraw_fees(context: Context) -> Result<()> { + pub fn withdraw_fees(context: &mut Context) -> Result<()> { instructions::withdraw_fees::handle_withdraw_fees(context) } } diff --git a/finance/order-book/anchor/programs/order-book/src/state/market.rs b/finance/order-book/anchor/programs/order-book/src/state/market.rs index f8db55790..9198a8d66 100644 --- a/finance/order-book/anchor/programs/order-book/src/state/market.rs +++ b/finance/order-book/anchor/programs/order-book/src/state/market.rs @@ -6,26 +6,26 @@ pub const MARKET_SEED: &[u8] = b"market"; // The market PDA itself is the authority of the token vaults, so funds can only // move out via program-signed CPIs (place/cancel/settle). #[derive(InitSpace)] -#[account] +#[account(borsh)] pub struct Market { - pub authority: Pubkey, + pub authority: Address, - pub base_mint: Pubkey, + pub base_mint: Address, - pub quote_mint: Pubkey, + pub quote_mint: Address, - pub base_vault: Pubkey, + pub base_vault: Address, - pub quote_vault: Pubkey, + pub quote_vault: Address, // Dedicated token account (quote mint) that accumulates taker fees. // Kept separate from `quote_vault` so user-owed balances and // market-earned fees cannot be confused. The market PDA signs transfers // out of it, so only program instruction handlers (notably `withdraw_fees`) // can drain it. - pub fee_vault: Pubkey, + pub fee_vault: Address, - pub order_book: Pubkey, + pub order_book: Address, pub fee_basis_points: u16, diff --git a/finance/order-book/anchor/programs/order-book/src/state/market_user.rs b/finance/order-book/anchor/programs/order-book/src/state/market_user.rs index 14d80ac9b..bfef5d9e5 100644 --- a/finance/order-book/anchor/programs/order-book/src/state/market_user.rs +++ b/finance/order-book/anchor/programs/order-book/src/state/market_user.rs @@ -6,11 +6,11 @@ pub const MARKET_USER_SEED: &[u8] = b"market_user"; // to the user (unsettled_*). Settlement moves those amounts from the vaults // to the user's token accounts in settle_funds. #[derive(InitSpace)] -#[account] +#[account(borsh)] pub struct MarketUser { - pub market: Pubkey, + pub market: Address, - pub owner: Pubkey, + pub owner: Address, pub unsettled_base: u64, diff --git a/finance/order-book/anchor/programs/order-book/src/state/mod.rs b/finance/order-book/anchor/programs/order-book/src/state/mod.rs index b1c058ee8..3dd3bd3dd 100644 --- a/finance/order-book/anchor/programs/order-book/src/state/mod.rs +++ b/finance/order-book/anchor/programs/order-book/src/state/mod.rs @@ -1,12 +1,12 @@ pub mod market; +pub mod market_user; pub mod matching; pub mod order; pub mod order_book; pub mod slab; -pub mod market_user; pub use market::*; +pub use market_user::*; pub use matching::*; pub use order::*; pub use order_book::*; -pub use market_user::*; diff --git a/finance/order-book/anchor/programs/order-book/src/state/order.rs b/finance/order-book/anchor/programs/order-book/src/state/order.rs index 24e4d264e..33d6b147e 100644 --- a/finance/order-book/anchor/programs/order-book/src/state/order.rs +++ b/finance/order-book/anchor/programs/order-book/src/state/order.rs @@ -2,13 +2,17 @@ use anchor_lang::prelude::*; pub const ORDER_SEED: &[u8] = b"order"; -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, InitSpace)] +#[derive( + Clone, Copy, PartialEq, Eq, InitSpace, IdlType, wincode::SchemaRead, wincode::SchemaWrite, +)] pub enum OrderSide { Bid, Ask, } -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, InitSpace)] +#[derive( + Clone, Copy, PartialEq, Eq, InitSpace, IdlType, wincode::SchemaRead, wincode::SchemaWrite, +)] pub enum OrderStatus { Open, PartiallyFilled, @@ -17,11 +21,11 @@ pub enum OrderStatus { } #[derive(InitSpace)] -#[account] +#[account(borsh)] pub struct Order { - pub market: Pubkey, + pub market: Address, - pub owner: Pubkey, + pub owner: Address, pub order_id: u64, @@ -41,5 +45,7 @@ pub struct Order { } pub fn remaining_quantity(order: &Order) -> u64 { - order.original_quantity.saturating_sub(order.filled_quantity) + order + .original_quantity + .saturating_sub(order.filled_quantity) } diff --git a/finance/order-book/anchor/programs/order-book/src/state/order_book.rs b/finance/order-book/anchor/programs/order-book/src/state/order_book.rs index 1786d5ac5..35438c13b 100644 --- a/finance/order-book/anchor/programs/order-book/src/state/order_book.rs +++ b/finance/order-book/anchor/programs/order-book/src/state/order_book.rs @@ -19,16 +19,16 @@ pub const MAX_ORDERS_PER_SIDE: usize = MAX_TREE_NODES; /// counter that gives every order a unique tie-break and acts as the public /// `order_id`. /// -/// Stored as one `AccountLoader` (zero-copy). The account is far +/// Stored as one `Account` (zero-copy). The account is far /// larger than Anchor's borsh `Account` would happily deserialize on every /// instruction - zero-copy gives us per-field memory access without paying /// the (de)serialization cost. -#[account(zero_copy(unsafe))] +#[account] #[repr(C)] pub struct OrderBook { /// Market PDA this book belongs to. Constrained onchain via the - /// `market` `has_one = order_book` (and vice-versa) bindings. - pub market: Pubkey, + /// `market` / `order_book` address bindings. + pub market: Address, /// Tree roots for the two sides. Kept on this struct (rather than inside /// the OrderTreeNodes blobs) so each side's `leaf_count` is cheap to read @@ -59,7 +59,10 @@ pub const ORDER_BOOK_ACCOUNT_SIZE: usize = 8 + std::mem::size_of::(); // + 8 (next_order_id) + 1 (bump) + 7 (pad) + 2 * (12 + 88*1024) // = 64 + 2 * 90124 = 180312 bytes for the struct itself. const _: () = { - assert!(std::mem::size_of::() == 32 + 8 + 8 + 8 + 1 + 7 + 2 * (1 + 3 + 4 + 4 + 4 + NODE_SIZE * MAX_TREE_NODES)); + assert!( + std::mem::size_of::() + == 32 + 8 + 8 + 8 + 1 + 7 + 2 * (1 + 3 + 4 + 4 + 4 + NODE_SIZE * MAX_TREE_NODES) + ); }; /// A compact view of one resting order for the matching engine. Returned @@ -68,14 +71,14 @@ pub struct RestingOrderView { pub order_id: u64, pub price: u64, pub quantity: u64, - pub owner: Pubkey, + pub owner: Address, } impl OrderBook { /// First-time initialization. Sets the market binding, the order-id /// counter, and stamps each slab with its side tag so the iterator knows /// which way to walk. - pub fn initialize(&mut self, market: Pubkey, bump: u8) { + pub fn initialize(&mut self, market: Address, bump: u8) { self.market = market; self.bids_root = OrderTreeRoot::default(); self.asks_root = OrderTreeRoot::default(); @@ -99,7 +102,7 @@ impl OrderBook { self.next_order_id = self .next_order_id .checked_add(1) - .ok_or_else(|| error!(ErrorCode::NumericalOverflow))?; + .ok_or_else(|| ErrorCode::NumericalOverflow)?; Ok(id) } @@ -112,7 +115,7 @@ impl OrderBook { side: OrderSide, price: u64, quantity: u64, - owner: Pubkey, + owner: Address, order_id: u64, timestamp: i64, ) -> Result<()> { @@ -218,11 +221,11 @@ impl OrderBook { let (handle, remaining_after) = { let (handle, leaf) = nodes .find_by_key(root, key) - .ok_or_else(|| error!(ErrorCode::OrderNotFound))?; + .ok_or_else(|| ErrorCode::OrderNotFound)?; let remaining = leaf .quantity .checked_sub(fill_quantity) - .ok_or_else(|| error!(ErrorCode::NumericalOverflow))?; + .ok_or_else(|| ErrorCode::NumericalOverflow)?; (handle, remaining) }; if remaining_after == 0 { @@ -234,7 +237,7 @@ impl OrderBook { let leaf = nodes .node_mut(handle) .and_then(AnyNode::as_leaf_mut) - .ok_or_else(|| error!(ErrorCode::OrderNotFound))?; + .ok_or_else(|| ErrorCode::OrderNotFound)?; leaf.quantity = remaining_after; } Ok(()) diff --git a/finance/order-book/anchor/programs/order-book/src/state/slab/nodes.rs b/finance/order-book/anchor/programs/order-book/src/state/slab/nodes.rs index 2067f6eed..6ac3290c6 100644 --- a/finance/order-book/anchor/programs/order-book/src/state/slab/nodes.rs +++ b/finance/order-book/anchor/programs/order-book/src/state/slab/nodes.rs @@ -64,7 +64,11 @@ impl NodeTag { /// largest key first; inverting seq_num makes the earlier order's seq_num /// the larger one at any given price.) pub fn new_node_key(side: OrderSide, price_data: u64, seq_num: u64) -> u128 { - let seq_num = if side == OrderSide::Bid { !seq_num } else { seq_num }; + let seq_num = if side == OrderSide::Bid { + !seq_num + } else { + seq_num + }; ((price_data as u128) << 64) | (seq_num as u128) } @@ -163,7 +167,7 @@ pub struct LeafNode { /// Owner of this resting order. Same as the corresponding `Order` /// account's `owner`; cached here so the matching loop doesn't have to /// deserialize the maker account just to read the owner. - pub owner: Pubkey, + pub owner: Address, /// Quantity remaining (in base tokens). Decremented as fills consume the /// order; the leaf is removed when it hits 0. @@ -186,13 +190,7 @@ const_assert_eq!(size_of::(), NODE_SIZE); const_assert_eq!(size_of::() % 8, 0); impl LeafNode { - pub fn new( - key: u128, - owner: Pubkey, - quantity: u64, - order_id: u64, - timestamp: i64, - ) -> Self { + pub fn new(key: u128, owner: Address, quantity: u64, order_id: u64, timestamp: i64) -> Self { Self { tag: NodeTag::LeafNode as u8, padding: [0; 7], diff --git a/finance/order-book/anchor/programs/order-book/src/state/slab/ordertree.rs b/finance/order-book/anchor/programs/order-book/src/state/slab/ordertree.rs index ea6e983e1..b5a9eb53b 100644 --- a/finance/order-book/anchor/programs/order-book/src/state/slab/ordertree.rs +++ b/finance/order-book/anchor/programs/order-book/src/state/slab/ordertree.rs @@ -135,7 +135,11 @@ impl OrderTreeNodes { } /// Look up a leaf by its full 128-bit key. - pub fn find_by_key(&self, root: &OrderTreeRoot, search_key: u128) -> Option<(NodeHandle, &LeafNode)> { + pub fn find_by_key( + &self, + root: &OrderTreeRoot, + search_key: u128, + ) -> Option<(NodeHandle, &LeafNode)> { let mut handle = root.node()?; loop { let node = self.node(handle)?; @@ -277,7 +281,7 @@ impl OrderTreeNodes { loop { let parent_contents = *self .node(parent_handle) - .ok_or_else(|| error!(ErrorCode::OrderBookFull))?; + .ok_or_else(|| ErrorCode::OrderBookFull)?; let parent_key = parent_contents.key().unwrap(); // Exact-key collision: only possible if the existing slot is a diff --git a/finance/order-book/anchor/programs/order-book/tests/test_order_book.rs b/finance/order-book/anchor/programs/order-book/tests/test_order_book.rs index 0d25dbb54..28445131c 100644 --- a/finance/order-book/anchor/programs/order-book/tests/test_order_book.rs +++ b/finance/order-book/anchor/programs/order-book/tests/test_order_book.rs @@ -13,10 +13,11 @@ use { anchor_lang::{ solana_program::{ instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - system_instruction, system_program, + system_instruction, }, - Discriminator, InstructionData, ToAccountMetas, + // `system_program` moved to the crate root in v2, and `Pubkey` is + // compat-only: `Address` is the same 32-byte type. + system_program, Address, Discriminator, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, @@ -77,7 +78,7 @@ const BID_QUANTITY: u64 = 10; const ASK_PRICE: u64 = 100; const ASK_QUANTITY: u64 = 5; -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { // The program accepts either the Classic Token Program or the Token // Extensions Program via `TokenInterface`; we use the Classic Token // Program for tests because solana-kite's helpers create classic mints. @@ -86,24 +87,24 @@ fn token_program_id() -> Pubkey { .unwrap() } -fn market_pda(program_id: &Pubkey, base_mint: &Pubkey, quote_mint: &Pubkey) -> Pubkey { - let (market, _) = Pubkey::find_program_address( +fn market_pda(program_id: &Address, base_mint: &Address, quote_mint: &Address) -> Address { + let (market, _) = Address::find_program_address( &[MARKET_SEED, base_mint.as_ref(), quote_mint.as_ref()], program_id, ); market } -fn market_user_pda(program_id: &Pubkey, market: &Pubkey, owner: &Pubkey) -> Pubkey { - let (market_user, _) = Pubkey::find_program_address( +fn market_user_pda(program_id: &Address, market: &Address, owner: &Address) -> Address { + let (market_user, _) = Address::find_program_address( &[MARKET_USER_SEED, market.as_ref(), owner.as_ref()], program_id, ); market_user } -fn order_pda(program_id: &Pubkey, market: &Pubkey, order_id: u64) -> Pubkey { - let (order, _) = Pubkey::find_program_address( +fn order_pda(program_id: &Address, market: &Address, order_id: u64) -> Address { + let (order, _) = Address::find_program_address( &[ORDER_SEED, market.as_ref(), &order_id.to_le_bytes()], program_id, ); @@ -116,7 +117,7 @@ fn order_pda(program_id: &Pubkey, market: &Pubkey, order_id: u64) -> Pubkey { struct Scenario { svm: LiteSVM, - program_id: Pubkey, + program_id: Address, // `payer` funds the mint authority + ATA creations during setup but is // not used directly by the tests afterwards. #[allow(dead_code)] @@ -124,26 +125,26 @@ struct Scenario { authority: Keypair, buyer: Keypair, seller: Keypair, - base_mint: Pubkey, - quote_mint: Pubkey, + base_mint: Address, + quote_mint: Address, base_vault: Keypair, quote_vault: Keypair, // Fees accumulate here (quote mint). Created fresh per Scenario; the // market PDA is the signer, same as the other two vaults. fee_vault: Keypair, - market: Pubkey, + market: Address, // The order book is a ~180 KB zero-copy account owned by the program. // It's NOT a PDA - the BPF runtime caps inner-CPI allocations at 10 KB, // so the client must allocate it directly via system_program::CreateAccount // and pass it in as a signer. See `build_initialize_market_tx` for the // full setup. order_book: Keypair, - buyer_base_ata: Pubkey, - buyer_quote_ata: Pubkey, - seller_base_ata: Pubkey, - seller_quote_ata: Pubkey, - buyer_market_user: Pubkey, - seller_market_user: Pubkey, + buyer_base_ata: Address, + buyer_quote_ata: Address, + seller_base_ata: Address, + seller_quote_ata: Address, + buyer_market_user: Address, + seller_market_user: Address, } fn full_setup() -> Scenario { @@ -236,10 +237,7 @@ fn full_setup() -> Scenario { /// /// Rent is whatever LiteSVM's bank quotes for that size at the current /// rent rate; we use `minimum_balance` so the account is rent-exempt. -fn build_create_order_book_account_ix( - sc: &Scenario, - payer: &Pubkey, -) -> Instruction { +fn build_create_order_book_account_ix(sc: &Scenario, payer: &Address) -> Instruction { // LiteSVM uses the default rent schedule; minimum_balance() on the // 180 KB account is around 1.25 SOL - well within the 100 SOL we fund // the test payer with in `full_setup`. @@ -283,13 +281,13 @@ fn build_initialize_market_ix( fee_vault: sc.fee_vault.pubkey(), authority: sc.authority.pubkey(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) } -fn build_initialize_market_user_ix(sc: &Scenario, owner: &Pubkey) -> Instruction { +fn build_initialize_market_user_ix(sc: &Scenario, owner: &Address) -> Instruction { let market_user = market_user_pda(&sc.program_id, &sc.market, owner); Instruction::new_with_bytes( sc.program_id, @@ -298,7 +296,7 @@ fn build_initialize_market_user_ix(sc: &Scenario, owner: &Pubkey) -> Instruction market_user, market: sc.market, owner: *owner, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) @@ -308,9 +306,9 @@ fn build_initialize_market_user_ix(sc: &Scenario, owner: &Pubkey) -> Instruction fn build_place_order_ix( sc: &Scenario, owner: &Keypair, - market_user: Pubkey, - user_base_account: Pubkey, - user_quote_account: Pubkey, + market_user: Address, + user_base_account: Address, + user_quote_account: Address, side: order_book::state::OrderSide, order_id: u64, price: u64, @@ -339,7 +337,7 @@ fn build_place_order_ix( quote_mint: sc.quote_mint, owner: owner.pubkey(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) @@ -356,14 +354,14 @@ fn build_place_order_ix( fn build_place_order_with_makers_ix( sc: &Scenario, owner: &Keypair, - market_user: Pubkey, - user_base_account: Pubkey, - user_quote_account: Pubkey, + market_user: Address, + user_base_account: Address, + user_quote_account: Address, side: order_book::state::OrderSide, order_id: u64, price: u64, quantity: u64, - maker_pairs: &[(u64, Pubkey)], + maker_pairs: &[(u64, Address)], ) -> Instruction { let mut ix = build_place_order_ix( sc, @@ -379,8 +377,7 @@ fn build_place_order_with_makers_ix( for (maker_order_id, maker_market_user) in maker_pairs { let maker_order = order_pda(&sc.program_id, &sc.market, *maker_order_id); - ix.accounts - .push(AccountMeta::new(maker_order, false)); + ix.accounts.push(AccountMeta::new(maker_order, false)); ix.accounts .push(AccountMeta::new(*maker_market_user, false)); } @@ -388,10 +385,7 @@ fn build_place_order_with_makers_ix( ix } -fn build_withdraw_fees_ix( - sc: &Scenario, - authority_quote_account: Pubkey, -) -> Instruction { +fn build_withdraw_fees_ix(sc: &Scenario, authority_quote_account: Address) -> Instruction { Instruction::new_with_bytes( sc.program_id, &order_book::instruction::WithdrawFees {}.data(), @@ -409,8 +403,8 @@ fn build_withdraw_fees_ix( fn build_cancel_order_ix( sc: &Scenario, - owner: &Pubkey, - market_user: Pubkey, + owner: &Address, + market_user: Address, order_id: u64, ) -> Instruction { let order = order_pda(&sc.program_id, &sc.market, order_id); @@ -430,10 +424,10 @@ fn build_cancel_order_ix( fn build_settle_funds_ix( sc: &Scenario, - owner: &Pubkey, - market_user: Pubkey, - user_base_account: Pubkey, - user_quote_account: Pubkey, + owner: &Address, + market_user: Address, + user_base_account: Address, + user_quote_account: Address, ) -> Instruction { Instruction::new_with_bytes( sc.program_id, @@ -462,7 +456,14 @@ fn initialize_market_and_users(sc: &mut Scenario) { // program, zero-initialized) before initialize_market's `#[account(zero)]` // check passes. let create_ix = build_create_order_book_account_ix(sc, &sc.authority.pubkey()); - let init_ix = build_initialize_market_ix(sc, FEE_BASIS_POINTS, TICK_SIZE, BASE_LOT_SIZE, QUOTE_LOT_SIZE, MIN_ORDER_SIZE); + let init_ix = build_initialize_market_ix( + sc, + FEE_BASIS_POINTS, + TICK_SIZE, + BASE_LOT_SIZE, + QUOTE_LOT_SIZE, + MIN_ORDER_SIZE, + ); send_transaction_from_instructions( &mut sc.svm, vec![create_ix, init_ix], @@ -505,7 +506,14 @@ fn initialize_market_sets_market_and_order_book() { let mut sc = full_setup(); let create_ix = build_create_order_book_account_ix(&sc, &sc.authority.pubkey()); - let ix = build_initialize_market_ix(&sc, FEE_BASIS_POINTS, TICK_SIZE, BASE_LOT_SIZE, QUOTE_LOT_SIZE, MIN_ORDER_SIZE); + let ix = build_initialize_market_ix( + &sc, + FEE_BASIS_POINTS, + TICK_SIZE, + BASE_LOT_SIZE, + QUOTE_LOT_SIZE, + MIN_ORDER_SIZE, + ); send_transaction_from_instructions( &mut sc.svm, vec![create_ix, ix], @@ -553,7 +561,14 @@ fn initialize_market_user_tracks_market_and_owner() { let mut sc = full_setup(); let create_ix = build_create_order_book_account_ix(&sc, &sc.authority.pubkey()); - let init_ix = build_initialize_market_ix(&sc, FEE_BASIS_POINTS, TICK_SIZE, BASE_LOT_SIZE, QUOTE_LOT_SIZE, MIN_ORDER_SIZE); + let init_ix = build_initialize_market_ix( + &sc, + FEE_BASIS_POINTS, + TICK_SIZE, + BASE_LOT_SIZE, + QUOTE_LOT_SIZE, + MIN_ORDER_SIZE, + ); send_transaction_from_instructions( &mut sc.svm, vec![create_ix, init_ix], @@ -683,12 +698,8 @@ fn place_order_rejects_zero_price() { 0, BID_QUANTITY, ); - let result = send_transaction_from_instructions( - &mut sc.svm, - vec![ix], - &[&sc.buyer], - &sc.buyer.pubkey(), - ); + let result = + send_transaction_from_instructions(&mut sc.svm, vec![ix], &[&sc.buyer], &sc.buyer.pubkey()); assert!(result.is_err(), "order at price 0 must be rejected"); } @@ -700,8 +711,14 @@ fn place_order_rejects_unaligned_tick() { // price and see the tick check fire. let unusual_tick_size: u64 = 50; let create_ix = build_create_order_book_account_ix(&sc, &sc.authority.pubkey()); - let init_ix = - build_initialize_market_ix(&sc, FEE_BASIS_POINTS, unusual_tick_size, BASE_LOT_SIZE, QUOTE_LOT_SIZE, MIN_ORDER_SIZE); + let init_ix = build_initialize_market_ix( + &sc, + FEE_BASIS_POINTS, + unusual_tick_size, + BASE_LOT_SIZE, + QUOTE_LOT_SIZE, + MIN_ORDER_SIZE, + ); send_transaction_from_instructions( &mut sc.svm, vec![create_ix, init_ix], @@ -738,12 +755,8 @@ fn place_order_rejects_unaligned_tick() { unaligned_price, BID_QUANTITY, ); - let result = send_transaction_from_instructions( - &mut sc.svm, - vec![ix], - &[&sc.buyer], - &sc.buyer.pubkey(), - ); + let result = + send_transaction_from_instructions(&mut sc.svm, vec![ix], &[&sc.buyer], &sc.buyer.pubkey()); assert!( result.is_err(), "unaligned price must be rejected by tick check" @@ -757,8 +770,14 @@ fn place_order_rejects_below_min_order_size() { // Force a higher min_order_size so we can place an order below it. let elevated_min_order_size: u64 = 10; let create_ix = build_create_order_book_account_ix(&sc, &sc.authority.pubkey()); - let init_ix = - build_initialize_market_ix(&sc, FEE_BASIS_POINTS, TICK_SIZE, BASE_LOT_SIZE, QUOTE_LOT_SIZE, elevated_min_order_size); + let init_ix = build_initialize_market_ix( + &sc, + FEE_BASIS_POINTS, + TICK_SIZE, + BASE_LOT_SIZE, + QUOTE_LOT_SIZE, + elevated_min_order_size, + ); send_transaction_from_instructions( &mut sc.svm, vec![create_ix, init_ix], @@ -980,12 +999,8 @@ fn cancel_and_settle_bid_refunds_full_quote() { BID_PRICE, BID_QUANTITY, ); - let cancel_ix = build_cancel_order_ix( - &sc, - &sc.buyer.pubkey(), - sc.buyer_market_user, - bid_order_id, - ); + let cancel_ix = + build_cancel_order_ix(&sc, &sc.buyer.pubkey(), sc.buyer_market_user, bid_order_id); let settle_ix = build_settle_funds_ix( &sc, &sc.buyer.pubkey(), @@ -1014,10 +1029,10 @@ fn cancel_and_settle_bid_refunds_full_quote() { // Regression test for the fee-drain attack on settle_funds. Pre-fix, // `SettleFundsAccountConstraints` did not bind `quote_vault` to `market.quote_vault` via -// `has_one`, so a caller could pass `market.fee_vault` (same mint and +// an address constraint, so a caller could pass `market.fee_vault` (same mint and // same authority) where `quote_vault` was expected and drain accumulated // taker fees while spending their own unsettled_quote credit. The -// has_one constraint now bound on the `market` field must surface this +// address constraint now bound on the vault field must surface this // as `ConstraintHasOne` (anchor error 2001) before any transfer runs. #[test] fn settle_funds_rejects_fee_vault_substituted_for_quote_vault() { @@ -1040,12 +1055,8 @@ fn settle_funds_rejects_fee_vault_substituted_for_quote_vault() { BID_PRICE, BID_QUANTITY, ); - let cancel_ix = build_cancel_order_ix( - &sc, - &sc.buyer.pubkey(), - sc.buyer_market_user, - bid_order_id, - ); + let cancel_ix = + build_cancel_order_ix(&sc, &sc.buyer.pubkey(), sc.buyer_market_user, bid_order_id); send_transaction_from_instructions( &mut sc.svm, vec![place_ix, cancel_ix], @@ -1057,7 +1068,7 @@ fn settle_funds_rejects_fee_vault_substituted_for_quote_vault() { // Build a settle_funds ix but swap fee_vault in for quote_vault. // Everything else (base_vault, mints, user accounts, owner) stays // correct, so the only thing that should reject this is the new - // has_one constraint on the market PDA. + // address constraint tying the vault to the market PDA. let attack_ix = Instruction::new_with_bytes( sc.program_id, &order_book::instruction::SettleFunds {}.data(), @@ -1095,7 +1106,14 @@ fn initialize_market_rejects_zero_tick_size() { let zero_tick_size: u64 = 0; let create_ix = build_create_order_book_account_ix(&sc, &sc.authority.pubkey()); - let ix = build_initialize_market_ix(&sc, FEE_BASIS_POINTS, zero_tick_size, BASE_LOT_SIZE, QUOTE_LOT_SIZE, MIN_ORDER_SIZE); + let ix = build_initialize_market_ix( + &sc, + FEE_BASIS_POINTS, + zero_tick_size, + BASE_LOT_SIZE, + QUOTE_LOT_SIZE, + MIN_ORDER_SIZE, + ); let result = send_transaction_from_instructions( &mut sc.svm, vec![create_ix, ix], @@ -1116,7 +1134,14 @@ fn initialize_market_rejects_zero_base_lot_size() { let mut sc = full_setup(); let create_ix = build_create_order_book_account_ix(&sc, &sc.authority.pubkey()); - let ix = build_initialize_market_ix(&sc, FEE_BASIS_POINTS, TICK_SIZE, 0, QUOTE_LOT_SIZE, MIN_ORDER_SIZE); + let ix = build_initialize_market_ix( + &sc, + FEE_BASIS_POINTS, + TICK_SIZE, + 0, + QUOTE_LOT_SIZE, + MIN_ORDER_SIZE, + ); let result = send_transaction_from_instructions( &mut sc.svm, vec![create_ix, ix], @@ -1137,7 +1162,14 @@ fn initialize_market_rejects_zero_quote_lot_size() { let mut sc = full_setup(); let create_ix = build_create_order_book_account_ix(&sc, &sc.authority.pubkey()); - let ix = build_initialize_market_ix(&sc, FEE_BASIS_POINTS, TICK_SIZE, BASE_LOT_SIZE, 0, MIN_ORDER_SIZE); + let ix = build_initialize_market_ix( + &sc, + FEE_BASIS_POINTS, + TICK_SIZE, + BASE_LOT_SIZE, + 0, + MIN_ORDER_SIZE, + ); let result = send_transaction_from_instructions( &mut sc.svm, vec![create_ix, ix], @@ -1196,8 +1228,8 @@ fn initialize_market_rejects_oversized_fee() { // MarketUser field offsets after the 8-byte Anchor discriminator. Layout // (see programs/order_book/src/state/market_user.rs): -// market: Pubkey (32) -// owner: Pubkey (32) +// market: Address (32) +// owner: Address (32) // unsettled_base: u64 (8) // unsettled_quote: u64 (8) // ... @@ -1207,8 +1239,8 @@ const USER_ACCOUNT_UNSETTLED_BASE_OFFSET: usize = 8 + 32 + 32; const USER_ACCOUNT_UNSETTLED_QUOTE_OFFSET: usize = USER_ACCOUNT_UNSETTLED_BASE_OFFSET + 8; // Order layout after 8-byte discriminator (see state/order.rs): -// market: Pubkey (32) -// owner: Pubkey (32) +// market: Address (32) +// owner: Address (32) // order_id: u64 (8) // side: u8 (Borsh-encoded enum tag) (1) // price: u64 (8) @@ -1220,7 +1252,7 @@ const ORDER_STATUS_OPEN: u8 = 0; const ORDER_STATUS_PARTIALLY_FILLED: u8 = 1; const ORDER_STATUS_FILLED: u8 = 2; -fn read_user_unsettled(svm: &LiteSVM, market_user: &Pubkey) -> (u64, u64) { +fn read_user_unsettled(svm: &LiteSVM, market_user: &Address) -> (u64, u64) { let data = svm .get_account(market_user) .expect("user account missing") @@ -1239,7 +1271,7 @@ fn read_user_unsettled(svm: &LiteSVM, market_user: &Pubkey) -> (u64, u64) { (base, quote) } -fn read_order_fill_and_status(svm: &LiteSVM, order: &Pubkey) -> (u64, u8) { +fn read_order_fill_and_status(svm: &LiteSVM, order: &Address) -> (u64, u8) { let data = svm .get_account(order) .expect("order account missing") @@ -1448,13 +1480,8 @@ fn taker_partially_fills_resting_order_rest_stays_on_book() { TAKER_BID_QUANTITY, &[(MAKER_ASK_ID, sc.seller_market_user)], ); - send_transaction_from_instructions( - &mut sc.svm, - vec![bid_ix], - &[&sc.buyer], - &sc.buyer.pubkey(), - ) - .unwrap(); + send_transaction_from_instructions(&mut sc.svm, vec![bid_ix], &[&sc.buyer], &sc.buyer.pubkey()) + .unwrap(); // Maker order: still PartiallyFilled, filled_quantity == TAKER_BID_QUANTITY. let maker_order = order_pda(&sc.program_id, &sc.market, MAKER_ASK_ID); @@ -1523,13 +1550,8 @@ fn taker_partially_filled_remainder_rests_on_book() { TAKER_BID_QUANTITY, &[(MAKER_ASK_ID, sc.seller_market_user)], ); - send_transaction_from_instructions( - &mut sc.svm, - vec![bid_ix], - &[&sc.buyer], - &sc.buyer.pubkey(), - ) - .unwrap(); + send_transaction_from_instructions(&mut sc.svm, vec![bid_ix], &[&sc.buyer], &sc.buyer.pubkey()) + .unwrap(); // Maker ask is fully filled. let maker_order = order_pda(&sc.program_id, &sc.market, MAKER_ASK_ID); @@ -1640,8 +1662,14 @@ fn taker_crosses_multiple_resting_orders_best_price_first() { // Both resting asks are fully filled. let order_one = order_pda(&sc.program_id, &sc.market, BEST_ASK_ID); let order_two = order_pda(&sc.program_id, &sc.market, SECOND_ASK_ID); - assert_eq!(read_order_fill_and_status(&sc.svm, &order_one).1, ORDER_STATUS_FILLED); - assert_eq!(read_order_fill_and_status(&sc.svm, &order_two).1, ORDER_STATUS_FILLED); + assert_eq!( + read_order_fill_and_status(&sc.svm, &order_one).1, + ORDER_STATUS_FILLED + ); + assert_eq!( + read_order_fill_and_status(&sc.svm, &order_two).1, + ORDER_STATUS_FILLED + ); // Taker got TAKER_BID_QUANTITY lots = TAKER_BID_QUANTITY * BASE_LOT_SIZE raw base tokens. let (buyer_base, buyer_quote_rebate) = read_user_unsettled(&sc.svm, &sc.buyer_market_user); @@ -1649,7 +1677,8 @@ fn taker_crosses_multiple_resting_orders_best_price_first() { // Price-improvement rebate: taker locked at 1000/unit but 30 units // filled at 900. Rebate = (1000 - 900) * 30 * quote_lot_size. - const PRICE_IMPROVEMENT_REBATE: u64 = (TAKER_BID_PRICE - BEST_ASK_PRICE) * BEST_ASK_QUANTITY * QUOTE_LOT_SIZE; + const PRICE_IMPROVEMENT_REBATE: u64 = + (TAKER_BID_PRICE - BEST_ASK_PRICE) * BEST_ASK_QUANTITY * QUOTE_LOT_SIZE; assert_eq!(buyer_quote_rebate, PRICE_IMPROVEMENT_REBATE); // Seller's net unsettled_quote = sum of (fill_price * fill_qty * quote_lot_size - fee). @@ -1694,10 +1723,16 @@ fn resting_orders_at_same_price_fill_by_time_priority() { &sc.authority, ) .unwrap(); - let second_seller_market_user = market_user_pda(&sc.program_id, &sc.market, &second_seller.pubkey()); + let second_seller_market_user = + market_user_pda(&sc.program_id, &sc.market, &second_seller.pubkey()); let __ix1 = build_initialize_market_user_ix(&sc, &second_seller.pubkey()); - send_transaction_from_instructions(&mut sc.svm, vec![__ix1], &[&second_seller], - &second_seller.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut sc.svm, + vec![__ix1], + &[&second_seller], + &second_seller.pubkey(), + ) + .unwrap(); const FIRST_ASK_ID: u64 = 1; const SECOND_ASK_ID: u64 = 2; @@ -1706,32 +1741,42 @@ fn resting_orders_at_same_price_fill_by_time_priority() { // Seller 1 first in. let __ix2 = build_place_order_ix( - &sc, - &sc.seller, - sc.seller_market_user, - sc.seller_base_ata, - sc.seller_quote_ata, - order_book::state::OrderSide::Ask, - FIRST_ASK_ID, - ASK_PRICE_SHARED, - ASK_QUANTITY_EACH, - ); - send_transaction_from_instructions(&mut sc.svm, vec![__ix2], &[&sc.seller], - &sc.seller.pubkey()).unwrap(); + &sc, + &sc.seller, + sc.seller_market_user, + sc.seller_base_ata, + sc.seller_quote_ata, + order_book::state::OrderSide::Ask, + FIRST_ASK_ID, + ASK_PRICE_SHARED, + ASK_QUANTITY_EACH, + ); + send_transaction_from_instructions( + &mut sc.svm, + vec![__ix2], + &[&sc.seller], + &sc.seller.pubkey(), + ) + .unwrap(); // Seller 2 second in at the same price. let __ix3 = build_place_order_ix( - &sc, - &second_seller, - second_seller_market_user, - second_seller_base_ata, - second_seller_quote_ata, - order_book::state::OrderSide::Ask, - SECOND_ASK_ID, - ASK_PRICE_SHARED, - ASK_QUANTITY_EACH, - ); - send_transaction_from_instructions(&mut sc.svm, vec![__ix3], &[&second_seller], - &second_seller.pubkey()).unwrap(); + &sc, + &second_seller, + second_seller_market_user, + second_seller_base_ata, + second_seller_quote_ata, + order_book::state::OrderSide::Ask, + SECOND_ASK_ID, + ASK_PRICE_SHARED, + ASK_QUANTITY_EACH, + ); + send_transaction_from_instructions( + &mut sc.svm, + vec![__ix3], + &[&second_seller], + &second_seller.pubkey(), + ) + .unwrap(); // Taker bid buys only enough to cross seller 1's ask. const TAKER_BID_ID: u64 = 3; @@ -1758,8 +1803,14 @@ fn resting_orders_at_same_price_fill_by_time_priority() { // Time priority: seller 1 filled, seller 2 still open. let order_one = order_pda(&sc.program_id, &sc.market, FIRST_ASK_ID); let order_two = order_pda(&sc.program_id, &sc.market, SECOND_ASK_ID); - assert_eq!(read_order_fill_and_status(&sc.svm, &order_one).1, ORDER_STATUS_FILLED); - assert_eq!(read_order_fill_and_status(&sc.svm, &order_two).1, ORDER_STATUS_OPEN); + assert_eq!( + read_order_fill_and_status(&sc.svm, &order_one).1, + ORDER_STATUS_FILLED + ); + assert_eq!( + read_order_fill_and_status(&sc.svm, &order_two).1, + ORDER_STATUS_OPEN + ); } #[test] @@ -1776,18 +1827,23 @@ fn taker_bid_gets_price_improvement_from_resting_ask() { // Maker ask. let __ix4 = build_place_order_ix( - &sc, - &sc.seller, - sc.seller_market_user, - sc.seller_base_ata, - sc.seller_quote_ata, - order_book::state::OrderSide::Ask, - MAKER_ASK_ID, - MAKER_ASK_PRICE, - QUANTITY, - ); - send_transaction_from_instructions(&mut sc.svm, vec![__ix4], &[&sc.seller], - &sc.seller.pubkey()).unwrap(); + &sc, + &sc.seller, + sc.seller_market_user, + sc.seller_base_ata, + sc.seller_quote_ata, + order_book::state::OrderSide::Ask, + MAKER_ASK_ID, + MAKER_ASK_PRICE, + QUANTITY, + ); + send_transaction_from_instructions( + &mut sc.svm, + vec![__ix4], + &[&sc.seller], + &sc.seller.pubkey(), + ) + .unwrap(); // Taker bid - limit 1000. const TAKER_BID_ID: u64 = 2; @@ -1855,8 +1911,13 @@ fn fee_rounds_up_when_gross_is_not_a_bps_multiple() { PRICE, QUANTITY, ); - send_transaction_from_instructions(&mut sc.svm, vec![maker_ix], &[&sc.seller], - &sc.seller.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut sc.svm, + vec![maker_ix], + &[&sc.seller], + &sc.seller.pubkey(), + ) + .unwrap(); const TAKER_BID_ID: u64 = 2; let taker_ix = build_place_order_with_makers_ix( @@ -1871,8 +1932,13 @@ fn fee_rounds_up_when_gross_is_not_a_bps_multiple() { QUANTITY, &[(MAKER_ASK_ID, sc.seller_market_user)], ); - send_transaction_from_instructions(&mut sc.svm, vec![taker_ix], &[&sc.buyer], - &sc.buyer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut sc.svm, + vec![taker_ix], + &[&sc.buyer], + &sc.buyer.pubkey(), + ) + .unwrap(); assert_eq!( get_token_account_balance(&sc.svm, &sc.fee_vault.pubkey()).unwrap(), @@ -1897,34 +1963,39 @@ fn fee_vault_receives_exactly_bps_of_taker_gross() { const EXPECTED_FEE: u64 = fee_ceil(GROSS); let __ix5 = build_place_order_ix( - &sc, - &sc.seller, - sc.seller_market_user, - sc.seller_base_ata, - sc.seller_quote_ata, - order_book::state::OrderSide::Ask, - MAKER_ASK_ID, - PRICE, - QUANTITY, - ); - send_transaction_from_instructions(&mut sc.svm, vec![__ix5], &[&sc.seller], - &sc.seller.pubkey()).unwrap(); + &sc, + &sc.seller, + sc.seller_market_user, + sc.seller_base_ata, + sc.seller_quote_ata, + order_book::state::OrderSide::Ask, + MAKER_ASK_ID, + PRICE, + QUANTITY, + ); + send_transaction_from_instructions( + &mut sc.svm, + vec![__ix5], + &[&sc.seller], + &sc.seller.pubkey(), + ) + .unwrap(); const TAKER_BID_ID: u64 = 2; let __ix6 = build_place_order_with_makers_ix( - &sc, - &sc.buyer, - sc.buyer_market_user, - sc.buyer_base_ata, - sc.buyer_quote_ata, - order_book::state::OrderSide::Bid, - TAKER_BID_ID, - PRICE, - QUANTITY, - &[(MAKER_ASK_ID, sc.seller_market_user)], + &sc, + &sc.buyer, + sc.buyer_market_user, + sc.buyer_base_ata, + sc.buyer_quote_ata, + order_book::state::OrderSide::Bid, + TAKER_BID_ID, + PRICE, + QUANTITY, + &[(MAKER_ASK_ID, sc.seller_market_user)], ); - send_transaction_from_instructions(&mut sc.svm, vec![__ix6], &[&sc.buyer], &sc.buyer.pubkey()).unwrap(); - + send_transaction_from_instructions(&mut sc.svm, vec![__ix6], &[&sc.buyer], &sc.buyer.pubkey()) + .unwrap(); assert_eq!( get_token_account_balance(&sc.svm, &sc.fee_vault.pubkey()).unwrap(), @@ -1954,34 +2025,39 @@ fn authority_can_withdraw_fees_after_match() { const EXPECTED_FEE: u64 = fee_ceil(GROSS); let __ix7 = build_place_order_ix( - &sc, - &sc.seller, - sc.seller_market_user, - sc.seller_base_ata, - sc.seller_quote_ata, - order_book::state::OrderSide::Ask, - MAKER_ASK_ID, - PRICE, - QUANTITY, - ); - send_transaction_from_instructions(&mut sc.svm, vec![__ix7], &[&sc.seller], - &sc.seller.pubkey()).unwrap(); + &sc, + &sc.seller, + sc.seller_market_user, + sc.seller_base_ata, + sc.seller_quote_ata, + order_book::state::OrderSide::Ask, + MAKER_ASK_ID, + PRICE, + QUANTITY, + ); + send_transaction_from_instructions( + &mut sc.svm, + vec![__ix7], + &[&sc.seller], + &sc.seller.pubkey(), + ) + .unwrap(); const TAKER_BID_ID: u64 = 2; let __ix8 = build_place_order_with_makers_ix( - &sc, - &sc.buyer, - sc.buyer_market_user, - sc.buyer_base_ata, - sc.buyer_quote_ata, - order_book::state::OrderSide::Bid, - TAKER_BID_ID, - PRICE, - QUANTITY, - &[(MAKER_ASK_ID, sc.seller_market_user)], + &sc, + &sc.buyer, + sc.buyer_market_user, + sc.buyer_base_ata, + sc.buyer_quote_ata, + order_book::state::OrderSide::Bid, + TAKER_BID_ID, + PRICE, + QUANTITY, + &[(MAKER_ASK_ID, sc.seller_market_user)], ); - send_transaction_from_instructions(&mut sc.svm, vec![__ix8], &[&sc.buyer], &sc.buyer.pubkey()).unwrap(); - + send_transaction_from_instructions(&mut sc.svm, vec![__ix8], &[&sc.buyer], &sc.buyer.pubkey()) + .unwrap(); assert_eq!( get_token_account_balance(&sc.svm, &sc.fee_vault.pubkey()).unwrap(), @@ -2025,53 +2101,63 @@ fn settle_funds_after_match_pays_out_both_unsettled_balances() { // Maker posts and taker crosses. let __ix9 = build_place_order_ix( - &sc, - &sc.seller, - sc.seller_market_user, - sc.seller_base_ata, - sc.seller_quote_ata, - order_book::state::OrderSide::Ask, - MAKER_ASK_ID, - PRICE, - QUANTITY, - ); - send_transaction_from_instructions(&mut sc.svm, vec![__ix9], &[&sc.seller], - &sc.seller.pubkey()).unwrap(); + &sc, + &sc.seller, + sc.seller_market_user, + sc.seller_base_ata, + sc.seller_quote_ata, + order_book::state::OrderSide::Ask, + MAKER_ASK_ID, + PRICE, + QUANTITY, + ); + send_transaction_from_instructions( + &mut sc.svm, + vec![__ix9], + &[&sc.seller], + &sc.seller.pubkey(), + ) + .unwrap(); const TAKER_BID_ID: u64 = 2; let __ix10 = build_place_order_with_makers_ix( - &sc, - &sc.buyer, - sc.buyer_market_user, - sc.buyer_base_ata, - sc.buyer_quote_ata, - order_book::state::OrderSide::Bid, - TAKER_BID_ID, - PRICE, - QUANTITY, - &[(MAKER_ASK_ID, sc.seller_market_user)], + &sc, + &sc.buyer, + sc.buyer_market_user, + sc.buyer_base_ata, + sc.buyer_quote_ata, + order_book::state::OrderSide::Bid, + TAKER_BID_ID, + PRICE, + QUANTITY, + &[(MAKER_ASK_ID, sc.seller_market_user)], ); - send_transaction_from_instructions(&mut sc.svm, vec![__ix10], &[&sc.buyer], &sc.buyer.pubkey()).unwrap(); - + send_transaction_from_instructions(&mut sc.svm, vec![__ix10], &[&sc.buyer], &sc.buyer.pubkey()) + .unwrap(); // Settle both sides. let __ix11 = build_settle_funds_ix( - &sc, - &sc.buyer.pubkey(), - sc.buyer_market_user, - sc.buyer_base_ata, - sc.buyer_quote_ata, - ); - send_transaction_from_instructions(&mut sc.svm, vec![__ix11], &[&sc.buyer], - &sc.buyer.pubkey()).unwrap(); + &sc, + &sc.buyer.pubkey(), + sc.buyer_market_user, + sc.buyer_base_ata, + sc.buyer_quote_ata, + ); + send_transaction_from_instructions(&mut sc.svm, vec![__ix11], &[&sc.buyer], &sc.buyer.pubkey()) + .unwrap(); let __ix12 = build_settle_funds_ix( - &sc, - &sc.seller.pubkey(), - sc.seller_market_user, - sc.seller_base_ata, - sc.seller_quote_ata, - ); - send_transaction_from_instructions(&mut sc.svm, vec![__ix12], &[&sc.seller], - &sc.seller.pubkey()).unwrap(); + &sc, + &sc.seller.pubkey(), + sc.seller_market_user, + sc.seller_base_ata, + sc.seller_quote_ata, + ); + send_transaction_from_instructions( + &mut sc.svm, + vec![__ix12], + &[&sc.seller], + &sc.seller.pubkey(), + ) + .unwrap(); // Buyer should now hold `QUANTITY` lots of extra base tokens // (QUANTITY * BASE_LOT_SIZE raw minor units) and have paid the gross @@ -2097,4 +2183,3 @@ fn settle_funds_after_match_pays_out_both_unsettled_balances() { EXPECTED_NET_QUOTE_TO_SELLER ); } - diff --git a/finance/perpetual-futures/anchor/programs/mock-switchboard/Cargo.toml b/finance/perpetual-futures/anchor/programs/mock-switchboard/Cargo.toml index 275ec0d52..f199b8d30 100644 --- a/finance/perpetual-futures/anchor/programs/mock-switchboard/Cargo.toml +++ b/finance/perpetual-futures/anchor/programs/mock-switchboard/Cargo.toml @@ -20,7 +20,15 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/finance/perpetual-futures/anchor/programs/mock-switchboard/src/lib.rs b/finance/perpetual-futures/anchor/programs/mock-switchboard/src/lib.rs index 2a3f79232..f2f638fc5 100644 --- a/finance/perpetual-futures/anchor/programs/mock-switchboard/src/lib.rs +++ b/finance/perpetual-futures/anchor/programs/mock-switchboard/src/lib.rs @@ -24,13 +24,13 @@ pub mod mock_switchboard { /// Initialize the mock feed with an initial price. The signer becomes the /// authority allowed to push later price updates. pub fn initialize_feed( - context: Context, + context: &mut Context, price: i128, scale: u32, confidence: u64, ) -> Result<()> { let feed = &mut context.accounts.feed; - feed.authority = context.accounts.authority.key(); + feed.authority = *context.accounts.authority.address(); feed.price = price; feed.scale = scale; feed.last_update_slot = Clock::get()?.slot; @@ -43,7 +43,7 @@ pub mod mock_switchboard { /// is an authority-gated write, because the goal is to drive deterministic /// test scenarios. pub fn set_price( - context: Context, + context: &mut Context, price: i128, confidence: u64, ) -> Result<()> { @@ -56,38 +56,36 @@ pub mod mock_switchboard { } #[derive(Accounts)] -pub struct InitializeFeedAccountConstraints<'info> { +pub struct InitializeFeedAccountConstraints { #[account( init, payer = authority, space = MockFeed::DISCRIMINATOR.len() + MockFeed::INIT_SPACE, )] - pub feed: Account<'info, MockFeed>, + pub feed: BorshAccount, #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, - pub system_program: Program<'info, System>, + pub system_program: Program, } #[derive(Accounts)] -pub struct SetPriceAccountConstraints<'info> { - #[account( - mut, - has_one = authority, - )] - pub feed: Account<'info, MockFeed>, +pub struct SetPriceAccountConstraints { + #[account(mut)] + pub feed: BorshAccount, - pub authority: Signer<'info>, + #[account(address = feed.authority)] + pub authority: Signer, } /// Mock of a Switchboard On-Demand feed. Real feeds carry many more fields /// (median, range, sample window, signatures) — this is the bare minimum the /// perpetual-futures program needs to do a price comparison. #[derive(InitSpace)] -#[account] +#[account(borsh)] pub struct MockFeed { - pub authority: Pubkey, + pub authority: Address, /// Signed 128-bit fixed-point price. Real Switchboard prices are also i128. pub price: i128, diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/Cargo.toml b/finance/perpetual-futures/anchor/programs/perpetual-futures/Cargo.toml index 5c8300a5d..80c7602c8 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/Cargo.toml +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/Cargo.toml @@ -14,7 +14,7 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] @@ -23,8 +23,20 @@ custom-panic = [] # init-if-needed lets add_liquidity create the provider's liquidity-provider # token account on their first deposit. The provider is the payer, so this does # not let one party fund another's rent. -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# `last_restart.rs` reads the LastRestartSlot sysvar through pinocchio's +# `get_sysvar` wrapper: pinocchio (and so anchor-lang v2) ships only the +# Clock and Rent sysvars as typed accessors. +pinocchio = "0.11" +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" # Not used directly. Declared so Cargo feature unification turns on # `no-entrypoint` for the spl-token that anchor-spl pulls in; without it the # integration-test binary links two `entrypoint` symbols (this program's and @@ -38,6 +50,7 @@ solana-sysvar = "3" [dev-dependencies] litesvm = "0.13.1" +solana-clock = "3.0.1" solana-signer = "3.0.0" solana-keypair = "3.0.1" solana-kite = "0.4.0" diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/add_liquidity.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/add_liquidity.rs index 5b174f7ec..e6d83746d 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/add_liquidity.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/add_liquidity.rs @@ -12,7 +12,7 @@ use crate::instructions::shared::{liquidity_provider_aum, refresh_price_and_fund use crate::state::Pool; pub fn handle_add_liquidity( - context: Context, + context: &mut Context, amount: u64, minimum_shares_out: u64, ) -> Result<()> { @@ -21,7 +21,7 @@ pub fn handle_add_liquidity( let pool = &mut context.accounts.pool; let price = refresh_price_and_funding(pool, &context.accounts.oracle_feed)?; - let lp_supply = context.accounts.lp_mint.supply; + let lp_supply = context.accounts.lp_mint.supply(); let shares: u64 = if lp_supply == 0 { // Bootstrap: shares track collateral one-for-one, less the withheld // minimum, so the share supply can never start at a dust amount. @@ -53,27 +53,27 @@ pub fn handle_add_liquidity( transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.provider_collateral.to_account_info(), - mint: context.accounts.collateral_mint.to_account_info(), - to: context.accounts.custody_vault.to_account_info(), - authority: context.accounts.provider.to_account_info(), + from: context.accounts.provider_collateral.to_cpi_handle_mut(), + mint: context.accounts.collateral_mint.to_cpi_handle(), + to: context.accounts.custody_vault.to_cpi_handle_mut(), + authority: context.accounts.provider.cpi_handle(), }, ), amount, - context.accounts.collateral_mint.decimals, + context.accounts.collateral_mint.decimals(), )?; - let pool_key = pool.key(); + let pool_key = pool.address(); let authority_seeds: &[&[u8]] = &[AUTHORITY_SEED, pool_key.as_ref(), &[pool.authority_bump]]; mint_to( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), MintTo { - mint: context.accounts.lp_mint.to_account_info(), - to: context.accounts.provider_lp.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + mint: context.accounts.lp_mint.to_cpi_handle_mut(), + to: context.accounts.provider_lp.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, &[authority_seeds], ), @@ -84,42 +84,41 @@ pub fn handle_add_liquidity( } #[derive(Accounts)] -pub struct AddLiquidityAccountConstraints<'info> { +pub struct AddLiquidityAccountConstraints { #[account(mut)] - pub provider: Signer<'info>, + pub provider: Signer, #[account( mut, seeds = [POOL_SEED, pool.collateral_mint.as_ref(), pool.oracle_feed.as_ref()], bump = pool.bump, - has_one = collateral_mint, - has_one = lp_mint, - has_one = custody_vault, - has_one = oracle_feed, )] - pub pool: Box>, + pub pool: Box>, /// CHECK: PDA authority over the vault and liquidity-provider mint. #[account( - seeds = [AUTHORITY_SEED, pool.key().as_ref()], + seeds = [AUTHORITY_SEED, pool.address().as_ref()], bump = pool.authority_bump, )] - pub pool_authority: UncheckedAccount<'info>, + pub pool_authority: UncheckedAccount, - /// CHECK: validated by the `has_one = oracle_feed` constraint on the pool. - pub oracle_feed: UncheckedAccount<'info>, + /// CHECK: validated by the `address = pool.oracle_feed` constraint below. + #[account(address = pool.oracle_feed)] + pub oracle_feed: UncheckedAccount, - pub collateral_mint: Box>, + #[account(address = pool.collateral_mint)] + pub collateral_mint: Box>, - #[account(mut)] - pub lp_mint: Box>, + #[account(mut, address = pool.lp_mint)] + pub lp_mint: Box>, #[account( mut, - seeds = [VAULT_SEED, pool.key().as_ref()], + seeds = [VAULT_SEED, pool.address().as_ref()], bump, + address = pool.custody_vault, )] - pub custody_vault: Box>, + pub custody_vault: Box>, #[account( mut, @@ -127,7 +126,7 @@ pub struct AddLiquidityAccountConstraints<'info> { associated_token::authority = provider, associated_token::token_program = token_program, )] - pub provider_collateral: Box>, + pub provider_collateral: Box>, #[account( init_if_needed, @@ -136,9 +135,9 @@ pub struct AddLiquidityAccountConstraints<'info> { associated_token::authority = provider, associated_token::token_program = token_program, )] - pub provider_lp: Box>, + pub provider_lp: Box>, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/close_position.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/close_position.rs index fd50ea002..692ad7d87 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/close_position.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/close_position.rs @@ -10,7 +10,7 @@ use crate::instructions::shared::{basis_points_of, refresh_price_and_funding, se use crate::state::{Pool, Position}; pub fn handle_close_position( - context: Context, + context: &mut Context, minimum_payout: u64, ) -> Result<()> { let pool = &mut context.accounts.pool; @@ -66,69 +66,68 @@ pub fn handle_close_position( .checked_add(close_fee) .ok_or(PerpError::MathOverflow)?; - let pool_key = pool.key(); + let pool_key = pool.address(); let authority_seeds: &[&[u8]] = &[AUTHORITY_SEED, pool_key.as_ref(), &[pool.authority_bump]]; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.custody_vault.to_account_info(), - mint: context.accounts.collateral_mint.to_account_info(), - to: context.accounts.trader_collateral.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + from: context.accounts.custody_vault.to_cpi_handle_mut(), + mint: context.accounts.collateral_mint.to_cpi_handle(), + to: context.accounts.trader_collateral.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, &[authority_seeds], ), payout, - context.accounts.collateral_mint.decimals, + context.accounts.collateral_mint.decimals(), )?; Ok(()) } #[derive(Accounts)] -pub struct ClosePositionAccountConstraints<'info> { - #[account(mut)] - pub owner: Signer<'info>, +pub struct ClosePositionAccountConstraints { + #[account(mut, address = position.owner)] + pub owner: Signer, #[account( mut, seeds = [POOL_SEED, pool.collateral_mint.as_ref(), pool.oracle_feed.as_ref()], bump = pool.bump, - has_one = collateral_mint, - has_one = custody_vault, - has_one = oracle_feed, + address = position.pool, )] - pub pool: Box>, + pub pool: Box>, #[account( mut, close = owner, - seeds = [POSITION_SEED, pool.key().as_ref(), owner.key().as_ref(), position.side.as_seed()], + seeds = [POSITION_SEED, pool.address().as_ref(), owner.address().as_ref(), position.side.as_seed()], bump = position.bump, - has_one = owner, - has_one = pool, )] - pub position: Box>, + pub position: Box>, /// CHECK: PDA authority over the vault. #[account( - seeds = [AUTHORITY_SEED, pool.key().as_ref()], + seeds = [AUTHORITY_SEED, pool.address().as_ref()], bump = pool.authority_bump, )] - pub pool_authority: UncheckedAccount<'info>, + pub pool_authority: UncheckedAccount, - /// CHECK: validated by the `has_one = oracle_feed` constraint on the pool. - pub oracle_feed: UncheckedAccount<'info>, + /// CHECK: validated by the `address = pool.oracle_feed` constraint below. + #[account(address = pool.oracle_feed)] + pub oracle_feed: UncheckedAccount, - pub collateral_mint: Box>, + #[account(address = pool.collateral_mint)] + pub collateral_mint: Box>, #[account( mut, - seeds = [VAULT_SEED, pool.key().as_ref()], + seeds = [VAULT_SEED, pool.address().as_ref()], bump, + address = pool.custody_vault, )] - pub custody_vault: Box>, + pub custody_vault: Box>, #[account( mut, @@ -136,9 +135,9 @@ pub struct ClosePositionAccountConstraints<'info> { associated_token::authority = owner, associated_token::token_program = token_program, )] - pub trader_collateral: Box>, + pub trader_collateral: Box>, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/collect_fees.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/collect_fees.rs index e97c68853..094790e39 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/collect_fees.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/collect_fees.rs @@ -8,7 +8,7 @@ use crate::constants::{AUTHORITY_SEED, POOL_SEED, VAULT_SEED}; use crate::errors::PerpError; use crate::state::Pool; -pub fn handle_collect_fees(context: Context) -> Result<()> { +pub fn handle_collect_fees(context: &mut Context) -> Result<()> { let pool = &mut context.accounts.pool; let amount = pool.protocol_fees; require!(amount > 0, PerpError::NothingToClaim); @@ -16,56 +16,55 @@ pub fn handle_collect_fees(context: Context) -> R // Effects before interaction: zero the balance, then transfer. pool.protocol_fees = 0; - let pool_key = pool.key(); + let pool_key = pool.address(); let authority_seeds: &[&[u8]] = &[AUTHORITY_SEED, pool_key.as_ref(), &[pool.authority_bump]]; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.custody_vault.to_account_info(), - mint: context.accounts.collateral_mint.to_account_info(), - to: context.accounts.authority_collateral.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + from: context.accounts.custody_vault.to_cpi_handle_mut(), + mint: context.accounts.collateral_mint.to_cpi_handle(), + to: context.accounts.authority_collateral.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, &[authority_seeds], ), amount, - context.accounts.collateral_mint.decimals, + context.accounts.collateral_mint.decimals(), )?; Ok(()) } #[derive(Accounts)] -pub struct CollectFeesAccountConstraints<'info> { - #[account(mut)] - pub authority: Signer<'info>, +pub struct CollectFeesAccountConstraints { + #[account(mut, address = pool.authority)] + pub authority: Signer, #[account( mut, seeds = [POOL_SEED, pool.collateral_mint.as_ref(), pool.oracle_feed.as_ref()], bump = pool.bump, - has_one = authority, - has_one = collateral_mint, - has_one = custody_vault, )] - pub pool: Box>, + pub pool: Box>, /// CHECK: PDA authority over the vault. #[account( - seeds = [AUTHORITY_SEED, pool.key().as_ref()], + seeds = [AUTHORITY_SEED, pool.address().as_ref()], bump = pool.authority_bump, )] - pub pool_authority: UncheckedAccount<'info>, + pub pool_authority: UncheckedAccount, - pub collateral_mint: Box>, + #[account(address = pool.collateral_mint)] + pub collateral_mint: Box>, #[account( mut, - seeds = [VAULT_SEED, pool.key().as_ref()], + seeds = [VAULT_SEED, pool.address().as_ref()], bump, + address = pool.custody_vault, )] - pub custody_vault: Box>, + pub custody_vault: Box>, #[account( init_if_needed, @@ -74,9 +73,9 @@ pub struct CollectFeesAccountConstraints<'info> { associated_token::authority = authority, associated_token::token_program = token_program, )] - pub authority_collateral: Box>, + pub authority_collateral: Box>, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/initialize_pool.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/initialize_pool.rs index 3699414cb..fdedfa08d 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/initialize_pool.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/initialize_pool.rs @@ -1,4 +1,6 @@ use anchor_lang::prelude::*; +use anchor_spl::mint; +use anchor_spl::token; use anchor_spl::{ associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface}, @@ -13,7 +15,7 @@ use crate::state::Pool; /// Trading parameters set once at pool creation. Bundled into one struct so the /// instruction signature stays readable. -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +#[derive(Clone, IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub struct PoolParameters { /// Decimal places the oracle quotes its price in (e.g. 8). pub oracle_scale: u32, @@ -33,7 +35,7 @@ pub struct PoolParameters { } pub fn handle_initialize_pool( - context: Context, + context: &mut Context, parameters: PoolParameters, ) -> Result<()> { let denominator = BASIS_POINTS_DENOMINATOR as u16; @@ -76,12 +78,12 @@ pub fn handle_initialize_pool( ); let pool = &mut context.accounts.pool; - pool.authority = context.accounts.authority.key(); - pool.collateral_mint = context.accounts.collateral_mint.key(); - pool.oracle_feed = context.accounts.oracle_feed.key(); + pool.authority = *context.accounts.authority.address(); + pool.collateral_mint = *context.accounts.collateral_mint.address(); + pool.oracle_feed = *context.accounts.oracle_feed.address(); pool.oracle_scale = parameters.oracle_scale; - pool.custody_vault = context.accounts.custody_vault.key(); - pool.lp_mint = context.accounts.lp_mint.key(); + pool.custody_vault = *context.accounts.custody_vault.address(); + pool.lp_mint = *context.accounts.lp_mint.address(); pool.liquidity = 0; pool.reserved_liquidity = 0; pool.total_collateral = 0; @@ -106,57 +108,57 @@ pub fn handle_initialize_pool( } #[derive(Accounts)] -pub struct InitializePoolAccountConstraints<'info> { +pub struct InitializePoolAccountConstraints { #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, #[account( init, payer = authority, space = Pool::DISCRIMINATOR.len() + Pool::INIT_SPACE, - seeds = [POOL_SEED, collateral_mint.key().as_ref(), oracle_feed.key().as_ref()], + seeds = [POOL_SEED, collateral_mint.address().as_ref(), oracle_feed.address().as_ref()], bump, )] - pub pool: Box>, + pub pool: Box>, - pub collateral_mint: Box>, + pub collateral_mint: Box>, /// CHECK: The oracle feed account. Its key is stored on the pool and every /// read validates the layout, scale, and freshness; it is never trusted by /// type. Swap for a real Switchboard feed in production. - pub oracle_feed: UncheckedAccount<'info>, + pub oracle_feed: UncheckedAccount, /// CHECK: PDA that owns the vault and the liquidity-provider mint. Holds no /// data; used only to sign vault and mint CPIs. #[account( - seeds = [AUTHORITY_SEED, pool.key().as_ref()], + seeds = [AUTHORITY_SEED, pool.address().as_ref()], bump, )] - pub pool_authority: UncheckedAccount<'info>, + pub pool_authority: UncheckedAccount, #[account( init, payer = authority, - seeds = [LP_MINT_SEED, pool.key().as_ref()], + seeds = [LP_MINT_SEED, pool.address().as_ref()], bump, - mint::decimals = collateral_mint.decimals, + mint::decimals = collateral_mint.decimals(), mint::authority = pool_authority, mint::token_program = token_program, )] - pub lp_mint: Box>, + pub lp_mint: Box>, #[account( init, payer = authority, - seeds = [VAULT_SEED, pool.key().as_ref()], + seeds = [VAULT_SEED, pool.address().as_ref()], bump, token::mint = collateral_mint, token::authority = pool_authority, token::token_program = token_program, )] - pub custody_vault: Box>, + pub custody_vault: Box>, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/liquidate_position.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/liquidate_position.rs index 26c87d25e..09bc517d6 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/liquidate_position.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/liquidate_position.rs @@ -10,7 +10,7 @@ use crate::instructions::shared::{basis_points_of, refresh_price_and_funding, se use crate::state::{Pool, Position}; pub fn handle_liquidate_position( - context: Context, + context: &mut Context, ) -> Result<()> { let pool = &mut context.accounts.pool; let price = refresh_price_and_funding(pool, &context.accounts.oracle_feed)?; @@ -60,40 +60,40 @@ pub fn handle_liquidate_position( .try_into() .map_err(|_| PerpError::MathOverflow)?; - let pool_key = pool.key(); + let pool_key = pool.address(); let authority_seeds: &[&[u8]] = &[AUTHORITY_SEED, pool_key.as_ref(), &[pool.authority_bump]]; if liquidator_payout > 0 { transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.custody_vault.to_account_info(), - mint: context.accounts.collateral_mint.to_account_info(), - to: context.accounts.liquidator_collateral.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + from: context.accounts.custody_vault.to_cpi_handle_mut(), + mint: context.accounts.collateral_mint.to_cpi_handle(), + to: context.accounts.liquidator_collateral.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, &[authority_seeds], ), liquidator_payout, - context.accounts.collateral_mint.decimals, + context.accounts.collateral_mint.decimals(), )?; } if trader_refund > 0 { transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.custody_vault.to_account_info(), - mint: context.accounts.collateral_mint.to_account_info(), - to: context.accounts.trader_collateral.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + from: context.accounts.custody_vault.to_cpi_handle_mut(), + mint: context.accounts.collateral_mint.to_cpi_handle(), + to: context.accounts.trader_collateral.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, &[authority_seeds], ), trader_refund, - context.accounts.collateral_mint.decimals, + context.accounts.collateral_mint.decimals(), )?; } @@ -101,53 +101,52 @@ pub fn handle_liquidate_position( } #[derive(Accounts)] -pub struct LiquidatePositionAccountConstraints<'info> { +pub struct LiquidatePositionAccountConstraints { #[account(mut)] - pub liquidator: Signer<'info>, + pub liquidator: Signer, - /// CHECK: the position owner, validated by the position's `has_one = owner`. + /// CHECK: the position owner, validated by `address = position.owner` below. /// Receives the position account's rent and any equity refund. - #[account(mut)] - pub owner: UncheckedAccount<'info>, + #[account(mut, address = position.owner)] + pub owner: UncheckedAccount, #[account( mut, seeds = [POOL_SEED, pool.collateral_mint.as_ref(), pool.oracle_feed.as_ref()], bump = pool.bump, - has_one = collateral_mint, - has_one = custody_vault, - has_one = oracle_feed, + address = position.pool, )] - pub pool: Box>, + pub pool: Box>, #[account( mut, close = owner, - seeds = [POSITION_SEED, pool.key().as_ref(), owner.key().as_ref(), position.side.as_seed()], + seeds = [POSITION_SEED, pool.address().as_ref(), owner.address().as_ref(), position.side.as_seed()], bump = position.bump, - has_one = owner, - has_one = pool, )] - pub position: Box>, + pub position: Box>, /// CHECK: PDA authority over the vault. #[account( - seeds = [AUTHORITY_SEED, pool.key().as_ref()], + seeds = [AUTHORITY_SEED, pool.address().as_ref()], bump = pool.authority_bump, )] - pub pool_authority: UncheckedAccount<'info>, + pub pool_authority: UncheckedAccount, - /// CHECK: validated by the `has_one = oracle_feed` constraint on the pool. - pub oracle_feed: UncheckedAccount<'info>, + /// CHECK: validated by the `address = pool.oracle_feed` constraint below. + #[account(address = pool.oracle_feed)] + pub oracle_feed: UncheckedAccount, - pub collateral_mint: Box>, + #[account(address = pool.collateral_mint)] + pub collateral_mint: Box>, #[account( mut, - seeds = [VAULT_SEED, pool.key().as_ref()], + seeds = [VAULT_SEED, pool.address().as_ref()], bump, + address = pool.custody_vault, )] - pub custody_vault: Box>, + pub custody_vault: Box>, #[account( mut, @@ -155,7 +154,7 @@ pub struct LiquidatePositionAccountConstraints<'info> { associated_token::authority = owner, associated_token::token_program = token_program, )] - pub trader_collateral: Box>, + pub trader_collateral: Box>, #[account( init_if_needed, @@ -164,9 +163,9 @@ pub struct LiquidatePositionAccountConstraints<'info> { associated_token::authority = liquidator, associated_token::token_program = token_program, )] - pub liquidator_collateral: Box>, + pub liquidator_collateral: Box>, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/open_position.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/open_position.rs index 8c3ec8dc9..4c6d543bf 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/open_position.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/open_position.rs @@ -10,7 +10,7 @@ use crate::instructions::shared::{basis_points_of, refresh_price_and_funding, sc use crate::state::{Pool, Position, Side}; pub fn handle_open_position( - context: Context, + context: &mut Context, side: Side, collateral_amount: u64, size: u64, @@ -66,8 +66,8 @@ pub fn handle_open_position( // Effects: record the position and the pool's new aggregates before moving // any tokens. let position = &mut context.accounts.position; - position.owner = context.accounts.owner.key(); - position.pool = pool.key(); + position.owner = *context.accounts.owner.address(); + position.pool = *pool.address(); position.side = side; position.collateral = net_collateral; position.size = size; @@ -110,16 +110,16 @@ pub fn handle_open_position( transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.trader_collateral.to_account_info(), - mint: context.accounts.collateral_mint.to_account_info(), - to: context.accounts.custody_vault.to_account_info(), - authority: context.accounts.owner.to_account_info(), + from: context.accounts.trader_collateral.to_cpi_handle_mut(), + mint: context.accounts.collateral_mint.to_cpi_handle(), + to: context.accounts.custody_vault.to_cpi_handle_mut(), + authority: context.accounts.owner.cpi_handle(), }, ), collateral_amount, - context.accounts.collateral_mint.decimals, + context.accounts.collateral_mint.decimals(), )?; Ok(()) @@ -127,40 +127,40 @@ pub fn handle_open_position( #[derive(Accounts)] #[instruction(side: Side)] -pub struct OpenPositionAccountConstraints<'info> { +pub struct OpenPositionAccountConstraints { #[account(mut)] - pub owner: Signer<'info>, + pub owner: Signer, #[account( mut, seeds = [POOL_SEED, pool.collateral_mint.as_ref(), pool.oracle_feed.as_ref()], bump = pool.bump, - has_one = collateral_mint, - has_one = custody_vault, - has_one = oracle_feed, )] - pub pool: Box>, + pub pool: Box>, #[account( init, payer = owner, space = Position::DISCRIMINATOR.len() + Position::INIT_SPACE, - seeds = [POSITION_SEED, pool.key().as_ref(), owner.key().as_ref(), side.as_seed()], + seeds = [POSITION_SEED, pool.address().as_ref(), owner.address().as_ref(), side.as_seed()], bump, )] - pub position: Box>, + pub position: Box>, - /// CHECK: validated by the `has_one = oracle_feed` constraint on the pool. - pub oracle_feed: UncheckedAccount<'info>, + /// CHECK: validated by the `address = pool.oracle_feed` constraint below. + #[account(address = pool.oracle_feed)] + pub oracle_feed: UncheckedAccount, - pub collateral_mint: Box>, + #[account(address = pool.collateral_mint)] + pub collateral_mint: Box>, #[account( mut, - seeds = [VAULT_SEED, pool.key().as_ref()], + seeds = [VAULT_SEED, pool.address().as_ref()], bump, + address = pool.custody_vault, )] - pub custody_vault: Box>, + pub custody_vault: Box>, #[account( mut, @@ -168,9 +168,9 @@ pub struct OpenPositionAccountConstraints<'info> { associated_token::authority = owner, associated_token::token_program = token_program, )] - pub trader_collateral: Box>, + pub trader_collateral: Box>, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/remove_liquidity.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/remove_liquidity.rs index 2426c556c..294f4cb05 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/remove_liquidity.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/remove_liquidity.rs @@ -12,7 +12,7 @@ use crate::instructions::shared::{liquidity_provider_aum, refresh_price_and_fund use crate::state::Pool; pub fn handle_remove_liquidity( - context: Context, + context: &mut Context, shares: u64, minimum_amount_out: u64, ) -> Result<()> { @@ -21,7 +21,7 @@ pub fn handle_remove_liquidity( let pool = &mut context.accounts.pool; let price = refresh_price_and_funding(pool, &context.accounts.oracle_feed)?; - let lp_supply = context.accounts.lp_mint.supply; + let lp_supply = context.accounts.lp_mint.supply(); let aum = liquidity_provider_aum(pool, price)?; require!(aum > 0, PerpError::PoolInsolvent); @@ -58,73 +58,72 @@ pub fn handle_remove_liquidity( burn( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), Burn { - mint: context.accounts.lp_mint.to_account_info(), - from: context.accounts.provider_lp.to_account_info(), - authority: context.accounts.provider.to_account_info(), + mint: context.accounts.lp_mint.to_cpi_handle_mut(), + from: context.accounts.provider_lp.to_cpi_handle_mut(), + authority: context.accounts.provider.cpi_handle(), }, ), shares, )?; - let pool_key = pool.key(); + let pool_key = pool.address(); let authority_seeds: &[&[u8]] = &[AUTHORITY_SEED, pool_key.as_ref(), &[pool.authority_bump]]; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.custody_vault.to_account_info(), - mint: context.accounts.collateral_mint.to_account_info(), - to: context.accounts.provider_collateral.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + from: context.accounts.custody_vault.to_cpi_handle_mut(), + mint: context.accounts.collateral_mint.to_cpi_handle(), + to: context.accounts.provider_collateral.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, &[authority_seeds], ), amount_out, - context.accounts.collateral_mint.decimals, + context.accounts.collateral_mint.decimals(), )?; Ok(()) } #[derive(Accounts)] -pub struct RemoveLiquidityAccountConstraints<'info> { +pub struct RemoveLiquidityAccountConstraints { #[account(mut)] - pub provider: Signer<'info>, + pub provider: Signer, #[account( mut, seeds = [POOL_SEED, pool.collateral_mint.as_ref(), pool.oracle_feed.as_ref()], bump = pool.bump, - has_one = collateral_mint, - has_one = lp_mint, - has_one = custody_vault, - has_one = oracle_feed, )] - pub pool: Box>, + pub pool: Box>, /// CHECK: PDA authority over the vault and liquidity-provider mint. #[account( - seeds = [AUTHORITY_SEED, pool.key().as_ref()], + seeds = [AUTHORITY_SEED, pool.address().as_ref()], bump = pool.authority_bump, )] - pub pool_authority: UncheckedAccount<'info>, + pub pool_authority: UncheckedAccount, - /// CHECK: validated by the `has_one = oracle_feed` constraint on the pool. - pub oracle_feed: UncheckedAccount<'info>, + /// CHECK: validated by the `address = pool.oracle_feed` constraint below. + #[account(address = pool.oracle_feed)] + pub oracle_feed: UncheckedAccount, - pub collateral_mint: Box>, + #[account(address = pool.collateral_mint)] + pub collateral_mint: Box>, - #[account(mut)] - pub lp_mint: Box>, + #[account(mut, address = pool.lp_mint)] + pub lp_mint: Box>, #[account( mut, - seeds = [VAULT_SEED, pool.key().as_ref()], + seeds = [VAULT_SEED, pool.address().as_ref()], bump, + address = pool.custody_vault, )] - pub custody_vault: Box>, + pub custody_vault: Box>, #[account( mut, @@ -132,7 +131,7 @@ pub struct RemoveLiquidityAccountConstraints<'info> { associated_token::authority = provider, associated_token::token_program = token_program, )] - pub provider_collateral: Box>, + pub provider_collateral: Box>, #[account( mut, @@ -140,9 +139,9 @@ pub struct RemoveLiquidityAccountConstraints<'info> { associated_token::authority = provider, associated_token::token_program = token_program, )] - pub provider_lp: Box>, + pub provider_lp: Box>, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/shared.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/shared.rs index e8e322092..2fb9ae7f3 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/shared.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/shared.rs @@ -213,7 +213,7 @@ pub fn basis_points_of(amount: u64, basis_points: u16) -> Result { /// price, then bring the pool's funding index up to the current slot, so the /// settlement that follows uses fresh numbers for both. Centralized so no /// handler can settle a position against a stale funding index. -pub fn refresh_price_and_funding(pool: &mut Pool, oracle_feed: &AccountInfo) -> Result { +pub fn refresh_price_and_funding(pool: &mut Pool, oracle_feed: &AccountView) -> Result { let price = crate::state::oracle::read_oracle_price( oracle_feed, pool.oracle_scale, diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/last_restart.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/last_restart.rs new file mode 100644 index 000000000..a341431f0 --- /dev/null +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/last_restart.rs @@ -0,0 +1,45 @@ +//! The LastRestartSlot sysvar: the slot of the most recent cluster restart, +//! or 0 if the cluster has never restarted (SIMD-0047). anchor-lang v2 is +//! built on pinocchio, which ships only the Clock and Rent sysvars, so this +//! program declares the 8-byte layout itself and reads it through +//! `pinocchio::sysvars::get_sysvar`, the same syscall wrapper pinocchio's own +//! sysvars use. (`solana-sysvar`'s `LastRestartSlot::get` is not usable here: +//! it is bound to that crate's `Sysvar` trait, not pinocchio's.) +//! +//! Why the program reads it: a halt stops the slot count but not the wall +//! clock, so after a restart an oracle price can look fresh in slots while +//! its value is hours old. `read_oracle_price` rejects any price stamped at +//! or before the restart slot, so the pool pauses valuation until the +//! publisher posts again. + +use anchor_lang::prelude::*; + +/// `SysvarLastRestartS1ot1111111111111111111111`, decoded at compile time. +const LAST_RESTART_SLOT_ID: Address = + anchor_lang::address!("SysvarLastRestartS1ot1111111111111111111111"); + +/// The sysvar's whole data: one little-endian u64. +#[repr(C)] +#[derive(Copy, Clone)] +pub struct LastRestartSlot { + pub last_restart_slot: [u8; 8], +} + +const _: () = assert!(core::mem::size_of::() == 8); + +impl LastRestartSlot { + /// Slot of the most recent cluster restart, 0 if there has never been one. + pub fn last_restart_slot(&self) -> u64 { + u64::from_le_bytes(self.last_restart_slot) + } + + pub fn get() -> Result { + // `pinocchio::sysvars::get_sysvar` is the safe wrapper over the + // `sol_get_sysvar` syscall. Off-chain (IDL builds, client compilation) + // it is a no-op that leaves the buffer zeroed, which reads as "the + // cluster has never restarted". + let mut last_restart_slot = [0u8; 8]; + pinocchio::sysvars::get_sysvar(&mut last_restart_slot, &LAST_RESTART_SLOT_ID, 0)?; + Ok(Self { last_restart_slot }) + } +} diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/lib.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/lib.rs index 2923d6a16..8d263a055 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/lib.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/lib.rs @@ -2,6 +2,7 @@ use anchor_lang::prelude::*; mod constants; mod errors; +mod last_restart; // Public so the LiteSVM integration tests can build instruction arguments // (`PoolParameters`, `Side`) against the program's own types. pub mod instructions; @@ -20,7 +21,7 @@ pub mod perpetual_futures { /// oracle feed. Sets the trading parameters and creates the custody vault /// and liquidity-provider mint. pub fn initialize_pool( - context: Context, + context: &mut Context, parameters: PoolParameters, ) -> Result<()> { instructions::handle_initialize_pool(context, parameters) @@ -29,7 +30,7 @@ pub mod perpetual_futures { /// Deposit collateral into the pool and receive liquidity-provider shares. /// `minimum_shares_out` is slippage protection; pass `0` to opt out. pub fn add_liquidity( - context: Context, + context: &mut Context, amount: u64, minimum_shares_out: u64, ) -> Result<()> { @@ -39,7 +40,7 @@ pub mod perpetual_futures { /// Burn liquidity-provider shares and withdraw the matching collateral. /// `minimum_amount_out` is slippage protection; pass `0` to opt out. pub fn remove_liquidity( - context: Context, + context: &mut Context, shares: u64, minimum_amount_out: u64, ) -> Result<()> { @@ -50,7 +51,7 @@ pub mod perpetual_futures { /// oracle price. `acceptable_price` bounds the fill (longs reject above it, /// shorts reject below it); pass `0` to opt out. pub fn open_position( - context: Context, + context: &mut Context, side: Side, collateral_amount: u64, size: u64, @@ -63,7 +64,7 @@ pub mod perpetual_futures { /// and the close fee. `minimum_payout` is slippage protection; pass `0` to /// opt out. pub fn close_position( - context: Context, + context: &mut Context, minimum_payout: u64, ) -> Result<()> { instructions::handle_close_position(context, minimum_payout) @@ -71,12 +72,14 @@ pub mod perpetual_futures { /// Permissionlessly close a position whose equity has fallen to or below /// the maintenance margin. The caller earns the liquidation fee. - pub fn liquidate_position(context: Context) -> Result<()> { + pub fn liquidate_position( + context: &mut Context, + ) -> Result<()> { instructions::handle_liquidate_position(context) } /// Pool authority sweeps the accumulated protocol fees from the vault. - pub fn collect_fees(context: Context) -> Result<()> { + pub fn collect_fees(context: &mut Context) -> Result<()> { instructions::handle_collect_fees(context) } diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/oracle.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/oracle.rs index e05b7ce32..04b0b5057 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/oracle.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/oracle.rs @@ -1,12 +1,12 @@ +use crate::last_restart::LastRestartSlot; use anchor_lang::prelude::*; -use solana_sysvar::last_restart_slot::LastRestartSlot; use crate::constants::{BASIS_POINTS_DENOMINATOR, MAX_PRICE_STALENESS_SLOTS}; use crate::errors::PerpError; // Byte layout of the feed account this program reads. It matches the // `mock_switchboard::MockFeed` account: an 8-byte Anchor discriminator followed -// by `authority: Pubkey (32)`, `price: i128 (16)`, `scale: u32 (4)`, +// by `authority: Address (32)`, `price: i128 (16)`, `scale: u32 (4)`, // `last_update_slot: u64 (8)`, `confidence: u64 (8)`. // // We read the raw bytes rather than deserializing the mock account type so this @@ -40,11 +40,11 @@ const FEED_MINIMUM_LENGTH: usize = CONFIDENCE_OFFSET + 8; /// non-positive price, a feed whose scale differs from the pool's pinned scale, /// and a price whose confidence band exceeds `max_confidence_bps` of the price. pub fn read_oracle_price( - feed: &AccountInfo, + feed: &AccountView, expected_scale: u32, max_confidence_bps: u16, ) -> Result { - let data = feed.try_borrow_data()?; + let data = feed.try_borrow()?; require!( data.len() >= FEED_MINIMUM_LENGTH, PerpError::OracleDataTooShort @@ -88,7 +88,7 @@ pub fn read_oracle_price( // market-wide equity error, so reject any price stamped at or before the // restart slot; the pool pauses valuation until the publisher posts // again. Zero means the cluster has never restarted. - let last_restart_slot = LastRestartSlot::get()?.last_restart_slot; + let last_restart_slot = LastRestartSlot::get()?.last_restart_slot(); require!( last_restart_slot == 0 || last_update_slot > last_restart_slot, PerpError::PricePredatesRestart diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/pool.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/pool.rs index eae178c51..4b068a98c 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/pool.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/pool.rs @@ -6,26 +6,26 @@ use anchor_lang::prelude::*; /// /// Money fields are raw base units of the collateral token. The pool never /// assumes decimals — `transfer_checked` carries them through every CPI. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Pool { /// Admin: configures the pool and sweeps protocol fees. Not a custody /// escape hatch — it cannot touch liquidity-provider or trader funds. - pub authority: Pubkey, + pub authority: Address, - pub collateral_mint: Pubkey, + pub collateral_mint: Address, /// Oracle feed this market reads its price from. Stored so handlers can /// reject any substituted feed account. - pub oracle_feed: Pubkey, + pub oracle_feed: Address, /// Decimal places the oracle price is quoted in. Pinned at creation so a /// feed that silently changes scale is rejected rather than mis-read. pub oracle_scale: u32, - pub custody_vault: Pubkey, + pub custody_vault: Address, - pub lp_mint: Pubkey, + pub lp_mint: Address, /// Liquidity-provider-owned assets, in collateral base units. Grows with /// deposits, trader losses, fees-to-LPs; shrinks with withdrawals and diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/position.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/position.rs index 29a0d7080..1f55efe60 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/position.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/position.rs @@ -1,6 +1,8 @@ use anchor_lang::prelude::*; -#[derive(AnchorSerialize, AnchorDeserialize, InitSpace, Clone, Copy, PartialEq, Eq, Debug)] +#[derive( + InitSpace, Clone, Copy, PartialEq, Eq, Debug, IdlType, wincode::SchemaRead, wincode::SchemaWrite, +)] pub enum Side { Long, Short, @@ -19,12 +21,12 @@ impl Side { } /// A single trader's leveraged position. One PDA per (pool, owner, side). -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Position { - pub owner: Pubkey, + pub owner: Address, - pub pool: Pubkey, + pub pool: Address, pub side: Side, diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/tests/test_perpetual_futures.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/tests/test_perpetual_futures.rs index 0610d1df0..6d7bf79c3 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/tests/test_perpetual_futures.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/tests/test_perpetual_futures.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - AccountDeserialize, InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, AccountDeserialize, Address, + InstructionData, ToAccountMetas, }, litesvm::LiteSVM, perpetual_futures::{instructions::initialize_pool::PoolParameters, state::Pool, state::Side}, @@ -22,20 +22,20 @@ const DECIMALS: u8 = 6; // The oracle quotes prices with 8 decimals, so $100 is 100 * 10^8. const ORACLE_SCALE: u32 = 8; -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn ata_program_id() -> Pubkey { +fn ata_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - Pubkey::find_program_address( +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + Address::find_program_address( &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &ata_program_id(), ) @@ -52,12 +52,12 @@ struct Market { svm: LiteSVM, payer: Keypair, admin: Keypair, - collateral_mint: Pubkey, - feed: Pubkey, - pool: Pubkey, - pool_authority: Pubkey, - lp_mint: Pubkey, - custody_vault: Pubkey, + collateral_mint: Address, + feed: Address, + pool: Address, + pool_authority: Address, + lp_mint: Address, + custody_vault: Address, } impl Market { @@ -97,7 +97,8 @@ impl Market { "/../../target/deploy/mock_switchboard.so" )) .expect("mock_switchboard.so not found - run `anchor build` first"); - svm.add_program(mock_switchboard::id(), &switchboard_bytes).unwrap(); + svm.add_program(mock_switchboard::id(), &switchboard_bytes) + .unwrap(); let payer = create_wallet(&mut svm, 100_000_000_000).unwrap(); let admin = create_wallet(&mut svm, 100_000_000_000).unwrap(); @@ -117,7 +118,7 @@ impl Market { mock_switchboard::accounts::InitializeFeedAccountConstraints { feed: feed_keypair.pubkey(), authority: admin.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -130,18 +131,18 @@ impl Market { .unwrap(); let feed = feed_keypair.pubkey(); - let pool = Pubkey::find_program_address( + let pool = Address::find_program_address( &[b"pool", collateral_mint.as_ref(), feed.as_ref()], &perpetual_futures::id(), ) .0; let pool_authority = - Pubkey::find_program_address(&[b"authority", pool.as_ref()], &perpetual_futures::id()) + Address::find_program_address(&[b"authority", pool.as_ref()], &perpetual_futures::id()) .0; let lp_mint = - Pubkey::find_program_address(&[b"lp_mint", pool.as_ref()], &perpetual_futures::id()).0; + Address::find_program_address(&[b"lp_mint", pool.as_ref()], &perpetual_futures::id()).0; let custody_vault = - Pubkey::find_program_address(&[b"vault", pool.as_ref()], &perpetual_futures::id()).0; + Address::find_program_address(&[b"vault", pool.as_ref()], &perpetual_futures::id()).0; let initialize_pool = Instruction::new_with_bytes( perpetual_futures::id(), @@ -156,7 +157,7 @@ impl Market { custody_vault, token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -220,20 +221,21 @@ impl Market { } fn current_slot(&self) -> u64 { - self.svm.get_sysvar::().slot + self.svm.get_sysvar::().slot } /// Simulate a cluster restart at `slot`: prices stamped at or before it /// must be rejected until the publisher posts again. fn set_last_restart_slot(&mut self, slot: u64) { - self.svm.set_sysvar(&solana_sysvar::last_restart_slot::LastRestartSlot { - last_restart_slot: slot, - }); + self.svm + .set_sysvar(&solana_sysvar::last_restart_slot::LastRestartSlot { + last_restart_slot: slot, + }); } /// Create a wallet holding `amount` collateral tokens in its associated /// token account. - fn funded_trader(&mut self, amount: u64) -> (Keypair, Pubkey) { + fn funded_trader(&mut self, amount: u64) -> (Keypair, Address) { let trader = create_wallet(&mut self.svm, 100_000_000_000).unwrap(); let token_account = create_associated_token_account( &mut self.svm, @@ -256,7 +258,7 @@ impl Market { fn add_liquidity( &mut self, provider: &Keypair, - provider_collateral: Pubkey, + provider_collateral: Address, amount: u64, minimum_shares_out: u64, ) -> Result<(), ()> { @@ -280,7 +282,7 @@ impl Market { provider_lp, token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -297,7 +299,7 @@ impl Market { fn remove_liquidity( &mut self, provider: &Keypair, - provider_collateral: Pubkey, + provider_collateral: Address, shares: u64, minimum_amount_out: u64, ) -> Result<(), ()> { @@ -321,7 +323,7 @@ impl Market { provider_lp, token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -335,12 +337,12 @@ impl Market { .map_err(|_| ()) } - fn position_pda(&self, owner: &Pubkey, side: Side) -> Pubkey { + fn position_pda(&self, owner: &Address, side: Side) -> Address { let side_seed: &[u8] = match side { Side::Long => b"long", Side::Short => b"short", }; - Pubkey::find_program_address( + Address::find_program_address( &[b"position", self.pool.as_ref(), owner.as_ref(), side_seed], &perpetual_futures::id(), ) @@ -350,7 +352,7 @@ impl Market { fn open_position( &mut self, trader: &Keypair, - trader_collateral: Pubkey, + trader_collateral: Address, side: Side, collateral_amount: u64, size: u64, @@ -376,7 +378,7 @@ impl Market { trader_collateral, token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -393,7 +395,7 @@ impl Market { fn close_position( &mut self, trader: &Keypair, - trader_collateral: Pubkey, + trader_collateral: Address, side: Side, minimum_payout: u64, ) -> Result<(), ()> { @@ -412,7 +414,7 @@ impl Market { trader_collateral, token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -429,8 +431,8 @@ impl Market { fn liquidate( &mut self, liquidator: &Keypair, - owner: &Pubkey, - owner_collateral: Pubkey, + owner: &Address, + owner_collateral: Address, side: Side, ) -> Result<(), ()> { let position = self.position_pda(owner, side); @@ -451,7 +453,7 @@ impl Market { liquidator_collateral, token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -479,7 +481,7 @@ impl Market { authority_collateral, token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -518,7 +520,7 @@ impl Market { /// Deposit a large amount of liquidity so the pool can pay trader profits, /// returning the provider and its collateral account. - fn seed_liquidity(&mut self, amount: u64) -> (Keypair, Pubkey) { + fn seed_liquidity(&mut self, amount: u64) -> (Keypair, Address) { let (provider, provider_collateral) = self.funded_trader(amount); self.add_liquidity(&provider, provider_collateral, amount, 0) .unwrap(); @@ -847,7 +849,7 @@ fn test_open_rejects_price_from_before_a_restart() { Side::Long, collateral, 5_000 * ONE_USDC, - u64::MAX + u64::MAX, ) .expect("a freshly published price must be accepted after a restart"); } diff --git a/finance/prop-amm/anchor/programs/mock-switchboard/Cargo.toml b/finance/prop-amm/anchor/programs/mock-switchboard/Cargo.toml index 5374a001c..047c5c133 100644 --- a/finance/prop-amm/anchor/programs/mock-switchboard/Cargo.toml +++ b/finance/prop-amm/anchor/programs/mock-switchboard/Cargo.toml @@ -20,7 +20,15 @@ custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/finance/prop-amm/anchor/programs/mock-switchboard/src/lib.rs b/finance/prop-amm/anchor/programs/mock-switchboard/src/lib.rs index 66d29a659..03ae2006a 100644 --- a/finance/prop-amm/anchor/programs/mock-switchboard/src/lib.rs +++ b/finance/prop-amm/anchor/programs/mock-switchboard/src/lib.rs @@ -24,13 +24,13 @@ pub mod mock_switchboard { /// Initialize the mock feed with an initial price. The signer becomes the /// authority allowed to push later price updates. pub fn initialize_feed( - context: Context, + context: &mut Context, price: i128, scale: u32, confidence: u64, ) -> Result<()> { let feed = &mut context.accounts.feed; - feed.authority = context.accounts.authority.key(); + feed.authority = *context.accounts.authority.address(); feed.price = price; feed.scale = scale; feed.last_update_slot = Clock::get()?.slot; @@ -43,7 +43,7 @@ pub mod mock_switchboard { /// is an authority-gated write, because the goal is to drive deterministic /// test scenarios. pub fn set_price( - context: Context, + context: &mut Context, price: i128, confidence: u64, ) -> Result<()> { @@ -56,38 +56,36 @@ pub mod mock_switchboard { } #[derive(Accounts)] -pub struct InitializeFeedAccountConstraints<'info> { +pub struct InitializeFeedAccountConstraints { #[account( init, payer = authority, space = MockFeed::DISCRIMINATOR.len() + MockFeed::INIT_SPACE, )] - pub feed: Account<'info, MockFeed>, + pub feed: BorshAccount, #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, - pub system_program: Program<'info, System>, + pub system_program: Program, } #[derive(Accounts)] -pub struct SetPriceAccountConstraints<'info> { - #[account( - mut, - has_one = authority, - )] - pub feed: Account<'info, MockFeed>, +pub struct SetPriceAccountConstraints { + #[account(mut)] + pub feed: BorshAccount, - pub authority: Signer<'info>, + #[account(address = feed.authority)] + pub authority: Signer, } /// Mock of a Switchboard On-Demand feed. Real feeds carry many more fields /// (median, range, sample window, signatures) — this is the bare minimum the /// prop-amm program needs to price a quote. #[derive(InitSpace)] -#[account] +#[account(borsh)] pub struct MockFeed { - pub authority: Pubkey, + pub authority: Address, /// Signed 128-bit fixed-point price. Real Switchboard prices are also i128. pub price: i128, diff --git a/finance/prop-amm/anchor/programs/prop-amm/Cargo.toml b/finance/prop-amm/anchor/programs/prop-amm/Cargo.toml index fb8a4f9a5..1d25f29d9 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/Cargo.toml +++ b/finance/prop-amm/anchor/programs/prop-amm/Cargo.toml @@ -14,7 +14,7 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] @@ -22,8 +22,20 @@ custom-panic = [] [dependencies] # init-if-needed: swap creates the trader's destination token account when it # doesn't exist yet, so a first-time buyer needs no separate setup transaction. -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# `last_restart.rs` reads the LastRestartSlot sysvar through pinocchio's +# `get_sysvar` wrapper: pinocchio (and so anchor-lang v2) ships only the +# Clock and Rent sysvars as typed accessors. +pinocchio = "0.11" +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" # For the LastRestartSlot sysvar (not re-exported by anchor-lang): the oracle # reader rejects prices from before a cluster restart. Same major as the # solana-sysvar anchor-lang itself uses, so only one copy is compiled in. @@ -35,6 +47,7 @@ spl-associated-token-account = { version = "8.0.0", features = ["no-entrypoint"] [dev-dependencies] litesvm = "0.13.1" +solana-clock = "3.0.1" solana-signer = "3.0.0" solana-keypair = "3.0.1" solana-kite = "0.4.0" diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/deposit_inventory.rs b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/deposit_inventory.rs index 7cd7b6817..d2d7fe8e4 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/deposit_inventory.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/deposit_inventory.rs @@ -8,7 +8,7 @@ use crate::errors::PropAmmError; use crate::state::Market; pub fn handle_deposit_inventory( - context: Context, + context: &mut Context, base_amount: u64, quote_amount: u64, ) -> Result<()> { @@ -20,32 +20,32 @@ pub fn handle_deposit_inventory( if base_amount > 0 { transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.operator_base.to_account_info(), - mint: context.accounts.base_mint.to_account_info(), - to: context.accounts.base_vault.to_account_info(), - authority: context.accounts.operator.to_account_info(), + from: context.accounts.operator_base.to_cpi_handle_mut(), + mint: context.accounts.base_mint.to_cpi_handle(), + to: context.accounts.base_vault.to_cpi_handle_mut(), + authority: context.accounts.operator.cpi_handle(), }, ), base_amount, - context.accounts.base_mint.decimals, + context.accounts.base_mint.decimals(), )?; } if quote_amount > 0 { transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.operator_quote.to_account_info(), - mint: context.accounts.quote_mint.to_account_info(), - to: context.accounts.quote_vault.to_account_info(), - authority: context.accounts.operator.to_account_info(), + from: context.accounts.operator_quote.to_cpi_handle_mut(), + mint: context.accounts.quote_mint.to_cpi_handle(), + to: context.accounts.quote_vault.to_cpi_handle_mut(), + authority: context.accounts.operator.cpi_handle(), }, ), quote_amount, - context.accounts.quote_mint.decimals, + context.accounts.quote_mint.decimals(), )?; } @@ -53,40 +53,39 @@ pub fn handle_deposit_inventory( } #[derive(Accounts)] -pub struct DepositInventoryAccountConstraints<'info> { - // `has_one = operator` on the market is the whole access control: only the +pub struct DepositInventoryAccountConstraints { + // `address = market.operator` on the operator is the whole access control: only the // firm's key can stock the market. - #[account(mut)] - pub operator: Signer<'info>, + #[account(mut, address = market.operator)] + pub operator: Signer, #[account( seeds = [MARKET_SEED, market.base_mint.as_ref(), market.quote_mint.as_ref()], bump = market.bump, - has_one = operator, - has_one = base_mint, - has_one = quote_mint, - has_one = base_vault, - has_one = quote_vault, )] - pub market: Box>, + pub market: Box>, - pub base_mint: Box>, + #[account(address = market.base_mint)] + pub base_mint: Box>, - pub quote_mint: Box>, + #[account(address = market.quote_mint)] + pub quote_mint: Box>, #[account( mut, - seeds = [BASE_VAULT_SEED, market.key().as_ref()], + seeds = [BASE_VAULT_SEED, market.address().as_ref()], bump, + address = market.base_vault, )] - pub base_vault: Box>, + pub base_vault: Box>, #[account( mut, - seeds = [QUOTE_VAULT_SEED, market.key().as_ref()], + seeds = [QUOTE_VAULT_SEED, market.address().as_ref()], bump, + address = market.quote_vault, )] - pub quote_vault: Box>, + pub quote_vault: Box>, #[account( mut, @@ -94,7 +93,7 @@ pub struct DepositInventoryAccountConstraints<'info> { associated_token::authority = operator, associated_token::token_program = token_program, )] - pub operator_base: Box>, + pub operator_base: Box>, #[account( mut, @@ -102,7 +101,7 @@ pub struct DepositInventoryAccountConstraints<'info> { associated_token::authority = operator, associated_token::token_program = token_program, )] - pub operator_quote: Box>, + pub operator_quote: Box>, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/initialize_market.rs b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/initialize_market.rs index 056fc96ac..592c8f9ff 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/initialize_market.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/initialize_market.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::{ associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface}, @@ -12,7 +13,7 @@ use crate::state::Market; /// Quote parameters set at market creation. Bundled into one struct so the /// instruction signature stays readable. -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +#[derive(Clone, IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub struct MarketParameters { /// Decimal places the oracle quotes its price in (e.g. 8). pub oracle_scale: u32, @@ -25,14 +26,14 @@ pub struct MarketParameters { } pub fn handle_initialize_market( - context: Context, + context: &mut Context, parameters: MarketParameters, ) -> Result<()> { let denominator = BASIS_POINTS_DENOMINATOR as u16; // A market quoting the same token against itself prices nothing. require_keys_neq!( - context.accounts.base_mint.key(), - context.accounts.quote_mint.key(), + context.accounts.base_mint.address(), + context.accounts.quote_mint.address(), PropAmmError::InvalidParameter ); // Zero spread means quoting the oracle price for free while paying adverse @@ -51,15 +52,15 @@ pub fn handle_initialize_market( ); let market = &mut context.accounts.market; - market.operator = context.accounts.operator.key(); - market.base_mint = context.accounts.base_mint.key(); - market.quote_mint = context.accounts.quote_mint.key(); - market.oracle_feed = context.accounts.oracle_feed.key(); - market.base_vault = context.accounts.base_vault.key(); - market.quote_vault = context.accounts.quote_vault.key(); + market.operator = *context.accounts.operator.address(); + market.base_mint = *context.accounts.base_mint.address(); + market.quote_mint = *context.accounts.quote_mint.address(); + market.oracle_feed = *context.accounts.oracle_feed.address(); + market.base_vault = *context.accounts.base_vault.address(); + market.quote_vault = *context.accounts.quote_vault.address(); market.oracle_scale = parameters.oracle_scale; - market.base_decimals = context.accounts.base_mint.decimals; - market.quote_decimals = context.accounts.quote_mint.decimals; + market.base_decimals = context.accounts.base_mint.decimals(); + market.quote_decimals = context.accounts.quote_mint.decimals(); market.spread_bps = parameters.spread_bps; market.max_confidence_bps = parameters.max_confidence_bps; market.paused = false; @@ -70,9 +71,9 @@ pub fn handle_initialize_market( } #[derive(Accounts)] -pub struct InitializeMarketAccountConstraints<'info> { +pub struct InitializeMarketAccountConstraints { #[account(mut)] - pub operator: Signer<'info>, + pub operator: Signer, // One market per pair: the deployment IS the firm. A real prop AMM is a // closed program deployed by the market-making firm itself, so there is no @@ -81,51 +82,51 @@ pub struct InitializeMarketAccountConstraints<'info> { init, payer = operator, space = Market::DISCRIMINATOR.len() + Market::INIT_SPACE, - seeds = [MARKET_SEED, base_mint.key().as_ref(), quote_mint.key().as_ref()], + seeds = [MARKET_SEED, base_mint.address().as_ref(), quote_mint.address().as_ref()], bump, )] - pub market: Box>, + pub market: Box>, - pub base_mint: Box>, + pub base_mint: Box>, - pub quote_mint: Box>, + pub quote_mint: Box>, /// CHECK: The oracle feed account. Its key is stored on the market and /// every read validates the layout, scale, and freshness; it is never /// trusted by type. Swap for a real Switchboard feed in production. - pub oracle_feed: UncheckedAccount<'info>, + pub oracle_feed: UncheckedAccount, /// CHECK: PDA that owns both vaults. Holds no data; used only to sign /// vault CPIs. #[account( - seeds = [AUTHORITY_SEED, market.key().as_ref()], + seeds = [AUTHORITY_SEED, market.address().as_ref()], bump, )] - pub market_authority: UncheckedAccount<'info>, + pub market_authority: UncheckedAccount, #[account( init, payer = operator, - seeds = [BASE_VAULT_SEED, market.key().as_ref()], + seeds = [BASE_VAULT_SEED, market.address().as_ref()], bump, token::mint = base_mint, token::authority = market_authority, token::token_program = token_program, )] - pub base_vault: Box>, + pub base_vault: Box>, #[account( init, payer = operator, - seeds = [QUOTE_VAULT_SEED, market.key().as_ref()], + seeds = [QUOTE_VAULT_SEED, market.address().as_ref()], bump, token::mint = quote_mint, token::authority = market_authority, token::token_program = token_program, )] - pub quote_vault: Box>, + pub quote_vault: Box>, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/set_quote.rs b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/set_quote.rs index 0b00ce17c..265bfb31e 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/set_quote.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/set_quote.rs @@ -12,7 +12,7 @@ use crate::state::Market; /// widen the spread or stop quoting entirely. Onchain prop AMMs do exactly /// this — during fast markets their quotes vanish and return minutes later. pub fn handle_set_quote( - context: Context, + context: &mut Context, spread_bps: u16, paused: bool, ) -> Result<()> { @@ -31,14 +31,14 @@ pub fn handle_set_quote( } #[derive(Accounts)] -pub struct SetQuoteAccountConstraints<'info> { - pub operator: Signer<'info>, +pub struct SetQuoteAccountConstraints { + #[account(address = market.operator)] + pub operator: Signer, #[account( mut, seeds = [MARKET_SEED, market.base_mint.as_ref(), market.quote_mint.as_ref()], bump = market.bump, - has_one = operator, )] - pub market: Box>, + pub market: Box>, } diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/swap.rs b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/swap.rs index ea0f26f97..30087c746 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/swap.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/swap.rs @@ -18,7 +18,7 @@ use crate::state::{Direction, Market}; /// you — a curve AMM's reserves are its pricing input, a prop AMM's inventory /// is just its ammunition. pub fn handle_swap( - context: Context, + context: &mut Context, direction: Direction, amount_in: u64, minimum_amount_out: u64, @@ -98,12 +98,12 @@ pub fn handle_swap( let (vault_out_balance, out_mint_decimals) = match direction { Direction::BuyBase => ( - context.accounts.base_vault.amount, - context.accounts.base_mint.decimals, + context.accounts.base_vault.amount(), + context.accounts.base_mint.decimals(), ), Direction::SellBase => ( - context.accounts.quote_vault.amount, - context.accounts.quote_mint.decimals, + context.accounts.quote_vault.amount(), + context.accounts.quote_mint.decimals(), ), }; require!( @@ -111,7 +111,7 @@ pub fn handle_swap( PropAmmError::InsufficientInventory ); - let market_key = market.key(); + let market_key = market.address(); let authority_seeds: &[&[u8]] = &[ AUTHORITY_SEED, market_key.as_ref(), @@ -123,25 +123,25 @@ pub fn handle_swap( Direction::BuyBase => { transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.trader_quote.to_account_info(), - mint: context.accounts.quote_mint.to_account_info(), - to: context.accounts.quote_vault.to_account_info(), - authority: context.accounts.trader.to_account_info(), + from: context.accounts.trader_quote.to_cpi_handle_mut(), + mint: context.accounts.quote_mint.to_cpi_handle(), + to: context.accounts.quote_vault.to_cpi_handle_mut(), + authority: context.accounts.trader.cpi_handle(), }, ), amount_in, - context.accounts.quote_mint.decimals, + context.accounts.quote_mint.decimals(), )?; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.base_vault.to_account_info(), - mint: context.accounts.base_mint.to_account_info(), - to: context.accounts.trader_base.to_account_info(), - authority: context.accounts.market_authority.to_account_info(), + from: context.accounts.base_vault.to_cpi_handle_mut(), + mint: context.accounts.base_mint.to_cpi_handle(), + to: context.accounts.trader_base.to_cpi_handle_mut(), + authority: context.accounts.market_authority.cpi_handle(), }, &[authority_seeds], ), @@ -152,25 +152,25 @@ pub fn handle_swap( Direction::SellBase => { transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.trader_base.to_account_info(), - mint: context.accounts.base_mint.to_account_info(), - to: context.accounts.base_vault.to_account_info(), - authority: context.accounts.trader.to_account_info(), + from: context.accounts.trader_base.to_cpi_handle_mut(), + mint: context.accounts.base_mint.to_cpi_handle(), + to: context.accounts.base_vault.to_cpi_handle_mut(), + authority: context.accounts.trader.cpi_handle(), }, ), amount_in, - context.accounts.base_mint.decimals, + context.accounts.base_mint.decimals(), )?; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.quote_vault.to_account_info(), - mint: context.accounts.quote_mint.to_account_info(), - to: context.accounts.trader_quote.to_account_info(), - authority: context.accounts.market_authority.to_account_info(), + from: context.accounts.quote_vault.to_cpi_handle_mut(), + mint: context.accounts.quote_mint.to_cpi_handle(), + to: context.accounts.trader_quote.to_cpi_handle_mut(), + authority: context.accounts.market_authority.cpi_handle(), }, &[authority_seeds], ), @@ -184,48 +184,48 @@ pub fn handle_swap( } #[derive(Accounts)] -pub struct SwapAccountConstraints<'info> { +pub struct SwapAccountConstraints { #[account(mut)] - pub trader: Signer<'info>, + pub trader: Signer, #[account( seeds = [MARKET_SEED, market.base_mint.as_ref(), market.quote_mint.as_ref()], bump = market.bump, - has_one = base_mint, - has_one = quote_mint, - has_one = oracle_feed, - has_one = base_vault, - has_one = quote_vault, )] - pub market: Box>, + pub market: Box>, /// CHECK: PDA authority over both vaults; holds no data, only signs. #[account( - seeds = [AUTHORITY_SEED, market.key().as_ref()], + seeds = [AUTHORITY_SEED, market.address().as_ref()], bump = market.authority_bump, )] - pub market_authority: UncheckedAccount<'info>, + pub market_authority: UncheckedAccount, - /// CHECK: validated by the `has_one = oracle_feed` constraint on the market. - pub oracle_feed: UncheckedAccount<'info>, + /// CHECK: validated by the `address = market.oracle_feed` constraint below. + #[account(address = market.oracle_feed)] + pub oracle_feed: UncheckedAccount, - pub base_mint: Box>, + #[account(address = market.base_mint)] + pub base_mint: Box>, - pub quote_mint: Box>, + #[account(address = market.quote_mint)] + pub quote_mint: Box>, #[account( mut, - seeds = [BASE_VAULT_SEED, market.key().as_ref()], + seeds = [BASE_VAULT_SEED, market.address().as_ref()], bump, + address = market.base_vault, )] - pub base_vault: Box>, + pub base_vault: Box>, #[account( mut, - seeds = [QUOTE_VAULT_SEED, market.key().as_ref()], + seeds = [QUOTE_VAULT_SEED, market.address().as_ref()], bump, + address = market.quote_vault, )] - pub quote_vault: Box>, + pub quote_vault: Box>, #[account( init_if_needed, @@ -234,7 +234,7 @@ pub struct SwapAccountConstraints<'info> { associated_token::authority = trader, associated_token::token_program = token_program, )] - pub trader_base: Box>, + pub trader_base: Box>, #[account( init_if_needed, @@ -243,9 +243,9 @@ pub struct SwapAccountConstraints<'info> { associated_token::authority = trader, associated_token::token_program = token_program, )] - pub trader_quote: Box>, + pub trader_quote: Box>, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/withdraw_inventory.rs b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/withdraw_inventory.rs index 6caa3649c..cc4c7800d 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/withdraw_inventory.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/withdraw_inventory.rs @@ -14,7 +14,7 @@ use crate::state::Market; /// providers. The capital being quoted is the firm's own, so its exit needs no /// waterfall, no share burn, and no pro-rata math. pub fn handle_withdraw_inventory( - context: Context, + context: &mut Context, base_amount: u64, quote_amount: u64, ) -> Result<()> { @@ -23,16 +23,16 @@ pub fn handle_withdraw_inventory( PropAmmError::ZeroAmount ); require!( - base_amount <= context.accounts.base_vault.amount, + base_amount <= context.accounts.base_vault.amount(), PropAmmError::InsufficientInventory ); require!( - quote_amount <= context.accounts.quote_vault.amount, + quote_amount <= context.accounts.quote_vault.amount(), PropAmmError::InsufficientInventory ); let market = &context.accounts.market; - let market_key = market.key(); + let market_key = market.address(); let authority_seeds: &[&[u8]] = &[ AUTHORITY_SEED, market_key.as_ref(), @@ -42,34 +42,34 @@ pub fn handle_withdraw_inventory( if base_amount > 0 { transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.base_vault.to_account_info(), - mint: context.accounts.base_mint.to_account_info(), - to: context.accounts.operator_base.to_account_info(), - authority: context.accounts.market_authority.to_account_info(), + from: context.accounts.base_vault.to_cpi_handle_mut(), + mint: context.accounts.base_mint.to_cpi_handle(), + to: context.accounts.operator_base.to_cpi_handle_mut(), + authority: context.accounts.market_authority.cpi_handle(), }, &[authority_seeds], ), base_amount, - context.accounts.base_mint.decimals, + context.accounts.base_mint.decimals(), )?; } if quote_amount > 0 { transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.quote_vault.to_account_info(), - mint: context.accounts.quote_mint.to_account_info(), - to: context.accounts.operator_quote.to_account_info(), - authority: context.accounts.market_authority.to_account_info(), + from: context.accounts.quote_vault.to_cpi_handle_mut(), + mint: context.accounts.quote_mint.to_cpi_handle(), + to: context.accounts.operator_quote.to_cpi_handle_mut(), + authority: context.accounts.market_authority.cpi_handle(), }, &[authority_seeds], ), quote_amount, - context.accounts.quote_mint.decimals, + context.accounts.quote_mint.decimals(), )?; } @@ -77,45 +77,44 @@ pub fn handle_withdraw_inventory( } #[derive(Accounts)] -pub struct WithdrawInventoryAccountConstraints<'info> { - #[account(mut)] - pub operator: Signer<'info>, +pub struct WithdrawInventoryAccountConstraints { + #[account(mut, address = market.operator)] + pub operator: Signer, #[account( seeds = [MARKET_SEED, market.base_mint.as_ref(), market.quote_mint.as_ref()], bump = market.bump, - has_one = operator, - has_one = base_mint, - has_one = quote_mint, - has_one = base_vault, - has_one = quote_vault, )] - pub market: Box>, + pub market: Box>, /// CHECK: PDA authority over both vaults; holds no data, only signs. #[account( - seeds = [AUTHORITY_SEED, market.key().as_ref()], + seeds = [AUTHORITY_SEED, market.address().as_ref()], bump = market.authority_bump, )] - pub market_authority: UncheckedAccount<'info>, + pub market_authority: UncheckedAccount, - pub base_mint: Box>, + #[account(address = market.base_mint)] + pub base_mint: Box>, - pub quote_mint: Box>, + #[account(address = market.quote_mint)] + pub quote_mint: Box>, #[account( mut, - seeds = [BASE_VAULT_SEED, market.key().as_ref()], + seeds = [BASE_VAULT_SEED, market.address().as_ref()], bump, + address = market.base_vault, )] - pub base_vault: Box>, + pub base_vault: Box>, #[account( mut, - seeds = [QUOTE_VAULT_SEED, market.key().as_ref()], + seeds = [QUOTE_VAULT_SEED, market.address().as_ref()], bump, + address = market.quote_vault, )] - pub quote_vault: Box>, + pub quote_vault: Box>, #[account( mut, @@ -123,7 +122,7 @@ pub struct WithdrawInventoryAccountConstraints<'info> { associated_token::authority = operator, associated_token::token_program = token_program, )] - pub operator_base: Box>, + pub operator_base: Box>, #[account( mut, @@ -131,7 +130,7 @@ pub struct WithdrawInventoryAccountConstraints<'info> { associated_token::authority = operator, associated_token::token_program = token_program, )] - pub operator_quote: Box>, + pub operator_quote: Box>, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/last_restart.rs b/finance/prop-amm/anchor/programs/prop-amm/src/last_restart.rs new file mode 100644 index 000000000..a341431f0 --- /dev/null +++ b/finance/prop-amm/anchor/programs/prop-amm/src/last_restart.rs @@ -0,0 +1,45 @@ +//! The LastRestartSlot sysvar: the slot of the most recent cluster restart, +//! or 0 if the cluster has never restarted (SIMD-0047). anchor-lang v2 is +//! built on pinocchio, which ships only the Clock and Rent sysvars, so this +//! program declares the 8-byte layout itself and reads it through +//! `pinocchio::sysvars::get_sysvar`, the same syscall wrapper pinocchio's own +//! sysvars use. (`solana-sysvar`'s `LastRestartSlot::get` is not usable here: +//! it is bound to that crate's `Sysvar` trait, not pinocchio's.) +//! +//! Why the program reads it: a halt stops the slot count but not the wall +//! clock, so after a restart an oracle price can look fresh in slots while +//! its value is hours old. `read_oracle_price` rejects any price stamped at +//! or before the restart slot, so the pool pauses valuation until the +//! publisher posts again. + +use anchor_lang::prelude::*; + +/// `SysvarLastRestartS1ot1111111111111111111111`, decoded at compile time. +const LAST_RESTART_SLOT_ID: Address = + anchor_lang::address!("SysvarLastRestartS1ot1111111111111111111111"); + +/// The sysvar's whole data: one little-endian u64. +#[repr(C)] +#[derive(Copy, Clone)] +pub struct LastRestartSlot { + pub last_restart_slot: [u8; 8], +} + +const _: () = assert!(core::mem::size_of::() == 8); + +impl LastRestartSlot { + /// Slot of the most recent cluster restart, 0 if there has never been one. + pub fn last_restart_slot(&self) -> u64 { + u64::from_le_bytes(self.last_restart_slot) + } + + pub fn get() -> Result { + // `pinocchio::sysvars::get_sysvar` is the safe wrapper over the + // `sol_get_sysvar` syscall. Off-chain (IDL builds, client compilation) + // it is a no-op that leaves the buffer zeroed, which reads as "the + // cluster has never restarted". + let mut last_restart_slot = [0u8; 8]; + pinocchio::sysvars::get_sysvar(&mut last_restart_slot, &LAST_RESTART_SLOT_ID, 0)?; + Ok(Self { last_restart_slot }) + } +} diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/lib.rs b/finance/prop-amm/anchor/programs/prop-amm/src/lib.rs index 0706dfb43..d89023ea2 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/src/lib.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/src/lib.rs @@ -2,6 +2,7 @@ use anchor_lang::prelude::*; mod constants; mod errors; +mod last_restart; // Public so the LiteSVM integration tests can build instruction arguments // (`MarketParameters`, `Direction`) against the program's own types. pub mod instructions; @@ -31,7 +32,7 @@ pub mod prop_amm { /// The signer becomes the market's operator: the only party allowed to /// move inventory or change the quote. pub fn initialize_market( - context: Context, + context: &mut Context, parameters: MarketParameters, ) -> Result<()> { instructions::handle_initialize_market(context, parameters) @@ -40,7 +41,7 @@ pub mod prop_amm { /// Operator moves inventory into the market's vaults. Either amount may be /// zero, but not both. pub fn deposit_inventory( - context: Context, + context: &mut Context, base_amount: u64, quote_amount: u64, ) -> Result<()> { @@ -51,7 +52,7 @@ pub mod prop_amm { /// it, at any time. The capital is the operator's own; nobody else has a /// claim on it. pub fn withdraw_inventory( - context: Context, + context: &mut Context, base_amount: u64, quote_amount: u64, ) -> Result<()> { @@ -62,7 +63,7 @@ pub mod prop_amm { /// during volatility is not an emergency measure for a market maker; it is /// Tuesday. pub fn set_quote( - context: Context, + context: &mut Context, spread_bps: u16, paused: bool, ) -> Result<()> { @@ -73,7 +74,7 @@ pub mod prop_amm { /// spread, or sell it at oracle minus spread. Permissionless. /// `minimum_amount_out` is slippage protection; pass `0` to opt out. pub fn swap( - context: Context, + context: &mut Context, direction: Direction, amount_in: u64, minimum_amount_out: u64, diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/quote_math.rs b/finance/prop-amm/anchor/programs/prop-amm/src/quote_math.rs index 849e1c303..ef6c689c0 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/src/quote_math.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/src/quote_math.rs @@ -67,8 +67,9 @@ pub fn quote_out_for_base_in( let numerator = (base_in as u128) .checked_mul(bid)? .checked_mul(10u128.checked_pow(quote_decimals as u32)?)?; - let denominator = - 10u128.checked_pow(oracle_scale)?.checked_mul(10u128.checked_pow(base_decimals as u32)?)?; + let denominator = 10u128 + .checked_pow(oracle_scale)? + .checked_mul(10u128.checked_pow(base_decimals as u32)?)?; if denominator == 0 { return None; } diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/state/market.rs b/finance/prop-amm/anchor/programs/prop-amm/src/state/market.rs index 8aac7c7b0..96927d0e6 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/src/state/market.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/src/state/market.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; /// Which side of the quote a swap takes. -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub enum Direction { /// Spend the quote token, receive the base token, priced at the ask /// (oracle plus spread). @@ -19,25 +19,25 @@ pub enum Direction { /// on. The price comes from the oracle; the vault balances only bound how much /// of a fill is possible. And because nobody but the operator has a claim on /// the vaults, the token balances themselves are the complete accounting. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Market { /// The market-making firm. Deposits and withdraws inventory, sets the /// spread, pauses quoting. Cannot touch anyone else's funds, because the /// market never holds anyone else's funds. - pub operator: Pubkey, + pub operator: Address, - pub base_mint: Pubkey, + pub base_mint: Address, - pub quote_mint: Pubkey, + pub quote_mint: Address, /// Oracle feed this market quotes from. Stored so handlers can reject any /// substituted feed account. - pub oracle_feed: Pubkey, + pub oracle_feed: Address, - pub base_vault: Pubkey, + pub base_vault: Address, - pub quote_vault: Pubkey, + pub quote_vault: Address, /// Decimal places the oracle price is quoted in. Pinned at creation so a /// feed that silently changes scale is rejected rather than mis-read. diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/state/oracle.rs b/finance/prop-amm/anchor/programs/prop-amm/src/state/oracle.rs index 83479816a..f7aba055b 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/src/state/oracle.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/src/state/oracle.rs @@ -1,12 +1,12 @@ +use crate::last_restart::LastRestartSlot; use anchor_lang::prelude::*; -use solana_sysvar::last_restart_slot::LastRestartSlot; use crate::constants::{BASIS_POINTS_DENOMINATOR, MAX_PRICE_STALENESS_SLOTS}; use crate::errors::PropAmmError; // Byte layout of the feed account this program reads. It matches the // `mock_switchboard::MockFeed` account: an 8-byte Anchor discriminator followed -// by `authority: Pubkey (32)`, `price: i128 (16)`, `scale: u32 (4)`, +// by `authority: Address (32)`, `price: i128 (16)`, `scale: u32 (4)`, // `last_update_slot: u64 (8)`, `confidence: u64 (8)`. // // We read the raw bytes rather than deserializing the mock account type so this @@ -42,11 +42,11 @@ const FEED_MINIMUM_LENGTH: usize = CONFIDENCE_OFFSET + 8; /// scale, and a price whose confidence band exceeds `max_confidence_bps` of /// the price. pub fn read_oracle_price( - feed: &AccountInfo, + feed: &AccountView, expected_scale: u32, max_confidence_bps: u16, ) -> Result { - let data = feed.try_borrow_data()?; + let data = feed.try_borrow()?; require!( data.len() >= FEED_MINIMUM_LENGTH, PropAmmError::OracleDataTooShort @@ -90,7 +90,7 @@ pub fn read_oracle_price( // whoever trades first, so reject any price stamped at or before the // restart slot; the market refuses to quote until the publisher posts // again. Zero means the cluster has never restarted. - let last_restart_slot = LastRestartSlot::get()?.last_restart_slot; + let last_restart_slot = LastRestartSlot::get()?.last_restart_slot(); require!( last_restart_slot == 0 || last_update_slot > last_restart_slot, PropAmmError::PricePredatesRestart diff --git a/finance/prop-amm/anchor/programs/prop-amm/tests/test_prop_amm.rs b/finance/prop-amm/anchor/programs/prop-amm/tests/test_prop_amm.rs index 5f10fb4a6..8ec07e9e4 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/tests/test_prop_amm.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/tests/test_prop_amm.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - AccountDeserialize, InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, AccountDeserialize, Address, + InstructionData, ToAccountMetas, }, litesvm::LiteSVM, prop_amm::{ @@ -29,20 +29,20 @@ const ORACLE_SCALE: u32 = 8; const SPREAD_BPS: u16 = 10; const MAX_CONFIDENCE_BPS: u16 = 100; -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn ata_program_id() -> Pubkey { +fn ata_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - Pubkey::find_program_address( +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + Address::find_program_address( &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &ata_program_id(), ) @@ -59,15 +59,15 @@ struct Market { svm: LiteSVM, payer: Keypair, operator: Keypair, - operator_base: Pubkey, - operator_quote: Pubkey, - base_mint: Pubkey, - quote_mint: Pubkey, - feed: Pubkey, - market: Pubkey, - market_authority: Pubkey, - base_vault: Pubkey, - quote_vault: Pubkey, + operator_base: Address, + operator_quote: Address, + base_mint: Address, + quote_mint: Address, + feed: Address, + market: Address, + market_authority: Address, + base_vault: Address, + quote_vault: Address, } impl Market { @@ -123,7 +123,7 @@ impl Market { mock_switchboard::accounts::InitializeFeedAccountConstraints { feed: feed_keypair.pubkey(), authority: operator.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -136,17 +136,17 @@ impl Market { .unwrap(); let feed = feed_keypair.pubkey(); - let market = Pubkey::find_program_address( + let market = Address::find_program_address( &[b"market", base_mint.as_ref(), quote_mint.as_ref()], &prop_amm::id(), ) .0; let market_authority = - Pubkey::find_program_address(&[b"authority", market.as_ref()], &prop_amm::id()).0; + Address::find_program_address(&[b"authority", market.as_ref()], &prop_amm::id()).0; let base_vault = - Pubkey::find_program_address(&[b"base_vault", market.as_ref()], &prop_amm::id()).0; + Address::find_program_address(&[b"base_vault", market.as_ref()], &prop_amm::id()).0; let quote_vault = - Pubkey::find_program_address(&[b"quote_vault", market.as_ref()], &prop_amm::id()).0; + Address::find_program_address(&[b"quote_vault", market.as_ref()], &prop_amm::id()).0; let initialize_market = Instruction::new_with_bytes( prop_amm::id(), @@ -162,7 +162,7 @@ impl Market { quote_vault, token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -175,20 +175,12 @@ impl Market { .map_err(|_| ())?; // Fund the operator's inventory accounts. - let operator_base = create_associated_token_account( - &mut svm, - &operator.pubkey(), - &base_mint, - &payer, - ) - .unwrap(); - let operator_quote = create_associated_token_account( - &mut svm, - &operator.pubkey(), - "e_mint, - &payer, - ) - .unwrap(); + let operator_base = + create_associated_token_account(&mut svm, &operator.pubkey(), &base_mint, &payer) + .unwrap(); + let operator_quote = + create_associated_token_account(&mut svm, &operator.pubkey(), "e_mint, &payer) + .unwrap(); mint_tokens_to_token_account( &mut svm, &base_mint, @@ -265,20 +257,21 @@ impl Market { } fn current_slot(&self) -> u64 { - self.svm.get_sysvar::().slot + self.svm.get_sysvar::().slot } /// Simulate a cluster restart at `slot`: prices stamped at or before it /// must be rejected until the publisher posts again. fn set_last_restart_slot(&mut self, slot: u64) { - self.svm.set_sysvar(&solana_sysvar::last_restart_slot::LastRestartSlot { - last_restart_slot: slot, - }); + self.svm + .set_sysvar(&solana_sysvar::last_restart_slot::LastRestartSlot { + last_restart_slot: slot, + }); } /// Create a wallet holding `base` and `quote` minor units in associated /// token accounts. - fn funded_trader(&mut self, base: u64, quote: u64) -> (Keypair, Pubkey, Pubkey) { + fn funded_trader(&mut self, base: u64, quote: u64) -> (Keypair, Address, Address) { let trader = create_wallet(&mut self.svm, 100_000_000_000).unwrap(); let base_account = create_associated_token_account( &mut self.svm, @@ -372,9 +365,14 @@ impl Market { .to_account_metas(None), ) }; - send_transaction_from_instructions(&mut self.svm, vec![instruction], &[signer], &signer.pubkey()) - .map(|_| ()) - .map_err(|_| ()) + send_transaction_from_instructions( + &mut self.svm, + vec![instruction], + &[signer], + &signer.pubkey(), + ) + .map(|_| ()) + .map_err(|_| ()) } fn deposit_inventory(&mut self, base_amount: u64, quote_amount: u64) -> Result<(), ()> { @@ -397,9 +395,14 @@ impl Market { } .to_account_metas(None), ); - send_transaction_from_instructions(&mut self.svm, vec![instruction], &[signer], &signer.pubkey()) - .map(|_| ()) - .map_err(|_| ()) + send_transaction_from_instructions( + &mut self.svm, + vec![instruction], + &[signer], + &signer.pubkey(), + ) + .map(|_| ()) + .map_err(|_| ()) } fn set_quote(&mut self, spread_bps: u16, paused: bool) -> Result<(), ()> { @@ -437,16 +440,21 @@ impl Market { trader_quote, token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut self.svm, vec![instruction], &[trader], &trader.pubkey()) - .map(|_| ()) - .map_err(|_| ()) + send_transaction_from_instructions( + &mut self.svm, + vec![instruction], + &[trader], + &trader.pubkey(), + ) + .map(|_| ()) + .map_err(|_| ()) } - fn balance(&self, token_account: &Pubkey) -> u64 { + fn balance(&self, token_account: &Address) -> u64 { get_token_account_balance(&self.svm, token_account).unwrap() } } @@ -587,9 +595,7 @@ fn test_operator_can_withdraw_everything_and_swaps_then_fail() { #[test] fn test_withdraw_more_than_inventory_fails() { let mut market = Market::default_market(); - assert!(market - .withdraw_inventory(1_001 * ONE_TOKEN, 0) - .is_err()); + assert!(market.withdraw_inventory(1_001 * ONE_TOKEN, 0).is_err()); } #[test] @@ -723,7 +729,9 @@ fn test_swap_rejects_insufficient_inventory() { // but the vault only holds 1,000 NVDAx. let quote_in = 181_681_500_000; let (whale, _, _) = market.funded_trader(0, quote_in); - assert!(market.swap(&whale, Direction::BuyBase, quote_in, 0).is_err()); + assert!(market + .swap(&whale, Direction::BuyBase, quote_in, 0) + .is_err()); } // =========================================================================== diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/Cargo.toml b/finance/token-fundraiser/anchor/programs/fundraiser/Cargo.toml index 66991a7cf..1294231ad 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/Cargo.toml +++ b/finance/token-fundraiser/anchor/programs/fundraiser/Cargo.toml @@ -14,17 +14,26 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = "1.1.2" +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" +solana-clock = "3.0.1" solana-signer = "3.0.0" solana-keypair = "3.0.1" solana-kite = "0.4.0" diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/constants.rs b/finance/token-fundraiser/anchor/programs/fundraiser/src/constants.rs index fc0697b02..2af19dd91 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/src/constants.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/src/constants.rs @@ -1,4 +1,4 @@ pub const MIN_AMOUNT_TO_RAISE: u64 = 3; pub const SECONDS_TO_DAYS: i64 = 86400; pub const MAX_CONTRIBUTION_PERCENTAGE: u64 = 10; -pub const PERCENTAGE_SCALER: u64 = 100; \ No newline at end of file +pub const PERCENTAGE_SCALER: u64 = 100; diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs index 333bae498..16b5916bd 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs @@ -10,19 +10,19 @@ use anchor_spl::{ use crate::{state::Fundraiser, FundraiserError}; #[derive(Accounts)] -pub struct CheckContributionsAccountConstraints<'info> { +pub struct CheckContributionsAccountConstraints { #[account(mut)] - pub maker: Signer<'info>, + pub maker: Signer, - pub mint_to_raise: InterfaceAccount<'info, Mint>, + pub mint_to_raise: InterfaceAccount, #[account( mut, - seeds = [b"fundraiser".as_ref(), maker.key().as_ref()], + seeds = [b"fundraiser".as_ref(), maker.address().as_ref()], bump = fundraiser.bump, close = maker, )] - pub fundraiser: Account<'info, Fundraiser>, + pub fundraiser: BorshAccount, #[account( mut, @@ -30,7 +30,7 @@ pub struct CheckContributionsAccountConstraints<'info> { associated_token::authority = fundraiser, associated_token::token_program = token_program, )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, #[account( init_if_needed, @@ -39,13 +39,13 @@ pub struct CheckContributionsAccountConstraints<'info> { associated_token::authority = maker, associated_token::token_program = token_program, )] - pub maker_ata: InterfaceAccount<'info, TokenAccount>, + pub maker_ata: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, - pub system_program: Program<'info, System>, + pub system_program: Program, - pub associated_token_program: Program<'info, AssociatedToken>, + pub associated_token_program: Program, } pub fn handle_check_contributions( @@ -58,44 +58,55 @@ pub fn handle_check_contributions( FundraiserError::TargetNotMet ); + // Read these before any of the CPI handles below take their borrows. + let maker_address = *accounts.maker.address(); + let vault_amount = accounts.vault.amount(); + let mint_decimals = accounts.mint_to_raise.decimals(); + + // `fundraiser` signs both CPIs below. It is a data account holding a live + // borrow on its buffer, so release it across the CPIs. The runtime rejects + // a CPI that borrows an account we still hold. Take it back after. + let fundraiser_bump = accounts.fundraiser.bump; + accounts.fundraiser.release_borrow()?; + let fundraiser_view = *accounts.fundraiser.account(); + // The vault is owned by the fundraiser PDA, so both CPIs are signed with // its seeds. let signer_seeds: [&[&[u8]]; 1] = [&[ b"fundraiser".as_ref(), - accounts.maker.to_account_info().key.as_ref(), - &[accounts.fundraiser.bump], + maker_address.as_ref(), + &[fundraiser_bump], ]]; // Drain the whole vault (including any direct donations) to the maker. let transfer_accounts = TransferChecked { - from: accounts.vault.to_account_info(), - mint: accounts.mint_to_raise.to_account_info(), - to: accounts.maker_ata.to_account_info(), - authority: accounts.fundraiser.to_account_info(), + from: accounts.vault.cpi_handle_mut(), + mint: accounts.mint_to_raise.cpi_handle(), + to: accounts.maker_ata.cpi_handle_mut(), + authority: CpiHandle::readonly(&fundraiser_view), }; let transfer_context = CpiContext::new_with_signer( - accounts.token_program.key(), + accounts.token_program.address(), transfer_accounts, &signer_seeds, ); - transfer_checked( - transfer_context, - accounts.vault.amount, - accounts.mint_to_raise.decimals, - )?; + transfer_checked(transfer_context, vault_amount, mint_decimals)?; // Close the empty vault so its rent goes back to the maker. let close_accounts = CloseAccount { - account: accounts.vault.to_account_info(), - destination: accounts.maker.to_account_info(), - authority: accounts.fundraiser.to_account_info(), + account: accounts.vault.cpi_handle_mut(), + destination: accounts.maker.cpi_handle_mut(), + authority: CpiHandle::readonly(&fundraiser_view), }; let close_context = CpiContext::new_with_signer( - accounts.token_program.key(), + accounts.token_program.address(), close_accounts, &signer_seeds, ); close_account(close_context)?; + // Take the borrow back before the derive's exit path touches it again. + accounts.fundraiser.reacquire_borrow_mut()?; + Ok(()) } diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/close.rs b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/close.rs index c888cbc26..4873bf3b7 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/close.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/close.rs @@ -10,20 +10,20 @@ use anchor_spl::{ use crate::{state::Fundraiser, FundraiserError, SECONDS_TO_DAYS}; #[derive(Accounts)] -pub struct CloseFundraiserAccountConstraints<'info> { +pub struct CloseFundraiserAccountConstraints { #[account(mut)] - pub maker: Signer<'info>, + pub maker: Signer, - pub mint_to_raise: InterfaceAccount<'info, Mint>, + #[account(address = fundraiser.mint_to_raise)] + pub mint_to_raise: InterfaceAccount, #[account( mut, - has_one = mint_to_raise, - seeds = [b"fundraiser".as_ref(), maker.key().as_ref()], + seeds = [b"fundraiser".as_ref(), maker.address().as_ref()], bump = fundraiser.bump, close = maker, )] - pub fundraiser: Account<'info, Fundraiser>, + pub fundraiser: BorshAccount, #[account( mut, @@ -31,7 +31,7 @@ pub struct CloseFundraiserAccountConstraints<'info> { associated_token::authority = fundraiser, associated_token::token_program = token_program, )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, #[account( init_if_needed, @@ -40,13 +40,13 @@ pub struct CloseFundraiserAccountConstraints<'info> { associated_token::authority = maker, associated_token::token_program = token_program, )] - pub maker_ata: InterfaceAccount<'info, TokenAccount>, + pub maker_ata: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, - pub system_program: Program<'info, System>, + pub system_program: Program, - pub associated_token_program: Program<'info, AssociatedToken>, + pub associated_token_program: Program, } /// Retires a failed fundraiser so the maker can raise again. @@ -83,49 +83,60 @@ pub fn handle_close_fundraiser(accounts: &mut CloseFundraiserAccountConstraints) FundraiserError::RefundsOutstanding ); + // Read these before any of the CPI handles below take their borrows. + let maker_address = *accounts.maker.address(); + let vault_amount = accounts.vault.amount(); + let mint_decimals = accounts.mint_to_raise.decimals(); + + // `fundraiser` signs both CPIs below. It is a data account holding a live + // borrow on its buffer, so release it across the CPIs. The runtime rejects + // a CPI that borrows an account we still hold. Take it back after. + let fundraiser_bump = accounts.fundraiser.bump; + accounts.fundraiser.release_borrow()?; + let fundraiser_view = *accounts.fundraiser.account(); + // The vault is owned by the fundraiser PDA, so both CPIs are signed with // its seeds. let signer_seeds: [&[&[u8]]; 1] = [&[ b"fundraiser".as_ref(), - accounts.maker.to_account_info().key.as_ref(), - &[accounts.fundraiser.bump], + maker_address.as_ref(), + &[fundraiser_bump], ]]; // Refunds have already drained every tracked contribution, so anything // left in the vault is a direct donation; sweep it to the maker rather // than burn it with the account. - if accounts.vault.amount > 0 { + if accounts.vault.amount() > 0 { let transfer_accounts = TransferChecked { - from: accounts.vault.to_account_info(), - mint: accounts.mint_to_raise.to_account_info(), - to: accounts.maker_ata.to_account_info(), - authority: accounts.fundraiser.to_account_info(), + from: accounts.vault.cpi_handle_mut(), + mint: accounts.mint_to_raise.cpi_handle(), + to: accounts.maker_ata.cpi_handle_mut(), + authority: CpiHandle::readonly(&fundraiser_view), }; let transfer_context = CpiContext::new_with_signer( - accounts.token_program.key(), + accounts.token_program.address(), transfer_accounts, &signer_seeds, ); - transfer_checked( - transfer_context, - accounts.vault.amount, - accounts.mint_to_raise.decimals, - )?; + transfer_checked(transfer_context, vault_amount, mint_decimals)?; } // Close the empty vault so its rent goes back to the maker. The // fundraiser account itself is closed by its close = maker constraint. let close_accounts = CloseAccount { - account: accounts.vault.to_account_info(), - destination: accounts.maker.to_account_info(), - authority: accounts.fundraiser.to_account_info(), + account: accounts.vault.cpi_handle_mut(), + destination: accounts.maker.cpi_handle_mut(), + authority: CpiHandle::readonly(&fundraiser_view), }; let close_context = CpiContext::new_with_signer( - accounts.token_program.key(), + accounts.token_program.address(), close_accounts, &signer_seeds, ); close_account(close_context)?; + // Take the borrow back before the derive's exit path touches it again. + accounts.fundraiser.reacquire_borrow_mut()?; + Ok(()) } diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs index 67e73d2f6..d6b7e306f 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs @@ -9,28 +9,28 @@ use crate::{ }; #[derive(Accounts)] -pub struct ContributeAccountConstraints<'info> { +pub struct ContributeAccountConstraints { #[account(mut)] - pub contributor: Signer<'info>, + pub contributor: Signer, - pub mint_to_raise: InterfaceAccount<'info, Mint>, + #[account(address = fundraiser.mint_to_raise)] + pub mint_to_raise: InterfaceAccount, #[account( mut, - has_one = mint_to_raise, seeds = [b"fundraiser".as_ref(), fundraiser.maker.as_ref()], bump = fundraiser.bump, )] - pub fundraiser: Account<'info, Fundraiser>, + pub fundraiser: BorshAccount, #[account( init_if_needed, payer = contributor, - seeds = [b"contributor", fundraiser.key().as_ref(), contributor.key().as_ref()], + seeds = [b"contributor", fundraiser.address().as_ref(), contributor.address().as_ref()], bump, space = Contributor::DISCRIMINATOR.len() + Contributor::INIT_SPACE, )] - pub contributor_account: Account<'info, Contributor>, + pub contributor_account: BorshAccount, #[account( mut, @@ -38,19 +38,19 @@ pub struct ContributeAccountConstraints<'info> { associated_token::authority = contributor, associated_token::token_program = token_program, )] - pub contributor_ata: InterfaceAccount<'info, TokenAccount>, + pub contributor_ata: InterfaceAccount, #[account( mut, - associated_token::mint = fundraiser.mint_to_raise, + associated_token::mint = mint_to_raise, associated_token::authority = fundraiser, associated_token::token_program = token_program, )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, - pub system_program: Program<'info, System>, + pub system_program: Program, } /// Caps a single contributor at MAX_CONTRIBUTION_PERCENTAGE percent of the @@ -62,7 +62,7 @@ fn calculate_max_contribution(amount_to_raise: u64) -> Result { .checked_div(PERCENTAGE_SCALER as u128) .ok_or(FundraiserError::MathOverflow)? .try_into() - .map_err(|_| error!(FundraiserError::MathOverflow)) + .map_err(|_| FundraiserError::MathOverflow.into()) } pub fn handle_contribute( @@ -72,7 +72,7 @@ pub fn handle_contribute( ) -> Result<()> { // The minimum contribution is one major unit, which is 10^decimals minor units. let one_major_unit = 10_u64 - .checked_pow(accounts.mint_to_raise.decimals as u32) + .checked_pow(accounts.mint_to_raise.decimals() as u32) .ok_or(FundraiserError::MathOverflow)?; require!( amount >= one_major_unit, @@ -124,13 +124,13 @@ pub fn handle_contribute( // Transfer the funds from the contributor to the vault. let cpi_accounts = TransferChecked { - from: accounts.contributor_ata.to_account_info(), - mint: accounts.mint_to_raise.to_account_info(), - to: accounts.vault.to_account_info(), - authority: accounts.contributor.to_account_info(), + from: accounts.contributor_ata.cpi_handle_mut(), + mint: accounts.mint_to_raise.cpi_handle(), + to: accounts.vault.cpi_handle_mut(), + authority: accounts.contributor.cpi_handle(), }; - let cpi_context = CpiContext::new(accounts.token_program.key(), cpi_accounts); - transfer_checked(cpi_context, amount, accounts.mint_to_raise.decimals)?; + let cpi_context = CpiContext::new(accounts.token_program.address(), cpi_accounts); + transfer_checked(cpi_context, amount, accounts.mint_to_raise.decimals())?; Ok(()) } diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs index b105d17a5..0f7adf7ce 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs @@ -7,20 +7,20 @@ use anchor_spl::{ use crate::{state::Fundraiser, FundraiserError, MIN_AMOUNT_TO_RAISE}; #[derive(Accounts)] -pub struct InitializeFundraiserAccountConstraints<'info> { +pub struct InitializeFundraiserAccountConstraints { #[account(mut)] - pub maker: Signer<'info>, + pub maker: Signer, - pub mint_to_raise: InterfaceAccount<'info, Mint>, + pub mint_to_raise: InterfaceAccount, #[account( init, payer = maker, - seeds = [b"fundraiser", maker.key().as_ref()], + seeds = [b"fundraiser", maker.address().as_ref()], bump, space = Fundraiser::DISCRIMINATOR.len() + Fundraiser::INIT_SPACE, )] - pub fundraiser: Account<'info, Fundraiser>, + pub fundraiser: BorshAccount, #[account( init, @@ -29,13 +29,13 @@ pub struct InitializeFundraiserAccountConstraints<'info> { associated_token::authority = fundraiser, associated_token::token_program = token_program, )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, + pub associated_token_program: Program, } pub fn handle_initialize_fundraiser( @@ -47,7 +47,7 @@ pub fn handle_initialize_fundraiser( // The target must be at least MIN_AMOUNT_TO_RAISE major units, expressed // in minor units: MIN_AMOUNT_TO_RAISE * 10^decimals. let one_major_unit = 10_u64 - .checked_pow(accounts.mint_to_raise.decimals as u32) + .checked_pow(accounts.mint_to_raise.decimals() as u32) .ok_or(FundraiserError::MathOverflow)?; let minimum_amount_to_raise = MIN_AMOUNT_TO_RAISE .checked_mul(one_major_unit) @@ -57,15 +57,15 @@ pub fn handle_initialize_fundraiser( FundraiserError::InvalidAmount ); - accounts.fundraiser.set_inner(Fundraiser { - maker: accounts.maker.key(), - mint_to_raise: accounts.mint_to_raise.key(), + *accounts.fundraiser = Fundraiser { + maker: *accounts.maker.address(), + mint_to_raise: *accounts.mint_to_raise.address(), amount_to_raise: amount, current_amount: 0, time_started: Clock::get()?.unix_timestamp, duration, bump: bumps.fundraiser, - }); + }; Ok(()) } diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs index 7ae564501..b192e20f4 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs @@ -1,11 +1,11 @@ -pub mod initialize_fundraiser; -pub mod contribute; pub mod checker; -pub mod refund; pub mod close; +pub mod contribute; +pub mod initialize_fundraiser; +pub mod refund; -pub use initialize_fundraiser::*; -pub use contribute::*; pub use checker::*; +pub use close::*; +pub use contribute::*; +pub use initialize_fundraiser::*; pub use refund::*; -pub use close::*; \ No newline at end of file diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs index 61fb2babe..7e54c7a00 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs @@ -9,29 +9,29 @@ use crate::{ }; #[derive(Accounts)] -pub struct RefundAccountConstraints<'info> { +pub struct RefundAccountConstraints { #[account(mut)] - pub contributor: Signer<'info>, + pub contributor: Signer, - pub maker: SystemAccount<'info>, + pub maker: SystemAccount, - pub mint_to_raise: InterfaceAccount<'info, Mint>, + #[account(address = fundraiser.mint_to_raise)] + pub mint_to_raise: InterfaceAccount, #[account( mut, - has_one = mint_to_raise, - seeds = [b"fundraiser", maker.key().as_ref()], + seeds = [b"fundraiser", maker.address().as_ref()], bump = fundraiser.bump, )] - pub fundraiser: Account<'info, Fundraiser>, + pub fundraiser: BorshAccount, #[account( mut, - seeds = [b"contributor", fundraiser.key().as_ref(), contributor.key().as_ref()], + seeds = [b"contributor", fundraiser.address().as_ref(), contributor.address().as_ref()], bump = contributor_account.bump, close = contributor, )] - pub contributor_account: Account<'info, Contributor>, + pub contributor_account: BorshAccount, #[account( mut, @@ -39,7 +39,7 @@ pub struct RefundAccountConstraints<'info> { associated_token::authority = contributor, associated_token::token_program = token_program, )] - pub contributor_ata: InterfaceAccount<'info, TokenAccount>, + pub contributor_ata: InterfaceAccount, #[account( mut, @@ -47,11 +47,11 @@ pub struct RefundAccountConstraints<'info> { associated_token::authority = fundraiser, associated_token::token_program = token_program, )] - pub vault: InterfaceAccount<'info, TokenAccount>, + pub vault: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, - pub system_program: Program<'info, System>, + pub system_program: Program, } pub fn handle_refund(accounts: &mut RefundAccountConstraints) -> Result<()> { @@ -85,29 +85,39 @@ pub fn handle_refund(accounts: &mut RefundAccountConstraints) -> Result<()> { .ok_or(FundraiserError::MathOverflow)?; accounts.contributor_account.amount = 0; + // Read these before any CPI handle below takes its borrow. `maker` is a + // read-only account here, so asking it for a writable handle would panic. + let maker_address = *accounts.maker.address(); + let mint_decimals = accounts.mint_to_raise.decimals(); + let fundraiser_bump = accounts.fundraiser.bump; + + // `fundraiser` signs the transfer. It is a data account holding a live + // borrow on its buffer, so release it across the CPI and take it back after. + accounts.fundraiser.release_borrow()?; + let fundraiser_view = *accounts.fundraiser.account(); + // Transfer the funds from the vault back to the contributor. The vault is // owned by the fundraiser PDA, so the CPI is signed with its seeds. let cpi_accounts = TransferChecked { - from: accounts.vault.to_account_info(), - mint: accounts.mint_to_raise.to_account_info(), - to: accounts.contributor_ata.to_account_info(), - authority: accounts.fundraiser.to_account_info(), + from: accounts.vault.cpi_handle_mut(), + mint: accounts.mint_to_raise.cpi_handle(), + to: accounts.contributor_ata.cpi_handle_mut(), + authority: CpiHandle::readonly(&fundraiser_view), }; let signer_seeds: [&[&[u8]]; 1] = [&[ b"fundraiser".as_ref(), - accounts.maker.to_account_info().key.as_ref(), - &[accounts.fundraiser.bump], + maker_address.as_ref(), + &[fundraiser_bump], ]]; let cpi_context = CpiContext::new_with_signer( - accounts.token_program.key(), + accounts.token_program.address(), cpi_accounts, &signer_seeds, ); - transfer_checked( - cpi_context, - refund_amount, - accounts.mint_to_raise.decimals, - )?; + transfer_checked(cpi_context, refund_amount, mint_decimals)?; + + // Take the borrow back before the derive's exit path touches it again. + accounts.fundraiser.reacquire_borrow_mut()?; Ok(()) } diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/lib.rs b/finance/token-fundraiser/anchor/programs/fundraiser/src/lib.rs index ccbee926a..d0a92e80e 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/src/lib.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/src/lib.rs @@ -16,7 +16,7 @@ pub mod fundraiser { use super::*; pub fn initialize_fundraiser( - mut context: Context, + mut context: &mut Context, amount: u64, duration: u16, ) -> Result<()> { @@ -26,7 +26,7 @@ pub mod fundraiser { } pub fn contribute( - mut context: Context, + mut context: &mut Context, amount: u64, ) -> Result<()> { handle_contribute(&mut context.accounts, amount, &context.bumps)?; @@ -35,20 +35,22 @@ pub mod fundraiser { } pub fn check_contributions( - mut context: Context, + mut context: &mut Context, ) -> Result<()> { handle_check_contributions(&mut context.accounts)?; Ok(()) } - pub fn refund(mut context: Context) -> Result<()> { + pub fn refund(mut context: &mut Context) -> Result<()> { handle_refund(&mut context.accounts)?; Ok(()) } - pub fn close_fundraiser(mut context: Context) -> Result<()> { + pub fn close_fundraiser( + mut context: &mut Context, + ) -> Result<()> { handle_close_fundraiser(&mut context.accounts)?; Ok(()) diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/state/contributor.rs b/finance/token-fundraiser/anchor/programs/fundraiser/src/state/contributor.rs index 33b0c00e4..1003a50ba 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/src/state/contributor.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/src/state/contributor.rs @@ -1,9 +1,9 @@ use anchor_lang::prelude::*; -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Contributor { pub amount: u64, /// Canonical bump for this PDA. pub bump: u8, -} \ No newline at end of file +} diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs b/finance/token-fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs index cab7d1c60..12ba8bc72 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs @@ -1,13 +1,13 @@ use anchor_lang::prelude::*; -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Fundraiser { - pub maker: Pubkey, - pub mint_to_raise: Pubkey, + pub maker: Address, + pub mint_to_raise: Address, pub amount_to_raise: u64, pub current_amount: u64, pub time_started: i64, pub duration: u16, pub bump: u8, -} \ No newline at end of file +} diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/state/mod.rs b/finance/token-fundraiser/anchor/programs/fundraiser/src/state/mod.rs index 3e6b07fdb..d4c1b8235 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/src/state/mod.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/src/state/mod.rs @@ -1,5 +1,5 @@ -pub mod fundraiser; pub mod contributor; +pub mod fundraiser; +pub use contributor::*; pub use fundraiser::*; -pub use contributor::*; \ No newline at end of file diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs b/finance/token-fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs index c32e479e0..fb386016c 100644 --- a/finance/token-fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs +++ b/finance/token-fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs @@ -1,15 +1,18 @@ use { anchor_lang::{ - solana_program::{clock::Clock, instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, borsh::BorshDeserialize, fundraiser::SECONDS_TO_DAYS, litesvm::LiteSVM, + // LiteSVM's get_sysvar wants the host-side Clock, not pinocchio's. + solana_clock::Clock, solana_keypair::Keypair, solana_kite::{ create_associated_token_account, create_token_mint, create_wallet, - get_token_account_balance, mint_tokens_to_token_account, send_transaction_from_instructions, + get_token_account_balance, mint_tokens_to_token_account, + send_transaction_from_instructions, }, solana_signer::Signer, }; @@ -24,20 +27,20 @@ const MAX_CONTRIBUTION: u64 = AMOUNT_TO_RAISE / 10; const DURATION_DAYS: u16 = 7; const CONTRIBUTOR_STARTING_BALANCE: u64 = 10 * ONE_TOKEN; -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn ata_program_id() -> Pubkey { +fn ata_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - let (ata, _bump) = Pubkey::find_program_address( +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + let (ata, _bump) = Address::find_program_address( &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &ata_program_id(), ); @@ -66,12 +69,12 @@ struct ContributorState { const ANCHOR_DISCRIMINATOR_LENGTH: usize = 8; -fn read_fundraiser_state(svm: &LiteSVM, fundraiser_pda: &Pubkey) -> FundraiserState { +fn read_fundraiser_state(svm: &LiteSVM, fundraiser_pda: &Address) -> FundraiserState { let account = svm.get_account(fundraiser_pda).unwrap(); FundraiserState::try_from_slice(&account.data[ANCHOR_DISCRIMINATOR_LENGTH..]).unwrap() } -fn read_contributor_state(svm: &LiteSVM, contributor_pda: &Pubkey) -> ContributorState { +fn read_contributor_state(svm: &LiteSVM, contributor_pda: &Address) -> ContributorState { let account = svm.get_account(contributor_pda).unwrap(); ContributorState::try_from_slice(&account.data[ANCHOR_DISCRIMINATOR_LENGTH..]).unwrap() } @@ -85,12 +88,12 @@ fn warp_days_forward(svm: &mut LiteSVM, days: i64) { struct FundraiserSetup { svm: LiteSVM, - program_id: Pubkey, + program_id: Address, payer: Keypair, maker: Keypair, - mint: Pubkey, - fundraiser_pda: Pubkey, - vault: Pubkey, + mint: Address, + fundraiser_pda: Address, + vault: Address, } fn full_setup() -> FundraiserSetup { @@ -107,7 +110,7 @@ fn full_setup() -> FundraiserSetup { let mint = create_token_mint(&mut svm, &payer, MINT_DECIMALS, None).unwrap(); let (fundraiser_pda, _bump) = - Pubkey::find_program_address(&[b"fundraiser", maker.pubkey().as_ref()], &program_id); + Address::find_program_address(&[b"fundraiser", maker.pubkey().as_ref()], &program_id); // The vault is the ATA of the fundraiser PDA for the mint. let vault = derive_ata(&fundraiser_pda, &mint); @@ -132,7 +135,7 @@ fn initialize_fundraiser(setup: &mut FundraiserSetup, amount: u64, duration: u16 mint_to_raise: setup.mint, fundraiser: setup.fundraiser_pda, vault: setup.vault, - system_program: system_program::id(), + system_program: system_program::ID, token_program: token_program_id(), associated_token_program: ata_program_id(), } @@ -149,7 +152,7 @@ fn initialize_fundraiser(setup: &mut FundraiserSetup, amount: u64, duration: u16 /// Creates a contributor wallet with a funded ATA and returns /// (contributor keypair, contributor ATA, contributor account PDA). -fn create_funded_contributor(setup: &mut FundraiserSetup) -> (Keypair, Pubkey, Pubkey) { +fn create_funded_contributor(setup: &mut FundraiserSetup) -> (Keypair, Address, Address) { let contributor = create_wallet(&mut setup.svm, 10_000_000_000).unwrap(); let contributor_ata = create_associated_token_account( @@ -169,7 +172,7 @@ fn create_funded_contributor(setup: &mut FundraiserSetup) -> (Keypair, Pubkey, P ) .unwrap(); - let (contributor_account_pda, _bump) = Pubkey::find_program_address( + let (contributor_account_pda, _bump) = Address::find_program_address( &[ b"contributor", setup.fundraiser_pda.as_ref(), @@ -183,9 +186,9 @@ fn create_funded_contributor(setup: &mut FundraiserSetup) -> (Keypair, Pubkey, P fn build_contribute_instruction( setup: &FundraiserSetup, - contributor: &Pubkey, - contributor_ata: &Pubkey, - contributor_account_pda: &Pubkey, + contributor: &Address, + contributor_ata: &Address, + contributor_account_pda: &Address, amount: u64, ) -> Instruction { Instruction::new_with_bytes( @@ -199,7 +202,7 @@ fn build_contribute_instruction( contributor_ata: *contributor_ata, vault: setup.vault, token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) @@ -207,9 +210,9 @@ fn build_contribute_instruction( fn build_refund_instruction( setup: &FundraiserSetup, - contributor: &Pubkey, - contributor_ata: &Pubkey, - contributor_account_pda: &Pubkey, + contributor: &Address, + contributor_ata: &Address, + contributor_account_pda: &Address, ) -> Instruction { Instruction::new_with_bytes( setup.program_id, @@ -223,7 +226,7 @@ fn build_refund_instruction( contributor_ata: *contributor_ata, vault: setup.vault, token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) @@ -231,7 +234,7 @@ fn build_refund_instruction( fn build_check_contributions_instruction( setup: &FundraiserSetup, - maker_ata: &Pubkey, + maker_ata: &Address, ) -> Instruction { Instruction::new_with_bytes( setup.program_id, @@ -243,14 +246,14 @@ fn build_check_contributions_instruction( vault: setup.vault, maker_ata: *maker_ata, token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, associated_token_program: ata_program_id(), } .to_account_metas(None), ) } -fn build_close_fundraiser_instruction(setup: &FundraiserSetup, maker_ata: &Pubkey) -> Instruction { +fn build_close_fundraiser_instruction(setup: &FundraiserSetup, maker_ata: &Address) -> Instruction { Instruction::new_with_bytes( setup.program_id, &fundraiser::instruction::CloseFundraiser {}.data(), @@ -261,7 +264,7 @@ fn build_close_fundraiser_instruction(setup: &FundraiserSetup, maker_ata: &Pubke vault: setup.vault, maker_ata: *maker_ata, token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, associated_token_program: ata_program_id(), } .to_account_metas(None), @@ -279,7 +282,10 @@ fn test_initialize_fundraiser() { assert_eq!(fundraiser_state.current_amount, 0); assert_eq!(fundraiser_state.duration, DURATION_DAYS); - assert_eq!(get_token_account_balance(&setup.svm, &setup.vault).unwrap(), 0); + assert_eq!( + get_token_account_balance(&setup.svm, &setup.vault).unwrap(), + 0 + ); } #[test] @@ -300,7 +306,7 @@ fn test_initialize_below_minimum_target_fails() { mint_to_raise: setup.mint, fundraiser: setup.fundraiser_pda, vault: setup.vault, - system_program: system_program::id(), + system_program: system_program::ID, token_program: token_program_id(), associated_token_program: ata_program_id(), } @@ -312,7 +318,10 @@ fn test_initialize_below_minimum_target_fails() { &[&setup.maker], &setup.maker.pubkey(), ); - assert!(result.is_err(), "Target below 3 major units must be rejected"); + assert!( + result.is_err(), + "Target below 3 major units must be rejected" + ); assert!( setup.svm.get_account(&setup.fundraiser_pda).is_none(), "Fundraiser account must not exist after a failed initialize" @@ -387,7 +396,10 @@ fn test_contribute_after_deadline_fails() { ); assert!(result.is_err(), "Contributing after the deadline must fail"); - assert_eq!(get_token_account_balance(&setup.svm, &setup.vault).unwrap(), 0); + assert_eq!( + get_token_account_balance(&setup.svm, &setup.vault).unwrap(), + 0 + ); assert_eq!( get_token_account_balance(&setup.svm, &contributor_ata).unwrap(), CONTRIBUTOR_STARTING_BALANCE @@ -419,7 +431,10 @@ fn test_contribute_below_one_major_unit_fails() { result.is_err(), "Contributions below one major unit must fail" ); - assert_eq!(get_token_account_balance(&setup.svm, &setup.vault).unwrap(), 0); + assert_eq!( + get_token_account_balance(&setup.svm, &setup.vault).unwrap(), + 0 + ); } #[test] @@ -508,7 +523,10 @@ fn test_refund_after_deadline_target_not_met_succeeds() { ) .unwrap(); - assert_eq!(get_token_account_balance(&setup.svm, &setup.vault).unwrap(), 0); + assert_eq!( + get_token_account_balance(&setup.svm, &setup.vault).unwrap(), + 0 + ); assert_eq!( get_token_account_balance(&setup.svm, &contributor_ata).unwrap(), CONTRIBUTOR_STARTING_BALANCE @@ -655,7 +673,10 @@ fn test_contribute_above_cap_fails() { result.is_err(), "A single contribution above the 10% cap must fail" ); - assert_eq!(get_token_account_balance(&setup.svm, &setup.vault).unwrap(), 0); + assert_eq!( + get_token_account_balance(&setup.svm, &setup.vault).unwrap(), + 0 + ); } #[test] @@ -780,7 +801,10 @@ fn test_close_fundraiser_after_failed_raise_allows_a_new_raise() { let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); assert_eq!(fundraiser_state.current_amount, 0); assert_eq!(fundraiser_state.amount_to_raise, AMOUNT_TO_RAISE); - assert_eq!(get_token_account_balance(&setup.svm, &setup.vault).unwrap(), 0); + assert_eq!( + get_token_account_balance(&setup.svm, &setup.vault).unwrap(), + 0 + ); } #[test] @@ -905,8 +929,14 @@ fn test_close_fundraiser_sweeps_direct_donations_to_maker() { // accounting; on close they go to the maker instead of being burned // with the account. let donation = 5 * ONE_TOKEN; - mint_tokens_to_token_account(&mut setup.svm, &setup.mint, &setup.vault, donation, &setup.payer) - .unwrap(); + mint_tokens_to_token_account( + &mut setup.svm, + &setup.mint, + &setup.vault, + donation, + &setup.payer, + ) + .unwrap(); warp_days_forward(&mut setup.svm, DURATION_DAYS as i64 + 1); diff --git a/finance/token-swap/README.md b/finance/token-swap/README.md index b833026bf..4ed34a75f 100644 --- a/finance/token-swap/README.md +++ b/finance/token-swap/README.md @@ -16,7 +16,7 @@ The pool keeps `x * y = K` invariant: if `x` is the reserve of token A and `y` i - **Caller-supplied [slippage](https://www.investopedia.com/terms/s/slippage.asp) floors on every state-changing [instruction](https://solana.com/docs/terminology#instruction):** swaps revert with `SlippageExceeded` if the output falls below `min_output_amount`, deposits revert with `DepositBelowMinimum` if the LP mint amount falls below `minimum_lp_tokens_out`, withdrawals revert with `WithdrawalBelowMinimum` if either side falls below its floor. - **Defence-in-depth invariant check:** every swap re-verifies `effective_pool_a * effective_pool_b` doesn't decrease after the transfers, so a bug in the curve math fails the transaction instead of silently giving the trader too much. - All financial math in `u128` with checked arithmetic, matching how production Solana AMMs (Orca, Raydium, Meteora, Saber) do it. -- Anchor 1.0 Rust [program](https://solana.com/docs/terminology#program) with LiteSVM integration tests. +- Anchor 2.0.0-rc.1 Rust [program](https://solana.com/docs/terminology#program) with LiteSVM integration tests. ## Why a CPAMM @@ -45,7 +45,7 @@ Implementation choices: ## Onchain-design principles applied here -- **Store keys in the account.** Even for PDAs, storing the parent keys in the account state makes lookups easier (you can rebuild the PDA without consulting external data) and works well with Anchor's `has_one` constraint. +- **Store keys in the account.** Even for PDAs, storing the parent keys in the account state makes lookups easier (you can rebuild the PDA without consulting external data) and works well with Anchor's `address` constraint on the sibling field. - **Keep seeds simple.** Start with the parent's seeds, then the current object's identifiers in alphabetical order. For the pool, that means `[config, mint_a, mint_b]`. - **Keep instruction scope small.** Smaller instructions touch fewer accounts, leaving room in the transaction and improving composability and security. @@ -142,7 +142,7 @@ Burns LP tokens and returns a proportional share of the **effective reserves** ( Lets the address stored in `Config.admin` sweep their accumulated trading-fee claim out of a pool. Transfers `admin_fees_owed_a` from `pool_a` to the admin's token-A account and `admin_fees_owed_b` from `pool_b` to the admin's token-B account, signed by `pool_authority`. Then resets both accumulators to zero. -- Authorisation: enforced by Anchor's `has_one = admin` constraint on `config` plus the `Signer` constraint on `admin`. Calls from any other signer are rejected. +- Authorisation: enforced by Anchor's `address = config.admin` constraint on `admin` plus the `Signer` constraint on the same field. Calls from any other signer are rejected. - The admin's token accounts (`admin_token_a`, `admin_token_b`) must already exist - this handler doesn't auto-create them (keeps the example small). - Idempotent: calling again with the accumulators at zero is a successful no-op (transfers are skipped when owed = 0). @@ -342,7 +342,7 @@ After trading activity on both pools, Alice sweeps her accumulated slice from th - **Handler:** `claim_admin_fees` - **Accounts (`ClaimAdminFeesAccounts`):** - - `config` - Alice's `Config` (the `has_one = admin` constraint enforces that only she can call this) + - `config` - Alice's `Config` (the `address = config.admin` constraint on `admin` enforces that only she can call this) - `pool_config`, `pool_authority` - `mint_a`, `mint_b` - `pool_a`, `pool_b` (the pool's reserves - the source of the transfers) diff --git a/finance/token-swap/anchor/programs/token-swap/Cargo.toml b/finance/token-swap/anchor/programs/token-swap/Cargo.toml index 18f746194..e7041985c 100644 --- a/finance/token-swap/anchor/programs/token-swap/Cargo.toml +++ b/finance/token-swap/anchor/programs/token-swap/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = { version = "1.1.2", features = ["metadata", "spl-token-interface"] } +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = { version = "2.0.0-rc.1", features = ["metadata"] } # `fixed` removed: all financial math is now u128 + checked_*, matching how # production Solana AMMs (Orca, Raydium, Meteora, Saber) do it. Floats / # fixed-point types are not used for money in this program. diff --git a/finance/token-swap/anchor/programs/token-swap/src/instructions/admin/claim_admin_fees.rs b/finance/token-swap/anchor/programs/token-swap/src/instructions/admin/claim_admin_fees.rs index 78ddb1795..85db74579 100644 --- a/finance/token-swap/anchor/programs/token-swap/src/instructions/admin/claim_admin_fees.rs +++ b/finance/token-swap/anchor/programs/token-swap/src/instructions/admin/claim_admin_fees.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::token_interface::{self, Mint, TokenAccount, TokenInterface, TransferChecked}; use crate::{ @@ -15,11 +16,13 @@ use crate::{ /// `admin_fees_owed_b`). This handler transfers those amounts out of the /// pool reserves into the admin's ATAs and resets the accumulators to zero. /// -/// Authorisation: the `has_one = admin` constraint on `config` plus the +/// Authorisation: the `address = config.admin` constraint on `admin` plus the /// `Signer` constraint on `admin` together mean only the address stored in /// `Config.admin` can call this. Any other signer will be rejected by -/// Anchor's built-in `has_one` check. -pub fn handle_claim_admin_fees(context: Context) -> Result<()> { +/// Anchor's built-in `address` check. +pub fn handle_claim_admin_fees( + context: &mut Context, +) -> Result<()> { let owed_a = context.accounts.pool_config.admin_fees_owed_a; let owed_b = context.accounts.pool_config.admin_fees_owed_b; @@ -37,8 +40,8 @@ pub fn handle_claim_admin_fees(context: Context 0 { token_interface::transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.pool_a.to_account_info(), - mint: context.accounts.mint_a.to_account_info(), - to: context.accounts.admin_token_a.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + from: context.accounts.pool_a.to_cpi_handle_mut(), + mint: context.accounts.mint_a.to_cpi_handle(), + to: context.accounts.admin_token_a.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, signer_seeds, ), owed_a, - context.accounts.mint_a.decimals, + context.accounts.mint_a.decimals(), )?; } if owed_b > 0 { token_interface::transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.pool_b.to_account_info(), - mint: context.accounts.mint_b.to_account_info(), - to: context.accounts.admin_token_b.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + from: context.accounts.pool_b.to_cpi_handle_mut(), + mint: context.accounts.mint_b.to_cpi_handle(), + to: context.accounts.admin_token_b.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, signer_seeds, ), owed_b, - context.accounts.mint_b.decimals, + context.accounts.mint_b.decimals(), )?; } - msg!("Admin swept fees: {} of mint_a, {} of mint_b", owed_a, owed_b); + msg!( + "Admin swept fees: {} of mint_a, {} of mint_b", + owed_a, + owed_b + ); Ok(()) } #[derive(Accounts)] -pub struct ClaimAdminFeesAccountConstraints<'info> { - #[account( - seeds = [CONFIG_SEED], - bump, - has_one = admin, - )] - pub config: Account<'info, Config>, +pub struct ClaimAdminFeesAccountConstraints { + #[account(seeds = [CONFIG_SEED], + bump, address = pool_config.config)] + pub config: BorshAccount, #[account( mut, seeds = [ pool_config.config.as_ref(), - pool_config.mint_a.key().as_ref(), - pool_config.mint_b.key().as_ref(), + pool_config.mint_a.as_ref(), + pool_config.mint_b.as_ref(), ], bump, - has_one = config, - has_one = mint_a, - has_one = mint_b, )] - pub pool_config: Account<'info, PoolConfig>, + pub pool_config: BorshAccount, /// CHECK: PDA that owns the pool reserves; signs the outbound transfers. #[account( seeds = [ pool_config.config.as_ref(), - mint_a.key().as_ref(), - mint_b.key().as_ref(), + mint_a.address().as_ref(), + mint_b.address().as_ref(), AUTHORITY_SEED, ], bump, )] - pub pool_authority: UncheckedAccount<'info>, + pub pool_authority: UncheckedAccount, - pub mint_a: Box>, + #[account(address = pool_config.mint_a)] + pub mint_a: Box>, - pub mint_b: Box>, + #[account(address = pool_config.mint_b)] + pub mint_b: Box>, /// The pool's token-A reserve. The admin's owed token-A fees are paid out /// of this account. @@ -144,7 +147,7 @@ pub struct ClaimAdminFeesAccountConstraints<'info> { associated_token::authority = pool_authority, associated_token::token_program = token_program, )] - pub pool_a: Box>, + pub pool_a: Box>, /// The pool's token-B reserve. The admin's owed token-B fees are paid out /// of this account. @@ -154,11 +157,12 @@ pub struct ClaimAdminFeesAccountConstraints<'info> { associated_token::authority = pool_authority, associated_token::token_program = token_program, )] - pub pool_b: Box>, + pub pool_b: Box>, /// Must match the address stored in `Config.admin` (enforced by - /// `has_one = admin` above). - pub admin: Signer<'info>, + /// `address = config.admin` above). + #[account(address = config.admin)] + pub admin: Signer, /// Admin's token-A receiving account. Must already exist; the admin is /// expected to create it themselves before calling. Keeps this handler @@ -169,7 +173,7 @@ pub struct ClaimAdminFeesAccountConstraints<'info> { token::authority = admin, token::token_program = token_program, )] - pub admin_token_a: Box>, + pub admin_token_a: Box>, /// Admin's token-B receiving account. Same constraints as `admin_token_a`. #[account( @@ -178,7 +182,7 @@ pub struct ClaimAdminFeesAccountConstraints<'info> { token::authority = admin, token::token_program = token_program, )] - pub admin_token_b: Box>, + pub admin_token_b: Box>, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } diff --git a/finance/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs b/finance/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs index a6d97ff26..2b1191729 100644 --- a/finance/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs +++ b/finance/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs @@ -28,7 +28,7 @@ fn integer_sqrt(n: u128) -> u128 { } pub fn handle_deposit_liquidity( - context: Context, + context: &mut Context, amount_a: u64, amount_b: u64, minimum_lp_tokens_out: u64, @@ -37,8 +37,7 @@ pub fn handle_deposit_liquidity( // silently clamped to the available balance, which broke slippage protection // for callers building on top - they expected their input amount to be the // amount actually deposited. - if amount_a > context.accounts.token_a.amount - || amount_b > context.accounts.token_b.amount + if amount_a > context.accounts.token_a.amount() || amount_b > context.accounts.token_b.amount() { return err!(AmmError::InsufficientBalance); } @@ -69,11 +68,11 @@ pub fn handle_deposit_liquidity( // checked_sub: admin_fees_owed is an invariant subset of the vault balance; // a raw `-` would wrap silently on a BPF release build if that ever broke. let effective_pool_a = pool_a - .amount + .amount() .checked_sub(pool_config.admin_fees_owed_a) .ok_or(AmmError::MathOverflow)?; let effective_pool_b = pool_b - .amount + .amount() .checked_sub(pool_config.admin_fees_owed_b) .ok_or(AmmError::MathOverflow)?; // Defining pool creation like this allows attackers to frontrun pool creation with bad ratios @@ -150,8 +149,7 @@ pub fn handle_deposit_liquidity( .checked_mul(amount_b as u128) .ok_or(AmmError::MathOverflow)?; let sqrt_product = integer_sqrt(product); - let sqrt_product_u64 = u64::try_from(sqrt_product) - .map_err(|_| AmmError::MathOverflow)?; + let sqrt_product_u64 = u64::try_from(sqrt_product).map_err(|_| AmmError::MathOverflow)?; if sqrt_product_u64 < MINIMUM_LIQUIDITY { return err!(AmmError::DepositTooSmall); } @@ -159,7 +157,7 @@ pub fn handle_deposit_liquidity( .checked_sub(MINIMUM_LIQUIDITY) .ok_or(AmmError::MathOverflow)? } else { - let total_supply = context.accounts.liquidity_provider_mint.supply as u128; + let total_supply = context.accounts.liquidity_provider_mint.supply() as u128; let liquidity_from_a = (amount_a as u128) .checked_mul(total_supply) .ok_or(AmmError::MathOverflow)? @@ -198,48 +196,48 @@ pub fn handle_deposit_liquidity( // decimal-mismatch bugs (and is the modern recommended path). token_interface::transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.token_a.to_account_info(), - mint: context.accounts.mint_a.to_account_info(), - to: context.accounts.pool_a.to_account_info(), - authority: context.accounts.depositor.to_account_info(), + from: context.accounts.token_a.to_cpi_handle_mut(), + mint: context.accounts.mint_a.to_cpi_handle(), + to: context.accounts.pool_a.to_cpi_handle_mut(), + authority: context.accounts.depositor.cpi_handle(), }, ), amount_a, - context.accounts.mint_a.decimals, + context.accounts.mint_a.decimals(), )?; token_interface::transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.token_b.to_account_info(), - mint: context.accounts.mint_b.to_account_info(), - to: context.accounts.pool_b.to_account_info(), - authority: context.accounts.depositor.to_account_info(), + from: context.accounts.token_b.to_cpi_handle_mut(), + mint: context.accounts.mint_b.to_cpi_handle(), + to: context.accounts.pool_b.to_cpi_handle_mut(), + authority: context.accounts.depositor.cpi_handle(), }, ), amount_b, - context.accounts.mint_b.decimals, + context.accounts.mint_b.decimals(), )?; // Mint the liquidity to user let authority_bump = context.bumps.pool_authority; let authority_seeds = &[ &context.accounts.pool_config.config.to_bytes(), - &context.accounts.mint_a.key().to_bytes(), - &context.accounts.mint_b.key().to_bytes(), + &context.accounts.mint_a.address().to_bytes(), + &context.accounts.mint_b.address().to_bytes(), AUTHORITY_SEED, &[authority_bump], ]; let signer_seeds = &[&authority_seeds[..]]; token_interface::mint_to( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), MintTo { - mint: context.accounts.liquidity_provider_mint.to_account_info(), - to: context.accounts.liquidity_provider_token.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + mint: context.accounts.liquidity_provider_mint.to_cpi_handle_mut(), + to: context.accounts.liquidity_provider_token.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, signer_seeds, ), @@ -250,49 +248,49 @@ pub fn handle_deposit_liquidity( } #[derive(Accounts)] -pub struct DepositLiquidityAccountConstraints<'info> { +pub struct DepositLiquidityAccountConstraints { #[account( seeds = [ pool_config.config.as_ref(), - pool_config.mint_a.key().as_ref(), - pool_config.mint_b.key().as_ref(), + pool_config.mint_a.as_ref(), + pool_config.mint_b.as_ref(), ], bump, - has_one = mint_a, - has_one = mint_b, )] - pub pool_config: Box>, + pub pool_config: Box>, /// CHECK: Read only authority #[account( seeds = [ pool_config.config.as_ref(), - mint_a.key().as_ref(), - mint_b.key().as_ref(), + mint_a.address().as_ref(), + mint_b.address().as_ref(), AUTHORITY_SEED, ], bump, )] - pub pool_authority: UncheckedAccount<'info>, + pub pool_authority: UncheckedAccount, /// The account paying for all rents - pub depositor: Signer<'info>, + pub depositor: Signer, #[account( mut, seeds = [ pool_config.config.as_ref(), - mint_a.key().as_ref(), - mint_b.key().as_ref(), + mint_a.address().as_ref(), + mint_b.address().as_ref(), LIQUIDITY_SEED, ], bump, )] - pub liquidity_provider_mint: Box>, + pub liquidity_provider_mint: Box>, - pub mint_a: Box>, + #[account(address = pool_config.mint_a)] + pub mint_a: Box>, - pub mint_b: Box>, + #[account(address = pool_config.mint_b)] + pub mint_b: Box>, #[account( mut, @@ -300,7 +298,7 @@ pub struct DepositLiquidityAccountConstraints<'info> { associated_token::authority = pool_authority, associated_token::token_program = token_program, )] - pub pool_a: Box>, + pub pool_a: Box>, #[account( mut, @@ -308,7 +306,7 @@ pub struct DepositLiquidityAccountConstraints<'info> { associated_token::authority = pool_authority, associated_token::token_program = token_program, )] - pub pool_b: Box>, + pub pool_b: Box>, #[account( init_if_needed, @@ -317,7 +315,7 @@ pub struct DepositLiquidityAccountConstraints<'info> { associated_token::authority = depositor, associated_token::token_program = token_program, )] - pub liquidity_provider_token: Box>, + pub liquidity_provider_token: Box>, #[account( mut, @@ -325,7 +323,7 @@ pub struct DepositLiquidityAccountConstraints<'info> { associated_token::authority = depositor, associated_token::token_program = token_program, )] - pub token_a: Box>, + pub token_a: Box>, #[account( mut, @@ -333,14 +331,14 @@ pub struct DepositLiquidityAccountConstraints<'info> { associated_token::authority = depositor, associated_token::token_program = token_program, )] - pub token_b: Box>, + pub token_b: Box>, /// The account paying for all rents #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, /// Solana ecosystem accounts - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/token-swap/anchor/programs/token-swap/src/instructions/initialize_config.rs b/finance/token-swap/anchor/programs/token-swap/src/instructions/initialize_config.rs index b5739d3e5..5d6413c95 100644 --- a/finance/token-swap/anchor/programs/token-swap/src/instructions/initialize_config.rs +++ b/finance/token-swap/anchor/programs/token-swap/src/instructions/initialize_config.rs @@ -7,13 +7,13 @@ use crate::{ }; pub fn handle_initialize_config( - context: Context, + context: &mut Context, fee: u16, admin_share_bps: u16, ) -> Result<()> { let bump = context.bumps.config; let config = &mut context.accounts.config; - config.admin = context.accounts.admin.key(); + config.admin = *context.accounts.admin.address(); config.fee = fee; config.admin_share_bps = admin_share_bps; config.bump = bump; @@ -23,7 +23,7 @@ pub fn handle_initialize_config( #[derive(Accounts)] #[instruction(fee: u16, admin_share_bps: u16)] -pub struct InitializeConfigAccountConstraints<'info> { +pub struct InitializeConfigAccountConstraints { #[account( init, payer = payer, @@ -33,16 +33,16 @@ pub struct InitializeConfigAccountConstraints<'info> { constraint = (fee as u64) < BASIS_POINTS_DIVISOR @ AmmError::InvalidFee, constraint = (admin_share_bps as u64) < BASIS_POINTS_DIVISOR @ AmmError::AdminShareTooHigh, )] - pub config: Account<'info, Config>, + pub config: BorshAccount, /// The admin of the AMM /// CHECK: Read only, delegatable creation - pub admin: UncheckedAccount<'info>, + pub admin: UncheckedAccount, /// The account paying for all rents #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, /// Solana ecosystem accounts - pub system_program: Program<'info, System>, + pub system_program: Program, } diff --git a/finance/token-swap/anchor/programs/token-swap/src/instructions/initialize_pool.rs b/finance/token-swap/anchor/programs/token-swap/src/instructions/initialize_pool.rs index 4a869c743..462b24f5f 100644 --- a/finance/token-swap/anchor/programs/token-swap/src/instructions/initialize_pool.rs +++ b/finance/token-swap/anchor/programs/token-swap/src/instructions/initialize_pool.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::mint; use anchor_spl::{ associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface}, @@ -10,69 +11,74 @@ use crate::{ state::{Config, PoolConfig}, }; -pub fn handle_initialize_pool(context: Context) -> Result<()> { +pub fn handle_initialize_pool( + context: &mut Context, +) -> Result<()> { let bump = context.bumps.pool_config; let pool_config = &mut context.accounts.pool_config; - pool_config.config = context.accounts.config.key(); - pool_config.mint_a = context.accounts.mint_a.key(); - pool_config.mint_b = context.accounts.mint_b.key(); + pool_config.config = *context.accounts.config.address(); + pool_config.mint_a = *context.accounts.mint_a.address(); + pool_config.mint_b = *context.accounts.mint_b.address(); pool_config.bump = bump; Ok(()) } #[derive(Accounts)] -pub struct InitializePoolAccountConstraints<'info> { +pub struct InitializePoolAccountConstraints { #[account( seeds = [CONFIG_SEED], bump, )] - pub config: Box>, + pub config: Box>, #[account( init, payer = payer, space = PoolConfig::DISCRIMINATOR.len() + PoolConfig::INIT_SPACE, seeds = [ - config.key().as_ref(), - mint_a.key().as_ref(), - mint_b.key().as_ref(), + config.address().as_ref(), + mint_a.address().as_ref(), + mint_b.address().as_ref(), ], bump, - constraint = mint_a.key() < mint_b.key() @ AmmError::InvalidMintOrder, + constraint = mint_a.address() < mint_b.address() @ AmmError::InvalidMintOrder, )] - pub pool_config: Box>, + pub pool_config: Box>, /// CHECK: Read only authority #[account( seeds = [ - config.key().as_ref(), - mint_a.key().as_ref(), - mint_b.key().as_ref(), + config.address().as_ref(), + mint_a.address().as_ref(), + mint_b.address().as_ref(), AUTHORITY_SEED, ], bump, )] - pub pool_authority: UncheckedAccount<'info>, + pub pool_authority: UncheckedAccount, #[account( init, payer = payer, seeds = [ - config.key().as_ref(), - mint_a.key().as_ref(), - mint_b.key().as_ref(), + config.address().as_ref(), + mint_a.address().as_ref(), + mint_b.address().as_ref(), LIQUIDITY_SEED, ], bump, mint::decimals = 6, mint::authority = pool_authority, + // Required when the token program is an `Interface`: without it the + // init CPI is rejected with InvalidArgument. + mint::token_program = token_program, )] - pub liquidity_provider_mint: Box>, + pub liquidity_provider_mint: Box>, - pub mint_a: Box>, + pub mint_a: Box>, - pub mint_b: Box>, + pub mint_b: Box>, #[account( init, @@ -81,7 +87,7 @@ pub struct InitializePoolAccountConstraints<'info> { associated_token::authority = pool_authority, associated_token::token_program = token_program, )] - pub pool_a: Box>, + pub pool_a: Box>, #[account( init, @@ -90,14 +96,14 @@ pub struct InitializePoolAccountConstraints<'info> { associated_token::authority = pool_authority, associated_token::token_program = token_program, )] - pub pool_b: Box>, + pub pool_b: Box>, /// The account paying for all rents #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, /// Solana ecosystem accounts - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/token-swap/anchor/programs/token-swap/src/instructions/mod.rs b/finance/token-swap/anchor/programs/token-swap/src/instructions/mod.rs index fd4e825c8..0fd5becc5 100644 --- a/finance/token-swap/anchor/programs/token-swap/src/instructions/mod.rs +++ b/finance/token-swap/anchor/programs/token-swap/src/instructions/mod.rs @@ -1,13 +1,13 @@ mod admin; +mod deposit_liquidity; mod initialize_config; mod initialize_pool; -mod deposit_liquidity; mod swap_tokens; mod withdraw_liquidity; pub use admin::*; +pub use deposit_liquidity::*; pub use initialize_config::*; pub use initialize_pool::*; -pub use deposit_liquidity::*; pub use swap_tokens::*; pub use withdraw_liquidity::*; diff --git a/finance/token-swap/anchor/programs/token-swap/src/instructions/swap_tokens.rs b/finance/token-swap/anchor/programs/token-swap/src/instructions/swap_tokens.rs index c9b4b2b8e..12ce59314 100644 --- a/finance/token-swap/anchor/programs/token-swap/src/instructions/swap_tokens.rs +++ b/finance/token-swap/anchor/programs/token-swap/src/instructions/swap_tokens.rs @@ -11,7 +11,7 @@ use crate::{ }; pub fn handle_swap_tokens( - context: Context, + context: &mut Context, input_is_token_a: bool, input_amount: u64, min_output_amount: u64, @@ -21,10 +21,10 @@ pub fn handle_swap_tokens( // for callers - their min_output_amount is computed against the requested // input, not the clamped one, so the trade could succeed with worse terms // than expected. - if input_is_token_a && input_amount > context.accounts.token_a.amount { + if input_is_token_a && input_amount > context.accounts.token_a.amount() { return err!(AmmError::InsufficientBalance); } - if !input_is_token_a && input_amount > context.accounts.token_b.amount { + if !input_is_token_a && input_amount > context.accounts.token_b.amount() { return err!(AmmError::InsufficientBalance); } // Split the trading fee between LPs and the admin. The full fee is taken @@ -54,12 +54,13 @@ pub fn handle_swap_tokens( // is u64), so the cast is safe - but use try_into anyway to make the // invariant explicit in the type system. let fee_amount: u64 = u64::try_from(fee_amount).map_err(|_| AmmError::MathOverflow)?; - let admin_portion: u64 = - u64::try_from(admin_portion).map_err(|_| AmmError::MathOverflow)?; + let admin_portion: u64 = u64::try_from(admin_portion).map_err(|_| AmmError::MathOverflow)?; // The LP portion stays in the pool reserves (as today - it's "less output // for the same input"), boosting the LP curve. The admin portion is // accounted for separately so it does *not* grow LP yield. - let taxed_input = input_amount.checked_sub(fee_amount).ok_or(AmmError::MathOverflow)?; + let taxed_input = input_amount + .checked_sub(fee_amount) + .ok_or(AmmError::MathOverflow)?; // Effective reserves = raw vault balance - admin's accumulated claim. // The constant-product curve runs on the LP-claimable portion only, so @@ -72,11 +73,11 @@ pub fn handle_swap_tokens( // but a raw `-` would wrap silently on a BPF release build if that invariant // were ever violated, handing the curve a giant effective reserve. let effective_pool_a = pool_a - .amount + .amount() .checked_sub(pool_config.admin_fees_owed_a) .ok_or(AmmError::MathOverflow)?; let effective_pool_b = pool_b - .amount + .amount() .checked_sub(pool_config.admin_fees_owed_b) .ok_or(AmmError::MathOverflow)?; @@ -124,10 +125,7 @@ pub fn handle_swap_tokens( // they're willing to accept (computed offchain at quote time). If the // pool shifted between quoting and landing, we revert rather than fill // at the worse rate. - require!( - output >= min_output_amount, - AmmError::SlippageExceeded - ); + require!(output >= min_output_amount, AmmError::SlippageExceeded); // Compute the invariant on the *effective* reserves before the trade. // Using raw balances here would let the admin's accumulated fees count @@ -144,8 +142,8 @@ pub fn handle_swap_tokens( // to_bytes() returns an owned [u8; 32] copy so there are no borrow conflicts. let authority_bump = context.bumps.pool_authority; let config_bytes = context.accounts.pool_config.config.to_bytes(); - let mint_a_bytes = context.accounts.mint_a.key().to_bytes(); - let mint_b_bytes = context.accounts.mint_b.key().to_bytes(); + let mint_a_bytes = context.accounts.mint_a.address().to_bytes(); + let mint_b_bytes = context.accounts.mint_b.address().to_bytes(); // Effects: update admin_fees before CPIs (Checks-Effects-Interactions). // The fee always comes off the input side, so the admin's claim accumulates @@ -177,58 +175,58 @@ pub fn handle_swap_tokens( if input_is_token_a { token_interface::transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.token_a.to_account_info(), - mint: context.accounts.mint_a.to_account_info(), - to: context.accounts.pool_a.to_account_info(), - authority: context.accounts.trader.to_account_info(), + from: context.accounts.token_a.to_cpi_handle_mut(), + mint: context.accounts.mint_a.to_cpi_handle(), + to: context.accounts.pool_a.to_cpi_handle_mut(), + authority: context.accounts.trader.cpi_handle(), }, ), input_amount, - context.accounts.mint_a.decimals, + context.accounts.mint_a.decimals(), )?; token_interface::transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.pool_b.to_account_info(), - mint: context.accounts.mint_b.to_account_info(), - to: context.accounts.token_b.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + from: context.accounts.pool_b.to_cpi_handle_mut(), + mint: context.accounts.mint_b.to_cpi_handle(), + to: context.accounts.token_b.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, signer_seeds, ), output, - context.accounts.mint_b.decimals, + context.accounts.mint_b.decimals(), )?; } else { token_interface::transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.pool_a.to_account_info(), - mint: context.accounts.mint_a.to_account_info(), - to: context.accounts.token_a.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + from: context.accounts.pool_a.to_cpi_handle_mut(), + mint: context.accounts.mint_a.to_cpi_handle(), + to: context.accounts.token_a.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, signer_seeds, ), output, - context.accounts.mint_a.decimals, + context.accounts.mint_a.decimals(), )?; token_interface::transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.token_b.to_account_info(), - mint: context.accounts.mint_b.to_account_info(), - to: context.accounts.pool_b.to_account_info(), - authority: context.accounts.trader.to_account_info(), + from: context.accounts.token_b.to_cpi_handle_mut(), + mint: context.accounts.mint_b.to_cpi_handle(), + to: context.accounts.pool_b.to_cpi_handle_mut(), + authority: context.accounts.trader.cpi_handle(), }, ), input_amount, - context.accounts.mint_b.decimals, + context.accounts.mint_b.decimals(), )?; } @@ -251,19 +249,22 @@ pub fn handle_swap_tokens( // the pool). // // u128 + checked: same overflow concern as the pre-trade invariant. - context.accounts.pool_a.reload()?; - context.accounts.pool_b.reload()?; + // v2's token accounts are zero-copy, so the balances below are read live + // from the runtime buffer, so there is nothing to reload. What the CPI can + // change is the schema, so re-run the load-time checks instead. + context.accounts.pool_a.revalidate_after_cpi()?; + context.accounts.pool_b.revalidate_after_cpi()?; let pool_config = &context.accounts.pool_config; let effective_pool_a_after = context .accounts .pool_a - .amount + .amount() .checked_sub(pool_config.admin_fees_owed_a) .ok_or(AmmError::MathOverflow)?; let effective_pool_b_after = context .accounts .pool_b - .amount + .amount() .checked_sub(pool_config.admin_fees_owed_b) .ok_or(AmmError::MathOverflow)?; let new_invariant = (effective_pool_a_after as u128) @@ -275,45 +276,42 @@ pub fn handle_swap_tokens( } #[derive(Accounts)] -pub struct SwapTokensAccountConstraints<'info> { - #[account( - seeds = [CONFIG_SEED], - bump, - )] - pub config: Account<'info, Config>, +pub struct SwapTokensAccountConstraints { + #[account(seeds = [CONFIG_SEED], + bump, address = pool_config.config)] + pub config: BorshAccount, #[account( mut, seeds = [ pool_config.config.as_ref(), - pool_config.mint_a.key().as_ref(), - pool_config.mint_b.key().as_ref(), + pool_config.mint_a.as_ref(), + pool_config.mint_b.as_ref(), ], bump, - has_one = config, - has_one = mint_a, - has_one = mint_b, )] - pub pool_config: Account<'info, PoolConfig>, + pub pool_config: BorshAccount, /// CHECK: Read only authority #[account( seeds = [ pool_config.config.as_ref(), - mint_a.key().as_ref(), - mint_b.key().as_ref(), + mint_a.address().as_ref(), + mint_b.address().as_ref(), AUTHORITY_SEED, ], bump, )] - pub pool_authority: UncheckedAccount<'info>, + pub pool_authority: UncheckedAccount, /// The account doing the swap - pub trader: Signer<'info>, + pub trader: Signer, - pub mint_a: Box>, + #[account(address = pool_config.mint_a)] + pub mint_a: Box>, - pub mint_b: Box>, + #[account(address = pool_config.mint_b)] + pub mint_b: Box>, #[account( mut, @@ -321,7 +319,7 @@ pub struct SwapTokensAccountConstraints<'info> { associated_token::authority = pool_authority, associated_token::token_program = token_program, )] - pub pool_a: Box>, + pub pool_a: Box>, #[account( mut, @@ -329,7 +327,7 @@ pub struct SwapTokensAccountConstraints<'info> { associated_token::authority = pool_authority, associated_token::token_program = token_program, )] - pub pool_b: Box>, + pub pool_b: Box>, #[account( init_if_needed, @@ -338,7 +336,7 @@ pub struct SwapTokensAccountConstraints<'info> { associated_token::authority = trader, associated_token::token_program = token_program, )] - pub token_a: Box>, + pub token_a: Box>, #[account( init_if_needed, @@ -347,14 +345,14 @@ pub struct SwapTokensAccountConstraints<'info> { associated_token::authority = trader, associated_token::token_program = token_program, )] - pub token_b: Box>, + pub token_b: Box>, /// The account paying for all rents #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, /// Solana ecosystem accounts - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs b/finance/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs index db8ef01e9..a6fac0e91 100644 --- a/finance/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs +++ b/finance/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs @@ -11,7 +11,7 @@ use crate::{ }; pub fn handle_withdraw_liquidity( - context: Context, + context: &mut Context, amount: u64, minimum_token_a_out: u64, minimum_token_b_out: u64, @@ -19,8 +19,8 @@ pub fn handle_withdraw_liquidity( let authority_bump = context.bumps.pool_authority; let authority_seeds = &[ &context.accounts.pool_config.config.to_bytes(), - &context.accounts.mint_a.key().to_bytes(), - &context.accounts.mint_b.key().to_bytes(), + &context.accounts.mint_a.address().to_bytes(), + &context.accounts.mint_b.address().to_bytes(), AUTHORITY_SEED, &[authority_bump], ]; @@ -36,13 +36,13 @@ pub fn handle_withdraw_liquidity( let effective_pool_a = context .accounts .pool_a - .amount + .amount() .checked_sub(pool_config.admin_fees_owed_a) .ok_or(AmmError::MathOverflow)?; let effective_pool_b = context .accounts .pool_b - .amount + .amount() .checked_sub(pool_config.admin_fees_owed_b) .ok_or(AmmError::MathOverflow)?; @@ -62,7 +62,7 @@ pub fn handle_withdraw_liquidity( // Both amounts are computed up-front (before the slippage checks) so // the LP gets a consistent error regardless of which side trips first, // and so we don't transfer one side then revert. - let divisor = (context.accounts.liquidity_provider_mint.supply as u128) + let divisor = (context.accounts.liquidity_provider_mint.supply() as u128) .checked_add(MINIMUM_LIQUIDITY as u128) .ok_or(AmmError::MathOverflow)?; let amount_a_u128 = (amount as u128) @@ -94,43 +94,43 @@ pub fn handle_withdraw_liquidity( // transfer_checked verifies the mint + decimals at the token program. token_interface::transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.pool_a.to_account_info(), - mint: context.accounts.mint_a.to_account_info(), - to: context.accounts.token_a.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + from: context.accounts.pool_a.to_cpi_handle_mut(), + mint: context.accounts.mint_a.to_cpi_handle(), + to: context.accounts.token_a.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, signer_seeds, ), amount_a, - context.accounts.mint_a.decimals, + context.accounts.mint_a.decimals(), )?; token_interface::transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.pool_b.to_account_info(), - mint: context.accounts.mint_b.to_account_info(), - to: context.accounts.token_b.to_account_info(), - authority: context.accounts.pool_authority.to_account_info(), + from: context.accounts.pool_b.to_cpi_handle_mut(), + mint: context.accounts.mint_b.to_cpi_handle(), + to: context.accounts.token_b.to_cpi_handle_mut(), + authority: context.accounts.pool_authority.cpi_handle(), }, signer_seeds, ), amount_b, - context.accounts.mint_b.decimals, + context.accounts.mint_b.decimals(), )?; // Burn the liquidity tokens // It will fail if the amount is invalid token_interface::burn( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), Burn { - mint: context.accounts.liquidity_provider_mint.to_account_info(), - from: context.accounts.liquidity_provider_token.to_account_info(), - authority: context.accounts.withdrawer.to_account_info(), + mint: context.accounts.liquidity_provider_mint.to_cpi_handle_mut(), + from: context.accounts.liquidity_provider_token.to_cpi_handle_mut(), + authority: context.accounts.withdrawer.cpi_handle(), }, ), amount, @@ -140,56 +140,54 @@ pub fn handle_withdraw_liquidity( } #[derive(Accounts)] -pub struct WithdrawLiquidityAccountConstraints<'info> { +pub struct WithdrawLiquidityAccountConstraints { #[account( seeds = [CONFIG_SEED], bump, )] - pub config: Account<'info, Config>, + pub config: BorshAccount, #[account( seeds = [ pool_config.config.as_ref(), - pool_config.mint_a.key().as_ref(), - pool_config.mint_b.key().as_ref(), + pool_config.mint_a.as_ref(), + pool_config.mint_b.as_ref(), ], bump, - has_one = mint_a, - has_one = mint_b, )] - pub pool_config: Account<'info, PoolConfig>, + pub pool_config: BorshAccount, /// CHECK: Read only authority #[account( seeds = [ pool_config.config.as_ref(), - mint_a.key().as_ref(), - mint_b.key().as_ref(), + mint_a.address().as_ref(), + mint_b.address().as_ref(), AUTHORITY_SEED, ], bump, )] - pub pool_authority: UncheckedAccount<'info>, + pub pool_authority: UncheckedAccount, - pub withdrawer: Signer<'info>, + pub withdrawer: Signer, #[account( mut, seeds = [ pool_config.config.as_ref(), - mint_a.key().as_ref(), - mint_b.key().as_ref(), + mint_a.address().as_ref(), + mint_b.address().as_ref(), LIQUIDITY_SEED, ], bump, )] - pub liquidity_provider_mint: Box>, + pub liquidity_provider_mint: Box>, - #[account(mut)] - pub mint_a: Box>, + #[account(mut, address = pool_config.mint_a)] + pub mint_a: Box>, - #[account(mut)] - pub mint_b: Box>, + #[account(mut, address = pool_config.mint_b)] + pub mint_b: Box>, #[account( mut, @@ -197,7 +195,7 @@ pub struct WithdrawLiquidityAccountConstraints<'info> { associated_token::authority = pool_authority, associated_token::token_program = token_program, )] - pub pool_a: Box>, + pub pool_a: Box>, #[account( mut, @@ -205,7 +203,7 @@ pub struct WithdrawLiquidityAccountConstraints<'info> { associated_token::authority = pool_authority, associated_token::token_program = token_program, )] - pub pool_b: Box>, + pub pool_b: Box>, #[account( mut, @@ -213,7 +211,7 @@ pub struct WithdrawLiquidityAccountConstraints<'info> { associated_token::authority = withdrawer, associated_token::token_program = token_program, )] - pub liquidity_provider_token: Box>, + pub liquidity_provider_token: Box>, #[account( init_if_needed, @@ -222,7 +220,7 @@ pub struct WithdrawLiquidityAccountConstraints<'info> { associated_token::authority = withdrawer, associated_token::token_program = token_program, )] - pub token_a: Box>, + pub token_a: Box>, #[account( init_if_needed, @@ -231,14 +229,14 @@ pub struct WithdrawLiquidityAccountConstraints<'info> { associated_token::authority = withdrawer, associated_token::token_program = token_program, )] - pub token_b: Box>, + pub token_b: Box>, /// The account paying for all rents #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, /// Solana ecosystem accounts - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } diff --git a/finance/token-swap/anchor/programs/token-swap/src/lib.rs b/finance/token-swap/anchor/programs/token-swap/src/lib.rs index 780516598..df2acb44c 100644 --- a/finance/token-swap/anchor/programs/token-swap/src/lib.rs +++ b/finance/token-swap/anchor/programs/token-swap/src/lib.rs @@ -1,45 +1,44 @@ use anchor_lang::prelude::*; mod constants; -mod errors; -mod instructions; +pub mod errors; +pub mod instructions; mod state; +// The `#[derive(Accounts)]` client modules are generated beside their structs, +// and `#[program]` resolves them at `super::`, the crate root. Re-exporting +// here rather than inside the module puts them where it looks. +use instructions::*; + declare_id!("GahM6PrXesrBkHiGJ5no4EskLNnVBCaSwVKbM4UtzyK6"); #[program] pub mod swap_example { - pub use super::instructions::*; use super::*; pub fn initialize_config( - context: Context, + context: &mut Context, fee: u16, admin_share_bps: u16, ) -> Result<()> { instructions::handle_initialize_config(context, fee, admin_share_bps) } - pub fn initialize_pool(context: Context) -> Result<()> { + pub fn initialize_pool(context: &mut Context) -> Result<()> { instructions::handle_initialize_pool(context) } pub fn deposit_liquidity( - context: Context, + context: &mut Context, amount_a: u64, amount_b: u64, minimum_lp_tokens_out: u64, ) -> Result<()> { - instructions::handle_deposit_liquidity( - context, - amount_a, - amount_b, - minimum_lp_tokens_out, - ) + instructions::handle_deposit_liquidity(context, amount_a, amount_b, minimum_lp_tokens_out) } pub fn withdraw_liquidity( - context: Context, + context: &mut Context, amount: u64, minimum_token_a_out: u64, minimum_token_b_out: u64, @@ -53,20 +52,15 @@ pub mod swap_example { } pub fn swap_tokens( - context: Context, + context: &mut Context, input_is_token_a: bool, input_amount: u64, min_output_amount: u64, ) -> Result<()> { - instructions::handle_swap_tokens( - context, - input_is_token_a, - input_amount, - min_output_amount, - ) + instructions::handle_swap_tokens(context, input_is_token_a, input_amount, min_output_amount) } - pub fn claim_admin_fees(context: Context) -> Result<()> { + pub fn claim_admin_fees(context: &mut Context) -> Result<()> { instructions::handle_claim_admin_fees(context) } } diff --git a/finance/token-swap/anchor/programs/token-swap/src/state/config.rs b/finance/token-swap/anchor/programs/token-swap/src/state/config.rs index ff005d9d0..06a7dfc97 100644 --- a/finance/token-swap/anchor/programs/token-swap/src/state/config.rs +++ b/finance/token-swap/anchor/programs/token-swap/src/state/config.rs @@ -8,11 +8,11 @@ use anchor_lang::prelude::*; /// program-level config is global by construction). Parameterising the config /// by an `id` was leftover complexity from the original example; removing it /// makes the onchain layout simpler and matches realistic deployment. -#[account] +#[account(borsh)] #[derive(Default, InitSpace)] pub struct Config { /// Account that has admin authority over the AMM. - pub admin: Pubkey, + pub admin: Address, /// The trading fee taken on each swap, in basis points (out of 10_000). /// diff --git a/finance/token-swap/anchor/programs/token-swap/src/state/pool_config.rs b/finance/token-swap/anchor/programs/token-swap/src/state/pool_config.rs index d39a93e59..f5a443624 100644 --- a/finance/token-swap/anchor/programs/token-swap/src/state/pool_config.rs +++ b/finance/token-swap/anchor/programs/token-swap/src/state/pool_config.rs @@ -13,19 +13,19 @@ use anchor_lang::prelude::*; /// `admin_fees_owed_b`). Those fees physically sit in the existing `pool_a` / /// `pool_b` reserves; the accumulators are a *virtual* obligation against /// those balances. LP-facing math (deposit, withdraw, swap curve) uses -/// `pool_X.amount - admin_fees_owed_X` so the admin's owed slice is not +/// `pool_X.amount() - admin_fees_owed_X` so the admin's owed slice is not /// counted toward LP yield. -#[account] +#[account(borsh)] #[derive(Default, InitSpace)] pub struct PoolConfig { /// Address of the parent `Config` account this pool belongs to. - pub config: Pubkey, + pub config: Address, /// Mint of token A. - pub mint_a: Pubkey, + pub mint_a: Address, /// Mint of token B. - pub mint_b: Pubkey, + pub mint_b: Address, /// Admin's accumulated fee claim on token A, in base units. Sits /// physically in `pool_a` but is excluded from the LP curve and from diff --git a/finance/token-swap/anchor/programs/token-swap/tests/test_swap.rs b/finance/token-swap/anchor/programs/token-swap/tests/test_swap.rs index 14820efa3..8b2539e3b 100644 --- a/finance/token-swap/anchor/programs/token-swap/tests/test_swap.rs +++ b/finance/token-swap/anchor/programs/token-swap/tests/test_swap.rs @@ -1,38 +1,41 @@ +use swap_example::errors::AmmError; + use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, solana_kite::{ create_associated_token_account, create_token_mint, create_wallet, - get_token_account_balance, mint_tokens_to_token_account, send_transaction_from_instructions, + get_token_account_balance, mint_tokens_to_token_account, + send_transaction_from_instructions, }, solana_signer::Signer, }; -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn ata_program_id() -> Pubkey { +fn ata_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - let (ata, _bump) = Pubkey::find_program_address( +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + let (ata, _bump) = Address::find_program_address( &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &ata_program_id(), ); ata } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = swap_example::id(); let mut svm = LiteSVM::new(); @@ -44,7 +47,7 @@ fn setup() -> (LiteSVM, Pubkey, Keypair) { } /// Ensure mint_a < mint_b by pubkey ordering (the program may require this). -fn ordered_mints(svm: &mut LiteSVM, authority: &Keypair, decimals: u8) -> (Pubkey, Pubkey) { +fn ordered_mints(svm: &mut LiteSVM, authority: &Keypair, decimals: u8) -> (Address, Address) { loop { let a = create_token_mint(svm, authority, decimals, None).unwrap(); let b = create_token_mint(svm, authority, decimals, None).unwrap(); @@ -56,20 +59,20 @@ fn ordered_mints(svm: &mut LiteSVM, authority: &Keypair, decimals: u8) -> (Pubke struct TestSetup { svm: LiteSVM, - program_id: Pubkey, + program_id: Address, payer: Keypair, admin: Keypair, - config_key: Pubkey, - mint_a: Pubkey, - mint_b: Pubkey, - pool_config_key: Pubkey, - pool_authority: Pubkey, - liquidity_provider_mint: Pubkey, - pool_a: Pubkey, - pool_b: Pubkey, - holder_account_a: Pubkey, - holder_account_b: Pubkey, - liquidity_account: Pubkey, + config_key: Address, + mint_a: Address, + mint_b: Address, + pool_config_key: Address, + pool_authority: Address, + liquidity_provider_mint: Address, + pool_a: Address, + pool_b: Address, + holder_account_a: Address, + holder_account_b: Address, + liquidity_account: Address, } fn full_setup() -> TestSetup { @@ -87,12 +90,12 @@ fn full_setup() -> TestSetup { // Derive the singleton Config PDA (seeds = [b"config"]). One config per // deployed program. - let (config_key, _) = Pubkey::find_program_address(&[b"config"], &program_id); - let (pool_config_key, _) = Pubkey::find_program_address( + let (config_key, _) = Address::find_program_address(&[b"config"], &program_id); + let (pool_config_key, _) = Address::find_program_address( &[config_key.as_ref(), mint_a.as_ref(), mint_b.as_ref()], &program_id, ); - let (pool_authority, _) = Pubkey::find_program_address( + let (pool_authority, _) = Address::find_program_address( &[ config_key.as_ref(), mint_a.as_ref(), @@ -101,7 +104,7 @@ fn full_setup() -> TestSetup { ], &program_id, ); - let (liquidity_provider_mint, _) = Pubkey::find_program_address( + let (liquidity_provider_mint, _) = Address::find_program_address( &[ config_key.as_ref(), mint_a.as_ref(), @@ -121,18 +124,24 @@ fn full_setup() -> TestSetup { let holder_account_b = create_associated_token_account(&mut svm, &admin.pubkey(), &mint_b, &payer).unwrap(); - mint_tokens_to_token_account(&mut svm, &mint_a, &holder_account_a, minted_amount, &admin).unwrap(); - mint_tokens_to_token_account(&mut svm, &mint_b, &holder_account_b, minted_amount, &admin).unwrap(); + mint_tokens_to_token_account(&mut svm, &mint_a, &holder_account_a, minted_amount, &admin) + .unwrap(); + mint_tokens_to_token_account(&mut svm, &mint_b, &holder_account_b, minted_amount, &admin) + .unwrap(); // Create AMM let initialize_config_ix = Instruction::new_with_bytes( program_id, - &swap_example::instruction::InitializeConfig { fee, admin_share_bps }.data(), + &swap_example::instruction::InitializeConfig { + fee, + admin_share_bps, + } + .data(), swap_example::accounts::InitializeConfigAccountConstraints { config: config_key, admin: admin.pubkey(), payer: payer.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -160,7 +169,7 @@ fn full_setup() -> TestSetup { payer: payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -191,6 +200,20 @@ fn full_setup() -> TestSetup { } } +/// v2's `#[error_code]` does not log the variant name, so a failed transaction +/// carries only the numeric custom code: the enum discriminant plus anchor's +/// default 6000 offset. Assert on that rather than on a name that is no longer +/// in the logs. +const ANCHOR_ERROR_OFFSET: u32 = 6000; + +fn assert_program_error(message: &str, expected: AmmError, name: &str) { + let code = expected as u32 + ANCHOR_ERROR_OFFSET; + assert!( + message.contains(&format!("Custom({code})")), + "expected {name} (Custom({code})), got: {message}" + ); +} + #[test] fn test_initialize_config() { let (mut svm, program_id, payer) = setup(); @@ -198,16 +221,20 @@ fn test_initialize_config() { let admin_share_bps: u16 = 1667; let admin = Keypair::new(); - let (config_key, _) = Pubkey::find_program_address(&[b"config"], &program_id); + let (config_key, _) = Address::find_program_address(&[b"config"], &program_id); let initialize_config_ix = Instruction::new_with_bytes( program_id, - &swap_example::instruction::InitializeConfig { fee, admin_share_bps }.data(), + &swap_example::instruction::InitializeConfig { + fee, + admin_share_bps, + } + .data(), swap_example::accounts::InitializeConfigAccountConstraints { config: config_key, admin: admin.pubkey(), payer: payer.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -257,7 +284,7 @@ fn test_deposit_liquidity() { payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -304,7 +331,7 @@ fn test_swap_a_to_b() { payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -342,7 +369,7 @@ fn test_swap_a_to_b() { payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -391,7 +418,7 @@ fn test_withdraw_liquidity() { payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -433,7 +460,7 @@ fn test_withdraw_liquidity() { payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -452,7 +479,12 @@ fn test_withdraw_liquidity() { /// Helper: do a deposit and one A->B swap on top of `full_setup`. /// Returns the swap input amount (token A base units) for fee-arithmetic checks. -fn deposit_and_swap_a_to_b(ts: &mut TestSetup, deposit_a: u64, deposit_b: u64, swap_in_a: u64) -> u64 { +fn deposit_and_swap_a_to_b( + ts: &mut TestSetup, + deposit_a: u64, + deposit_b: u64, + swap_in_a: u64, +) -> u64 { let deposit_ix = Instruction::new_with_bytes( ts.program_id, &swap_example::instruction::DepositLiquidity { @@ -477,7 +509,7 @@ fn deposit_and_swap_a_to_b(ts: &mut TestSetup, deposit_a: u64, deposit_b: u64, s payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -511,7 +543,7 @@ fn deposit_and_swap_a_to_b(ts: &mut TestSetup, deposit_a: u64, deposit_b: u64, s payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -572,7 +604,7 @@ fn swap_a_to_b(ts: &mut TestSetup, input_amount: u64) { payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -600,10 +632,8 @@ fn test_claim_admin_fees() { assert!(expected_admin_a > 0, "expected admin portion > 0"); // ---- Phase 1: first claim transfers the accumulated A-side fees ---- - let admin_balance_a_before = - get_token_account_balance(&ts.svm, &ts.holder_account_a).unwrap(); - let admin_balance_b_before = - get_token_account_balance(&ts.svm, &ts.holder_account_b).unwrap(); + let admin_balance_a_before = get_token_account_balance(&ts.svm, &ts.holder_account_a).unwrap(); + let admin_balance_b_before = get_token_account_balance(&ts.svm, &ts.holder_account_b).unwrap(); let claim_ix_first = claim_admin_fees_ix(&ts); send_transaction_from_instructions( @@ -614,10 +644,8 @@ fn test_claim_admin_fees() { ) .unwrap(); - let admin_balance_a_after = - get_token_account_balance(&ts.svm, &ts.holder_account_a).unwrap(); - let admin_balance_b_after = - get_token_account_balance(&ts.svm, &ts.holder_account_b).unwrap(); + let admin_balance_a_after = get_token_account_balance(&ts.svm, &ts.holder_account_a).unwrap(); + let admin_balance_b_after = get_token_account_balance(&ts.svm, &ts.holder_account_b).unwrap(); assert_eq!( admin_balance_a_after - admin_balance_a_before, @@ -640,8 +668,7 @@ fn test_claim_admin_fees() { let expected_admin_a_2 = fee_amount_2 * 1667 / 10_000; assert!(expected_admin_a_2 > 0, "expected second admin portion > 0"); - let balance_a_pre_claim_2 = - get_token_account_balance(&ts.svm, &ts.holder_account_a).unwrap(); + let balance_a_pre_claim_2 = get_token_account_balance(&ts.svm, &ts.holder_account_a).unwrap(); // Bump the blockhash so this claim-ix tx isn't byte-identical to the // earlier one (same accounts + same payload → same signature → @@ -656,8 +683,7 @@ fn test_claim_admin_fees() { ) .unwrap(); - let balance_a_post_claim_2 = - get_token_account_balance(&ts.svm, &ts.holder_account_a).unwrap(); + let balance_a_post_claim_2 = get_token_account_balance(&ts.svm, &ts.holder_account_a).unwrap(); assert_eq!( balance_a_post_claim_2 - balance_a_pre_claim_2, expected_admin_a_2, @@ -700,10 +726,7 @@ fn test_claim_admin_fees() { "claim with both accumulators at zero must revert" ); let err_msg = format!("{:?}", result.unwrap_err()); - assert!( - err_msg.contains("NothingToClaim") || err_msg.contains("0x1777") || err_msg.contains("6007"), - "expected NothingToClaim error, got: {err_msg}" - ); + assert_program_error(&err_msg, AmmError::NothingToClaim, "NothingToClaim"); // Balance unchanged - the revert rolled back any partial state. let balance_a_after_third_claim = @@ -750,7 +773,7 @@ fn test_claim_admin_fees_rejects_non_admin() { ); // Should fail because the signer (attacker) does not match Config.admin - // (enforced by Anchor's `has_one = admin` constraint). + // (enforced by Anchor's `address = config.admin` constraint). let result = send_transaction_from_instructions( &mut ts.svm, vec![claim_ix], @@ -791,7 +814,7 @@ fn deposit_ix(ts: &TestSetup, amount_a: u64, amount_b: u64) -> Instruction { payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) @@ -847,7 +870,10 @@ fn test_deposit_into_funded_pool_at_correct_ratio() { 2_000_000, "pool_b should grow by the full requested amount_b" ); - assert!(lp_after > lp_before, "LP tokens should be minted to depositor"); + assert!( + lp_after > lp_before, + "LP tokens should be minted to depositor" + ); } /// Test B: depositor offers more token B than the ratio needs. `amount_b` @@ -964,7 +990,7 @@ fn test_deposit_after_swap_uses_shifted_effective_ratio() { payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -980,8 +1006,8 @@ fn test_deposit_after_swap_uses_shifted_effective_ratio() { let pool_b_after_swap = get_token_account_balance(&ts.svm, &ts.pool_b).unwrap(); let admin_owed_a: u64 = { let account = ts.svm.get_account(&ts.pool_config_key).unwrap(); - // PoolConfig layout: 8-byte anchor discriminator, then Pubkey config - // (32), Pubkey mint_a (32), Pubkey mint_b (32), u64 + // PoolConfig layout: 8-byte anchor discriminator, then Address config + // (32), Address mint_a (32), Address mint_b (32), u64 // admin_fees_owed_a (8), u64 admin_fees_owed_b (8), u8 bump. let start = 8 + 32 * 3; u64::from_le_bytes(account.data[start..start + 8].try_into().unwrap()) @@ -999,8 +1025,8 @@ fn test_deposit_after_swap_uses_shifted_effective_ratio() { // ratio so both sides should be pulled in full. Pick a base of 1M on the // B side and compute the matching A side from effective reserves. let deposit_b = 1_000_000u64; - let deposit_a = ((deposit_b as u128) * (effective_pool_a as u128) - / (effective_pool_b as u128)) as u64; + let deposit_a = + ((deposit_b as u128) * (effective_pool_a as u128) / (effective_pool_b as u128)) as u64; let holder_a_before = get_token_account_balance(&ts.svm, &ts.holder_account_a).unwrap(); let holder_b_before = get_token_account_balance(&ts.svm, &ts.holder_account_b).unwrap(); @@ -1121,8 +1147,8 @@ fn test_lp_mint_after_swap_uses_effective_reserves() { // Deposit at exactly the effective ratio. Pick deposit_b, derive deposit_a. let deposit_b: u64 = 1_000_000; - let deposit_a = ((deposit_b as u128) * (effective_pool_a as u128) - / (effective_pool_b as u128)) as u64; + let deposit_a = + ((deposit_b as u128) * (effective_pool_a as u128) / (effective_pool_b as u128)) as u64; // Expected LP minted = min(a*supply/pool_a, b*supply/pool_b) using the // *clamped* (a, b) the program actually transfers. After clamp at the @@ -1131,12 +1157,12 @@ fn test_lp_mint_after_swap_uses_effective_reserves() { // We pass `deposit_a` exactly, so amount_b_required = deposit_a * pool_b // / pool_a, which rounds down to ≤ deposit_b. The program then uses // (deposit_a, amount_b_required). Compute the expected LP from that. - let amount_b_used = ((deposit_a as u128) * (effective_pool_b as u128) - / (effective_pool_a as u128)) as u64; - let expected_liquidity_from_a = (deposit_a as u128) * (total_supply_before_second as u128) - / (effective_pool_a as u128); - let expected_liquidity_from_b = (amount_b_used as u128) * (total_supply_before_second as u128) - / (effective_pool_b as u128); + let amount_b_used = + ((deposit_a as u128) * (effective_pool_b as u128) / (effective_pool_a as u128)) as u64; + let expected_liquidity_from_a = + (deposit_a as u128) * (total_supply_before_second as u128) / (effective_pool_a as u128); + let expected_liquidity_from_b = + (amount_b_used as u128) * (total_supply_before_second as u128) / (effective_pool_b as u128); let expected_liquidity = expected_liquidity_from_a.min(expected_liquidity_from_b) as u64; let lp_before = get_token_account_balance(&ts.svm, &ts.liquidity_account).unwrap(); @@ -1178,7 +1204,7 @@ fn swap_a_to_b_ix(ts: &TestSetup, input_amount: u64, min_output_amount: u64) -> payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) @@ -1214,7 +1240,7 @@ fn deposit_ix_with_min_lp( payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) @@ -1251,7 +1277,7 @@ fn withdraw_ix_with_min( payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) @@ -1294,10 +1320,7 @@ fn test_swap_reverts_when_output_below_min() { &ts.payer.pubkey(), ); let err = format!("{:?}", result.expect_err("must revert")); - assert!( - err.contains("SlippageExceeded"), - "expected SlippageExceeded, got: {err}" - ); + assert_program_error(&err, AmmError::SlippageExceeded, "SlippageExceeded"); } /// Slippage test: a deposit with `minimum_lp_tokens_out` strictly higher @@ -1321,8 +1344,7 @@ fn test_deposit_reverts_when_lp_below_min() { let achievable_lp = lp_from_a.min(lp_from_b) as u64; // Require *strictly more* than that - the deposit must revert. - let strict_ix = - deposit_ix_with_min_lp(&ts, 4_000_000, 1_000_000, achievable_lp + 1); + let strict_ix = deposit_ix_with_min_lp(&ts, 4_000_000, 1_000_000, achievable_lp + 1); let result = send_transaction_from_instructions( &mut ts.svm, vec![strict_ix], @@ -1330,10 +1352,7 @@ fn test_deposit_reverts_when_lp_below_min() { &ts.payer.pubkey(), ); let err = format!("{:?}", result.expect_err("must revert")); - assert!( - err.contains("DepositBelowMinimum"), - "expected DepositBelowMinimum, got: {err}" - ); + assert_program_error(&err, AmmError::DepositBelowMinimum, "DepositBelowMinimum"); // Sanity: the same deposit with `achievable_lp` as the floor succeeds. let ok_ix = deposit_ix_with_min_lp(&ts, 4_000_000, 1_000_000, achievable_lp); @@ -1366,10 +1385,7 @@ fn test_withdraw_reverts_when_below_min() { &ts.payer.pubkey(), ); let err = format!("{:?}", result.expect_err("must revert (A side)")); - assert!( - err.contains("WithdrawalBelowMinimum"), - "expected WithdrawalBelowMinimum (A side), got: {err}" - ); + assert_program_error(&err, AmmError::WithdrawalBelowMinimum, "WithdrawalBelowMinimum"); // Same on the B side. let strict_ix_b = withdraw_ix_with_min(&ts, lp / 2, 0, 4_000_000); @@ -1380,10 +1396,7 @@ fn test_withdraw_reverts_when_below_min() { &ts.payer.pubkey(), ); let err_b = format!("{:?}", result_b.expect_err("must revert (B side)")); - assert!( - err_b.contains("WithdrawalBelowMinimum"), - "expected WithdrawalBelowMinimum (B side), got: {err_b}" - ); + assert_program_error(&err_b, AmmError::WithdrawalBelowMinimum, "WithdrawalBelowMinimum"); } /// Slippage test: passing `min_output_amount = 0` is the explicit @@ -1431,7 +1444,7 @@ fn swap_b_to_a_ix(ts: &TestSetup, input_amount: u64, min_output_amount: u64) -> payer: ts.payer.pubkey(), token_program: token_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ) diff --git a/finance/vault-strategy/anchor/app/README.md b/finance/vault-strategy/anchor/app/README.md index 528983423..d85b472af 100644 --- a/finance/vault-strategy/anchor/app/README.md +++ b/finance/vault-strategy/anchor/app/README.md @@ -65,7 +65,7 @@ client mirrors it call-for-call. ## Where the IDL comes from `src/idl/vault_strategy.json` is generated from the program source with -`anchor idl build` (Anchor 1.1.2, the version CI installs) and committed. If you change +`anchor idl build` (Anchor 2.0.0-rc.1, the version CI installs) and committed. If you change the program, regenerate it: ```sh diff --git a/finance/vault-strategy/anchor/programs/mock-swap-router/Cargo.toml b/finance/vault-strategy/anchor/programs/mock-swap-router/Cargo.toml index b9968e2d7..c446a4ddc 100644 --- a/finance/vault-strategy/anchor/programs/mock-swap-router/Cargo.toml +++ b/finance/vault-strategy/anchor/programs/mock-swap-router/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/initialize_router.rs b/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/initialize_router.rs index 125ea422f..a6d24f719 100644 --- a/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/initialize_router.rs +++ b/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/initialize_router.rs @@ -4,11 +4,11 @@ use anchor_spl::token_interface::{Mint, TokenInterface}; use crate::state::RouterConfig; #[derive(Accounts)] -pub struct InitializeRouterAccountConstraints<'info> { +pub struct InitializeRouterAccountConstraints { #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, - pub usdc_mint: InterfaceAccount<'info, Mint>, + pub usdc_mint: InterfaceAccount, #[account( init, @@ -17,27 +17,27 @@ pub struct InitializeRouterAccountConstraints<'info> { seeds = [b"router_config"], bump )] - pub router_config: Account<'info, RouterConfig>, + pub router_config: BorshAccount, /// CHECK: PDA used as mint authority only - no data stored #[account( seeds = [b"router_authority"], bump )] - pub router_authority: UncheckedAccount<'info>, + pub router_authority: UncheckedAccount, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } pub fn handle_initialize_router( - context: Context, - _usdc_mint: Pubkey, + context: &mut Context, + _usdc_mint: Address, ) -> Result<()> { - context.accounts.router_config.set_inner(RouterConfig { - authority: context.accounts.authority.key(), - usdc_mint: context.accounts.usdc_mint.key(), + *context.accounts.router_config = RouterConfig { + authority: *context.accounts.authority.address(), + usdc_mint: *context.accounts.usdc_mint.address(), bump: context.bumps.router_config, - }); + }; Ok(()) } diff --git a/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/set_rate.rs b/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/set_rate.rs index 09c5d9548..2de07b58b 100644 --- a/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/set_rate.rs +++ b/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/set_rate.rs @@ -7,36 +7,33 @@ use anchor_spl::{ use crate::state::{AssetRate, RouterConfig}; #[derive(Accounts)] -pub struct SetRateAccountConstraints<'info> { - #[account(mut)] - pub authority: Signer<'info>, +pub struct SetRateAccountConstraints { + #[account(mut, address = router_config.authority)] + pub authority: Signer, - #[account( - has_one = authority, - seeds = [b"router_config"], - bump = router_config.bump - )] - pub router_config: Account<'info, RouterConfig>, + #[account(seeds = [b"router_config"], + bump = router_config.bump)] + pub router_config: BorshAccount, - pub asset_mint: InterfaceAccount<'info, Mint>, + pub asset_mint: InterfaceAccount, - pub usdc_mint: InterfaceAccount<'info, Mint>, + pub usdc_mint: InterfaceAccount, #[account( init_if_needed, payer = authority, space = AssetRate::DISCRIMINATOR.len() + AssetRate::INIT_SPACE, - seeds = [b"rate", asset_mint.key().as_ref()], + seeds = [b"rate", asset_mint.address().as_ref()], bump )] - pub asset_rate: Account<'info, AssetRate>, + pub asset_rate: BorshAccount, /// CHECK: PDA used as mint authority only #[account( seeds = [b"router_authority"], bump )] - pub router_authority: UncheckedAccount<'info>, + pub router_authority: UncheckedAccount, #[account( init_if_needed, @@ -45,22 +42,22 @@ pub struct SetRateAccountConstraints<'info> { associated_token::authority = router_authority, associated_token::token_program = token_program )] - pub router_usdc_treasury: InterfaceAccount<'info, TokenAccount>, + pub router_usdc_treasury: InterfaceAccount, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } pub fn handle_set_rate( - context: Context, - _mint: Pubkey, + context: &mut Context, + _mint: Address, usdc_per_token: u64, ) -> Result<()> { - context.accounts.asset_rate.set_inner(AssetRate { - mint: context.accounts.asset_mint.key(), + *context.accounts.asset_rate = AssetRate { + mint: *context.accounts.asset_mint.address(), usdc_per_token, bump: context.bumps.asset_rate, - }); + }; Ok(()) } diff --git a/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/swap_asset_for_usdc.rs b/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/swap_asset_for_usdc.rs index 66d33f376..4145db113 100644 --- a/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/swap_asset_for_usdc.rs +++ b/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/swap_asset_for_usdc.rs @@ -10,25 +10,25 @@ use crate::error::RouterError; use crate::state::{AssetRate, RouterConfig}; #[derive(Accounts)] -pub struct SwapAssetForUsdcAccountConstraints<'info> { - pub caller: Signer<'info>, +pub struct SwapAssetForUsdcAccountConstraints { + pub caller: Signer, #[account( seeds = [b"router_config"], bump = router_config.bump )] - pub router_config: Account<'info, RouterConfig>, + pub router_config: BorshAccount, #[account( - constraint = asset_rate.mint == asset_mint.key() @ RouterError::InvalidAssetMint + constraint = asset_rate.mint == *asset_mint.address() @ RouterError::InvalidAssetMint )] - pub asset_rate: Account<'info, AssetRate>, + pub asset_rate: BorshAccount, - #[account(constraint = usdc_mint.key() == router_config.usdc_mint @ RouterError::WrongUsdcMint)] - pub usdc_mint: Box>, + #[account(constraint = *usdc_mint.address() == router_config.usdc_mint @ RouterError::WrongUsdcMint)] + pub usdc_mint: Box>, #[account(mut)] - pub asset_mint: Box>, + pub asset_mint: Box>, /// Caller's asset token account - asset tokens are burned from here #[account( @@ -37,7 +37,7 @@ pub struct SwapAssetForUsdcAccountConstraints<'info> { associated_token::authority = caller, associated_token::token_program = token_program )] - pub caller_asset_account: Box>, + pub caller_asset_account: Box>, /// Caller's USDC account - receives the USDC #[account( @@ -46,7 +46,7 @@ pub struct SwapAssetForUsdcAccountConstraints<'info> { associated_token::authority = caller, associated_token::token_program = token_program )] - pub caller_usdc_account: Box>, + pub caller_usdc_account: Box>, /// Router's USDC treasury - sends the USDC #[account( @@ -55,22 +55,22 @@ pub struct SwapAssetForUsdcAccountConstraints<'info> { associated_token::authority = router_authority, associated_token::token_program = token_program )] - pub router_usdc_treasury: Box>, + pub router_usdc_treasury: Box>, /// CHECK: PDA used as treasury authority - validated by seeds constraint #[account( seeds = [b"router_authority"], bump )] - pub router_authority: UncheckedAccount<'info>, + pub router_authority: UncheckedAccount, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } pub fn handle_swap_asset_for_usdc( - context: Context, + context: &mut Context, asset_amount_in: u64, minimum_usdc_out: u64, ) -> Result<()> { @@ -87,12 +87,12 @@ pub fn handle_swap_asset_for_usdc( // Burn asset tokens from caller let burn_accounts = Burn { - mint: context.accounts.asset_mint.to_account_info(), - from: context.accounts.caller_asset_account.to_account_info(), - authority: context.accounts.caller.to_account_info(), + mint: context.accounts.asset_mint.to_cpi_handle_mut(), + from: context.accounts.caller_asset_account.to_cpi_handle_mut(), + authority: context.accounts.caller.cpi_handle(), }; burn( - CpiContext::new(context.accounts.token_program.key(), burn_accounts), + CpiContext::new(context.accounts.token_program.address(), burn_accounts), asset_amount_in, )?; @@ -101,19 +101,19 @@ pub fn handle_swap_asset_for_usdc( let signer_seeds: &[&[&[u8]]] = &[&[b"router_authority", &[router_authority_bump]]]; let transfer_accounts = TransferChecked { - from: context.accounts.router_usdc_treasury.to_account_info(), - mint: context.accounts.usdc_mint.to_account_info(), - to: context.accounts.caller_usdc_account.to_account_info(), - authority: context.accounts.router_authority.to_account_info(), + from: context.accounts.router_usdc_treasury.to_cpi_handle_mut(), + mint: context.accounts.usdc_mint.to_cpi_handle(), + to: context.accounts.caller_usdc_account.to_cpi_handle_mut(), + authority: context.accounts.router_authority.cpi_handle(), }; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), transfer_accounts, signer_seeds, ), usdc_out, - context.accounts.usdc_mint.decimals, + context.accounts.usdc_mint.decimals(), )?; Ok(()) diff --git a/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/swap_usdc_for_asset.rs b/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/swap_usdc_for_asset.rs index 784d16b7f..7d0955890 100644 --- a/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/swap_usdc_for_asset.rs +++ b/finance/vault-strategy/anchor/programs/mock-swap-router/src/instructions/swap_usdc_for_asset.rs @@ -10,25 +10,25 @@ use crate::error::RouterError; use crate::state::{AssetRate, RouterConfig}; #[derive(Accounts)] -pub struct SwapUsdcForAssetAccountConstraints<'info> { +pub struct SwapUsdcForAssetAccountConstraints { /// The caller - e.g. the vault strategy PDA (can be a signer or a PDA signer via CPI) - pub caller: Signer<'info>, + pub caller: Signer, #[account( seeds = [b"router_config"], bump = router_config.bump )] - pub router_config: Account<'info, RouterConfig>, + pub router_config: BorshAccount, #[account( - constraint = asset_rate.mint == asset_mint.key() @ RouterError::InvalidAssetMint + constraint = asset_rate.mint == *asset_mint.address() @ RouterError::InvalidAssetMint )] - pub asset_rate: Account<'info, AssetRate>, + pub asset_rate: BorshAccount, - pub usdc_mint: Box>, + pub usdc_mint: Box>, #[account(mut)] - pub asset_mint: Box>, + pub asset_mint: Box>, /// Caller's USDC token account - USDC flows from here to the treasury #[account( @@ -37,7 +37,7 @@ pub struct SwapUsdcForAssetAccountConstraints<'info> { associated_token::authority = caller, associated_token::token_program = token_program )] - pub caller_usdc_account: Box>, + pub caller_usdc_account: Box>, /// Caller's asset token account - minted asset tokens land here #[account( @@ -46,7 +46,7 @@ pub struct SwapUsdcForAssetAccountConstraints<'info> { associated_token::authority = caller, associated_token::token_program = token_program )] - pub caller_asset_account: Box>, + pub caller_asset_account: Box>, /// Router's USDC treasury - receives the USDC payment #[account( @@ -55,22 +55,22 @@ pub struct SwapUsdcForAssetAccountConstraints<'info> { associated_token::authority = router_authority, associated_token::token_program = token_program )] - pub router_usdc_treasury: Box>, + pub router_usdc_treasury: Box>, /// CHECK: PDA used as mint authority - validated by seeds constraint #[account( seeds = [b"router_authority"], bump )] - pub router_authority: UncheckedAccount<'info>, + pub router_authority: UncheckedAccount, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } pub fn handle_swap_usdc_for_asset( - context: Context, + context: &mut Context, usdc_amount_in: u64, minimum_asset_out: u64, ) -> Result<()> { @@ -91,25 +91,29 @@ pub fn handle_swap_usdc_for_asset( // Transfer USDC from caller to router treasury let transfer_accounts = TransferChecked { - from: context.accounts.caller_usdc_account.to_account_info(), - mint: context.accounts.usdc_mint.to_account_info(), - to: context.accounts.router_usdc_treasury.to_account_info(), - authority: context.accounts.caller.to_account_info(), + from: context.accounts.caller_usdc_account.to_cpi_handle_mut(), + mint: context.accounts.usdc_mint.to_cpi_handle(), + to: context.accounts.router_usdc_treasury.to_cpi_handle_mut(), + authority: context.accounts.caller.cpi_handle(), }; - let cpi_ctx = CpiContext::new(context.accounts.token_program.key(), transfer_accounts); - transfer_checked(cpi_ctx, usdc_amount_in, context.accounts.usdc_mint.decimals)?; + let cpi_ctx = CpiContext::new(context.accounts.token_program.address(), transfer_accounts); + transfer_checked( + cpi_ctx, + usdc_amount_in, + context.accounts.usdc_mint.decimals(), + )?; // Mint asset tokens to caller - router_authority PDA signs let router_authority_bump = context.bumps.router_authority; let signer_seeds: &[&[&[u8]]] = &[&[b"router_authority", &[router_authority_bump]]]; let mint_accounts = MintTo { - mint: context.accounts.asset_mint.to_account_info(), - to: context.accounts.caller_asset_account.to_account_info(), - authority: context.accounts.router_authority.to_account_info(), + mint: context.accounts.asset_mint.to_cpi_handle_mut(), + to: context.accounts.caller_asset_account.to_cpi_handle_mut(), + authority: context.accounts.router_authority.cpi_handle(), }; let cpi_ctx = CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), mint_accounts, signer_seeds, ); diff --git a/finance/vault-strategy/anchor/programs/mock-swap-router/src/lib.rs b/finance/vault-strategy/anchor/programs/mock-swap-router/src/lib.rs index 7ee358266..e2950e736 100644 --- a/finance/vault-strategy/anchor/programs/mock-swap-router/src/lib.rs +++ b/finance/vault-strategy/anchor/programs/mock-swap-router/src/lib.rs @@ -14,22 +14,22 @@ pub mod mock_swap_router { use super::*; pub fn initialize_router( - context: Context, - usdc_mint: Pubkey, + context: &mut Context, + usdc_mint: Address, ) -> Result<()> { instructions::initialize_router::handle_initialize_router(context, usdc_mint) } pub fn set_rate( - context: Context, - mint: Pubkey, + context: &mut Context, + mint: Address, usdc_per_token: u64, ) -> Result<()> { instructions::set_rate::handle_set_rate(context, mint, usdc_per_token) } pub fn swap_usdc_for_asset( - context: Context, + context: &mut Context, usdc_amount_in: u64, minimum_asset_out: u64, ) -> Result<()> { @@ -41,7 +41,7 @@ pub mod mock_swap_router { } pub fn swap_asset_for_usdc( - context: Context, + context: &mut Context, asset_amount_in: u64, minimum_usdc_out: u64, ) -> Result<()> { diff --git a/finance/vault-strategy/anchor/programs/mock-swap-router/src/state/asset_rate.rs b/finance/vault-strategy/anchor/programs/mock-swap-router/src/state/asset_rate.rs index 332ebfa8b..003a23dab 100644 --- a/finance/vault-strategy/anchor/programs/mock-swap-router/src/state/asset_rate.rs +++ b/finance/vault-strategy/anchor/programs/mock-swap-router/src/state/asset_rate.rs @@ -1,9 +1,9 @@ use anchor_lang::prelude::*; -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct AssetRate { - pub mint: Pubkey, + pub mint: Address, /// USDC base units per token base unit. /// e.g. 250 means 1 token base unit = 250 USDC base units /// (so 1.0 TSLAx = $250 when both have 6 decimals) diff --git a/finance/vault-strategy/anchor/programs/mock-swap-router/src/state/router_config.rs b/finance/vault-strategy/anchor/programs/mock-swap-router/src/state/router_config.rs index 308a2e4aa..68ffc9165 100644 --- a/finance/vault-strategy/anchor/programs/mock-swap-router/src/state/router_config.rs +++ b/finance/vault-strategy/anchor/programs/mock-swap-router/src/state/router_config.rs @@ -1,9 +1,9 @@ use anchor_lang::prelude::*; -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct RouterConfig { - pub authority: Pubkey, - pub usdc_mint: Pubkey, + pub authority: Address, + pub usdc_mint: Address, pub bump: u8, } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/Cargo.toml b/finance/vault-strategy/anchor/programs/vault-strategy/Cargo.toml index 917c27183..573c1df5c 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/Cargo.toml +++ b/finance/vault-strategy/anchor/programs/vault-strategy/Cargo.toml @@ -14,18 +14,27 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = "1.1.2" +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" mock-swap-router = { path = "../mock-swap-router", features = ["cpi"] } [dev-dependencies] litesvm = "0.13.1" +solana-clock = "3.0.1" solana-account = "3.0.0" solana-signer = "3.0.0" solana-keypair = "3.0.1" diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/add_asset.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/add_asset.rs index dc21ac1df..6f9c9ed34 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/add_asset.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/add_asset.rs @@ -8,39 +8,38 @@ use crate::error::VaultError; use crate::state::{ApprovedAsset, AssetConfig, Registry, Strategy, MAX_ASSETS}; #[derive(Accounts)] -pub struct AddAssetAccountConstraints<'info> { - #[account(mut)] - pub manager: Signer<'info>, +pub struct AddAssetAccountConstraints { + #[account(mut, address = strategy.manager)] + pub manager: Signer, #[account( mut, - has_one = manager, - has_one = registry @ VaultError::InvalidRegistry, - seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()], - bump = strategy.bump + seeds = [b"strategy", strategy.index.to_le_bytes()], + bump = strategy.bump, )] - pub strategy: Box>, + pub strategy: Box>, - pub registry: Box>, + #[account(address = strategy.registry @ VaultError::InvalidRegistry)] + pub registry: Box>, - pub asset_mint: Box>, + pub asset_mint: Box>, /// Proof the mint is approved, and the source of its official price feed. /// Seeds tie it to this registry and this mint; existence means approved. #[account( - seeds = [b"approved_asset", registry.key().as_ref(), asset_mint.key().as_ref()], + seeds = [b"approved_asset", registry.address().as_ref(), asset_mint.address().as_ref()], bump = approved_asset.bump )] - pub approved_asset: Box>, + pub approved_asset: Box>, #[account( init, payer = manager, space = AssetConfig::DISCRIMINATOR.len() + AssetConfig::INIT_SPACE, - seeds = [b"asset", strategy.key().as_ref(), &[strategy.asset_count]], + seeds = [b"asset", strategy.address().as_ref(), &[strategy.asset_count]], bump )] - pub asset_config: Box>, + pub asset_config: Box>, /// Strategy-owned vault for this asset. #[account( @@ -50,15 +49,15 @@ pub struct AddAssetAccountConstraints<'info> { associated_token::authority = strategy, associated_token::token_program = token_program )] - pub vault_asset: Box>, + pub vault_asset: Box>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } pub fn handle_add_asset( - context: Context, + context: &mut Context, weight_bps: u16, ) -> Result<()> { let strategy = &mut context.accounts.strategy; @@ -72,16 +71,16 @@ pub fn handle_add_asset( let index = strategy.asset_count; - context.accounts.asset_config.set_inner(AssetConfig { - strategy: strategy.key(), + **context.accounts.asset_config = AssetConfig { + strategy: *strategy.address(), index, - mint: context.accounts.asset_mint.key(), + mint: *context.accounts.asset_mint.address(), // Copied from the registry entry, never supplied by the manager. price_feed: context.accounts.approved_asset.price_feed, - vault: context.accounts.vault_asset.key(), + vault: *context.accounts.vault_asset.address(), weight_bps, bump: context.bumps.asset_config, - }); + }; strategy.asset_count = index.checked_add(1).ok_or(VaultError::MathOverflow)?; strategy.total_weight_bps = new_total as u16; diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/approve_asset.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/approve_asset.rs index 0dd06af72..0ec135e6c 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/approve_asset.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/approve_asset.rs @@ -4,40 +4,39 @@ use anchor_spl::token_interface::Mint; use crate::state::{ApprovedAsset, Registry}; #[derive(Accounts)] -pub struct ApproveAssetAccountConstraints<'info> { - #[account(mut)] - pub authority: Signer<'info>, +pub struct ApproveAssetAccountConstraints { + #[account(mut, address = registry.authority)] + pub authority: Signer, #[account( - has_one = authority, - seeds = [b"registry", authority.key().as_ref()], - bump = registry.bump + seeds = [b"registry", authority.address().as_ref()], + bump = registry.bump, )] - pub registry: Account<'info, Registry>, + pub registry: BorshAccount, - pub asset_mint: InterfaceAccount<'info, Mint>, + pub asset_mint: InterfaceAccount, #[account( init, payer = authority, space = ApprovedAsset::DISCRIMINATOR.len() + ApprovedAsset::INIT_SPACE, - seeds = [b"approved_asset", registry.key().as_ref(), asset_mint.key().as_ref()], + seeds = [b"approved_asset", registry.address().as_ref(), asset_mint.address().as_ref()], bump )] - pub approved_asset: Account<'info, ApprovedAsset>, + pub approved_asset: BorshAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } pub fn handle_approve_asset( - context: Context, - price_feed: Pubkey, + context: &mut Context, + price_feed: Address, ) -> Result<()> { - context.accounts.approved_asset.set_inner(ApprovedAsset { - registry: context.accounts.registry.key(), - mint: context.accounts.asset_mint.key(), + *context.accounts.approved_asset = ApprovedAsset { + registry: *context.accounts.registry.address(), + mint: *context.accounts.asset_mint.address(), price_feed, bump: context.bumps.approved_asset, - }); + }; Ok(()) } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/collect_fees.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/collect_fees.rs index 04e964d02..126cf8917 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/collect_fees.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/collect_fees.rs @@ -10,24 +10,24 @@ use crate::state::Strategy; const SECONDS_PER_YEAR: u64 = 31_536_000; #[derive(Accounts)] -pub struct CollectFeesAccountConstraints<'info> { +pub struct CollectFeesAccountConstraints { /// CHECK: manager is stored in strategy; we only read their pubkey for derivation - pub manager: UncheckedAccount<'info>, + #[account(address = strategy.manager)] + pub manager: UncheckedAccount, #[account( mut, - has_one = manager, - seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()], - bump = strategy.bump + seeds = [b"strategy", strategy.index.to_le_bytes()], + bump = strategy.bump, )] - pub strategy: Account<'info, Strategy>, + pub strategy: BorshAccount, #[account( mut, - seeds = [b"share_mint", strategy.key().as_ref()], + seeds = [b"share_mint", strategy.address().as_ref()], bump )] - pub share_mint: InterfaceAccount<'info, Mint>, + pub share_mint: InterfaceAccount, /// Manager's share token account - receives fee shares #[account( @@ -37,17 +37,17 @@ pub struct CollectFeesAccountConstraints<'info> { associated_token::authority = manager, associated_token::token_program = token_program )] - pub manager_share_account: InterfaceAccount<'info, TokenAccount>, + pub manager_share_account: InterfaceAccount, #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } -pub fn handle_collect_fees(context: Context) -> Result<()> { +pub fn handle_collect_fees(context: &mut Context) -> Result<()> { let clock = Clock::get()?; let current_ts = clock.unix_timestamp; let last_ts = context.accounts.strategy.last_fee_accrual_timestamp; @@ -89,17 +89,24 @@ pub fn handle_collect_fees(context: Context) -> R let index_bytes = strategy_index.to_le_bytes(); let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", index_bytes.as_ref(), &[strategy_bump]]]; + // `strategy` signs the CPI(s) below. It is a data account holding a live + // borrow on its buffer, which the runtime would reject when the CPI borrows + // the same account, so hand the borrow back for the duration. + context.accounts.strategy.release_borrow()?; + let mint_accounts = MintTo { - mint: context.accounts.share_mint.to_account_info(), - to: context.accounts.manager_share_account.to_account_info(), - authority: context.accounts.strategy.to_account_info(), + mint: context.accounts.share_mint.to_cpi_handle_mut(), + to: context.accounts.manager_share_account.cpi_handle_mut(), + authority: context.accounts.strategy.to_cpi_handle(), }; let cpi_ctx = CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), mint_accounts, signer_seeds, ); mint_to(cpi_ctx, fee_shares)?; + context.accounts.strategy.reacquire_borrow_mut()?; + Ok(()) } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/deposit.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/deposit.rs index 1a31f6c58..00babe66d 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/deposit.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/deposit.rs @@ -12,26 +12,26 @@ use crate::oracle::{asset_value_in_usdc, load_price, read_token_amount, PYTH_PRI use crate::state::{AssetConfig, Strategy}; #[derive(Accounts)] -pub struct DepositAccountConstraints<'info> { +pub struct DepositAccountConstraints { #[account(mut)] - pub depositor: Signer<'info>, + pub depositor: Signer, #[account( mut, - has_one = usdc_mint @ VaultError::InvalidUsdcMint, - seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()], - bump = strategy.bump + seeds = [b"strategy", strategy.index.to_le_bytes()], + bump = strategy.bump, )] - pub strategy: Box>, + pub strategy: Box>, #[account( mut, - seeds = [b"share_mint", strategy.key().as_ref()], + seeds = [b"share_mint", strategy.address().as_ref()], bump )] - pub share_mint: Box>, + pub share_mint: Box>, - pub usdc_mint: Box>, + #[account(address = strategy.usdc_mint @ VaultError::InvalidUsdcMint)] + pub usdc_mint: Box>, #[account( mut, @@ -39,7 +39,7 @@ pub struct DepositAccountConstraints<'info> { associated_token::authority = depositor, associated_token::token_program = token_program )] - pub depositor_usdc_account: Box>, + pub depositor_usdc_account: Box>, #[account( init_if_needed, @@ -48,7 +48,7 @@ pub struct DepositAccountConstraints<'info> { associated_token::authority = depositor, associated_token::token_program = token_program )] - pub depositor_share_account: Box>, + pub depositor_share_account: Box>, #[account( mut, @@ -56,28 +56,29 @@ pub struct DepositAccountConstraints<'info> { associated_token::authority = strategy, associated_token::token_program = token_program )] - pub vault_usdc: Box>, + pub vault_usdc: Box>, /// CHECK: Router config PDA from the mock-swap-router program #[account(mut)] - pub router_config: UncheckedAccount<'info>, + pub router_config: UncheckedAccount, /// CHECK: Router USDC treasury ATA #[account(mut)] - pub router_usdc_treasury: UncheckedAccount<'info>, + pub router_usdc_treasury: UncheckedAccount, /// CHECK: Router authority PDA from the mock-swap-router program #[account(mut)] - pub router_authority: UncheckedAccount<'info>, + pub router_authority: UncheckedAccount, #[account( - constraint = swap_router_program.key() == strategy.swap_router @ VaultError::InvalidSwapRouter + constraint = *swap_router_program.address() == strategy.swap_router @ VaultError::InvalidSwapRouter )] - pub swap_router_program: Program<'info, mock_swap_router::program::MockSwapRouter>, + /// CHECK: validated by the address constraint above + pub swap_router_program: UncheckedAccount, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, // remaining_accounts: for each asset index 0..asset_count, in order: // [asset_config, vault, asset_mint, asset_rate, price_feed] } @@ -88,8 +89,8 @@ pub struct DepositAccountConstraints<'info> { /// invested. For each asset the handler swaps `usdc_amount * weight_bps / 10000` /// through the registered router, so a depositor's money is invested in the same /// transaction they put it in (only sub-cent rounding dust can remain as USDC). -pub fn handle_deposit<'info>( - context: Context<'info, DepositAccountConstraints<'info>>, +pub fn handle_deposit( + context: &mut Context, usdc_amount: u64, minimum_shares: u64, ) -> Result<()> { @@ -101,12 +102,12 @@ pub fn handle_deposit<'info>( VaultError::StrategyNotFullyAllocated ); - let vault_usdc_amount = context.accounts.vault_usdc.amount; + let vault_usdc_amount = context.accounts.vault_usdc.amount(); let total_shares = context.accounts.strategy.total_shares; - let usdc_decimals = context.accounts.usdc_mint.decimals; + let usdc_decimals = context.accounts.usdc_mint.decimals(); let strategy_index = context.accounts.strategy.index; let strategy_bump = context.accounts.strategy.bump; - let strategy_key = context.accounts.strategy.key(); + let strategy_key = *context.accounts.strategy.address(); let max_slippage_bps = context.accounts.strategy.max_slippage_bps; let asset_count = context.accounts.strategy.asset_count as usize; @@ -115,7 +116,7 @@ pub fn handle_deposit<'info>( // Net asset value over the complete asset set. The assets are exactly indices // 0..asset_count, so requiring five accounts per index, in order, each with a // matching index, makes it impossible to omit an asset and understate NAV. - let remaining = context.remaining_accounts; + let remaining = context.remaining_accounts()?; require!( remaining.len() == asset_count * 5, VaultError::IncompleteAssetAccounts @@ -139,7 +140,7 @@ pub fn handle_deposit<'info>( VaultError::InvalidAssetAccount ); require_keys_eq!( - vault_account.key(), + *vault_account.address(), config.vault, VaultError::InvalidAssetAccount ); @@ -173,31 +174,38 @@ pub fn handle_deposit<'info>( // Pull the depositor's USDC into the strategy's USDC vault. let transfer_accounts = TransferChecked { - from: context.accounts.depositor_usdc_account.to_account_info(), - mint: context.accounts.usdc_mint.to_account_info(), - to: context.accounts.vault_usdc.to_account_info(), - authority: context.accounts.depositor.to_account_info(), + from: context.accounts.depositor_usdc_account.to_cpi_handle_mut(), + mint: context.accounts.usdc_mint.to_cpi_handle(), + to: context.accounts.vault_usdc.to_cpi_handle_mut(), + authority: context.accounts.depositor.cpi_handle(), }; - let cpi_ctx = CpiContext::new(context.accounts.token_program.key(), transfer_accounts); + let cpi_ctx = CpiContext::new(context.accounts.token_program.address(), transfer_accounts); transfer_checked(cpi_ctx, usdc_amount, usdc_decimals)?; let index_bytes = strategy_index.to_le_bytes(); let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", index_bytes.as_ref(), &[strategy_bump]]]; + // `strategy` signs every CPI below. It is a data account holding a live + // borrow on its buffer, which the runtime would reject when the CPI borrows + // the same account, so hand the borrow back for the duration. + // `release_borrow` flushes the pending writes and `reacquire_borrow_mut` + // re-reads them. + context.accounts.strategy.release_borrow()?; + // Deploy the deposit across the basket at its target weights. Each leg swaps a // weight-sized slice of the deposit through the router, under an oracle-computed // slippage floor. The strategy PDA signs, since the USDC leaves a vault only it // controls. for index in 0..asset_count { let config_account = &remaining[index * 5]; - let vault_account = &remaining[index * 5 + 1]; - let mint_account = &remaining[index * 5 + 2]; + let mut vault_account = remaining[index * 5 + 1]; + let mut mint_account = remaining[index * 5 + 2]; let rate_account = &remaining[index * 5 + 3]; let feed_account = &remaining[index * 5 + 4]; let config = AssetConfig::load_checked(config_account)?; require_keys_eq!( - mint_account.key(), + *mint_account.address(), config.mint, VaultError::InvalidAssetAccount ); @@ -233,21 +241,21 @@ pub fn handle_deposit<'info>( .map_err(|_| VaultError::MathOverflow)?; let cpi_accounts = RouterSwapAccounts { - caller: context.accounts.strategy.to_account_info(), - router_config: context.accounts.router_config.to_account_info(), - asset_rate: rate_account.clone(), - usdc_mint: context.accounts.usdc_mint.to_account_info(), - asset_mint: mint_account.clone(), - caller_usdc_account: context.accounts.vault_usdc.to_account_info(), - caller_asset_account: vault_account.clone(), - router_usdc_treasury: context.accounts.router_usdc_treasury.to_account_info(), - router_authority: context.accounts.router_authority.to_account_info(), - associated_token_program: context.accounts.associated_token_program.to_account_info(), - token_program: context.accounts.token_program.to_account_info(), - system_program: context.accounts.system_program.to_account_info(), + caller: context.accounts.strategy.to_cpi_handle(), + router_config: context.accounts.router_config.cpi_handle(), + asset_rate: CpiHandle::readonly(rate_account), + usdc_mint: context.accounts.usdc_mint.to_cpi_handle(), + asset_mint: CpiHandleMut::writable(&mut mint_account), + caller_usdc_account: context.accounts.vault_usdc.to_cpi_handle_mut(), + caller_asset_account: CpiHandleMut::writable(&mut vault_account), + router_usdc_treasury: context.accounts.router_usdc_treasury.cpi_handle_mut(), + router_authority: context.accounts.router_authority.cpi_handle(), + associated_token_program: context.accounts.associated_token_program.cpi_handle(), + token_program: context.accounts.token_program.cpi_handle(), + system_program: context.accounts.system_program.cpi_handle(), }; let cpi_ctx = CpiContext::new_with_signer( - context.accounts.swap_router_program.key(), + context.accounts.swap_router_program.address(), cpi_accounts, signer_seeds, ); @@ -256,16 +264,18 @@ pub fn handle_deposit<'info>( // Mint the shares last, with the strategy PDA signing as the share mint authority. let mint_accounts = MintTo { - mint: context.accounts.share_mint.to_account_info(), - to: context.accounts.depositor_share_account.to_account_info(), - authority: context.accounts.strategy.to_account_info(), + mint: context.accounts.share_mint.to_cpi_handle_mut(), + to: context.accounts.depositor_share_account.to_cpi_handle_mut(), + authority: context.accounts.strategy.to_cpi_handle(), }; let cpi_ctx = CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), mint_accounts, signer_seeds, ); mint_to(cpi_ctx, shares_to_mint)?; + context.accounts.strategy.reacquire_borrow_mut()?; + Ok(()) } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_registry.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_registry.rs index c0479af3e..9c814c566 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_registry.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_registry.rs @@ -3,28 +3,28 @@ use anchor_lang::prelude::*; use crate::state::Registry; #[derive(Accounts)] -pub struct InitializeRegistryAccountConstraints<'info> { +pub struct InitializeRegistryAccountConstraints { #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, #[account( init, payer = authority, space = Registry::DISCRIMINATOR.len() + Registry::INIT_SPACE, - seeds = [b"registry", authority.key().as_ref()], + seeds = [b"registry", authority.address().as_ref()], bump )] - pub registry: Account<'info, Registry>, + pub registry: BorshAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } pub fn handle_initialize_registry( - context: Context, + context: &mut Context, ) -> Result<()> { - context.accounts.registry.set_inner(Registry { - authority: context.accounts.authority.key(), + *context.accounts.registry = Registry { + authority: *context.accounts.authority.address(), bump: context.bumps.registry, - }); + }; Ok(()) } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_strategy.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_strategy.rs index 0cf96dd44..8bd80099d 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_strategy.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_strategy.rs @@ -1,4 +1,10 @@ +// v2's `#[derive(Accounts)]` binds the `#[instruction(...)]` args in more +// than one generated item, and only the one evaluating the constraints below +// reads them, so the binding looks unused to rustc even though `space` uses it. +#![allow(unused_variables)] + use anchor_lang::prelude::*; +use anchor_spl::mint; use anchor_spl::{ associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface}, @@ -21,23 +27,23 @@ pub const MAX_SLIPPAGE_BPS: u16 = 1_000; #[derive(Accounts)] #[instruction(index: u64)] -pub struct InitializeStrategyAccountConstraints<'info> { +pub struct InitializeStrategyAccountConstraints { #[account(mut)] - pub manager: Signer<'info>, + pub manager: Signer, - pub usdc_mint: InterfaceAccount<'info, Mint>, + pub usdc_mint: InterfaceAccount, /// Registry whose approved assets this strategy may hold. - pub registry: Account<'info, Registry>, + pub registry: BorshAccount, #[account( init, payer = manager, space = Strategy::DISCRIMINATOR.len() + Strategy::INIT_SPACE, - seeds = [b"strategy", index.to_le_bytes().as_ref()], + seeds = [b"strategy", index.to_le_bytes()], bump )] - pub strategy: Box>, + pub strategy: Box>, #[account( init, @@ -46,10 +52,10 @@ pub struct InitializeStrategyAccountConstraints<'info> { mint::authority = strategy, mint::freeze_authority = strategy, mint::token_program = token_program, - seeds = [b"share_mint", strategy.key().as_ref()], + seeds = [b"share_mint", strategy.address().as_ref()], bump )] - pub share_mint: Box>, + pub share_mint: Box>, /// Vault's USDC token account - strategy PDA is the authority #[account( @@ -59,19 +65,19 @@ pub struct InitializeStrategyAccountConstraints<'info> { associated_token::authority = strategy, associated_token::token_program = token_program )] - pub vault_usdc: Box>, + pub vault_usdc: Box>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } pub fn handle_initialize_strategy( - context: Context, + context: &mut Context, index: u64, fee_bps: u16, max_slippage_bps: u16, - swap_router: Pubkey, + swap_router: Address, ) -> Result<()> { require!(fee_bps <= MAX_FEE_BPS, VaultError::FeeTooHigh); require!( @@ -81,12 +87,12 @@ pub fn handle_initialize_strategy( let clock = Clock::get()?; - context.accounts.strategy.set_inner(Strategy { + **context.accounts.strategy = Strategy { index, - manager: context.accounts.manager.key(), - registry: context.accounts.registry.key(), - share_mint: context.accounts.share_mint.key(), - usdc_mint: context.accounts.usdc_mint.key(), + manager: *context.accounts.manager.address(), + registry: *context.accounts.registry.address(), + share_mint: *context.accounts.share_mint.address(), + usdc_mint: *context.accounts.usdc_mint.address(), swap_router, fee_bps, max_slippage_bps, @@ -95,7 +101,7 @@ pub fn handle_initialize_strategy( asset_count: 0, total_weight_bps: 0, bump: context.bumps.strategy, - }); + }; Ok(()) } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/rebalance.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/rebalance.rs index b75e1b1ae..ff354e6be 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/rebalance.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/rebalance.rs @@ -13,47 +13,47 @@ use crate::oracle::{load_price, PYTH_PRICE_PRECISION}; use crate::state::{AssetConfig, Strategy}; #[derive(Accounts)] -pub struct RebalanceAccountConstraints<'info> { - pub manager: Signer<'info>, +pub struct RebalanceAccountConstraints { + #[account(address = strategy.manager)] + pub manager: Signer, #[account( mut, - has_one = manager, - has_one = usdc_mint @ VaultError::InvalidUsdcMint, - seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()], - bump = strategy.bump + seeds = [b"strategy", strategy.index.to_le_bytes()], + bump = strategy.bump, )] - pub strategy: Box>, + pub strategy: Box>, - pub usdc_mint: Box>, + #[account(address = strategy.usdc_mint @ VaultError::InvalidUsdcMint)] + pub usdc_mint: Box>, #[account(mut)] - pub sell_mint: Box>, + pub sell_mint: Box>, #[account(mut)] - pub buy_mint: Box>, + pub buy_mint: Box>, #[account( - constraint = sell_config.strategy == strategy.key() @ VaultError::InvalidAssetAccount, - constraint = sell_config.mint == sell_mint.key() @ VaultError::AssetNotFound, - constraint = sell_config.vault == vault_sell.key() @ VaultError::InvalidAssetAccount, + constraint = sell_config.strategy == *strategy.address() @ VaultError::InvalidAssetAccount, + constraint = sell_config.mint == *sell_mint.address() @ VaultError::AssetNotFound, + constraint = sell_config.vault == *vault_sell.address() @ VaultError::InvalidAssetAccount, )] - pub sell_config: Box>, + pub sell_config: Box>, #[account( - constraint = buy_config.strategy == strategy.key() @ VaultError::InvalidAssetAccount, - constraint = buy_config.mint == buy_mint.key() @ VaultError::AssetNotFound, - constraint = buy_config.vault == vault_buy.key() @ VaultError::InvalidAssetAccount, + constraint = buy_config.strategy == *strategy.address() @ VaultError::InvalidAssetAccount, + constraint = buy_config.mint == *buy_mint.address() @ VaultError::AssetNotFound, + constraint = buy_config.vault == *vault_buy.address() @ VaultError::InvalidAssetAccount, )] - pub buy_config: Box>, + pub buy_config: Box>, /// CHECK: Pyth feed - validated against sell asset's registered feed - #[account(constraint = sell_price_feed.key() == sell_config.price_feed @ VaultError::InvalidPriceFeed)] - pub sell_price_feed: UncheckedAccount<'info>, + #[account(constraint = *sell_price_feed.address() == sell_config.price_feed @ VaultError::InvalidPriceFeed)] + pub sell_price_feed: UncheckedAccount, /// CHECK: Pyth feed - validated against buy asset's registered feed - #[account(constraint = buy_price_feed.key() == buy_config.price_feed @ VaultError::InvalidPriceFeed)] - pub buy_price_feed: UncheckedAccount<'info>, + #[account(constraint = *buy_price_feed.address() == buy_config.price_feed @ VaultError::InvalidPriceFeed)] + pub buy_price_feed: UncheckedAccount, #[account( mut, @@ -61,7 +61,7 @@ pub struct RebalanceAccountConstraints<'info> { associated_token::authority = strategy, associated_token::token_program = token_program )] - pub vault_sell: Box>, + pub vault_sell: Box>, #[account( mut, @@ -69,7 +69,7 @@ pub struct RebalanceAccountConstraints<'info> { associated_token::authority = strategy, associated_token::token_program = token_program )] - pub vault_buy: Box>, + pub vault_buy: Box>, #[account( mut, @@ -77,41 +77,42 @@ pub struct RebalanceAccountConstraints<'info> { associated_token::authority = strategy, associated_token::token_program = token_program )] - pub vault_usdc: Box>, + pub vault_usdc: Box>, - pub sell_rate: Account<'info, AssetRate>, + pub sell_rate: BorshAccount, - pub buy_rate: Account<'info, AssetRate>, + pub buy_rate: BorshAccount, /// CHECK: Router config PDA #[account(mut)] - pub router_config: UncheckedAccount<'info>, + pub router_config: UncheckedAccount, /// CHECK: Router USDC treasury ATA #[account(mut)] - pub router_usdc_treasury: UncheckedAccount<'info>, + pub router_usdc_treasury: UncheckedAccount, /// CHECK: Router authority PDA #[account(mut)] - pub router_authority: UncheckedAccount<'info>, + pub router_authority: UncheckedAccount, #[account( - constraint = swap_router_program.key() == strategy.swap_router @ VaultError::InvalidSwapRouter + constraint = *swap_router_program.address() == strategy.swap_router @ VaultError::InvalidSwapRouter )] - pub swap_router_program: Program<'info, mock_swap_router::program::MockSwapRouter>, + /// CHECK: validated by the address constraint above + pub swap_router_program: UncheckedAccount, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, } pub fn handle_rebalance( - context: Context, + context: &mut Context, sell_amount: u64, usdc_to_invest: u64, ) -> Result<()> { require!( - context.accounts.sell_mint.key() != context.accounts.buy_mint.key(), + context.accounts.sell_mint.address() != context.accounts.buy_mint.address(), VaultError::SameMint ); @@ -163,24 +164,29 @@ pub fn handle_rebalance( let index_bytes = strategy_index.to_le_bytes(); let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", index_bytes.as_ref(), &[strategy_bump]]]; + // `strategy` signs the CPI(s) below. It is a data account holding a live + // borrow on its buffer, which the runtime would reject when the CPI borrows + // the same account, so hand the borrow back for the duration. + context.accounts.strategy.release_borrow()?; + // Step 1: sell basket token -> USDC let sell_cpi_accounts = RouterSellAccounts { - caller: context.accounts.strategy.to_account_info(), - router_config: context.accounts.router_config.to_account_info(), - asset_rate: context.accounts.sell_rate.to_account_info(), - usdc_mint: context.accounts.usdc_mint.to_account_info(), - asset_mint: context.accounts.sell_mint.to_account_info(), - caller_asset_account: context.accounts.vault_sell.to_account_info(), - caller_usdc_account: context.accounts.vault_usdc.to_account_info(), - router_usdc_treasury: context.accounts.router_usdc_treasury.to_account_info(), - router_authority: context.accounts.router_authority.to_account_info(), - associated_token_program: context.accounts.associated_token_program.to_account_info(), - token_program: context.accounts.token_program.to_account_info(), - system_program: context.accounts.system_program.to_account_info(), + caller: context.accounts.strategy.to_cpi_handle(), + router_config: context.accounts.router_config.cpi_handle(), + asset_rate: context.accounts.sell_rate.cpi_handle(), + usdc_mint: context.accounts.usdc_mint.to_cpi_handle(), + asset_mint: context.accounts.sell_mint.to_cpi_handle_mut(), + caller_asset_account: context.accounts.vault_sell.to_cpi_handle_mut(), + caller_usdc_account: context.accounts.vault_usdc.to_cpi_handle_mut(), + router_usdc_treasury: context.accounts.router_usdc_treasury.cpi_handle_mut(), + router_authority: context.accounts.router_authority.cpi_handle(), + associated_token_program: context.accounts.associated_token_program.cpi_handle(), + token_program: context.accounts.token_program.cpi_handle(), + system_program: context.accounts.system_program.cpi_handle(), }; mock_swap_router::cpi::swap_asset_for_usdc( CpiContext::new_with_signer( - context.accounts.swap_router_program.key(), + context.accounts.swap_router_program.address(), sell_cpi_accounts, signer_seeds, ), @@ -190,22 +196,22 @@ pub fn handle_rebalance( // Step 2: buy basket token with USDC let buy_cpi_accounts = RouterBuyAccounts { - caller: context.accounts.strategy.to_account_info(), - router_config: context.accounts.router_config.to_account_info(), - asset_rate: context.accounts.buy_rate.to_account_info(), - usdc_mint: context.accounts.usdc_mint.to_account_info(), - asset_mint: context.accounts.buy_mint.to_account_info(), - caller_usdc_account: context.accounts.vault_usdc.to_account_info(), - caller_asset_account: context.accounts.vault_buy.to_account_info(), - router_usdc_treasury: context.accounts.router_usdc_treasury.to_account_info(), - router_authority: context.accounts.router_authority.to_account_info(), - associated_token_program: context.accounts.associated_token_program.to_account_info(), - token_program: context.accounts.token_program.to_account_info(), - system_program: context.accounts.system_program.to_account_info(), + caller: context.accounts.strategy.to_cpi_handle(), + router_config: context.accounts.router_config.cpi_handle(), + asset_rate: context.accounts.buy_rate.cpi_handle(), + usdc_mint: context.accounts.usdc_mint.to_cpi_handle(), + asset_mint: context.accounts.buy_mint.to_cpi_handle_mut(), + caller_usdc_account: context.accounts.vault_usdc.to_cpi_handle_mut(), + caller_asset_account: context.accounts.vault_buy.to_cpi_handle_mut(), + router_usdc_treasury: context.accounts.router_usdc_treasury.cpi_handle_mut(), + router_authority: context.accounts.router_authority.cpi_handle(), + associated_token_program: context.accounts.associated_token_program.cpi_handle(), + token_program: context.accounts.token_program.cpi_handle(), + system_program: context.accounts.system_program.cpi_handle(), }; mock_swap_router::cpi::swap_usdc_for_asset( CpiContext::new_with_signer( - context.accounts.swap_router_program.key(), + context.accounts.swap_router_program.address(), buy_cpi_accounts, signer_seeds, ), @@ -213,5 +219,7 @@ pub fn handle_rebalance( minimum_buy_amount, )?; + context.accounts.strategy.reacquire_borrow_mut()?; + Ok(()) } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/set_weight.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/set_weight.rs index d0cbd6b05..d9549712c 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/set_weight.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/set_weight.rs @@ -4,22 +4,22 @@ use crate::error::VaultError; use crate::state::{AssetConfig, Strategy}; #[derive(Accounts)] -pub struct SetWeightAccountConstraints<'info> { - pub manager: Signer<'info>, +pub struct SetWeightAccountConstraints { + #[account(address = strategy.manager)] + pub manager: Signer, #[account( mut, - has_one = manager, - seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()], - bump = strategy.bump + seeds = [b"strategy", strategy.index.to_le_bytes()], + bump = strategy.bump, )] - pub strategy: Box>, + pub strategy: Box>, #[account( mut, - constraint = asset_config.strategy == strategy.key() @ VaultError::InvalidAssetAccount, + constraint = asset_config.strategy == *strategy.address() @ VaultError::InvalidAssetAccount, )] - pub asset_config: Box>, + pub asset_config: Box>, } /// Change an asset's target weight. Setting it to zero retires the asset: deposits @@ -28,7 +28,7 @@ pub struct SetWeightAccountConstraints<'info> { /// contiguous 0..asset_count range the valuation handlers depend on stays intact. /// Funds do not move here; this only edits the target the manager trades toward. pub fn handle_set_weight( - context: Context, + context: &mut Context, weight_bps: u16, ) -> Result<()> { let strategy = &mut context.accounts.strategy; diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/withdraw.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/withdraw.rs index a15235d4d..d6f7d1e77 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/withdraw.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/withdraw.rs @@ -11,26 +11,26 @@ use crate::oracle::{read_mint_decimals, read_token_amount, read_token_mint_and_o use crate::state::{AssetConfig, Strategy}; #[derive(Accounts)] -pub struct WithdrawAccountConstraints<'info> { +pub struct WithdrawAccountConstraints { #[account(mut)] - pub user: Signer<'info>, + pub user: Signer, #[account( mut, - has_one = usdc_mint @ VaultError::InvalidUsdcMint, - seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()], - bump = strategy.bump + seeds = [b"strategy", strategy.index.to_le_bytes()], + bump = strategy.bump, )] - pub strategy: Box>, + pub strategy: Box>, #[account( mut, - seeds = [b"share_mint", strategy.key().as_ref()], + seeds = [b"share_mint", strategy.address().as_ref()], bump )] - pub share_mint: Box>, + pub share_mint: Box>, - pub usdc_mint: Box>, + #[account(address = strategy.usdc_mint @ VaultError::InvalidUsdcMint)] + pub usdc_mint: Box>, #[account( mut, @@ -38,7 +38,7 @@ pub struct WithdrawAccountConstraints<'info> { associated_token::authority = user, associated_token::token_program = token_program )] - pub user_share_account: Box>, + pub user_share_account: Box>, #[account( init_if_needed, @@ -47,7 +47,7 @@ pub struct WithdrawAccountConstraints<'info> { associated_token::authority = user, associated_token::token_program = token_program )] - pub user_usdc_account: Box>, + pub user_usdc_account: Box>, #[account( mut, @@ -55,18 +55,18 @@ pub struct WithdrawAccountConstraints<'info> { associated_token::authority = strategy, associated_token::token_program = token_program )] - pub vault_usdc: Box>, + pub vault_usdc: Box>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, // remaining_accounts: for each asset index 0..asset_count, in order: // [asset_config, vault, mint, user_token_account] // The user's asset token accounts must already exist. } -pub fn handle_withdraw<'info>( - context: Context<'info, WithdrawAccountConstraints<'info>>, +pub fn handle_withdraw( + context: &mut Context, shares_to_burn: u64, min_usdc_out: u64, ) -> Result<()> { @@ -75,16 +75,16 @@ pub fn handle_withdraw<'info>( let total_shares = context.accounts.strategy.total_shares; require!(total_shares > 0, VaultError::ZeroTotalShares); - let vault_usdc_amount = context.accounts.vault_usdc.amount; - let usdc_decimals = context.accounts.usdc_mint.decimals; + let vault_usdc_amount = context.accounts.vault_usdc.amount(); + let usdc_decimals = context.accounts.usdc_mint.decimals(); let strategy_index = context.accounts.strategy.index; let strategy_bump = context.accounts.strategy.bump; - let strategy_key = context.accounts.strategy.key(); - let user_key = context.accounts.user.key(); + let strategy_key = *context.accounts.strategy.address(); + let user_key = *context.accounts.user.address(); let asset_count = context.accounts.strategy.asset_count as usize; require!( - context.remaining_accounts.len() == asset_count * 4, + context.remaining_accounts()?.len() == asset_count * 4, VaultError::IncompleteAssetAccounts ); @@ -107,23 +107,27 @@ pub fn handle_withdraw<'info>( let index_bytes = strategy_index.to_le_bytes(); let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", index_bytes.as_ref(), &[strategy_bump]]]; - // Hoist owned account-info handles for every CPI up front, so the asset loop - // can borrow remaining_accounts without also re-borrowing `context.accounts` - // (Account is invariant over its lifetime, which otherwise fails to unify). - let strategy_info = context.accounts.strategy.to_account_info(); - let share_mint_info = context.accounts.share_mint.to_account_info(); - let usdc_mint_info = context.accounts.usdc_mint.to_account_info(); - let vault_usdc_info = context.accounts.vault_usdc.to_account_info(); - let user_info = context.accounts.user.to_account_info(); - let user_share_info = context.accounts.user_share_account.to_account_info(); - let user_usdc_info = context.accounts.user_usdc_account.to_account_info(); - let token_program_key = context.accounts.token_program.key(); + // `remaining_accounts()` takes `&mut Context`, so collect it before the + // per-account views below borrow `context.accounts`. + let remaining = context.remaining_accounts()?; + + // `strategy` signs the payouts below. It is a data account holding a live + // borrow on its buffer, so release it across the CPIs and take it back + // afterwards: the runtime rejects a CPI that borrows an account we hold. + context.accounts.strategy.release_borrow()?; + + // Every other account here goes through its own wrapper handle. A handle + // built by hand over a copy of the `AccountView` keeps the runtime borrow + // check on, and a mutable data account is marked exclusively borrowed, so + // the copy would be rejected where the wrapper's handle is not. + let strategy_view = *context.accounts.strategy.account(); + let token_program_key = context.accounts.token_program.address(); // Burn the user's shares. let burn_accounts = Burn { - mint: share_mint_info, - from: user_share_info, - authority: user_info, + mint: context.accounts.share_mint.to_cpi_handle_mut(), + from: context.accounts.user_share_account.to_cpi_handle_mut(), + authority: context.accounts.user.cpi_handle(), }; burn( CpiContext::new(token_program_key, burn_accounts), @@ -133,10 +137,10 @@ pub fn handle_withdraw<'info>( // USDC payout. if amount_usdc > 0 { let transfer_accounts = TransferChecked { - from: vault_usdc_info, - mint: usdc_mint_info, - to: user_usdc_info, - authority: strategy_info.clone(), + from: context.accounts.vault_usdc.to_cpi_handle_mut(), + mint: context.accounts.usdc_mint.to_cpi_handle(), + to: context.accounts.user_usdc_account.to_cpi_handle_mut(), + authority: CpiHandle::readonly(&strategy_view), }; transfer_checked( CpiContext::new_with_signer(token_program_key, transfer_accounts, signer_seeds), @@ -146,12 +150,11 @@ pub fn handle_withdraw<'info>( } // Each basket asset, paid in kind, proportional to shares burned. - let remaining = context.remaining_accounts; for i in 0..asset_count { let config_ai = &remaining[i * 4]; - let vault_ai = &remaining[i * 4 + 1]; - let mint_ai = &remaining[i * 4 + 2]; - let user_ata_ai = &remaining[i * 4 + 3]; + let mut vault_ai = remaining[i * 4 + 1]; + let mint_ai = remaining[i * 4 + 2]; + let mut user_ata_ai = remaining[i * 4 + 3]; let config = AssetConfig::load_checked(config_ai)?; require_keys_eq!( @@ -161,17 +164,21 @@ pub fn handle_withdraw<'info>( ); require!(config.index as usize == i, VaultError::InvalidAssetAccount); require_keys_eq!( - vault_ai.key(), + *vault_ai.address(), config.vault, VaultError::InvalidAssetAccount ); - require_keys_eq!(mint_ai.key(), config.mint, VaultError::InvalidAssetAccount); + require_keys_eq!( + *mint_ai.address(), + config.mint, + VaultError::InvalidAssetAccount + ); - let (recipient_mint, recipient_owner) = read_token_mint_and_owner(user_ata_ai)?; + let (recipient_mint, recipient_owner) = read_token_mint_and_owner(&user_ata_ai)?; require_keys_eq!(recipient_owner, user_key, VaultError::InvalidRecipient); require_keys_eq!(recipient_mint, config.mint, VaultError::InvalidRecipient); - let vault_balance = read_token_amount(vault_ai)?; + let vault_balance = read_token_amount(&vault_ai)?; let amount: u64 = (vault_balance as u128) .checked_mul(shares_u128) .ok_or(VaultError::MathOverflow)? @@ -179,12 +186,12 @@ pub fn handle_withdraw<'info>( .ok_or(VaultError::MathOverflow)? as u64; if amount > 0 { - let decimals = read_mint_decimals(mint_ai)?; + let decimals = read_mint_decimals(&mint_ai)?; let transfer_accounts = TransferChecked { - from: vault_ai.to_account_info(), - mint: mint_ai.to_account_info(), - to: user_ata_ai.to_account_info(), - authority: strategy_info.clone(), + from: CpiHandleMut::writable(&mut vault_ai), + mint: CpiHandle::readonly(&mint_ai), + to: CpiHandleMut::writable(&mut user_ata_ai), + authority: CpiHandle::readonly(&strategy_view), }; transfer_checked( CpiContext::new_with_signer(token_program_key, transfer_accounts, signer_seeds), diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/lib.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/lib.rs index 4773503cb..36c2c785a 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/lib.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/lib.rs @@ -17,15 +17,15 @@ pub mod vault_strategy { /// Create the curator record for an approved-asset set, owned by `authority` /// (not a manager). The set itself lives in per-asset ApprovedAsset accounts. pub fn initialize_registry( - context: Context, + context: &mut Context, ) -> Result<()> { instructions::initialize_registry::handle_initialize_registry(context) } /// Approve a mint and bind it to its official price feed. Registry authority only. pub fn approve_asset( - context: Context, - price_feed: Pubkey, + context: &mut Context, + price_feed: Address, ) -> Result<()> { instructions::approve_asset::handle_approve_asset(context, price_feed) } @@ -33,11 +33,11 @@ pub mod vault_strategy { /// Open a strategy at a caller-chosen index, e.g. index 0 derives the PDA /// from seeds `"strategy" + 0`. Manager pays and becomes the strategy's manager. pub fn initialize_strategy( - context: Context, + context: &mut Context, index: u64, fee_bps: u16, max_slippage_bps: u16, - swap_router: Pubkey, + swap_router: Address, ) -> Result<()> { instructions::initialize_strategy::handle_initialize_strategy( context, @@ -49,32 +49,35 @@ pub mod vault_strategy { } /// Add a curator-approved asset to the strategy at the next index. Manager only. - pub fn add_asset(context: Context, weight_bps: u16) -> Result<()> { + pub fn add_asset( + context: &mut Context, + weight_bps: u16, + ) -> Result<()> { instructions::add_asset::handle_add_asset(context, weight_bps) } /// Change an asset's target weight, or set it to zero to retire it. Manager only. pub fn set_weight( - context: Context, + context: &mut Context, weight_bps: u16, ) -> Result<()> { instructions::set_weight::handle_set_weight(context, weight_bps) } - pub fn deposit<'info>( - context: Context<'info, DepositAccountConstraints<'info>>, + pub fn deposit( + context: &mut Context, usdc_amount: u64, minimum_shares: u64, ) -> Result<()> { instructions::deposit::handle_deposit(context, usdc_amount, minimum_shares) } - pub fn collect_fees(context: Context) -> Result<()> { + pub fn collect_fees(context: &mut Context) -> Result<()> { instructions::collect_fees::handle_collect_fees(context) } - pub fn withdraw<'info>( - context: Context<'info, WithdrawAccountConstraints<'info>>, + pub fn withdraw( + context: &mut Context, shares_to_burn: u64, min_usdc_out: u64, ) -> Result<()> { @@ -82,7 +85,7 @@ pub mod vault_strategy { } pub fn rebalance( - context: Context, + context: &mut Context, sell_amount: u64, usdc_to_invest: u64, ) -> Result<()> { diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/oracle.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/oracle.rs index cfa41e11f..8faf037ef 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/oracle.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/oracle.rs @@ -17,9 +17,9 @@ const MAX_PRICE_AGE_SECONDS: i64 = 60; /// shared by the Classic Token Program and the Token Extensions Program, so this /// reads either. const TOKEN_AMOUNT_OFFSET: usize = 64; -/// `owner` Pubkey is at bytes 32..64. +/// `owner` Address is at bytes 32..64. const TOKEN_OWNER_OFFSET: usize = 32; -/// `mint` Pubkey is at bytes 0..32. +/// `mint` Address is at bytes 0..32. const TOKEN_MINT_OFFSET: usize = 0; fn read_pyth_raw(account_data: &[u8]) -> Result<(i64, i64)> { @@ -41,9 +41,9 @@ fn read_pyth_raw(account_data: &[u8]) -> Result<(i64, i64)> { /// Validate a price feed account against the one the strategy registered, then /// return its positive, fresh price as u128. `now` is the current unix timestamp. -pub fn load_price(price_feed: &AccountInfo, expected_key: &Pubkey, now: i64) -> Result { +pub fn load_price(price_feed: &AccountView, expected_key: &Address, now: i64) -> Result { require_keys_eq!( - price_feed.key(), + *price_feed.address(), *expected_key, VaultError::InvalidPriceFeed ); @@ -63,7 +63,7 @@ pub fn load_price(price_feed: &AccountInfo, expected_key: &Pubkey, now: i64) -> } /// Read the `amount` field of a token account from its raw data. -pub fn read_token_amount(account: &AccountInfo) -> Result { +pub fn read_token_amount(account: &AccountView) -> Result { let data = account.try_borrow_data()?; if data.len() < TOKEN_AMOUNT_OFFSET + 8 { return err!(VaultError::InvalidVaultAccount); @@ -77,7 +77,7 @@ pub fn read_token_amount(account: &AccountInfo) -> Result { /// Read the `decimals` byte of a mint account. Offset 44 in the Mint layout /// (mint_authority option 36 + supply 8), shared by both token programs. -pub fn read_mint_decimals(account: &AccountInfo) -> Result { +pub fn read_mint_decimals(account: &AccountView) -> Result { let data = account.try_borrow_data()?; const MINT_DECIMALS_OFFSET: usize = 44; if data.len() <= MINT_DECIMALS_OFFSET { @@ -87,14 +87,14 @@ pub fn read_mint_decimals(account: &AccountInfo) -> Result { } /// Read the `mint` and `owner` Pubkeys of a token account from its raw data. -pub fn read_token_mint_and_owner(account: &AccountInfo) -> Result<(Pubkey, Pubkey)> { +pub fn read_token_mint_and_owner(account: &AccountView) -> Result<(Address, Address)> { let data = account.try_borrow_data()?; if data.len() < TOKEN_OWNER_OFFSET + 32 { return err!(VaultError::InvalidVaultAccount); } - let mint = Pubkey::try_from(&data[TOKEN_MINT_OFFSET..TOKEN_MINT_OFFSET + 32]) + let mint = Address::try_from(&data[TOKEN_MINT_OFFSET..TOKEN_MINT_OFFSET + 32]) .map_err(|_| VaultError::InvalidVaultAccount)?; - let owner = Pubkey::try_from(&data[TOKEN_OWNER_OFFSET..TOKEN_OWNER_OFFSET + 32]) + let owner = Address::try_from(&data[TOKEN_OWNER_OFFSET..TOKEN_OWNER_OFFSET + 32]) .map_err(|_| VaultError::InvalidVaultAccount)?; Ok((mint, owner)) } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/state/registry.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/state/registry.rs index ffee0bb73..1fc465c5a 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/state/registry.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/state/registry.rs @@ -9,10 +9,10 @@ use anchor_lang::prelude::*; /// real assets (and which official price feed) are safe, and a manager only /// chooses among them. This is what stops a manager from listing a token they /// mint themselves, or pairing a real mint with a feed they control. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Registry { - pub authority: Pubkey, + pub authority: Address, pub bump: u8, } @@ -22,11 +22,11 @@ pub struct Registry { /// address and fails if no account is there. Created only by the registry /// authority; add_asset copies `price_feed` from here so the manager never /// supplies the feed. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct ApprovedAsset { - pub registry: Pubkey, - pub mint: Pubkey, - pub price_feed: Pubkey, + pub registry: Address, + pub mint: Address, + pub price_feed: Address, pub bump: u8, } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/state/strategy.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/state/strategy.rs index 05dc893a3..06accae27 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/state/strategy.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/state/strategy.rs @@ -15,18 +15,18 @@ pub const MAX_ASSETS: u8 = 16; /// e.g. seeds `"strategy" + 0`, so strategies are addressed by a simple counter /// rather than by the manager's key. The index is stored here so every handler /// can re-derive the PDA to sign for the vaults and share mint. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Strategy { /// Index used as the PDA seed, e.g. 0 for the first strategy. pub index: u64, - pub manager: Pubkey, + pub manager: Address, /// Registry whose curator approves assets. add_asset only accepts mints /// approved in this registry. - pub registry: Pubkey, - pub share_mint: Pubkey, - pub usdc_mint: Pubkey, - pub swap_router: Pubkey, + pub registry: Address, + pub share_mint: Address, + pub usdc_mint: Address, + pub swap_router: Address, /// Annual management fee in basis points (e.g. 100 = 1%). pub fee_bps: u16, /// Maximum tolerated deviation, in basis points, between a swap's output and @@ -46,17 +46,17 @@ pub struct Strategy { /// index, so the full set is the contiguous range 0..asset_count: any handler /// computing net asset value re-derives every index and refuses to proceed if an /// asset account is missing. -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct AssetConfig { - pub strategy: Pubkey, + pub strategy: Address, pub index: u8, - pub mint: Pubkey, + pub mint: Address, /// Pyth PriceUpdateV2 account, copied from the registry's ApprovedAsset at /// add time so the manager cannot substitute a feed they control. - pub price_feed: Pubkey, + pub price_feed: Address, /// Strategy-owned associated token account holding this asset. - pub vault: Pubkey, + pub vault: Address, /// Target share of the strategy's value in basis points. deposit deploys at these /// weights (the sum across assets must reach 10000 before deposits open), and the /// manager maintains them against price drift with rebalance. @@ -68,14 +68,21 @@ impl AssetConfig { /// Deserialize an AssetConfig passed via remaining_accounts to an owned value, /// verifying it is owned by this program and has the right discriminator. /// Avoids the lifetime invariance of `Account::try_from` on borrowed infos. - pub fn load_checked(account: &AccountInfo) -> Result { + pub fn load_checked(account: &AccountView) -> Result { require_keys_eq!( - *account.owner, + *account.owner(), crate::ID, crate::error::VaultError::InvalidAssetAccount ); - let data = account.try_borrow_data()?; - AssetConfig::try_deserialize(&mut &data[..]) - .map_err(|_| error!(crate::error::VaultError::InvalidAssetAccount)) + let data = account.try_borrow()?; + let disc_len = ::DISCRIMINATOR.len(); + require!( + data.len() > disc_len + && &data[..disc_len] == ::DISCRIMINATOR, + crate::error::VaultError::InvalidAssetAccount + ); + let mut payload = &data[disc_len..]; + >::get(&mut payload) + .map_err(|_| crate::error::VaultError::InvalidAssetAccount.into()) } } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/tests/vault_strategy.rs b/finance/vault-strategy/anchor/programs/vault-strategy/tests/vault_strategy.rs index 469eb2b41..392883277 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/tests/vault_strategy.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/tests/vault_strategy.rs @@ -1,14 +1,13 @@ use { anchor_lang::{ - solana_program::{ - clock::Clock, instruction::AccountMeta, instruction::Instruction, pubkey::Pubkey, - system_program, - }, - AccountDeserialize, InstructionData, ToAccountMetas, + solana_program::{instruction::AccountMeta, instruction::Instruction}, + system_program, AccountDeserialize, Address, InstructionData, ToAccountMetas, }, anchor_spl::token::spl_token, litesvm::LiteSVM, solana_account::Account as SolanaAccount, + // LiteSVM's get_sysvar / set_sysvar want the host-side Clock, not pinocchio's. + solana_clock::Clock, solana_keypair::Keypair, solana_kite::{ create_associated_token_account, create_token_mint, create_wallet, @@ -18,26 +17,26 @@ use { solana_signer::Signer, }; -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn ata_program_id() -> Pubkey { +fn ata_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn pyth_receiver_program_id() -> Pubkey { +fn pyth_receiver_program_id() -> Address { "rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ" .parse() .unwrap() } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - let (ata, _bump) = Pubkey::find_program_address( +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + let (ata, _bump) = Address::find_program_address( &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &ata_program_id(), ); @@ -64,7 +63,7 @@ fn build_mock_price_update_account(price: i64, exponent: i32, publish_time: i64) data } -fn set_price_feed(svm: &mut LiteSVM, key: Pubkey, price: i64) { +fn set_price_feed(svm: &mut LiteSVM, key: Address, price: i64) { let data = build_mock_price_update_account(price, -8, PUBLISH_TIME); let rent = svm.minimum_balance_for_rent_exemption(data.len()); svm.set_account( @@ -95,33 +94,33 @@ const STRATEGY_INDEX: u64 = 0; // strategy PDA seed: "strategy" + 0 struct TestContext { svm: LiteSVM, - vault_program_id: Pubkey, - router_program_id: Pubkey, + vault_program_id: Address, + router_program_id: Address, manager: Keypair, payer: Keypair, - usdc_mint: Pubkey, - tsla_mint: Pubkey, - nvda_mint: Pubkey, - strategy_pda: Pubkey, - share_mint_pda: Pubkey, - registry_pda: Pubkey, - approved_tsla: Pubkey, - approved_nvda: Pubkey, - router_config_pda: Pubkey, - router_authority_pda: Pubkey, - tsla_rate_pda: Pubkey, - nvda_rate_pda: Pubkey, - vault_usdc: Pubkey, - vault_tsla: Pubkey, - vault_nvda: Pubkey, - router_usdc_treasury: Pubkey, - price_feed_tsla: Pubkey, - price_feed_nvda: Pubkey, + usdc_mint: Address, + tsla_mint: Address, + nvda_mint: Address, + strategy_pda: Address, + share_mint_pda: Address, + registry_pda: Address, + approved_tsla: Address, + approved_nvda: Address, + router_config_pda: Address, + router_authority_pda: Address, + tsla_rate_pda: Address, + nvda_rate_pda: Address, + vault_usdc: Address, + vault_tsla: Address, + vault_nvda: Address, + router_usdc_treasury: Address, + price_feed_tsla: Address, + price_feed_nvda: Address, } impl TestContext { - fn asset_config(&self, index: u8) -> Pubkey { - Pubkey::find_program_address( + fn asset_config(&self, index: u8) -> Address { + Address::find_program_address( &[b"asset", self.strategy_pda.as_ref(), &[index]], &self.vault_program_id, ) @@ -168,7 +167,7 @@ fn setup_full() -> TestContext { let nvda_mint = create_token_mint(&mut svm, &payer, TOKEN_DECIMALS, None).unwrap(); let (router_authority_pda, _) = - Pubkey::find_program_address(&[b"router_authority"], &router_program_id); + Address::find_program_address(&[b"router_authority"], &router_program_id); // The router mints basket assets on swap, so it must hold their mint authority. for basket_mint in [&tsla_mint, &nvda_mint] { @@ -184,28 +183,28 @@ fn setup_full() -> TestContext { send_transaction_from_instructions(&mut svm, vec![ix], &[&payer], &payer.pubkey()).unwrap(); } - let (strategy_pda, _) = Pubkey::find_program_address( + let (strategy_pda, _) = Address::find_program_address( &[b"strategy", STRATEGY_INDEX.to_le_bytes().as_ref()], &vault_program_id, ); let (share_mint_pda, _) = - Pubkey::find_program_address(&[b"share_mint", strategy_pda.as_ref()], &vault_program_id); + Address::find_program_address(&[b"share_mint", strategy_pda.as_ref()], &vault_program_id); let (registry_pda, _) = - Pubkey::find_program_address(&[b"registry", payer.pubkey().as_ref()], &vault_program_id); - let (approved_tsla, _) = Pubkey::find_program_address( + Address::find_program_address(&[b"registry", payer.pubkey().as_ref()], &vault_program_id); + let (approved_tsla, _) = Address::find_program_address( &[b"approved_asset", registry_pda.as_ref(), tsla_mint.as_ref()], &vault_program_id, ); - let (approved_nvda, _) = Pubkey::find_program_address( + let (approved_nvda, _) = Address::find_program_address( &[b"approved_asset", registry_pda.as_ref(), nvda_mint.as_ref()], &vault_program_id, ); let (router_config_pda, _) = - Pubkey::find_program_address(&[b"router_config"], &router_program_id); + Address::find_program_address(&[b"router_config"], &router_program_id); let (tsla_rate_pda, _) = - Pubkey::find_program_address(&[b"rate", tsla_mint.as_ref()], &router_program_id); + Address::find_program_address(&[b"rate", tsla_mint.as_ref()], &router_program_id); let (nvda_rate_pda, _) = - Pubkey::find_program_address(&[b"rate", nvda_mint.as_ref()], &router_program_id); + Address::find_program_address(&[b"rate", nvda_mint.as_ref()], &router_program_id); let vault_usdc = derive_ata(&strategy_pda, &usdc_mint); let vault_tsla = derive_ata(&strategy_pda, &tsla_mint); @@ -227,7 +226,7 @@ fn setup_full() -> TestContext { router_config: router_config_pda, router_authority: router_authority_pda, token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -255,7 +254,7 @@ fn setup_full() -> TestContext { router_usdc_treasury, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -278,7 +277,7 @@ fn setup_full() -> TestContext { vault_strategy::accounts::InitializeRegistryAccountConstraints { authority: payer.pubkey(), registry: registry_pda, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -302,7 +301,7 @@ fn setup_full() -> TestContext { registry: registry_pda, asset_mint: mint, approved_asset: entry, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -336,7 +335,7 @@ fn setup_full() -> TestContext { } } -fn init_strategy(ctx: &mut TestContext, fee_bps: u16, slippage_bps: u16, router: Pubkey) { +fn init_strategy(ctx: &mut TestContext, fee_bps: u16, slippage_bps: u16, router: Address) { let ix = Instruction::new_with_bytes( ctx.vault_program_id, &vault_strategy::instruction::InitializeStrategy { @@ -355,7 +354,7 @@ fn init_strategy(ctx: &mut TestContext, fee_bps: u16, slippage_bps: u16, router: vault_usdc: ctx.vault_usdc, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -371,9 +370,9 @@ fn init_strategy(ctx: &mut TestContext, fee_bps: u16, slippage_bps: u16, router: fn add_asset( ctx: &mut TestContext, index: u8, - mint: Pubkey, - approved_asset: Pubkey, - vault: Pubkey, + mint: Address, + approved_asset: Address, + vault: Address, weight_bps: u16, ) -> Result<(), solana_kite::SolanaKiteError> { let asset_config = ctx.asset_config(index); @@ -390,7 +389,7 @@ fn add_asset( vault_asset: vault, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -416,11 +415,11 @@ fn standard_strategy(ctx: &mut TestContext) { /// [asset_config, vault, mint, rate, price_feed]. Deposit deploys into the asset, /// so vault and mint must be writable. fn asset_deposit_metas( - config: Pubkey, - vault: Pubkey, - mint: Pubkey, - rate: Pubkey, - feed: Pubkey, + config: Address, + vault: Address, + mint: Address, + rate: Address, + feed: Address, ) -> Vec { vec![ AccountMeta::new_readonly(config, false), @@ -470,7 +469,7 @@ fn deposit_named_metas(ctx: &TestContext, user: &Keypair) -> Vec { swap_router_program: ctx.router_program_id, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None) } @@ -501,7 +500,7 @@ fn do_deposit( user: &Keypair, usdc_amount: u64, minimum_shares: u64, -) -> Pubkey { +) -> Address { let remaining = deposit_remaining(ctx); let ix = deposit_instruction(ctx, user, usdc_amount, minimum_shares, remaining); send_transaction_from_instructions(&mut ctx.svm, vec![ix], &[user], &user.pubkey()).unwrap(); @@ -514,7 +513,7 @@ fn do_deposit_tsla_only( user: &Keypair, usdc_amount: u64, minimum_shares: u64, -) -> Pubkey { +) -> Address { let remaining = deposit_remaining_tsla(ctx); let ix = deposit_instruction(ctx, user, usdc_amount, minimum_shares, remaining); send_transaction_from_instructions(&mut ctx.svm, vec![ix], &[user], &user.pubkey()).unwrap(); @@ -523,7 +522,7 @@ fn do_deposit_tsla_only( /// Update the router's exchange rate for a mint (and its Pyth feed stays the caller's /// job). Used to keep the router quote in step with a price move. -fn set_router_rate(ctx: &mut TestContext, mint: Pubkey, rate: u64, rate_pda: Pubkey) { +fn set_router_rate(ctx: &mut TestContext, mint: Address, rate: u64, rate_pda: Address) { let ix = Instruction::new_with_bytes( ctx.router_program_id, &mock_swap_router::instruction::SetRate { @@ -541,7 +540,7 @@ fn set_router_rate(ctx: &mut TestContext, mint: Pubkey, rate: u64, rate_pda: Pub router_usdc_treasury: ctx.router_usdc_treasury, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -601,7 +600,7 @@ fn read_asset_config(ctx: &TestContext, index: u8) -> vault_strategy::state::Ass /// (mint, asset_config, price_feed, vault, rate_pda) for an asset in the two-asset /// standard strategy: index 0 is TSLAx, index 1 is NVDAx. -fn asset_accounts(ctx: &TestContext, index: u8) -> (Pubkey, Pubkey, Pubkey, Pubkey, Pubkey) { +fn asset_accounts(ctx: &TestContext, index: u8) -> (Address, Address, Address, Address, Address) { match index { 0 => ( ctx.tsla_mint, @@ -659,7 +658,7 @@ fn do_rebalance( swap_router_program: ctx.router_program_id, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -683,7 +682,7 @@ fn advance_one_year(ctx: &mut TestContext) { }); } -fn do_collect_fees(ctx: &mut TestContext) -> Pubkey { +fn do_collect_fees(ctx: &mut TestContext) -> Address { let manager_share = derive_ata(&ctx.manager.pubkey(), &ctx.share_mint_pda); let ix = Instruction::new_with_bytes( ctx.vault_program_id, @@ -696,7 +695,7 @@ fn do_collect_fees(ctx: &mut TestContext) -> Pubkey { payer: ctx.payer.pubkey(), associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -753,7 +752,7 @@ fn test_add_asset_rejects_unapproved() { // A mint that was never approved: its approved_asset PDA does not exist. let rogue_mint = create_token_mint(&mut ctx.svm, &ctx.payer, TOKEN_DECIMALS, None).unwrap(); - let (rogue_entry, _) = Pubkey::find_program_address( + let (rogue_entry, _) = Address::find_program_address( &[ b"approved_asset", ctx.registry_pda.as_ref(), @@ -782,9 +781,9 @@ fn test_add_asset_rejects_weight_overflow() { /// Create a fresh mint and approve it in the registry. The bound price feed is an /// arbitrary pubkey: callers that never value this asset (e.g. the cap boundary test) /// do not need a real feed account. -fn create_and_approve_mint(ctx: &mut TestContext) -> (Pubkey, Pubkey) { +fn create_and_approve_mint(ctx: &mut TestContext) -> (Address, Address) { let mint = create_token_mint(&mut ctx.svm, &ctx.payer, TOKEN_DECIMALS, None).unwrap(); - let (entry, _) = Pubkey::find_program_address( + let (entry, _) = Address::find_program_address( &[b"approved_asset", ctx.registry_pda.as_ref(), mint.as_ref()], &ctx.vault_program_id, ); @@ -799,7 +798,7 @@ fn create_and_approve_mint(ctx: &mut TestContext) -> (Pubkey, Pubkey) { registry: ctx.registry_pda, asset_mint: mint, approved_asset: entry, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -853,7 +852,7 @@ fn test_initialize_rejects_excessive_fee() { vault_usdc: ctx.vault_usdc, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -888,7 +887,7 @@ fn test_initialize_rejects_excessive_slippage() { vault_usdc: ctx.vault_usdc, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -988,7 +987,7 @@ fn test_deposit_rejects_slippage() { fn test_deposit_rejects_unregistered_router() { let mut ctx = setup_full(); // Register a different router than the deployed mock, then fully allocate 40/60. - let bogus_router = Pubkey::new_unique(); + let bogus_router = Address::new_unique(); init_strategy(&mut ctx, FEE_BPS, SLIPPAGE_BPS, bogus_router); let (tm, wt, vt) = (ctx.tsla_mint, ctx.approved_tsla, ctx.vault_tsla); add_asset(&mut ctx, 0, tm, wt, vt, 4000).unwrap(); @@ -1088,7 +1087,7 @@ fn test_collect_fees() { ); } -fn withdraw_remaining(ctx: &TestContext, user: &Pubkey) -> Vec { +fn withdraw_remaining(ctx: &TestContext, user: &Address) -> Vec { vec![ AccountMeta::new_readonly(ctx.asset_config(0), false), AccountMeta::new(ctx.vault_tsla, false), @@ -1128,7 +1127,7 @@ fn test_withdraw() { vault_usdc: ctx.vault_usdc, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None); metas.extend(withdraw_remaining(&ctx, &user.pubkey())); @@ -1181,7 +1180,7 @@ fn test_withdraw_rejects_slippage() { vault_usdc: ctx.vault_usdc, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None); metas.extend(withdraw_remaining(&ctx, &user.pubkey())); @@ -1232,7 +1231,7 @@ fn do_withdraw(ctx: &mut TestContext, user: &Keypair, shares: u64, min_usdc_out: vault_usdc: ctx.vault_usdc, associated_token_program: ata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None); metas.extend(withdraw_remaining(ctx, &user.pubkey())); diff --git a/tokens/create-token/anchor/programs/create-token/Cargo.toml b/tokens/create-token/anchor/programs/create-token/Cargo.toml index ca65253b2..6ccd329cd 100644 --- a/tokens/create-token/anchor/programs/create-token/Cargo.toml +++ b/tokens/create-token/anchor/programs/create-token/Cargo.toml @@ -14,15 +14,23 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = { version = "1.1.2", features = ["metadata"] } +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = { version = "2.0.0-rc.1", features = ["metadata"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/create-token/anchor/programs/create-token/src/lib.rs b/tokens/create-token/anchor/programs/create-token/src/lib.rs index 59173b779..92cf36cf7 100644 --- a/tokens/create-token/anchor/programs/create-token/src/lib.rs +++ b/tokens/create-token/anchor/programs/create-token/src/lib.rs @@ -5,7 +5,8 @@ use { create_metadata_accounts_v3, mpl_token_metadata::types::DataV2, CreateMetadataAccountsV3, Metadata, }, - token::{Mint, Token}, + mint::{self, Mint}, + token::Token, }, }; @@ -16,7 +17,7 @@ pub mod create_token { use super::*; pub fn create_token_mint( - context: Context, + context: &mut Context, _token_decimals: u8, token_name: String, token_symbol: String, @@ -25,22 +26,28 @@ pub mod create_token { msg!("Creating metadata account..."); msg!( "Metadata account address: {}", - &context.accounts.metadata_account.key() + &context.accounts.metadata_account.address() ); // Cross Program Invocation (CPI) // Invoking the create_metadata_account_v3 instruction on the token metadata program + // `payer` fills three CPI slots (payer, mint authority, update + // authority). v2's typed handles enforce borrow exclusivity at compile + // time, so the two read-only slots are built from a copy of the + // `AccountView`, and it still points at the same underlying account. + let payer_view = *context.accounts.payer.account(); + create_metadata_accounts_v3( CpiContext::new( - context.accounts.token_metadata_program.key(), + context.accounts.token_metadata_program.address(), CreateMetadataAccountsV3 { - metadata: context.accounts.metadata_account.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - mint_authority: context.accounts.payer.to_account_info(), - update_authority: context.accounts.payer.to_account_info(), - payer: context.accounts.payer.to_account_info(), - system_program: context.accounts.system_program.to_account_info(), - rent: context.accounts.rent.to_account_info(), + metadata: context.accounts.metadata_account.cpi_handle_mut(), + mint: context.accounts.mint_account.cpi_handle(), + mint_authority: CpiHandle::readonly(&payer_view), + update_authority: CpiHandle::readonly(&payer_view), + payer: context.accounts.payer.cpi_handle_mut(), + system_program: context.accounts.system_program.cpi_handle(), + update_authority_is_signer: true, }, ), DataV2 { @@ -53,7 +60,6 @@ pub mod create_token { uses: None, }, false, // Is mutable - true, // Update authority is signer None, // Collection details )?; @@ -65,29 +71,29 @@ pub mod create_token { #[derive(Accounts)] #[instruction(_token_decimals: u8)] -pub struct CreateTokenMintAccountConstraints<'info> { +pub struct CreateTokenMintAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, /// CHECK: Validate address by deriving pda #[account( mut, - seeds = [b"metadata", token_metadata_program.key().as_ref(), mint_account.key().as_ref()], + seeds = [b"metadata", token_metadata_program.address().as_ref(), mint_account.address().as_ref()], bump, - seeds::program = token_metadata_program.key(), + seeds::program = token_metadata_program.address(), )] - pub metadata_account: UncheckedAccount<'info>, + pub metadata_account: UncheckedAccount, // Create new mint account #[account( init, payer = payer, mint::decimals = _token_decimals, - mint::authority = payer.key(), + mint::authority = payer, )] - pub mint_account: Account<'info, Mint>, + pub mint_account: Account, - pub token_metadata_program: Program<'info, Metadata>, - pub token_program: Program<'info, Token>, - pub system_program: Program<'info, System>, - pub rent: Sysvar<'info, Rent>, + pub token_metadata_program: Program, + pub token_program: Program, + pub system_program: Program, + pub rent: Sysvar, } diff --git a/tokens/create-token/anchor/programs/create-token/tests/test_create_token.rs b/tokens/create-token/anchor/programs/create-token/tests/test_create_token.rs index 11e7bf7a0..8c4658322 100644 --- a/tokens/create-token/anchor/programs/create-token/tests/test_create_token.rs +++ b/tokens/create-token/anchor/programs/create-token/tests/test_create_token.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, @@ -9,25 +9,25 @@ use { solana_signer::Signer, }; -fn metadata_program_id() -> Pubkey { +fn metadata_program_id() -> Address { "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" .parse() .unwrap() } -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn rent_sysvar_id() -> Pubkey { +fn rent_sysvar_id() -> Address { "SysvarRent111111111111111111111111111111111" .parse() .unwrap() } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = create_token::id(); let mut svm = LiteSVM::new(); @@ -42,9 +42,9 @@ fn setup() -> (LiteSVM, Pubkey, Keypair) { (svm, program_id, payer) } -fn derive_metadata_pda(mint: &Pubkey) -> Pubkey { +fn derive_metadata_pda(mint: &Address) -> Address { let metadata_pid = metadata_program_id(); - let (pda, _bump) = Pubkey::find_program_address( + let (pda, _bump) = Address::find_program_address( &[b"metadata", metadata_pid.as_ref(), mint.as_ref()], &metadata_pid, ); @@ -72,7 +72,7 @@ fn test_create_spl_token() { mint_account: mint_keypair.pubkey(), token_metadata_program: metadata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, rent: rent_sysvar_id(), } .to_account_metas(None), @@ -90,7 +90,10 @@ fn test_create_spl_token() { let mint_account = svm .get_account(&mint_keypair.pubkey()) .expect("Mint account should exist"); - assert!(!mint_account.data.is_empty(), "Mint account should have data"); + assert!( + !mint_account.data.is_empty(), + "Mint account should have data" + ); // Verify the metadata account was created let meta_account = svm @@ -123,7 +126,7 @@ fn test_create_nft() { mint_account: mint_keypair.pubkey(), token_metadata_program: metadata_program_id(), token_program: token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, rent: rent_sysvar_id(), } .to_account_metas(None), @@ -141,5 +144,8 @@ fn test_create_nft() { let mint_account = svm .get_account(&mint_keypair.pubkey()) .expect("Mint account should exist"); - assert!(!mint_account.data.is_empty(), "Mint account should have data"); + assert!( + !mint_account.data.is_empty(), + "Mint account should have data" + ); } diff --git a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/Cargo.toml b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/Cargo.toml index a1ccd537b..e67b57e71 100644 --- a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/Cargo.toml +++ b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = { version = "1.1.2", features = ["metadata"] } +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = { version = "2.0.0-rc.1", features = ["metadata"] } sha3 = "0.10.8" solana-secp256k1-recover = "2.0.0" diff --git a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/authority_transfer.rs b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/authority_transfer.rs index 9fc507eea..e9c3d0073 100644 --- a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/authority_transfer.rs +++ b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/authority_transfer.rs @@ -6,48 +6,51 @@ use anchor_spl::token_interface::{ use crate::UserAccount; #[derive(Accounts)] -pub struct AuthorityTransferAccountConstraints<'info> { - #[account(has_one = authority)] - pub user_account: Account<'info, UserAccount>, +pub struct AuthorityTransferAccountConstraints { + pub user_account: BorshAccount, - pub authority: Signer<'info>, + #[account(address = user_account.authority)] + pub authority: Signer, - pub mint: InterfaceAccount<'info, Mint>, + pub mint: InterfaceAccount, #[account(mut)] - pub user_token_account: InterfaceAccount<'info, TokenAccount>, + pub user_token_account: InterfaceAccount, #[account(mut)] - pub recipient_token_account: InterfaceAccount<'info, TokenAccount>, + pub recipient_token_account: InterfaceAccount, #[account( - seeds = [user_account.key().as_ref()], + seeds = [user_account.address().as_ref()], bump, )] - pub user_pda: SystemAccount<'info>, + pub user_pda: SystemAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } -pub fn handler(context: Context, amount: u64) -> Result<()> { +pub fn handler( + context: &mut Context, + amount: u64, +) -> Result<()> { let transfer_accounts = TransferChecked { - from: context.accounts.user_token_account.to_account_info(), - mint: context.accounts.mint.to_account_info(), - to: context.accounts.recipient_token_account.to_account_info(), - authority: context.accounts.user_pda.to_account_info(), + from: context.accounts.user_token_account.cpi_handle_mut(), + mint: context.accounts.mint.cpi_handle(), + to: context.accounts.recipient_token_account.cpi_handle_mut(), + authority: context.accounts.user_pda.cpi_handle(), }; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), transfer_accounts, &[&[ - context.accounts.user_account.key().as_ref(), + context.accounts.user_account.address().as_ref(), &[context.bumps.user_pda], ]], ), amount, - context.accounts.mint.decimals, + context.accounts.mint.decimals(), )?; Ok(()) diff --git a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/initialize.rs b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/initialize.rs index 12890e6e8..3b566d5ab 100644 --- a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/initialize.rs +++ b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/initialize.rs @@ -3,23 +3,23 @@ use anchor_lang::prelude::*; use crate::UserAccount; #[derive(Accounts)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { #[account( init, payer = authority, space = UserAccount::DISCRIMINATOR.len() + UserAccount::INIT_SPACE, )] - pub user_account: Account<'info, UserAccount>, + pub user_account: BorshAccount, #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, - pub system_program: Program<'info, System>, + pub system_program: Program, } -pub fn handler(context: Context) -> Result<()> { +pub fn handler(context: &mut Context) -> Result<()> { let user_account = &mut context.accounts.user_account; - user_account.authority = context.accounts.authority.key(); + user_account.authority = *context.accounts.authority.address(); user_account.ethereum_address = [0; 20]; user_account.nonce = 0; Ok(()) diff --git a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/set_ethereum_address.rs b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/set_ethereum_address.rs index 1967fc1b4..7272c1ea3 100644 --- a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/set_ethereum_address.rs +++ b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/set_ethereum_address.rs @@ -3,15 +3,16 @@ use anchor_lang::prelude::*; use crate::UserAccount; #[derive(Accounts)] -pub struct SetEthereumAddressAccountConstraints<'info> { - #[account(mut, has_one = authority)] - pub user_account: Account<'info, UserAccount>, +pub struct SetEthereumAddressAccountConstraints { + #[account(mut)] + pub user_account: BorshAccount, - pub authority: Signer<'info>, + #[account(address = user_account.authority)] + pub authority: Signer, } pub fn handler( - context: Context, + context: &mut Context, ethereum_address: [u8; 20], ) -> Result<()> { let user_account = &mut context.accounts.user_account; diff --git a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/transfer_tokens.rs b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/transfer_tokens.rs index 4ab2c8631..2b6b2eeaa 100644 --- a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/transfer_tokens.rs +++ b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/instructions/transfer_tokens.rs @@ -3,46 +3,51 @@ use anchor_spl::token_interface::{ transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked, }; -use crate::{build_transfer_authorization_message, verify_ethereum_signature, ErrorCode, UserAccount}; +use crate::{ + build_transfer_authorization_message, verify_ethereum_signature, ErrorCode, UserAccount, +}; #[derive(Accounts)] -pub struct TransferTokensAccountConstraints<'info> { - #[account(mut, has_one = authority)] - pub user_account: Account<'info, UserAccount>, +pub struct TransferTokensAccountConstraints { + #[account(mut)] + pub user_account: BorshAccount, - pub authority: Signer<'info>, + #[account(address = user_account.authority)] + pub authority: Signer, - pub mint: InterfaceAccount<'info, Mint>, + pub mint: InterfaceAccount, #[account(mut)] - pub user_token_account: InterfaceAccount<'info, TokenAccount>, + pub user_token_account: InterfaceAccount, #[account(mut)] - pub recipient_token_account: InterfaceAccount<'info, TokenAccount>, + pub recipient_token_account: InterfaceAccount, #[account( - seeds = [user_account.key().as_ref()], + seeds = [user_account.address().as_ref()], bump, )] - pub user_pda: SystemAccount<'info>, + pub user_pda: SystemAccount, - pub token_program: Interface<'info, TokenInterface>, + pub token_program: Interface<'static, TokenInterface>, } pub fn handler( - context: Context, + context: &mut Context, amount: u64, signature: [u8; 65], ) -> Result<()> { + // Copy out what the message needs, so the shared borrow ends before the + // nonce bump below takes a mutable one. + let user_account_key = *context.accounts.user_account.address(); let user_account = &context.accounts.user_account; - let user_account_key = user_account.key(); // Rebuild the authorized message onchain so the signature commits to // this exact transfer (amount, recipient, and the current nonce). let message = build_transfer_authorization_message( &user_account_key, amount, - &context.accounts.recipient_token_account.key(), + &context.accounts.recipient_token_account.address(), user_account.nonce, ); @@ -60,20 +65,20 @@ pub fn handler( .ok_or(ErrorCode::NonceOverflow)?; let transfer_accounts = TransferChecked { - from: context.accounts.user_token_account.to_account_info(), - mint: context.accounts.mint.to_account_info(), - to: context.accounts.recipient_token_account.to_account_info(), - authority: context.accounts.user_pda.to_account_info(), + from: context.accounts.user_token_account.cpi_handle_mut(), + mint: context.accounts.mint.cpi_handle(), + to: context.accounts.recipient_token_account.cpi_handle_mut(), + authority: context.accounts.user_pda.cpi_handle(), }; transfer_checked( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), transfer_accounts, &[&[user_account_key.as_ref(), &[context.bumps.user_pda]]], ), amount, - context.accounts.mint.decimals, + context.accounts.mint.decimals(), )?; Ok(()) diff --git a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs index 27aed7a12..e1c21713f 100644 --- a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs +++ b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs @@ -11,19 +11,19 @@ declare_id!("FYPkt5VWMvtyWZDMGCwoKFkE3wXTzphicTpnNGuHWVbD"); pub mod external_delegate_token_master { use super::*; - pub fn initialize(context: Context) -> Result<()> { + pub fn initialize(context: &mut Context) -> Result<()> { instructions::initialize::handler(context) } pub fn set_ethereum_address( - context: Context, + context: &mut Context, ethereum_address: [u8; 20], ) -> Result<()> { instructions::set_ethereum_address::handler(context, ethereum_address) } pub fn transfer_tokens( - context: Context, + context: &mut Context, amount: u64, signature: [u8; 65], ) -> Result<()> { @@ -31,17 +31,17 @@ pub mod external_delegate_token_master { } pub fn authority_transfer( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { instructions::authority_transfer::handler(context, amount) } } -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct UserAccount { - pub authority: Pubkey, + pub authority: Address, pub ethereum_address: [u8; 20], /// Strictly increasing counter committed into every signed transfer /// authorization, so each Ethereum signature executes exactly once. @@ -63,9 +63,9 @@ pub enum ErrorCode { /// account's stored nonce, a signature is valid for exactly one /// (amount, recipient, nonce) execution and cannot be replayed. pub fn build_transfer_authorization_message( - user_account: &Pubkey, + user_account: &Address, amount: u64, - recipient_token_account: &Pubkey, + recipient_token_account: &Address, nonce: u64, ) -> [u8; 32] { let mut hasher = Keccak256::new(); diff --git a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/tests/test_external_delegate.rs b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/tests/test_external_delegate.rs index 4b6d73bdc..840fc67e5 100644 --- a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/tests/test_external_delegate.rs +++ b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/tests/test_external_delegate.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, borsh::BorshDeserialize, litesvm::LiteSVM, @@ -9,7 +9,8 @@ use { solana_keypair::Keypair, solana_kite::{ create_associated_token_account, create_token_mint, create_wallet, - get_token_account_balance, mint_tokens_to_token_account, send_transaction_from_instructions, + get_token_account_balance, mint_tokens_to_token_account, + send_transaction_from_instructions, }, solana_signer::Signer, }; @@ -23,7 +24,7 @@ const TRANSFER_AMOUNT: u64 = 500_000_000; /// the secp256k1 curve order works. const DELEGATE_SECP256K1_PRIVATE_KEY: [u8; 32] = [0x42; 32]; -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() @@ -38,7 +39,7 @@ struct UserAccountState { nonce: u64, } -fn read_user_account(svm: &LiteSVM, address: &Pubkey) -> UserAccountState { +fn read_user_account(svm: &LiteSVM, address: &Address) -> UserAccountState { let account = svm.get_account(address).expect("user account should exist"); let anchor_discriminator_len = 8; UserAccountState::try_from_slice(&account.data[anchor_discriminator_len..]).unwrap() @@ -62,10 +63,10 @@ fn ethereum_address_of(secret_key: &libsecp256k1::SecretKey) -> [u8; 20] { /// Builds the exact preimage the program reconstructs onchain: /// keccak256(program id || user account || amount LE || recipient token account || nonce LE). fn build_transfer_authorization_message( - program_id: &Pubkey, - user_account: &Pubkey, + program_id: &Address, + user_account: &Address, amount: u64, - recipient_token_account: &Pubkey, + recipient_token_account: &Address, nonce: u64, ) -> [u8; 32] { let mut hasher = Keccak256::new(); @@ -90,7 +91,7 @@ fn sign_transfer_authorization( bytes } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = external_delegate_token_master::id(); let mut svm = LiteSVM::new(); @@ -103,7 +104,7 @@ fn setup() -> (LiteSVM, Pubkey, Keypair) { fn initialize_user_account( svm: &mut LiteSVM, - program_id: &Pubkey, + program_id: &Address, authority: &Keypair, user_account: &Keypair, ) { @@ -113,7 +114,7 @@ fn initialize_user_account( external_delegate_token_master::accounts::InitializeAccountConstraints { user_account: user_account.pubkey(), authority: authority.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -128,9 +129,9 @@ fn initialize_user_account( fn set_ethereum_address( svm: &mut LiteSVM, - program_id: &Pubkey, + program_id: &Address, authority: &Keypair, - user_account: &Pubkey, + user_account: &Address, ethereum_address: [u8; 20], ) { let set_address_instruction = Instruction::new_with_bytes( @@ -157,13 +158,13 @@ fn set_ethereum_address( /// recipient token account. struct TransferFixture { svm: LiteSVM, - program_id: Pubkey, + program_id: Address, authority: Keypair, - user_account: Pubkey, - user_pda: Pubkey, - mint: Pubkey, - user_pda_token_account: Pubkey, - recipient_token_account: Pubkey, + user_account: Address, + user_pda: Address, + mint: Address, + user_pda_token_account: Address, + recipient_token_account: Address, } fn setup_transfer_fixture() -> TransferFixture { @@ -180,13 +181,19 @@ fn setup_transfer_fixture() -> TransferFixture { ethereum_address_of(&delegate_secret_key()), ); - let (user_pda, _bump) = Pubkey::find_program_address(&[user_account.as_ref()], &program_id); + let (user_pda, _bump) = Address::find_program_address(&[user_account.as_ref()], &program_id); let mint = create_token_mint(&mut svm, &authority, MINT_DECIMALS, None).unwrap(); let user_pda_token_account = create_associated_token_account(&mut svm, &user_pda, &mint, &authority).unwrap(); - mint_tokens_to_token_account(&mut svm, &mint, &user_pda_token_account, MINT_AMOUNT, &authority) - .unwrap(); + mint_tokens_to_token_account( + &mut svm, + &mint, + &user_pda_token_account, + MINT_AMOUNT, + &authority, + ) + .unwrap(); let recipient = Keypair::new(); let recipient_token_account = @@ -206,8 +213,8 @@ fn setup_transfer_fixture() -> TransferFixture { fn build_transfer_tokens_instruction( fixture: &TransferFixture, - authority: &Pubkey, - recipient_token_account: &Pubkey, + authority: &Address, + recipient_token_account: &Address, amount: u64, signature: [u8; 65], ) -> Instruction { @@ -295,7 +302,10 @@ fn test_transfer_tokens_with_valid_signature_moves_tokens_and_increments_nonce() get_token_account_balance(&fixture.svm, &fixture.user_pda_token_account).unwrap(), MINT_AMOUNT - TRANSFER_AMOUNT ); - assert_eq!(read_user_account(&fixture.svm, &fixture.user_account).nonce, 1); + assert_eq!( + read_user_account(&fixture.svm, &fixture.user_account).nonce, + 1 + ); } #[test] @@ -336,14 +346,20 @@ fn test_transfer_tokens_replayed_signature_fails() { &[&fixture.authority], &authority_pubkey, ); - assert!(replay_result.is_err(), "replayed signature must be rejected"); + assert!( + replay_result.is_err(), + "replayed signature must be rejected" + ); // Exactly one transfer happened. assert_eq!( get_token_account_balance(&fixture.svm, &fixture.recipient_token_account).unwrap(), TRANSFER_AMOUNT ); - assert_eq!(read_user_account(&fixture.svm, &fixture.user_account).nonce, 1); + assert_eq!( + read_user_account(&fixture.svm, &fixture.user_account).nonce, + 1 + ); } #[test] @@ -383,7 +399,10 @@ fn test_transfer_tokens_signature_over_different_amount_fails() { get_token_account_balance(&fixture.svm, &fixture.recipient_token_account).unwrap(), 0 ); - assert_eq!(read_user_account(&fixture.svm, &fixture.user_account).nonce, 0); + assert_eq!( + read_user_account(&fixture.svm, &fixture.user_account).nonce, + 0 + ); } #[test] diff --git a/tokens/nft-minter/anchor/programs/nft-minter/Cargo.toml b/tokens/nft-minter/anchor/programs/nft-minter/Cargo.toml index c721aa3d8..c528c3b77 100644 --- a/tokens/nft-minter/anchor/programs/nft-minter/Cargo.toml +++ b/tokens/nft-minter/anchor/programs/nft-minter/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = { version = "1.1.2", features = ["metadata"] } +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = { version = "2.0.0-rc.1", features = ["metadata"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/nft-minter/anchor/programs/nft-minter/src/lib.rs b/tokens/nft-minter/anchor/programs/nft-minter/src/lib.rs index 134a07cbb..7643a3737 100644 --- a/tokens/nft-minter/anchor/programs/nft-minter/src/lib.rs +++ b/tokens/nft-minter/anchor/programs/nft-minter/src/lib.rs @@ -7,6 +7,7 @@ use { mpl_token_metadata::types::DataV2, CreateMasterEditionV3, CreateMetadataAccountsV3, Metadata, }, + mint, token::{mint_to, Mint, MintTo, Token, TokenAccount}, }, }; @@ -18,21 +19,25 @@ pub mod nft_minter { use super::*; pub fn mint_nft( - context: Context, + context: &mut Context, nft_name: String, nft_symbol: String, nft_uri: String, ) -> Result<()> { + // `AccountView` is Copy, and a copy still points at the same + // account. v2's typed handles make the aliasing a compile error. + let mint_account_view = *context.accounts.mint_account.account(); + let payer_view = *context.accounts.payer.account(); msg!("Minting Token"); // Cross Program Invocation (CPI) // Invoking the mint_to instruction on the token program mint_to( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), MintTo { - mint: context.accounts.mint_account.to_account_info(), - to: context.accounts.associated_token_account.to_account_info(), - authority: context.accounts.payer.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), + to: context.accounts.associated_token_account.cpi_handle_mut(), + authority: CpiHandle::readonly(&payer_view), }, ), 1, @@ -43,15 +48,15 @@ pub mod nft_minter { // Invoking the create_metadata_account_v3 instruction on the token metadata program create_metadata_accounts_v3( CpiContext::new( - context.accounts.token_metadata_program.key(), + context.accounts.token_metadata_program.address(), CreateMetadataAccountsV3 { - metadata: context.accounts.metadata_account.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - mint_authority: context.accounts.payer.to_account_info(), - update_authority: context.accounts.payer.to_account_info(), - payer: context.accounts.payer.to_account_info(), - system_program: context.accounts.system_program.to_account_info(), - rent: context.accounts.rent.to_account_info(), + metadata: context.accounts.metadata_account.cpi_handle_mut(), + mint: context.accounts.mint_account.cpi_handle(), + mint_authority: CpiHandle::readonly(&payer_view), + update_authority: CpiHandle::readonly(&payer_view), + payer: context.accounts.payer.cpi_handle_mut(), + system_program: context.accounts.system_program.cpi_handle(), + update_authority_is_signer: true, }, ), DataV2 { @@ -64,7 +69,6 @@ pub mod nft_minter { uses: None, }, false, // Is mutable - true, // Update authority is signer None, // Collection details )?; @@ -73,17 +77,16 @@ pub mod nft_minter { // Invoking the create_master_edition_v3 instruction on the token metadata program create_master_edition_v3( CpiContext::new( - context.accounts.token_metadata_program.key(), + context.accounts.token_metadata_program.address(), CreateMasterEditionV3 { - edition: context.accounts.edition_account.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - update_authority: context.accounts.payer.to_account_info(), - mint_authority: context.accounts.payer.to_account_info(), - payer: context.accounts.payer.to_account_info(), - metadata: context.accounts.metadata_account.to_account_info(), - token_program: context.accounts.token_program.to_account_info(), - system_program: context.accounts.system_program.to_account_info(), - rent: context.accounts.rent.to_account_info(), + edition: context.accounts.edition_account.cpi_handle_mut(), + mint: context.accounts.mint_account.cpi_handle_mut(), + update_authority: CpiHandle::readonly(&payer_view), + mint_authority: CpiHandle::readonly(&payer_view), + payer: context.accounts.payer.cpi_handle_mut(), + metadata: context.accounts.metadata_account.cpi_handle_mut(), + token_program: context.accounts.token_program.cpi_handle(), + system_program: context.accounts.system_program.cpi_handle(), }, ), None, // Max Supply @@ -96,37 +99,37 @@ pub mod nft_minter { } #[derive(Accounts)] -pub struct MintNftAccountConstraints<'info> { +pub struct MintNftAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, /// CHECK: Validate address by deriving pda #[account( mut, - seeds = [b"metadata", token_metadata_program.key().as_ref(), mint_account.key().as_ref()], + seeds = [b"metadata", token_metadata_program.address().as_ref(), mint_account.address().as_ref()], bump, - seeds::program = token_metadata_program.key(), + seeds::program = token_metadata_program.address(), )] - pub metadata_account: UncheckedAccount<'info>, + pub metadata_account: UncheckedAccount, /// CHECK: Validate address by deriving pda #[account( mut, - seeds = [b"metadata", token_metadata_program.key().as_ref(), mint_account.key().as_ref(), b"edition"], + seeds = [b"metadata", token_metadata_program.address().as_ref(), mint_account.address().as_ref(), b"edition"], bump, - seeds::program = token_metadata_program.key(), + seeds::program = token_metadata_program.address(), )] - pub edition_account: UncheckedAccount<'info>, + pub edition_account: UncheckedAccount, // Create new mint account, NFTs have 0 decimals #[account( init, payer = payer, mint::decimals = 0, - mint::authority = payer.key(), - mint::freeze_authority = payer.key(), + mint::authority = payer, + mint::freeze_authority = payer, )] - pub mint_account: Account<'info, Mint>, + pub mint_account: Account, // Create associated token account, if needed // This is the account that will hold the NFT @@ -136,11 +139,11 @@ pub struct MintNftAccountConstraints<'info> { associated_token::mint = mint_account, associated_token::authority = payer, )] - pub associated_token_account: Account<'info, TokenAccount>, + pub associated_token_account: Account, - pub token_program: Program<'info, Token>, - pub token_metadata_program: Program<'info, Metadata>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, - pub rent: Sysvar<'info, Rent>, + pub token_program: Program, + pub token_metadata_program: Program, + pub associated_token_program: Program, + pub system_program: Program, + pub rent: Sysvar, } diff --git a/tokens/nft-minter/anchor/programs/nft-minter/tests/test_nft_minter.rs b/tokens/nft-minter/anchor/programs/nft-minter/tests/test_nft_minter.rs index 733c91afe..f850acc46 100644 --- a/tokens/nft-minter/anchor/programs/nft-minter/tests/test_nft_minter.rs +++ b/tokens/nft-minter/anchor/programs/nft-minter/tests/test_nft_minter.rs @@ -1,60 +1,58 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, - solana_kite::{ - create_wallet, get_token_account_balance, send_transaction_from_instructions, - }, + solana_kite::{create_wallet, get_token_account_balance, send_transaction_from_instructions}, solana_signer::Signer, }; -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn ata_program_id() -> Pubkey { +fn ata_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn metadata_program_id() -> Pubkey { +fn metadata_program_id() -> Address { "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" .parse() .unwrap() } -fn rent_sysvar_id() -> Pubkey { +fn rent_sysvar_id() -> Address { "SysvarRent111111111111111111111111111111111" .parse() .unwrap() } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - let (ata, _bump) = Pubkey::find_program_address( +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + let (ata, _bump) = Address::find_program_address( &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &ata_program_id(), ); ata } -fn derive_metadata_pda(mint: &Pubkey) -> Pubkey { +fn derive_metadata_pda(mint: &Address) -> Address { let metadata_pid = metadata_program_id(); - let (pda, _bump) = Pubkey::find_program_address( + let (pda, _bump) = Address::find_program_address( &[b"metadata", metadata_pid.as_ref(), mint.as_ref()], &metadata_pid, ); pda } -fn derive_edition_pda(mint: &Pubkey) -> Pubkey { +fn derive_edition_pda(mint: &Address) -> Address { let metadata_pid = metadata_program_id(); - let (pda, _bump) = Pubkey::find_program_address( + let (pda, _bump) = Address::find_program_address( &[ b"metadata", metadata_pid.as_ref(), @@ -66,7 +64,7 @@ fn derive_edition_pda(mint: &Pubkey) -> Pubkey { pda } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = nft_minter::id(); let mut svm = LiteSVM::new(); @@ -107,7 +105,7 @@ fn test_mint_nft() { token_program: token_program_id(), token_metadata_program: metadata_program_id(), associated_token_program: ata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, rent: rent_sysvar_id(), } .to_account_metas(None), diff --git a/tokens/nft-operations/anchor/README.md b/tokens/nft-operations/anchor/README.md index f60df0abd..4dbe277d9 100644 --- a/tokens/nft-operations/anchor/README.md +++ b/tokens/nft-operations/anchor/README.md @@ -379,7 +379,7 @@ pub fn handle_verify_collection( } ``` -> `INSTRUCTIONS_SYSVAR_ID` is the well-known sysvar address `Sysvar1nstructions1111111111111111111111111`, defined directly in [`verify_collection.rs`](programs/mint-nft/src/instructions/verify_collection.rs) because `sysvar::instructions::ID` moved in Anchor 1.0. +> `INSTRUCTIONS_SYSVAR_ID` is the well-known sysvar address `Sysvar1nstructions1111111111111111111111111`, defined directly in [`verify_collection.rs`](programs/mint-nft/src/instructions/verify_collection.rs) because pinocchio, which anchor-lang v2 is built on, does not re-export it. `verify_collection` performs a CPI to the Token Metadata program with the right accounts. The collection NFT's mint authority signs the CPI, and the NFT is verified as part of the collection. diff --git a/tokens/nft-operations/anchor/programs/mint-nft/Cargo.toml b/tokens/nft-operations/anchor/programs/mint-nft/Cargo.toml index e277d36a5..ed8eb4d18 100644 --- a/tokens/nft-operations/anchor/programs/mint-nft/Cargo.toml +++ b/tokens/nft-operations/anchor/programs/mint-nft/Cargo.toml @@ -14,14 +14,22 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = { version = "1.1.2", features = ["metadata"] } +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = { version = "2.0.0-rc.1", features = ["metadata"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/create_collection.rs b/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/create_collection.rs index 6fd2722cc..9da7392a0 100644 --- a/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/create_collection.rs +++ b/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/create_collection.rs @@ -1,25 +1,26 @@ use anchor_lang::prelude::*; +use anchor_spl::mint; use anchor_spl::{ associated_token::AssociatedToken, metadata::Metadata, token::{mint_to, Mint, MintTo, Token, TokenAccount}, }; -use anchor_spl::metadata::mpl_token_metadata::{ - instructions::{ - CreateMasterEditionV3Cpi, CreateMasterEditionV3CpiAccounts, - CreateMasterEditionV3InstructionArgs, CreateMetadataAccountV3Cpi, - CreateMetadataAccountV3CpiAccounts, CreateMetadataAccountV3InstructionArgs, - }, - types::{CollectionDetails, Creator, DataV2}, +// v2's anchor-spl wraps these CPIs in terms of `CpiHandle`s, so the raw +// mpl-token-metadata `*Cpi` builders (which want `&AccountInfo`) are not +// usable here. +use anchor_spl::metadata::{ + create_master_edition_v3, create_metadata_accounts_v3, + mpl_token_metadata::types::{CollectionDetails, Creator, DataV2}, + CreateMasterEditionV3, CreateMetadataAccountsV3, }; use super::validate_metadata_strings; #[derive(Accounts)] -pub struct CreateCollectionAccountConstraints<'info> { +pub struct CreateCollectionAccountConstraints { #[account(mut)] - user: Signer<'info>, + user: Signer, #[account( init, @@ -28,22 +29,22 @@ pub struct CreateCollectionAccountConstraints<'info> { mint::authority = mint_authority, mint::freeze_authority = mint_authority, )] - mint: Account<'info, Mint>, + mint: Account, #[account( seeds = [b"authority"], bump, )] /// CHECK: This account is not initialized and is being used for signing purposes only - pub mint_authority: UncheckedAccount<'info>, + pub mint_authority: UncheckedAccount, #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - metadata: UncheckedAccount<'info>, + metadata: UncheckedAccount, #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - master_edition: UncheckedAccount<'info>, + master_edition: UncheckedAccount, #[account( init, @@ -51,12 +52,12 @@ pub struct CreateCollectionAccountConstraints<'info> { associated_token::mint = mint, associated_token::authority = user )] - destination: Account<'info, TokenAccount>, + destination: Account, - system_program: Program<'info, System>, - token_program: Program<'info, Token>, - associated_token_program: Program<'info, AssociatedToken>, - token_metadata_program: Program<'info, Metadata>, + system_program: Program, + token_program: Program, + associated_token_program: Program, + token_metadata_program: Program, } /// Creates a collection NFT with caller-supplied metadata. @@ -72,80 +73,74 @@ pub fn handle_create_collection( ) -> Result<()> { validate_metadata_strings(&name, &symbol, &uri)?; - let metadata = &accounts.metadata.to_account_info(); - let master_edition = &accounts.master_edition.to_account_info(); - let mint = &accounts.mint.to_account_info(); - let authority = &accounts.mint_authority.to_account_info(); - let payer = &accounts.user.to_account_info(); - let system_program = &accounts.system_program.to_account_info(); - let spl_token_program = &accounts.token_program.to_account_info(); - let spl_metadata_program = &accounts.token_metadata_program.to_account_info(); - let seeds = &[&b"authority"[..], &[bumps.mint_authority]]; let signer_seeds = &[&seeds[..]]; let cpi_accounts = MintTo { - mint: accounts.mint.to_account_info(), - to: accounts.destination.to_account_info(), - authority: accounts.mint_authority.to_account_info(), + mint: accounts.mint.cpi_handle_mut(), + to: accounts.destination.cpi_handle_mut(), + authority: accounts.mint_authority.cpi_handle(), }; let cpi_ctx = - CpiContext::new_with_signer(accounts.token_program.key(), cpi_accounts, signer_seeds); + CpiContext::new_with_signer(accounts.token_program.address(), cpi_accounts, signer_seeds); mint_to(cpi_ctx, 1)?; msg!("Collection NFT minted!"); let creator = vec![Creator { - address: accounts.mint_authority.key(), + address: *accounts.mint_authority.address(), verified: true, share: 100, }]; - let metadata_account = CreateMetadataAccountV3Cpi::new( - spl_metadata_program, - CreateMetadataAccountV3CpiAccounts { - metadata, - mint, - mint_authority: authority, - payer, - update_authority: (authority, true), - system_program, - rent: None, - }, - CreateMetadataAccountV3InstructionArgs { - data: DataV2 { - name, - symbol, - uri, - seller_fee_basis_points: 0, - creators: Some(creator), - collection: None, - uses: None, + // Read-only slots use the wrapper's own `cpi_handle()`: it takes `&self`, + // so one account can fill several of them, and on a data account it also + // relaxes the runtime borrow check that a hand-built + // `CpiHandle::readonly(&copied_view)` would still trip. + create_metadata_accounts_v3( + CpiContext::new_with_signer( + accounts.token_metadata_program.address(), + CreateMetadataAccountsV3 { + metadata: accounts.metadata.cpi_handle_mut(), + mint: accounts.mint.cpi_handle(), + mint_authority: accounts.mint_authority.cpi_handle(), + payer: accounts.user.cpi_handle_mut(), + update_authority: accounts.mint_authority.cpi_handle(), + system_program: accounts.system_program.cpi_handle(), + update_authority_is_signer: true, }, - is_mutable: true, - collection_details: Some(CollectionDetails::V1 { size: 0 }), + signer_seeds, + ), + DataV2 { + name, + symbol, + uri, + seller_fee_basis_points: 0, + creators: Some(creator), + collection: None, + uses: None, }, - ); - metadata_account.invoke_signed(signer_seeds)?; + true, + Some(CollectionDetails::V1 { size: 0 }), + )?; msg!("Metadata Account created!"); - let master_edition_account = CreateMasterEditionV3Cpi::new( - spl_metadata_program, - CreateMasterEditionV3CpiAccounts { - edition: master_edition, - update_authority: authority, - mint_authority: authority, - mint, - payer, - metadata, - token_program: spl_token_program, - system_program, - rent: None, - }, - CreateMasterEditionV3InstructionArgs { - max_supply: Some(0), - }, - ); - master_edition_account.invoke_signed(signer_seeds)?; + create_master_edition_v3( + CpiContext::new_with_signer( + accounts.token_metadata_program.address(), + CreateMasterEditionV3 { + edition: accounts.master_edition.cpi_handle_mut(), + mint: accounts.mint.cpi_handle_mut(), + update_authority: accounts.mint_authority.cpi_handle(), + mint_authority: accounts.mint_authority.cpi_handle(), + payer: accounts.user.cpi_handle_mut(), + metadata: accounts.metadata.cpi_handle_mut(), + token_program: accounts.token_program.cpi_handle(), + system_program: accounts.system_program.cpi_handle(), + }, + signer_seeds, + ), + Some(0), + )?; msg!("Master Edition Account created"); Ok(()) diff --git a/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/mint_nft.rs b/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/mint_nft.rs index 04023aada..961dd5158 100644 --- a/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/mint_nft.rs +++ b/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/mint_nft.rs @@ -1,25 +1,26 @@ use anchor_lang::prelude::*; +use anchor_spl::mint; use anchor_spl::{ associated_token::AssociatedToken, metadata::Metadata, token::{mint_to, Mint, MintTo, Token, TokenAccount}, }; -use anchor_spl::metadata::mpl_token_metadata::{ - instructions::{ - CreateMasterEditionV3Cpi, CreateMasterEditionV3CpiAccounts, - CreateMasterEditionV3InstructionArgs, CreateMetadataAccountV3Cpi, - CreateMetadataAccountV3CpiAccounts, CreateMetadataAccountV3InstructionArgs, - }, - types::{Collection, Creator, DataV2}, +// v2's anchor-spl wraps these CPIs in terms of `CpiHandle`s, so the raw +// mpl-token-metadata `*Cpi` builders (which want `&AccountInfo`) are not +// usable here. +use anchor_spl::metadata::{ + create_master_edition_v3, create_metadata_accounts_v3, + mpl_token_metadata::types::{Collection, Creator, DataV2}, + CreateMasterEditionV3, CreateMetadataAccountsV3, }; use super::validate_metadata_strings; #[derive(Accounts)] -pub struct MintNftAccountConstraints<'info> { +pub struct MintNftAccountConstraints { #[account(mut)] - pub owner: Signer<'info>, + pub owner: Signer, #[account( init, @@ -28,7 +29,7 @@ pub struct MintNftAccountConstraints<'info> { mint::authority = mint_authority, mint::freeze_authority = mint_authority, )] - pub mint: Account<'info, Mint>, + pub mint: Account, #[account( init, @@ -36,30 +37,30 @@ pub struct MintNftAccountConstraints<'info> { associated_token::mint = mint, associated_token::authority = owner )] - pub destination: Account<'info, TokenAccount>, + pub destination: Account, #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - pub metadata: UncheckedAccount<'info>, + pub metadata: UncheckedAccount, #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - pub master_edition: UncheckedAccount<'info>, + pub master_edition: UncheckedAccount, #[account( seeds = [b"authority"], bump, )] /// CHECK: This is account is not initialized and is being used for signing purposes only - pub mint_authority: UncheckedAccount<'info>, + pub mint_authority: UncheckedAccount, #[account(mut)] - pub collection_mint: Account<'info, Mint>, + pub collection_mint: Account, - pub system_program: Program<'info, System>, - pub token_program: Program<'info, Token>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_metadata_program: Program<'info, Metadata>, + pub system_program: Program, + pub token_program: Program, + pub associated_token_program: Program, + pub token_metadata_program: Program, } /// Mints an NFT into the collection with caller-supplied metadata. @@ -76,82 +77,76 @@ pub fn handle_mint_nft( ) -> Result<()> { validate_metadata_strings(&name, &symbol, &uri)?; - let metadata = &accounts.metadata.to_account_info(); - let master_edition = &accounts.master_edition.to_account_info(); - let mint = &accounts.mint.to_account_info(); - let authority = &accounts.mint_authority.to_account_info(); - let payer = &accounts.owner.to_account_info(); - let system_program = &accounts.system_program.to_account_info(); - let spl_token_program = &accounts.token_program.to_account_info(); - let spl_metadata_program = &accounts.token_metadata_program.to_account_info(); - let seeds = &[&b"authority"[..], &[bumps.mint_authority]]; let signer_seeds = &[&seeds[..]]; let cpi_accounts = MintTo { - mint: accounts.mint.to_account_info(), - to: accounts.destination.to_account_info(), - authority: accounts.mint_authority.to_account_info(), + mint: accounts.mint.cpi_handle_mut(), + to: accounts.destination.cpi_handle_mut(), + authority: accounts.mint_authority.cpi_handle(), }; let cpi_ctx = - CpiContext::new_with_signer(accounts.token_program.key(), cpi_accounts, signer_seeds); + CpiContext::new_with_signer(accounts.token_program.address(), cpi_accounts, signer_seeds); mint_to(cpi_ctx, 1)?; msg!("NFT minted!"); let creator = vec![Creator { - address: accounts.mint_authority.key(), + address: *accounts.mint_authority.address(), verified: true, share: 100, }]; - let metadata_account = CreateMetadataAccountV3Cpi::new( - spl_metadata_program, - CreateMetadataAccountV3CpiAccounts { - metadata, - mint, - mint_authority: authority, - payer, - update_authority: (authority, true), - system_program, - rent: None, - }, - CreateMetadataAccountV3InstructionArgs { - data: DataV2 { - name, - symbol, - uri, - seller_fee_basis_points: 0, - creators: Some(creator), - collection: Some(Collection { - verified: false, - key: accounts.collection_mint.key(), - }), - uses: None, + // Read-only slots use the wrapper's own `cpi_handle()`: it takes `&self`, + // so one account can fill several of them, and on a data account it also + // relaxes the runtime borrow check that a hand-built + // `CpiHandle::readonly(&copied_view)` would still trip. + create_metadata_accounts_v3( + CpiContext::new_with_signer( + accounts.token_metadata_program.address(), + CreateMetadataAccountsV3 { + metadata: accounts.metadata.cpi_handle_mut(), + mint: accounts.mint.cpi_handle(), + mint_authority: accounts.mint_authority.cpi_handle(), + payer: accounts.owner.cpi_handle_mut(), + update_authority: accounts.mint_authority.cpi_handle(), + system_program: accounts.system_program.cpi_handle(), + update_authority_is_signer: true, }, - is_mutable: true, - collection_details: None, - }, - ); - metadata_account.invoke_signed(signer_seeds)?; - - let master_edition_account = CreateMasterEditionV3Cpi::new( - spl_metadata_program, - CreateMasterEditionV3CpiAccounts { - edition: master_edition, - update_authority: authority, - mint_authority: authority, - mint, - payer, - metadata, - token_program: spl_token_program, - system_program, - rent: None, + signer_seeds, + ), + DataV2 { + name, + symbol, + uri, + seller_fee_basis_points: 0, + creators: Some(creator), + collection: Some(Collection { + verified: false, + key: *accounts.collection_mint.address(), + }), + uses: None, }, - CreateMasterEditionV3InstructionArgs { - max_supply: Some(0), - }, - ); - master_edition_account.invoke_signed(signer_seeds)?; + true, + None, + )?; + + create_master_edition_v3( + CpiContext::new_with_signer( + accounts.token_metadata_program.address(), + CreateMasterEditionV3 { + edition: accounts.master_edition.cpi_handle_mut(), + mint: accounts.mint.cpi_handle_mut(), + update_authority: accounts.mint_authority.cpi_handle(), + mint_authority: accounts.mint_authority.cpi_handle(), + payer: accounts.owner.cpi_handle_mut(), + metadata: accounts.metadata.cpi_handle_mut(), + token_program: accounts.token_program.cpi_handle(), + system_program: accounts.system_program.cpi_handle(), + }, + signer_seeds, + ), + Some(0), + )?; Ok(()) } diff --git a/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/verify_collection.rs b/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/verify_collection.rs index 4748ec254..1fbd1ebb7 100644 --- a/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/verify_collection.rs +++ b/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/verify_collection.rs @@ -1,77 +1,66 @@ use anchor_lang::prelude::*; -use anchor_spl::metadata::mpl_token_metadata::instructions::{ - VerifyCollectionV1Cpi, - VerifyCollectionV1CpiAccounts, -}; +// v2's anchor-spl wraps this CPI in terms of `CpiHandle`s, so the raw +// mpl-token-metadata `*Cpi` builder (which wants `&AccountInfo`) is not +// usable here. The collection is created sized (`CollectionDetails::V1`), so +// the sized-item variant is the matching instruction. use anchor_spl::metadata::{ - MasterEditionAccount, - MetadataAccount, -}; -use anchor_spl::{ - token::Mint, - metadata::Metadata, + verify_sized_collection_item, MasterEditionAccount, MetadataAccount, + VerifySizedCollectionItem, }; -// In Anchor 1.0, sysvar::instructions::ID moved - use the well-known address directly -const INSTRUCTIONS_SYSVAR_ID: Pubkey = anchor_lang::solana_program::pubkey::pubkey!("Sysvar1nstructions1111111111111111111111111"); +use anchor_spl::{metadata::Metadata, token::Mint}; +// pinocchio does not re-export the instructions sysvar id; decode it here. +const INSTRUCTIONS_SYSVAR_ID: Address = + anchor_lang::address!("Sysvar1nstructions1111111111111111111111111"); #[derive(Accounts)] -pub struct VerifyCollectionMintAccountConstraints<'info> { - pub authority: Signer<'info>, +pub struct VerifyCollectionMintAccountConstraints { #[account(mut)] - pub metadata: Account<'info, MetadataAccount>, - pub mint: Account<'info, Mint>, + pub authority: Signer, + #[account(mut)] + pub metadata: MetadataAccount, + pub mint: Account, #[account( seeds = [b"authority"], bump, )] /// CHECK: This account is not initialized and is being used for signing purposes only - pub mint_authority: UncheckedAccount<'info>, - pub collection_mint: Account<'info, Mint>, + pub mint_authority: UncheckedAccount, + pub collection_mint: Account, #[account(mut)] - pub collection_metadata: Account<'info, MetadataAccount>, - pub collection_master_edition: Account<'info, MasterEditionAccount>, - pub system_program: Program<'info, System>, + pub collection_metadata: MetadataAccount, + pub collection_master_edition: MasterEditionAccount, + pub system_program: Program, #[account(address = INSTRUCTIONS_SYSVAR_ID)] /// CHECK: Sysvar instruction account that is being checked with an address constraint - pub sysvar_instruction: UncheckedAccount<'info>, - pub token_metadata_program: Program<'info, Metadata>, + pub sysvar_instruction: UncheckedAccount, + pub token_metadata_program: Program, } pub fn handle_verify_collection( accounts: &mut VerifyCollectionMintAccountConstraints, bumps: &VerifyCollectionMintAccountConstraintsBumps, ) -> Result<()> { - let metadata = &accounts.metadata.to_account_info(); - let authority = &accounts.mint_authority.to_account_info(); - let collection_mint = &accounts.collection_mint.to_account_info(); - let collection_metadata = &accounts.collection_metadata.to_account_info(); - let collection_master_edition = &accounts.collection_master_edition.to_account_info(); - let system_program = &accounts.system_program.to_account_info(); - let sysvar_instructions = &accounts.sysvar_instruction.to_account_info(); - let spl_metadata_program = &accounts.token_metadata_program.to_account_info(); + let seeds = &[&b"authority"[..], &[bumps.mint_authority]]; + let signer_seeds = &[&seeds[..]]; - let seeds = &[ - &b"authority"[..], - &[bumps.mint_authority] - ]; - let signer_seeds = &[&seeds[..]]; + verify_sized_collection_item( + CpiContext::new_with_signer( + accounts.token_metadata_program.address(), + VerifySizedCollectionItem { + metadata: accounts.metadata.cpi_handle_mut(), + collection_authority: accounts.mint_authority.cpi_handle(), + payer: accounts.authority.cpi_handle_mut(), + collection_mint: accounts.collection_mint.cpi_handle(), + collection_metadata: accounts.collection_metadata.cpi_handle_mut(), + collection_master_edition: accounts.collection_master_edition.cpi_handle(), + }, + signer_seeds, + ), + None, + )?; - let verify_collection = VerifyCollectionV1Cpi::new( - spl_metadata_program, - VerifyCollectionV1CpiAccounts { - authority, - delegate_record: None, - metadata, - collection_mint, - collection_metadata: Some(collection_metadata), - collection_master_edition: Some(collection_master_edition), - system_program, - sysvar_instructions, - }); - verify_collection.invoke_signed(signer_seeds)?; + msg!("Collection Verified!"); - msg!("Collection Verified!"); - - Ok(()) - } + Ok(()) +} diff --git a/tokens/nft-operations/anchor/programs/mint-nft/src/lib.rs b/tokens/nft-operations/anchor/programs/mint-nft/src/lib.rs index 2e1c3eac5..a44799564 100644 --- a/tokens/nft-operations/anchor/programs/mint-nft/src/lib.rs +++ b/tokens/nft-operations/anchor/programs/mint-nft/src/lib.rs @@ -14,7 +14,7 @@ pub mod mint_nft { /// Create a collection NFT with the given metadata. pub fn create_collection( - mut context: Context, + mut context: &mut Context, name: String, symbol: String, uri: String, @@ -30,17 +30,23 @@ pub mod mint_nft { /// Mint an NFT into the collection with the given metadata. pub fn mint_nft( - mut context: Context, + mut context: &mut Context, name: String, symbol: String, uri: String, ) -> Result<()> { - instructions::mint_nft::handle_mint_nft(&mut context.accounts, &context.bumps, name, symbol, uri) + instructions::mint_nft::handle_mint_nft( + &mut context.accounts, + &context.bumps, + name, + symbol, + uri, + ) } /// Verify an NFT as a member of the collection. pub fn verify_collection( - mut context: Context, + mut context: &mut Context, ) -> Result<()> { instructions::verify_collection::handle_verify_collection( &mut context.accounts, diff --git a/tokens/nft-operations/anchor/programs/mint-nft/tests/test_nft_operations.rs b/tokens/nft-operations/anchor/programs/mint-nft/tests/test_nft_operations.rs index 93bb7e51c..739719c4c 100644 --- a/tokens/nft-operations/anchor/programs/mint-nft/tests/test_nft_operations.rs +++ b/tokens/nft-operations/anchor/programs/mint-nft/tests/test_nft_operations.rs @@ -1,60 +1,58 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, - solana_kite::{ - create_wallet, get_token_account_balance, send_transaction_from_instructions, - }, + solana_kite::{create_wallet, get_token_account_balance, send_transaction_from_instructions}, solana_signer::Signer, }; -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn ata_program_id() -> Pubkey { +fn ata_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn metadata_program_id() -> Pubkey { +fn metadata_program_id() -> Address { "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" .parse() .unwrap() } -fn instructions_sysvar_id() -> Pubkey { +fn instructions_sysvar_id() -> Address { "Sysvar1nstructions1111111111111111111111111" .parse() .unwrap() } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - let (ata, _bump) = Pubkey::find_program_address( +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + let (ata, _bump) = Address::find_program_address( &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &ata_program_id(), ); ata } -fn derive_metadata_pda(mint: &Pubkey) -> Pubkey { +fn derive_metadata_pda(mint: &Address) -> Address { let metadata_pid = metadata_program_id(); - let (pda, _bump) = Pubkey::find_program_address( + let (pda, _bump) = Address::find_program_address( &[b"metadata", metadata_pid.as_ref(), mint.as_ref()], &metadata_pid, ); pda } -fn derive_edition_pda(mint: &Pubkey) -> Pubkey { +fn derive_edition_pda(mint: &Address) -> Address { let metadata_pid = metadata_program_id(); - let (pda, _bump) = Pubkey::find_program_address( + let (pda, _bump) = Address::find_program_address( &[ b"metadata", metadata_pid.as_ref(), @@ -70,10 +68,12 @@ fn derive_edition_pda(mint: &Pubkey) -> Pubkey { /// caller-supplied metadata strings landed in the Metaplex metadata account /// without fully deserializing the Metaplex layout. fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool { - haystack.windows(needle.len()).any(|window| window == needle) + haystack + .windows(needle.len()) + .any(|window| window == needle) } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = mint_nft::id(); let mut svm = LiteSVM::new(); @@ -93,7 +93,7 @@ fn test_create_collection() { let (mut svm, program_id, payer) = setup(); let collection_keypair = Keypair::new(); - let (mint_authority, _) = Pubkey::find_program_address(&[b"authority"], &program_id); + let (mint_authority, _) = Address::find_program_address(&[b"authority"], &program_id); let metadata = derive_metadata_pda(&collection_keypair.pubkey()); let master_edition = derive_edition_pda(&collection_keypair.pubkey()); @@ -114,7 +114,7 @@ fn test_create_collection() { metadata, master_edition, destination, - system_program: system_program::id(), + system_program: system_program::ID, token_program: token_program_id(), associated_token_program: ata_program_id(), token_metadata_program: metadata_program_id(), @@ -137,9 +137,7 @@ fn test_create_collection() { assert!(!mint_account.data.is_empty()); // Verify metadata exists and carries the caller-supplied name - let meta_account = svm - .get_account(&metadata) - .expect("Metadata should exist"); + let meta_account = svm.get_account(&metadata).expect("Metadata should exist"); assert!(!meta_account.data.is_empty()); assert!( contains_bytes(&meta_account.data, b"Example Collection"), @@ -161,7 +159,7 @@ fn test_create_collection() { fn test_mint_nft_to_collection() { let (mut svm, program_id, payer) = setup(); - let (mint_authority, _) = Pubkey::find_program_address(&[b"authority"], &program_id); + let (mint_authority, _) = Address::find_program_address(&[b"authority"], &program_id); // Step 1: Create the collection let collection_keypair = Keypair::new(); @@ -184,7 +182,7 @@ fn test_mint_nft_to_collection() { metadata: collection_metadata, master_edition: collection_master_edition, destination: collection_destination, - system_program: system_program::id(), + system_program: system_program::ID, token_program: token_program_id(), associated_token_program: ata_program_id(), token_metadata_program: metadata_program_id(), @@ -223,7 +221,7 @@ fn test_mint_nft_to_collection() { master_edition: nft_master_edition, mint_authority, collection_mint: collection_keypair.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, token_program: token_program_id(), associated_token_program: ata_program_id(), token_metadata_program: metadata_program_id(), @@ -258,7 +256,7 @@ fn test_mint_nft_to_collection() { fn test_verify_collection() { let (mut svm, program_id, payer) = setup(); - let (mint_authority, _) = Pubkey::find_program_address(&[b"authority"], &program_id); + let (mint_authority, _) = Address::find_program_address(&[b"authority"], &program_id); // Step 1: Create collection let collection_keypair = Keypair::new(); @@ -281,7 +279,7 @@ fn test_verify_collection() { metadata: collection_metadata, master_edition: collection_master_edition, destination: collection_destination, - system_program: system_program::id(), + system_program: system_program::ID, token_program: token_program_id(), associated_token_program: ata_program_id(), token_metadata_program: metadata_program_id(), @@ -320,7 +318,7 @@ fn test_verify_collection() { master_edition: nft_master_edition, mint_authority, collection_mint: collection_keypair.pubkey(), - system_program: system_program::id(), + system_program: system_program::ID, token_program: token_program_id(), associated_token_program: ata_program_id(), token_metadata_program: metadata_program_id(), @@ -349,20 +347,15 @@ fn test_verify_collection() { collection_mint: collection_keypair.pubkey(), collection_metadata, collection_master_edition, - system_program: system_program::id(), + system_program: system_program::ID, sysvar_instruction: instructions_sysvar_id(), token_metadata_program: metadata_program_id(), } .to_account_metas(None), ); - send_transaction_from_instructions( - &mut svm, - vec![verify_ix], - &[&payer], - &payer.pubkey(), - ) - .unwrap(); + send_transaction_from_instructions(&mut svm, vec![verify_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify the metadata still exists after verification let nft_meta = svm diff --git a/tokens/pda-mint-authority/anchor/programs/token-minter/Cargo.toml b/tokens/pda-mint-authority/anchor/programs/token-minter/Cargo.toml index 42928e358..89d54c306 100644 --- a/tokens/pda-mint-authority/anchor/programs/token-minter/Cargo.toml +++ b/tokens/pda-mint-authority/anchor/programs/token-minter/Cargo.toml @@ -14,14 +14,25 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = { version = "1.1.2", features = ["metadata"] } +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = { version = "2.0.0-rc.1", features = ["metadata"] } +# For `Pack`, which gives the classic mint its `LEN`. anchor-spl re-exports +# the state types but not the trait. +solana-program-pack = "3" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/pda-mint-authority/anchor/programs/token-minter/src/instructions/create.rs b/tokens/pda-mint-authority/anchor/programs/token-minter/src/instructions/create.rs index 75891fe9f..f39019e75 100644 --- a/tokens/pda-mint-authority/anchor/programs/token-minter/src/instructions/create.rs +++ b/tokens/pda-mint-authority/anchor/programs/token-minter/src/instructions/create.rs @@ -2,73 +2,109 @@ // This is to demonstrate that the same PDA can be used for both the address of an account and CPI signing use { anchor_lang::prelude::*, + // `Mint::LEN` comes from `Pack`, which anchor-spl does not re-export. + solana_program_pack::Pack, + anchor_lang::system_program::{create_account, CreateAccount}, anchor_spl::{ metadata::{ create_metadata_accounts_v3, mpl_token_metadata::types::DataV2, CreateMetadataAccountsV3, Metadata, }, - token::{Mint, Token}, + token::{initialize_mint2, spl_token::state::Mint as MintState, InitializeMint2, Token}, }, }; #[derive(Accounts)] -pub struct CreateTokenAccountConstraints<'info> { +pub struct CreateTokenAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, - // Create mint account - // Same PDA as address of the account and mint/freeze authority + // Create mint account. The same PDA is both the account's address and its + // mint/freeze authority, which is the point of this example, and which v2 + // cannot express as an `init` constraint: `mint::authority` has to name a + // sibling field, and referencing the account being initialized is rejected + // at macro-expansion time. So the mint is created by hand in + // `handle_create_token` below. + /// CHECK: created and initialized as a mint by this instruction. #[account( - init, + mut, seeds = [b"mint"], bump, - payer = payer, - mint::decimals = 9, - mint::authority = mint_account.key(), - mint::freeze_authority = mint_account.key(), - )] - pub mint_account: Account<'info, Mint>, + pub mint_account: UncheckedAccount, /// CHECK: Validate address by deriving pda #[account( mut, - seeds = [b"metadata", token_metadata_program.key().as_ref(), mint_account.key().as_ref()], + seeds = [b"metadata", token_metadata_program.address().as_ref(), mint_account.address().as_ref()], bump, - seeds::program = token_metadata_program.key(), + seeds::program = token_metadata_program.address(), )] - pub metadata_account: UncheckedAccount<'info>, + pub metadata_account: UncheckedAccount, - pub token_program: Program<'info, Token>, - pub token_metadata_program: Program<'info, Metadata>, - pub system_program: Program<'info, System>, - pub rent: Sysvar<'info, Rent>, + pub token_program: Program, + pub token_metadata_program: Program, + pub system_program: Program, + pub rent: Sysvar, } pub fn handle_create_token( - context: Context, + context: &mut Context, token_name: String, token_symbol: String, token_uri: String, ) -> Result<()> { - msg!("Creating metadata account"); - // PDA signer seeds let signer_seeds: &[&[&[u8]]] = &[&[b"mint", &[context.bumps.mint_account]]]; + msg!("Creating mint account"); + + // Allocate and initialize the mint, naming the mint PDA as both mint and + // freeze authority. `create_account` is signed by the PDA because the PDA + // is the account being created. + let mint_address = *context.accounts.mint_account.address(); + let lamports = Rent::get()?.try_minimum_balance(MintState::LEN)?; + create_account( + CpiContext::new( + context.accounts.system_program.address(), + CreateAccount { + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), + }, + ) + .with_signer(signer_seeds), + lamports, + MintState::LEN as u64, + context.accounts.token_program.address(), + )?; + + initialize_mint2( + CpiContext::new( + context.accounts.token_program.address(), + InitializeMint2 { + mint: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + 9, + &mint_address, + Some(&mint_address), + )?; + + msg!("Creating metadata account"); + // Cross Program Invocation (CPI) signed by PDA // Invoking the create_metadata_account_v3 instruction on the token metadata program create_metadata_accounts_v3( CpiContext::new( - context.accounts.token_metadata_program.key(), + context.accounts.token_metadata_program.address(), CreateMetadataAccountsV3 { - metadata: context.accounts.metadata_account.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - mint_authority: context.accounts.mint_account.to_account_info(), // PDA is mint authority - update_authority: context.accounts.mint_account.to_account_info(), // PDA is update authority - payer: context.accounts.payer.to_account_info(), - system_program: context.accounts.system_program.to_account_info(), - rent: context.accounts.rent.to_account_info(), + metadata: context.accounts.metadata_account.cpi_handle_mut(), + mint: context.accounts.mint_account.cpi_handle(), + mint_authority: context.accounts.mint_account.cpi_handle(), // PDA is mint authority + update_authority: context.accounts.mint_account.cpi_handle(), // PDA is update authority + payer: context.accounts.payer.cpi_handle_mut(), + system_program: context.accounts.system_program.cpi_handle(), + update_authority_is_signer: true, }, ) .with_signer(signer_seeds), @@ -82,7 +118,6 @@ pub fn handle_create_token( uses: None, }, false, // Is mutable - true, // Update authority is signer None, // Collection details )?; diff --git a/tokens/pda-mint-authority/anchor/programs/token-minter/src/instructions/mint.rs b/tokens/pda-mint-authority/anchor/programs/token-minter/src/instructions/mint.rs index eedae32c9..492cd30d2 100644 --- a/tokens/pda-mint-authority/anchor/programs/token-minter/src/instructions/mint.rs +++ b/tokens/pda-mint-authority/anchor/programs/token-minter/src/instructions/mint.rs @@ -7,9 +7,9 @@ use { }; #[derive(Accounts)] -pub struct MintTokenAccountConstraints<'info> { +pub struct MintTokenAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, // Mint account address is a PDA #[account( @@ -17,7 +17,7 @@ pub struct MintTokenAccountConstraints<'info> { seeds = [b"mint"], bump )] - pub mint_account: Account<'info, Mint>, + pub mint_account: Account, // Create Associated Token Account, if needed // This is the account that will hold the minted tokens @@ -27,11 +27,11 @@ pub struct MintTokenAccountConstraints<'info> { associated_token::mint = mint_account, associated_token::authority = payer, )] - pub associated_token_account: Account<'info, TokenAccount>, + pub associated_token_account: Account, - pub token_program: Program<'info, Token>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Program, + pub associated_token_program: Program, + pub system_program: Program, } /// Mints `amount` tokens to the payer's associated token account, signed by @@ -41,27 +41,34 @@ pub struct MintTokenAccountConstraints<'info> { /// on). Clients convert from major units, e.g. 1 token with 9 decimals is /// `1 * 10u64.pow(9)` minor units. pub fn handle_mint_token( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { msg!("Minting token to associated token account..."); - msg!("Mint: {}", &context.accounts.mint_account.key()); + msg!("Mint: {}", context.accounts.mint_account.address()); msg!( "Token Address: {}", - &context.accounts.associated_token_account.key() + context.accounts.associated_token_account.address() ); // PDA signer seeds let signer_seeds: &[&[&[u8]]] = &[&[b"mint", &[context.bumps.mint_account]]]; + // The mint is its own authority, so it fills a writable slot and a + // read-only one. `CpiHandleMut` is Copy and `into_readonly()` erases it + // while carrying the wrapper's relaxed borrow flag across, which a + // handle built by hand over a copy of the `AccountView` would not. + let mint_handle = context.accounts.mint_account.cpi_handle_mut(); + let mint_authority_handle = mint_handle.into_readonly(); + // Invoke the mint_to instruction on the token program mint_to( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), MintTo { - mint: context.accounts.mint_account.to_account_info(), - to: context.accounts.associated_token_account.to_account_info(), - authority: context.accounts.mint_account.to_account_info(), // PDA mint authority, required as signer + mint: mint_handle, + to: context.accounts.associated_token_account.cpi_handle_mut(), + authority: mint_authority_handle, }, ) .with_signer(signer_seeds), // using PDA to sign diff --git a/tokens/pda-mint-authority/anchor/programs/token-minter/src/lib.rs b/tokens/pda-mint-authority/anchor/programs/token-minter/src/lib.rs index fb5ba6795..d60398ee3 100644 --- a/tokens/pda-mint-authority/anchor/programs/token-minter/src/lib.rs +++ b/tokens/pda-mint-authority/anchor/programs/token-minter/src/lib.rs @@ -9,7 +9,7 @@ pub mod token_minter { use super::*; pub fn create_token( - context: Context, + context: &mut Context, token_name: String, token_symbol: String, token_uri: String, @@ -19,7 +19,7 @@ pub mod token_minter { /// Mint `amount` minor units of the token to the payer. pub fn mint_token( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { mint::handle_mint_token(context, amount) diff --git a/tokens/pda-mint-authority/anchor/programs/token-minter/tests/test_pda_mint.rs b/tokens/pda-mint-authority/anchor/programs/token-minter/tests/test_pda_mint.rs index f451e5a09..1c5f80a28 100644 --- a/tokens/pda-mint-authority/anchor/programs/token-minter/tests/test_pda_mint.rs +++ b/tokens/pda-mint-authority/anchor/programs/token-minter/tests/test_pda_mint.rs @@ -1,13 +1,11 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, - solana_kite::{ - create_wallet, get_token_account_balance, send_transaction_from_instructions, - }, + solana_kite::{create_wallet, get_token_account_balance, send_transaction_from_instructions}, solana_signer::Signer, }; @@ -21,52 +19,48 @@ fn to_minor_units(major_units: u64) -> u64 { major_units.checked_mul(10u64.pow(MINT_DECIMALS)).unwrap() } -fn metadata_program_id() -> Pubkey { +fn metadata_program_id() -> Address { "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" .parse() .unwrap() } -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn associated_token_program_id() -> Pubkey { +fn associated_token_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn rent_sysvar_id() -> Pubkey { +fn rent_sysvar_id() -> Address { "SysvarRent111111111111111111111111111111111" .parse() .unwrap() } -fn derive_metadata_pda(mint: &Pubkey) -> Pubkey { +fn derive_metadata_pda(mint: &Address) -> Address { let metadata_pid = metadata_program_id(); - let (pda, _bump) = Pubkey::find_program_address( + let (pda, _bump) = Address::find_program_address( &[b"metadata", metadata_pid.as_ref(), mint.as_ref()], &metadata_pid, ); pda } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - let (ata, _bump) = Pubkey::find_program_address( - &[ - wallet.as_ref(), - token_program_id().as_ref(), - mint.as_ref(), - ], +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + let (ata, _bump) = Address::find_program_address( + &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &associated_token_program_id(), ); ata } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = token_minter::id(); let mut svm = LiteSVM::new(); @@ -86,7 +80,7 @@ fn test_create_token_and_mint() { let (mut svm, program_id, payer) = setup(); // Derive the PDA mint account (seeds = [b"mint"]) - let (mint_pda, _bump) = Pubkey::find_program_address(&[b"mint"], &program_id); + let (mint_pda, _bump) = Address::find_program_address(&[b"mint"], &program_id); let metadata_account = derive_metadata_pda(&mint_pda); // 1. Create token @@ -104,18 +98,13 @@ fn test_create_token_and_mint() { metadata_account, token_program: token_program_id(), token_metadata_program: metadata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, rent: rent_sysvar_id(), } .to_account_metas(None), ); - send_transaction_from_instructions( - &mut svm, - vec![create_ix], - &[&payer], - &payer.pubkey(), - ) - .unwrap(); + send_transaction_from_instructions(&mut svm, vec![create_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify mint created let mint_account = svm.get_account(&mint_pda).expect("Mint PDA should exist"); @@ -143,17 +132,12 @@ fn test_create_token_and_mint() { associated_token_account: ata, token_program: token_program_id(), associated_token_program: associated_token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions( - &mut svm, - vec![mint_ix], - &[&payer], - &payer.pubkey(), - ) - .unwrap(); + send_transaction_from_instructions(&mut svm, vec![mint_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify 100 tokens minted (in minor units) let balance = get_token_account_balance(&svm, &ata).unwrap(); diff --git a/tokens/token-extensions/basics/anchor/programs/basics/Cargo.toml b/tokens/token-extensions/basics/anchor/programs/basics/Cargo.toml index fff167965..595ef823e 100644 --- a/tokens/token-extensions/basics/anchor/programs/basics/Cargo.toml +++ b/tokens/token-extensions/basics/anchor/programs/basics/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-spl = "1.1.2" -anchor-lang = { version = "1.1.2", features= ["init-if-needed"]} +anchor-spl = "2.0.0-rc.1" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_associated_token_account.rs b/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_associated_token_account.rs index 9c00f4be1..0474ad6f6 100644 --- a/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_associated_token_account.rs +++ b/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_associated_token_account.rs @@ -3,23 +3,28 @@ use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; #[derive(Accounts)] -pub struct CreateAssociatedTokenAccountAccountConstraints<'info> { +pub struct CreateAssociatedTokenAccountAccountConstraints { #[account(mut)] - pub signer: Signer<'info>, - pub mint: InterfaceAccount<'info, Mint>, + pub signer: Signer, + pub mint: InterfaceAccount, #[account( init, associated_token::mint = mint, payer = signer, associated_token::authority = signer, + // Required when the token program is an `Interface`: without it the + // init CPI is rejected with InvalidArgument. + associated_token::token_program = token_program, )] - pub token_account: InterfaceAccount<'info, TokenAccount>, - pub system_program: Program<'info, System>, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, + pub token_account: InterfaceAccount, + pub system_program: Program, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, } -pub fn handler(_context: Context) -> Result<()> { +pub fn handler( + _context: &mut Context, +) -> Result<()> { msg!("Create Associated Token Account"); Ok(()) } diff --git a/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_token.rs b/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_token.rs index bc08105ad..bc60b9c25 100644 --- a/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_token.rs +++ b/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_token.rs @@ -1,25 +1,32 @@ use anchor_lang::prelude::*; +use anchor_spl::mint; use anchor_spl::token_interface::{Mint, TokenInterface}; #[derive(Accounts)] #[instruction(token_name: String)] -pub struct CreateTokenAccountConstraints<'info> { +pub struct CreateTokenAccountConstraints { #[account(mut)] - pub signer: Signer<'info>, + pub signer: Signer, #[account( init, payer = signer, mint::decimals = 6, - mint::authority = signer.key(), - seeds = [b"token-2022-token", signer.key().as_ref(), token_name.as_bytes()], + mint::authority = signer, + // Required when the token program is an `Interface`: without it + // the init CPI is rejected with InvalidArgument. + mint::token_program = token_program, + seeds = [b"token-2022-token", signer.address().as_ref(), token_name.as_bytes()], bump, )] - pub mint: InterfaceAccount<'info, Mint>, - pub system_program: Program<'info, System>, - pub token_program: Interface<'info, TokenInterface>, + pub mint: InterfaceAccount, + pub system_program: Program, + pub token_program: Interface<'static, TokenInterface>, } -pub fn handler(_context: Context, _token_name: String) -> Result<()> { +pub fn handler( + _context: &mut Context, + _token_name: String, +) -> Result<()> { msg!("Create Token"); Ok(()) } diff --git a/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_token_account.rs b/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_token_account.rs index d5e87541e..0a607a723 100644 --- a/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_token_account.rs +++ b/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/create_token_account.rs @@ -1,25 +1,29 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; #[derive(Accounts)] -pub struct CreateTokenAccountAccountConstraints<'info> { +pub struct CreateTokenAccountAccountConstraints { #[account(mut)] - pub signer: Signer<'info>, - pub mint: InterfaceAccount<'info, Mint>, + pub signer: Signer, + pub mint: InterfaceAccount, #[account( init, token::mint = mint, token::authority = signer, + // Required when the token program is an `Interface`: without it + // the init CPI is rejected with InvalidArgument. + token::token_program = token_program, payer = signer, - seeds = [b"token-2022-token-account", signer.key().as_ref(), mint.key().as_ref()], + seeds = [b"token-2022-token-account", signer.address().as_ref(), mint.address().as_ref()], bump, )] - pub token_account: InterfaceAccount<'info, TokenAccount>, - pub system_program: Program<'info, System>, - pub token_program: Interface<'info, TokenInterface>, + pub token_account: InterfaceAccount, + pub system_program: Program, + pub token_program: Interface<'static, TokenInterface>, } -pub fn handler(_context: Context) -> Result<()> { +pub fn handler(_context: &mut Context) -> Result<()> { msg!("Create Token Account"); Ok(()) } diff --git a/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/mint_token.rs b/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/mint_token.rs index 26798f219..3d5f249fd 100644 --- a/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/mint_token.rs +++ b/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/mint_token.rs @@ -2,23 +2,23 @@ use anchor_lang::prelude::*; use anchor_spl::token_interface::{self, Mint, MintTo, TokenAccount, TokenInterface}; #[derive(Accounts)] -pub struct MintTokenAccountConstraints<'info> { +pub struct MintTokenAccountConstraints { #[account(mut)] - pub signer: Signer<'info>, + pub signer: Signer, #[account(mut)] - pub mint: InterfaceAccount<'info, Mint>, + pub mint: InterfaceAccount, #[account(mut)] - pub receiver: InterfaceAccount<'info, TokenAccount>, - pub token_program: Interface<'info, TokenInterface>, + pub receiver: InterfaceAccount, + pub token_program: Interface<'static, TokenInterface>, } -pub fn handler(context: Context, amount: u64) -> Result<()> { +pub fn handler(context: &mut Context, amount: u64) -> Result<()> { let cpi_accounts = MintTo { - mint: context.accounts.mint.to_account_info().clone(), - to: context.accounts.receiver.to_account_info().clone(), - authority: context.accounts.signer.to_account_info(), + mint: context.accounts.mint.cpi_handle_mut().clone(), + to: context.accounts.receiver.cpi_handle_mut().clone(), + authority: context.accounts.signer.cpi_handle(), }; - let cpi_program = context.accounts.token_program.key(); + let cpi_program = context.accounts.token_program.address(); let cpi_context = CpiContext::new(cpi_program, cpi_accounts); token_interface::mint_to(cpi_context, amount)?; msg!("Mint Token"); diff --git a/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/transfer_token.rs b/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/transfer_token.rs index 57ce4168a..d062a1c65 100644 --- a/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/transfer_token.rs +++ b/tokens/token-extensions/basics/anchor/programs/basics/src/instructions/transfer_token.rs @@ -3,36 +3,39 @@ use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token_interface::{self, Mint, TokenAccount, TokenInterface, TransferChecked}; #[derive(Accounts)] -pub struct TransferTokenAccountConstraints<'info> { +pub struct TransferTokenAccountConstraints { #[account(mut)] - pub signer: Signer<'info>, + pub signer: Signer, #[account(mut)] - pub from: InterfaceAccount<'info, TokenAccount>, - pub to: SystemAccount<'info>, + pub from: InterfaceAccount, + pub to: SystemAccount, #[account( init, associated_token::mint = mint, payer = signer, - associated_token::authority = to + associated_token::authority = to, + // Required when the token program is an `Interface`: without it the + // init CPI is rejected with InvalidArgument. + associated_token::token_program = token_program, )] - pub to_ata: InterfaceAccount<'info, TokenAccount>, + pub to_ata: InterfaceAccount, #[account(mut)] - pub mint: InterfaceAccount<'info, Mint>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, - pub associated_token_program: Program<'info, AssociatedToken>, + pub mint: InterfaceAccount, + pub token_program: Interface<'static, TokenInterface>, + pub system_program: Program, + pub associated_token_program: Program, } -pub fn handler(context: Context, amount: u64) -> Result<()> { +pub fn handler(context: &mut Context, amount: u64) -> Result<()> { let cpi_accounts = TransferChecked { - from: context.accounts.from.to_account_info().clone(), - mint: context.accounts.mint.to_account_info().clone(), - to: context.accounts.to_ata.to_account_info().clone(), - authority: context.accounts.signer.to_account_info(), + from: context.accounts.from.cpi_handle_mut().clone(), + mint: context.accounts.mint.cpi_handle().clone(), + to: context.accounts.to_ata.cpi_handle_mut().clone(), + authority: context.accounts.signer.cpi_handle(), }; - let cpi_program = context.accounts.token_program.key(); + let cpi_program = context.accounts.token_program.address(); let cpi_context = CpiContext::new(cpi_program, cpi_accounts); - token_interface::transfer_checked(cpi_context, amount, context.accounts.mint.decimals)?; + token_interface::transfer_checked(cpi_context, amount, context.accounts.mint.decimals())?; msg!("Transfer Token"); Ok(()) } diff --git a/tokens/token-extensions/basics/anchor/programs/basics/src/lib.rs b/tokens/token-extensions/basics/anchor/programs/basics/src/lib.rs index 0eb3bbf0b..94521ea83 100644 --- a/tokens/token-extensions/basics/anchor/programs/basics/src/lib.rs +++ b/tokens/token-extensions/basics/anchor/programs/basics/src/lib.rs @@ -10,25 +10,36 @@ pub mod anchor { use super::*; - pub fn create_token(context: Context, token_name: String) -> Result<()> { + pub fn create_token( + context: &mut Context, + token_name: String, + ) -> Result<()> { instructions::create_token::handler(context, token_name) } - pub fn create_token_account(context: Context) -> Result<()> { + pub fn create_token_account( + context: &mut Context, + ) -> Result<()> { instructions::create_token_account::handler(context) } pub fn create_associated_token_account( - context: Context, + context: &mut Context, ) -> Result<()> { instructions::create_associated_token_account::handler(context) } - pub fn transfer_token(context: Context, amount: u64) -> Result<()> { + pub fn transfer_token( + context: &mut Context, + amount: u64, + ) -> Result<()> { instructions::transfer_token::handler(context, amount) } - pub fn mint_token(context: Context, amount: u64) -> Result<()> { + pub fn mint_token( + context: &mut Context, + amount: u64, + ) -> Result<()> { instructions::mint_token::handler(context, amount) } } diff --git a/tokens/token-extensions/basics/anchor/programs/basics/tests/test_basics.rs b/tokens/token-extensions/basics/anchor/programs/basics/tests/test_basics.rs index 21b5218c8..589ed2506 100644 --- a/tokens/token-extensions/basics/anchor/programs/basics/tests/test_basics.rs +++ b/tokens/token-extensions/basics/anchor/programs/basics/tests/test_basics.rs @@ -1,26 +1,24 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ assert_token_account_balance, create_wallet, send_transaction_from_instructions, - token_extensions::{ - get_token_extensions_account_address, TOKEN_EXTENSIONS_PROGRAM_ID, - }, + token_extensions::{get_token_extensions_account_address, TOKEN_EXTENSIONS_PROGRAM_ID}, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn associated_token_program_id() -> Pubkey { +fn associated_token_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = anchor::id(); let mut svm = LiteSVM::new(); @@ -38,7 +36,7 @@ fn test_create_token_and_mint_and_transfer() { let token_name = "TestToken".to_string(); // Derive the mint PDA - let (mint, _bump) = Pubkey::find_program_address( + let (mint, _bump) = Address::find_program_address( &[ b"token-2022-token", payer.pubkey().as_ref(), @@ -57,13 +55,14 @@ fn test_create_token_and_mint_and_transfer() { anchor::accounts::CreateTokenAccountConstraints { signer: payer.pubkey(), mint, - system_program: system_program::id(), + system_program: system_program::ID, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![create_token_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![create_token_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify mint account exists let mint_account = svm.get_account(&mint).expect("Mint account should exist"); @@ -81,19 +80,18 @@ fn test_create_token_and_mint_and_transfer() { signer: payer.pubkey(), mint, token_account: payer_ata, - system_program: system_program::id(), + system_program: system_program::ID, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, associated_token_program: associated_token_program_id(), } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![create_ata_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![create_ata_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify ATA exists - let ata_account = svm - .get_account(&payer_ata) - .expect("Payer ATA should exist"); + let ata_account = svm.get_account(&payer_ata).expect("Payer ATA should exist"); assert!(!ata_account.data.is_empty(), "ATA should have data"); svm.expire_blockhash(); @@ -116,7 +114,8 @@ fn test_create_token_and_mint_and_transfer() { .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![mint_token_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![mint_token_ix], &[&payer], &payer.pubkey()) + .unwrap(); assert_token_account_balance( &svm, @@ -146,13 +145,14 @@ fn test_create_token_and_mint_and_transfer() { to_ata: receiver_ata, mint, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, associated_token_program: associated_token_program_id(), } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![transfer_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![transfer_ix], &[&payer], &payer.pubkey()) + .unwrap(); assert_token_account_balance( &svm, diff --git a/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/Cargo.toml b/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/Cargo.toml index 6f37da297..b684815f9 100644 --- a/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/Cargo.toml +++ b/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/src/lib.rs b/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/src/lib.rs index 23f51a37f..ac1616027 100644 --- a/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/src/lib.rs +++ b/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/src/lib.rs @@ -1,6 +1,12 @@ use anchor_lang::prelude::*; +use anchor_lang::system_program::{create_account, CreateAccount}; +use anchor_spl::token; use anchor_spl::{ - token_2022::{transfer_checked, TransferChecked}, + token_2022::{ + initialize_account3, + spl_token_2022::{extension::ExtensionType, pod::PodAccount}, + transfer_checked, InitializeAccount3, TransferChecked, + }, token_interface::{Mint, Token2022, TokenAccount}, }; @@ -12,45 +18,82 @@ declare_id!("6tU3MEowU6oxxeDZLSxEwzcEZsZrhBJsfUR6xECvShid"); pub mod cpi_guard { use super::*; - pub fn cpi_transfer(context: Context) -> Result<()> { + pub fn cpi_transfer(context: &mut Context) -> Result<()> { + // The recipient token account is a PDA that is its own authority. v2 + // rejects an `init` constraint naming the account being initialized + // (`token::authority` has to name a sibling field), so the account is + // created here instead: the `init_if_needed` semantics become an + // explicit "create when empty". + if context.accounts.recipient_token_account.account().data_len() == 0 { + let space = ExtensionType::try_calculate_account_len::(&[])?; + let lamports = Rent::get()?.try_minimum_balance(space)?; + let signer_seeds: &[&[&[u8]]] = + &[&[b"pda", &[context.bumps.recipient_token_account]]]; + + create_account( + CpiContext::new( + context.accounts.system_program.address(), + CreateAccount { + from: context.accounts.sender.cpi_handle_mut(), + to: context.accounts.recipient_token_account.cpi_handle_mut(), + }, + ) + .with_signer(signer_seeds), + lamports, + space as u64, + context.accounts.token_program.address(), + )?; + + let recipient_handle = context.accounts.recipient_token_account.cpi_handle_mut(); + initialize_account3( + CpiContext::new( + context.accounts.token_program.address(), + InitializeAccount3 { + account: recipient_handle, + mint: context.accounts.mint_account.cpi_handle(), + // The account is its own authority. + authority: recipient_handle.into_readonly(), + }, + ), + )?; + } + transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.sender_token_account.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - to: context.accounts.recipient_token_account.to_account_info(), - authority: context.accounts.sender.to_account_info(), + from: context.accounts.sender_token_account.cpi_handle_mut(), + mint: context.accounts.mint_account.cpi_handle(), + to: context.accounts.recipient_token_account.cpi_handle_mut(), + authority: context.accounts.sender.cpi_handle(), }, ), 1, - context.accounts.mint_account.decimals, + context.accounts.mint_account.decimals(), )?; Ok(()) } } #[derive(Accounts)] -pub struct CpiTransferAccountConstraints<'info> { +pub struct CpiTransferAccountConstraints { #[account(mut)] - pub sender: Signer<'info>, + pub sender: Signer, #[account( mut, token::mint = mint_account )] - pub sender_token_account: InterfaceAccount<'info, TokenAccount>, + pub sender_token_account: InterfaceAccount, + /// CHECK: created and initialized as a token account by this instruction, + /// with itself as the authority. See `cpi_transfer` above. #[account( - init_if_needed, - payer = sender, + mut, seeds = [b"pda"], bump, - token::mint = mint_account, - token::authority = recipient_token_account, - token::token_program = token_program )] - pub recipient_token_account: InterfaceAccount<'info, TokenAccount>, - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub recipient_token_account: UncheckedAccount, + pub mint_account: InterfaceAccount, + pub token_program: Program, + pub system_program: Program, } diff --git a/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/tests/test_cpi_guard.rs b/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/tests/test_cpi_guard.rs index 6f2145cc8..7bb8c0c29 100644 --- a/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/tests/test_cpi_guard.rs +++ b/tokens/token-extensions/cpi-guard/anchor/programs/cpi-guard/tests/test_cpi_guard.rs @@ -1,13 +1,10 @@ use { anchor_lang::{ - solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::{AccountMeta, Instruction}, + system_program, Address, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ assert_token_account_balance, create_wallet, send_transaction_from_instructions, token_extensions::{ @@ -15,11 +12,10 @@ use { TOKEN_EXTENSIONS_PROGRAM_ID, }, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = cpi_guard::id(); let mut svm = LiteSVM::new(); @@ -34,12 +30,12 @@ fn setup() -> (LiteSVM, Pubkey, Keypair) { /// Uses explicit keypair - kite's ATA creation won't work here because /// we need to reallocate and add the CPI Guard extension later. fn create_basic_token_account_instructions( - payer: &Pubkey, - token_account: &Pubkey, - mint: &Pubkey, - owner: &Pubkey, + payer: &Address, + token_account: &Address, + mint: &Address, + owner: &Address, ) -> Vec { - let rent_sysvar: Pubkey = "SysvarRent111111111111111111111111111111111" + let rent_sysvar: Address = "SysvarRent111111111111111111111111111111111" .parse() .unwrap(); let create_account_ix = anchor_lang::solana_program::system_instruction::create_account( @@ -64,9 +60,9 @@ fn create_basic_token_account_instructions( /// Reallocate instruction (instruction 29) to add extension types to a token account. fn reallocate_instruction( - token_account: &Pubkey, - payer: &Pubkey, - owner: &Pubkey, + token_account: &Address, + payer: &Address, + owner: &Address, extension_types: &[u16], ) -> Instruction { let mut data = vec![29u8]; @@ -78,7 +74,7 @@ fn reallocate_instruction( accounts: vec![ AccountMeta::new(*token_account, false), AccountMeta::new(*payer, true), - AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(system_program::ID, false), AccountMeta::new_readonly(*owner, true), ], data, @@ -86,7 +82,7 @@ fn reallocate_instruction( } /// EnableCpiGuard instruction (instruction 34, sub-instruction 0). -fn enable_cpi_guard_instruction(token_account: &Pubkey, owner: &Pubkey) -> Instruction { +fn enable_cpi_guard_instruction(token_account: &Address, owner: &Address) -> Instruction { Instruction { program_id: TOKEN_EXTENSIONS_PROGRAM_ID, accounts: vec![ @@ -98,7 +94,7 @@ fn enable_cpi_guard_instruction(token_account: &Pubkey, owner: &Pubkey) -> Instr } /// DisableCpiGuard instruction (instruction 34, sub-instruction 1). -fn disable_cpi_guard_instruction(token_account: &Pubkey, owner: &Pubkey) -> Instruction { +fn disable_cpi_guard_instruction(token_account: &Address, owner: &Address) -> Instruction { Instruction { program_id: TOKEN_EXTENSIONS_PROGRAM_ID, accounts: vec![ @@ -125,7 +121,13 @@ fn test_cpi_guard_prevents_transfer_then_allows_after_disable() { &mint, &payer.pubkey(), ); - send_transaction_from_instructions(&mut svm, token_ixs, &[&payer, &token_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + token_ixs, + &[&payer, &token_keypair], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Step 3: Reallocate to add CPI Guard extension space @@ -136,27 +138,23 @@ fn test_cpi_guard_prevents_transfer_then_allows_after_disable() { &payer.pubkey(), &[cpi_guard_extension_type], ); - send_transaction_from_instructions(&mut svm, vec![reallocate_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![reallocate_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 4: Enable CPI Guard let enable_ix = enable_cpi_guard_instruction(&token_keypair.pubkey(), &payer.pubkey()); - send_transaction_from_instructions(&mut svm, vec![enable_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![enable_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 5: Mint 1 token to the token account - mint_tokens_to_token_extensions_account( - &mut svm, - &mint, - &token_keypair.pubkey(), - 1, - &payer, - ).unwrap(); + mint_tokens_to_token_extensions_account(&mut svm, &mint, &token_keypair.pubkey(), 1, &payer) + .unwrap(); svm.expire_blockhash(); // Step 6: Try CPI transfer - should fail because CPI Guard is enabled - let (recipient_token_account, _bump) = - Pubkey::find_program_address(&[b"pda"], &program_id); + let (recipient_token_account, _bump) = Address::find_program_address(&[b"pda"], &program_id); let transfer_ix = Instruction::new_with_bytes( program_id, @@ -167,12 +165,13 @@ fn test_cpi_guard_prevents_transfer_then_allows_after_disable() { recipient_token_account, mint_account: mint, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - let result = send_transaction_from_instructions(&mut svm, vec![transfer_ix], &[&payer], &payer.pubkey()); + let result = + send_transaction_from_instructions(&mut svm, vec![transfer_ix], &[&payer], &payer.pubkey()); assert!( result.is_err(), "Transfer should fail when CPI Guard is enabled" @@ -181,7 +180,8 @@ fn test_cpi_guard_prevents_transfer_then_allows_after_disable() { // Step 7: Disable CPI Guard let disable_ix = disable_cpi_guard_instruction(&token_keypair.pubkey(), &payer.pubkey()); - send_transaction_from_instructions(&mut svm, vec![disable_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![disable_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 8: Transfer should now succeed @@ -194,12 +194,18 @@ fn test_cpi_guard_prevents_transfer_then_allows_after_disable() { recipient_token_account, mint_account: mint, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![transfer_ix2], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![transfer_ix2], &[&payer], &payer.pubkey()) + .unwrap(); - assert_token_account_balance(&svm, &recipient_token_account, 1, "Recipient should have 1 token"); + assert_token_account_balance( + &svm, + &recipient_token_account, + 1, + "Recipient should have 1 token", + ); } diff --git a/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/Cargo.toml b/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/Cargo.toml index 4073392e0..23e284f69 100644 --- a/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/Cargo.toml +++ b/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/instructions/initialize.rs b/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/instructions/initialize.rs index 939241aba..168392910 100644 --- a/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/instructions/initialize.rs +++ b/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/instructions/initialize.rs @@ -6,55 +6,51 @@ use anchor_spl::{ spl_token_2022::{extension::ExtensionType, pod::PodMint, state::AccountState}, InitializeMint2, }, - token_interface::{ - default_account_state_initialize, DefaultAccountStateInitialize, Token2022, - }, + token_interface::{default_account_state_initialize, DefaultAccountStateInitialize, Token2022}, }; #[derive(Accounts)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account(mut)] - pub mint_account: Signer<'info>, + pub mint_account: Signer, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub token_program: Program, + pub system_program: Program, } // There is currently not an anchor constraint to automatically initialize the DefaultAccountState extension // We can manually create and initialize the mint account via CPIs in the instruction handler -pub fn handler(context: Context) -> Result<()> { +pub fn handler(context: &mut Context) -> Result<()> { // Calculate space required for mint and extension data - let mint_size = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::DefaultAccountState, - ])?; + let mint_size = + ExtensionType::try_calculate_account_len::(&[ExtensionType::DefaultAccountState])?; // Calculate minimum lamports required for size of mint account with extensions - let lamports = (Rent::get()?).minimum_balance(mint_size); + let lamports = Rent::get()?.try_minimum_balance(mint_size)?; // Invoke System Program to create new account with space for mint and extension data create_account( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), CreateAccount { - from: context.accounts.payer.to_account_info(), - to: context.accounts.mint_account.to_account_info(), + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), }, ), - lamports, // Lamports - mint_size as u64, // Space - &context.accounts.token_program.key(), // Owner Program + lamports, // Lamports + mint_size as u64, // Space + &context.accounts.token_program.address(), // Owner Program )?; // Initialize the NonTransferable extension // This instruction must come before the instruction to initialize the mint data default_account_state_initialize( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), DefaultAccountStateInitialize { - token_program_id: context.accounts.token_program.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), }, ), &AccountState::Frozen, // default frozen token accounts @@ -63,14 +59,14 @@ pub fn handler(context: Context) -> Result<()> { // Initialize the standard mint account data initialize_mint2( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), InitializeMint2 { - mint: context.accounts.mint_account.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), }, ), - 2, // decimals - &context.accounts.payer.key(), // mint authority - Some(&context.accounts.payer.key()), // freeze authority + 2, // decimals + &context.accounts.payer.address(), // mint authority + Some(&context.accounts.payer.address()), // freeze authority )?; Ok(()) } diff --git a/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/instructions/update_default_state.rs b/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/instructions/update_default_state.rs index 7ed0ec0dc..9323ccf15 100644 --- a/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/instructions/update_default_state.rs +++ b/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/instructions/update_default_state.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::mint; use anchor_spl::token_interface::{ default_account_state_update, DefaultAccountStateUpdate, Mint, Token2022, }; @@ -6,21 +7,21 @@ use anchor_spl::token_interface::{ use crate::AnchorAccountState; #[derive(Accounts)] -pub struct UpdateDefaultStateAccountConstraints<'info> { +pub struct UpdateDefaultStateAccountConstraints { #[account(mut)] - pub freeze_authority: Signer<'info>, + pub freeze_authority: Signer, #[account( mut, mint::freeze_authority = freeze_authority, )] - pub mint_account: InterfaceAccount<'info, Mint>, + pub mint_account: InterfaceAccount, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub token_program: Program, + pub system_program: Program, } pub fn handler( - context: Context, + context: &mut Context, account_state: AnchorAccountState, ) -> Result<()> { // Convert AnchorAccountState to spl_token_2022::state::AccountState @@ -28,11 +29,10 @@ pub fn handler( default_account_state_update( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), DefaultAccountStateUpdate { - token_program_id: context.accounts.token_program.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - freeze_authority: context.accounts.freeze_authority.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), + freeze_authority: context.accounts.freeze_authority.cpi_handle(), }, ), &account_state, diff --git a/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/lib.rs b/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/lib.rs index 8d6ef76c6..cc491b70b 100644 --- a/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/lib.rs +++ b/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/src/lib.rs @@ -10,12 +10,12 @@ declare_id!("5LdYbHiUsFxVG8bfqoeBkhBYMRmWZb3BoLuABgYW7coB"); pub mod default_account_state { use super::*; - pub fn initialize(context: Context) -> Result<()> { + pub fn initialize(context: &mut Context) -> Result<()> { instructions::initialize::handler(context) } pub fn update_default_state( - context: Context, + context: &mut Context, account_state: AnchorAccountState, ) -> Result<()> { instructions::update_default_state::handler(context, account_state) @@ -24,7 +24,7 @@ pub mod default_account_state { // Custom enum to implement AnchorSerialize and AnchorDeserialize // This is required to pass the enum as an argument to the instruction -#[derive(AnchorSerialize, AnchorDeserialize)] +#[derive(IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub enum AnchorAccountState { Uninitialized, Initialized, diff --git a/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/tests/test_default_account_state.rs b/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/tests/test_default_account_state.rs index c23a86ef3..a14e03727 100644 --- a/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/tests/test_default_account_state.rs +++ b/tokens/token-extensions/default-account-state/anchor/programs/default-account-state/tests/test_default_account_state.rs @@ -1,32 +1,26 @@ use { anchor_lang::{ - solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::{AccountMeta, Instruction}, + system_program, Address, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ assert_token_account_balance, create_wallet, send_transaction_from_instructions, - token_extensions::{ - mint_tokens_to_token_extensions_account, TOKEN_EXTENSIONS_PROGRAM_ID, - }, + token_extensions::{mint_tokens_to_token_extensions_account, TOKEN_EXTENSIONS_PROGRAM_ID}, }, - solana_keypair::Keypair, solana_signer::Signer, }; /// Create a Token Extensions token account (165 bytes, no extra extensions). /// Uses explicit keypair - not an ATA - so we can inspect account state bytes. fn create_token_account_instruction( - payer: &Pubkey, - token_account: &Pubkey, - mint: &Pubkey, - owner: &Pubkey, + payer: &Address, + token_account: &Address, + mint: &Address, + owner: &Address, ) -> Vec { - let rent_sysvar: Pubkey = "SysvarRent111111111111111111111111111111111" + let rent_sysvar: Address = "SysvarRent111111111111111111111111111111111" .parse() .unwrap(); let create_ix = anchor_lang::solana_program::system_instruction::create_account( @@ -49,7 +43,7 @@ fn create_token_account_instruction( vec![create_ix, init_ix] } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = default_account_state::id(); let mut svm = LiteSVM::new(); @@ -73,11 +67,17 @@ fn test_default_account_state() { payer: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![initialize_ix], &[&payer, &mint_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![initialize_ix], + &[&payer, &mint_keypair], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Verify mint exists @@ -92,7 +92,13 @@ fn test_default_account_state() { &mint_keypair.pubkey(), &payer.pubkey(), ); - send_transaction_from_instructions(&mut svm, create_token1_ixs, &[&payer, &token1], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + create_token1_ixs, + &[&payer, &token1], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Verify token account state is frozen (byte 108 = account state: 0=uninitialized, 1=initialized, 2=frozen) @@ -110,10 +116,7 @@ fn test_default_account_state() { 1, &payer, ); - assert!( - result.is_err(), - "Minting to a frozen account should fail" - ); + assert!(result.is_err(), "Minting to a frozen account should fail"); svm.expire_blockhash(); // Step 4: Update default state to Initialized @@ -127,11 +130,12 @@ fn test_default_account_state() { freeze_authority: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![update_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![update_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 5: Create a new token account - should be initialized (not frozen) now @@ -142,7 +146,13 @@ fn test_default_account_state() { &mint_keypair.pubkey(), &payer.pubkey(), ); - send_transaction_from_instructions(&mut svm, create_token2_ixs, &[&payer, &token2], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + create_token2_ixs, + &[&payer, &token2], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Verify token2 is initialized (not frozen) @@ -159,7 +169,8 @@ fn test_default_account_state() { &token2.pubkey(), 1, &payer, - ).unwrap(); + ) + .unwrap(); assert_token_account_balance(&svm, &token2.pubkey(), 1, "Should have minted 1 token"); } diff --git a/tokens/token-extensions/group/anchor/programs/group/Cargo.toml b/tokens/token-extensions/group/anchor/programs/group/Cargo.toml index 1081814e8..d6c91ba9b 100644 --- a/tokens/token-extensions/group/anchor/programs/group/Cargo.toml +++ b/tokens/token-extensions/group/anchor/programs/group/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-extensions/group/anchor/programs/group/src/instructions/test_initialize_group.rs b/tokens/token-extensions/group/anchor/programs/group/src/instructions/test_initialize_group.rs index d7b992a0d..2e83d505c 100644 --- a/tokens/token-extensions/group/anchor/programs/group/src/instructions/test_initialize_group.rs +++ b/tokens/token-extensions/group/anchor/programs/group/src/instructions/test_initialize_group.rs @@ -1,66 +1,116 @@ use anchor_lang::prelude::*; -use anchor_spl::token_2022::spl_token_2022::extension::group_pointer::GroupPointer; -use anchor_spl::token_interface::{ - spl_token_2022::{ - extension::{BaseStateWithExtensions, StateWithExtensions}, - state::Mint as MintState, +use anchor_lang::system_program::{create_account, CreateAccount}; +use anchor_spl::{ + token_2022::{ + initialize_mint2, + spl_token_2022::{ + extension::{group_pointer::GroupPointer, ExtensionType}, + pod::PodMint, + }, + InitializeMint2, + }, + token_2022_extensions::group_pointer::{group_pointer_initialize, GroupPointerInitialize}, + token_interface::{ + spl_token_2022::{ + extension::{BaseStateWithExtensions, StateWithExtensions}, + state::Mint as MintState, + }, + Token2022, }, - Mint, Token2022, }; #[derive(Accounts)] -pub struct InitializeGroupAccountConstraints<'info> { +pub struct InitializeGroupAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, + /// CHECK: created and initialized by this instruction as a Token-2022 mint + /// carrying the GroupPointer extension. #[account( - init, + mut, seeds = [b"group"], bump, - payer = payer, - mint::decimals = 2, - mint::authority = mint_account, - mint::freeze_authority = mint_account, - extensions::group_pointer::authority = mint_account, - extensions::group_pointer::group_address = mint_account, )] - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub mint_account: UncheckedAccount, + pub token_program: Program, + pub system_program: Program, } -fn check_mint_data(accounts: &mut InitializeGroupAccountConstraints) -> Result<()> { - let mint = &accounts.mint_account.to_account_info(); - let mint_data = mint.data.borrow(); +fn check_mint_data(context: &Context) -> Result<()> { + let mint = context.accounts.mint_account.account(); + let mint_data = mint.try_borrow()?; let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; let extension_data = mint_with_extension.get_extension::()?; - msg!("{:?}", mint_with_extension); msg!("{:?}", extension_data); Ok(()) } -pub fn handler(mut context: Context) -> Result<()> { - check_mint_data(&mut context.accounts)?; +// There is currently not an anchor constraint to automatically initialize the +// GroupPointer extension. We can manually create and initialize the mint +// account via CPIs in the instruction handler. The mint is a PDA, so it signs +// its own creation with its seeds. +pub fn handler(context: &mut Context) -> Result<()> { + let bump = context.bumps.mint_account; + let signer_seeds: &[&[&[u8]]] = &[&[b"group", &[bump]]]; + + // Calculate space required for mint and extension data + let mint_size = + ExtensionType::try_calculate_account_len::(&[ExtensionType::GroupPointer])?; + + // Calculate minimum lamports required for size of mint account with extensions + let lamports = Rent::get()?.try_minimum_balance(mint_size)?; + + // The mint PDA is both the authority and the group address, so take a copy + // of its `AccountView` for the read-only uses. v2's typed handles enforce + // borrow exclusivity at compile time. + let mint_address = *context.accounts.mint_account.address(); + + // Invoke System Program to create new account with space for mint and extension data + create_account( + CpiContext::new( + context.accounts.system_program.address(), + CreateAccount { + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), + }, + ) + .with_signer(signer_seeds), + lamports, // Lamports + mint_size as u64, // Space + context.accounts.token_program.address(), // Owner Program + )?; + + // Initialize the GroupPointer extension + // This instruction must come before the instruction to initialize the mint data + group_pointer_initialize( + CpiContext::new( + context.accounts.token_program.address(), + GroupPointerInitialize { + mint: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + Some(&mint_address), + Some(&mint_address), + )?; + + // Initialize the standard mint account data + initialize_mint2( + CpiContext::new( + context.accounts.token_program.address(), + InitializeMint2 { + mint: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + 2, // decimals + &mint_address, // mint authority + Some(&mint_address), // freeze authority + )?; - // // Token Group and Token Member extensions features not enabled yet on the Token2022 program - // // This is temporary placeholder to update one extensions are live - // // Initializing the "pointers" works, but you can't initialize the group/member data yet + check_mint_data(context)?; - // let signer_seeds: &[&[&[u8]]] = &[&[b"group", &[context.bumps.mint_account]]]; - // token_group_initialize( - // CpiContext::new( - // context.accounts.token_program.to_account_info(), - // TokenGroupInitialize { - // token_program_id: context.accounts.token_program.to_account_info(), - // group: context.accounts.mint_account.to_account_info(), - // mint: context.accounts.mint_account.to_account_info(), - // mint_authority: context.accounts.mint_account.to_account_info(), - // }, - // ) - // .with_signer(signer_seeds), - // Some(context.accounts.payer.key()), // update_authority - // 10, // max_size - // )?; + // Token Group and Token Member extensions features not enabled yet on the Token2022 program + // This is temporary placeholder to update once extensions are live + // Initializing the "pointers" works, but you can't initialize the group/member data yet Ok(()) } diff --git a/tokens/token-extensions/group/anchor/programs/group/src/lib.rs b/tokens/token-extensions/group/anchor/programs/group/src/lib.rs index 4c2219231..f96a07fab 100644 --- a/tokens/token-extensions/group/anchor/programs/group/src/lib.rs +++ b/tokens/token-extensions/group/anchor/programs/group/src/lib.rs @@ -10,7 +10,9 @@ pub mod group { use super::*; - pub fn test_initialize_group(context: Context) -> Result<()> { + pub fn test_initialize_group( + context: &mut Context, + ) -> Result<()> { instructions::test_initialize_group::handler(context) } } diff --git a/tokens/token-extensions/group/anchor/programs/group/tests/test_group.rs b/tokens/token-extensions/group/anchor/programs/group/tests/test_group.rs index 331ca1fb3..786d957f3 100644 --- a/tokens/token-extensions/group/anchor/programs/group/tests/test_group.rs +++ b/tokens/token-extensions/group/anchor/programs/group/tests/test_group.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_kite::{ @@ -22,7 +22,7 @@ fn test_initialize_group() { let payer = create_wallet(&mut svm, 10_000_000_000).unwrap(); // Derive the mint PDA - let (mint_account, _bump) = Pubkey::find_program_address(&[b"group"], &program_id); + let (mint_account, _bump) = Address::find_program_address(&[b"group"], &program_id); let instruction = Instruction::new_with_bytes( program_id, @@ -31,12 +31,13 @@ fn test_initialize_group() { payer: payer.pubkey(), mint_account, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![instruction], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![instruction], &[&payer], &payer.pubkey()) + .unwrap(); // Verify mint was created with group pointer extension let mint_data = svm diff --git a/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/Cargo.toml b/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/Cargo.toml index ab8bfba85..a7531d126 100644 --- a/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/Cargo.toml +++ b/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/src/lib.rs b/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/src/lib.rs index e21a5e718..176d8bb1f 100644 --- a/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/src/lib.rs +++ b/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/src/lib.rs @@ -17,45 +17,47 @@ pub mod immutable_owner { // There is currently not an anchor constraint to automatically initialize the ImmutableOwner extension // We can manually create and initialize the token account via CPIs in the instruction handler - pub fn initialize(context: Context) -> Result<()> { + pub fn initialize(context: &mut Context) -> Result<()> { + // `AccountView` is Copy, and a copy still points at the same + // account. v2's typed handles make the aliasing a compile error. + let payer_view = *context.accounts.payer.account(); // Calculate space required for token and extension data let token_account_size = ExtensionType::try_calculate_account_len::(&[ ExtensionType::ImmutableOwner, ])?; // Calculate minimum lamports required for size of token account with extensions - let lamports = (Rent::get()?).minimum_balance(token_account_size); + let lamports = Rent::get()?.try_minimum_balance(token_account_size)?; // Invoke System Program to create new account with space for token account and extension data create_account( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), CreateAccount { - from: context.accounts.payer.to_account_info(), - to: context.accounts.token_account.to_account_info(), + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.token_account.cpi_handle_mut(), }, ), - lamports, // Lamports - token_account_size as u64, // Space - &context.accounts.token_program.key(), // Owner Program + lamports, // Lamports + token_account_size as u64, // Space + &context.accounts.token_program.address(), // Owner Program )?; // Initialize the token account with the immutable owner extension immutable_owner_initialize(CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), ImmutableOwnerInitialize { - token_program_id: context.accounts.token_program.to_account_info(), - token_account: context.accounts.token_account.to_account_info(), + token_account: context.accounts.token_account.cpi_handle_mut(), }, ))?; // Initialize the standard token account data initialize_account3(CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), InitializeAccount3 { - account: context.accounts.token_account.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - authority: context.accounts.payer.to_account_info(), + account: context.accounts.token_account.cpi_handle_mut(), + mint: context.accounts.mint_account.cpi_handle(), + authority: CpiHandle::readonly(&payer_view), }, ))?; Ok(()) @@ -63,13 +65,13 @@ pub mod immutable_owner { } #[derive(Accounts)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account(mut)] - pub token_account: Signer<'info>, - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub token_account: Signer, + pub mint_account: InterfaceAccount, + pub token_program: Program, + pub system_program: Program, } diff --git a/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/tests/test_immutable_owner.rs b/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/tests/test_immutable_owner.rs index 90710e4c9..1c4fb438c 100644 --- a/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/tests/test_immutable_owner.rs +++ b/tokens/token-extensions/immutable-owner/anchor/programs/immutable-owner/tests/test_immutable_owner.rs @@ -1,26 +1,22 @@ use { anchor_lang::{ - solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::{AccountMeta, Instruction}, + system_program, Address, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::{create_token_extensions_mint, TOKEN_EXTENSIONS_PROGRAM_ID}, }, - solana_keypair::Keypair, solana_signer::Signer, }; /// SetAuthority instruction for Token Extensions (instruction 6). fn set_authority_instruction( - account: &Pubkey, - current_authority: &Pubkey, - new_authority: Option<&Pubkey>, + account: &Address, + current_authority: &Address, + new_authority: Option<&Address>, authority_type: u8, ) -> Instruction { let mut data = vec![6u8, authority_type]; @@ -44,7 +40,7 @@ fn set_authority_instruction( } } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = immutable_owner::id(); let mut svm = LiteSVM::new(); @@ -73,11 +69,17 @@ fn test_create_token_account_with_immutable_owner() { token_account: token_keypair.pubkey(), mint_account: mint, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![initialize_ix], &[&payer, &token_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![initialize_ix], + &[&payer, &token_keypair], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Verify token account was created @@ -98,7 +100,12 @@ fn test_create_token_account_with_immutable_owner() { Some(&new_owner.pubkey()), 2, // AuthorityType::AccountOwner ); - let result = send_transaction_from_instructions(&mut svm, vec![set_authority_ix], &[&payer], &payer.pubkey()); + let result = send_transaction_from_instructions( + &mut svm, + vec![set_authority_ix], + &[&payer], + &payer.pubkey(), + ); assert!( result.is_err(), "Setting a new owner should fail due to ImmutableOwner extension" diff --git a/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/Cargo.toml b/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/Cargo.toml index b7a89a7c3..64866ee57 100644 --- a/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/Cargo.toml +++ b/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/Cargo.toml @@ -14,15 +14,23 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/instructions/initialize.rs b/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/instructions/initialize.rs index cb34219a7..523792575 100644 --- a/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/instructions/initialize.rs +++ b/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/instructions/initialize.rs @@ -9,72 +9,83 @@ use anchor_spl::{ token_interface::{interest_bearing_mint_initialize, InterestBearingMintInitialize, Token2022}, }; -use crate::check_mint_data; +use anchor_spl::token_2022::spl_token_2022::{ + extension::{ + interest_bearing_mint::InterestBearingConfig, BaseStateWithExtensions, StateWithExtensions, + }, + state::Mint as MintState, +}; + +use crate::check_rate_authority; #[derive(Accounts)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account(mut)] - pub mint_account: Signer<'info>, + pub mint_account: Signer, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub token_program: Program, + pub system_program: Program, } -pub fn handler(context: Context, rate: i16) -> Result<()> { +pub fn handler(context: &mut Context, rate: i16) -> Result<()> { // Calculate space required for mint and extension data let mint_size = ExtensionType::try_calculate_account_len::(&[ ExtensionType::InterestBearingConfig, ])?; // Calculate minimum lamports required for size of mint account with extensions - let lamports = (Rent::get()?).minimum_balance(mint_size); + let lamports = Rent::get()?.try_minimum_balance(mint_size)?; // Invoke System Program to create new account with space for mint and extension data create_account( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), CreateAccount { - from: context.accounts.payer.to_account_info(), - to: context.accounts.mint_account.to_account_info(), + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), }, ), - lamports, // Lamports - mint_size as u64, // Space - &context.accounts.token_program.key(), // Owner Program + lamports, // Lamports + mint_size as u64, // Space + &context.accounts.token_program.address(), // Owner Program )?; // Initialize the InterestBearingConfig extension // This instruction must come before the instruction to initialize the mint data interest_bearing_mint_initialize( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), InterestBearingMintInitialize { - token_program_id: context.accounts.token_program.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), }, ), - Some(context.accounts.payer.key()), + Some(context.accounts.payer.address()), rate, )?; // Initialize the standard mint account data initialize_mint2( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), InitializeMint2 { - mint: context.accounts.mint_account.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), }, ), - 2, // decimals - &context.accounts.payer.key(), // mint authority - Some(&context.accounts.payer.key()), // freeze authority + 2, // decimals + &context.accounts.payer.address(), // mint authority + Some(&context.accounts.payer.address()), // freeze authority )?; - check_mint_data( - &context.accounts.mint_account.to_account_info(), - &context.accounts.payer.key(), + // The mint is a `Signer` here, which holds no borrow on the account's data, + // so the TLV is read straight from the buffer. + let payer_address = *context.accounts.payer.address(); + let mint_data = context.accounts.mint_account.account().try_borrow()?; + let mint = StateWithExtensions::::unpack(&mint_data)?; + check_rate_authority( + mint.get_extension::()?, + &payer_address, )?; Ok(()) } diff --git a/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/instructions/update_rate.rs b/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/instructions/update_rate.rs index 87da63389..c35712356 100644 --- a/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/instructions/update_rate.rs +++ b/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/instructions/update_rate.rs @@ -3,35 +3,41 @@ use anchor_spl::token_interface::{ interest_bearing_mint_update_rate, InterestBearingMintUpdateRate, Mint, Token2022, }; -use crate::check_mint_data; +use anchor_spl::token_interface::TokenInterfaceAccountExtensions; + +use crate::check_rate_authority; #[derive(Accounts)] -pub struct UpdateRateAccountConstraints<'info> { +pub struct UpdateRateAccountConstraints { #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, #[account(mut)] - pub mint_account: InterfaceAccount<'info, Mint>, + pub mint_account: InterfaceAccount, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub token_program: Program, + pub system_program: Program, } -pub fn handler(context: Context, rate: i16) -> Result<()> { +pub fn handler(context: &mut Context, rate: i16) -> Result<()> { interest_bearing_mint_update_rate( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), InterestBearingMintUpdateRate { - token_program_id: context.accounts.token_program.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - rate_authority: context.accounts.authority.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), + rate_authority: context.accounts.authority.cpi_handle(), }, ), rate, )?; - check_mint_data( - &context.accounts.mint_account.to_account_info(), - &context.accounts.authority.key(), + // `mint_account` is an `InterfaceAccount` declared `mut`, so it holds + // the account's exclusive borrow and the program cannot take a second one. + // anchor-spl's accessor parses the TLV through that same borrow, and checks + // the mint is owned by Token-2022 on the way. + let authority_address = *context.accounts.authority.address(); + check_rate_authority( + context.accounts.mint_account.get_extension()?, + &authority_address, )?; Ok(()) } diff --git a/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/lib.rs b/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/lib.rs index b09782186..b15c1bfaf 100644 --- a/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/lib.rs +++ b/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/src/lib.rs @@ -1,11 +1,5 @@ use anchor_lang::prelude::*; -use anchor_spl::token_2022::spl_token_2022::{ - extension::{ - interest_bearing_mint::InterestBearingConfig, BaseStateWithExtensions, - StateWithExtensions, - }, - state::Mint as MintState, -}; +use anchor_spl::token_2022::spl_token_2022::extension::interest_bearing_mint::InterestBearingConfig; use anchor_spl::token_interface::spl_pod::optional_keys::OptionalNonZeroPubkey; mod instructions; @@ -18,25 +12,31 @@ pub mod interest_bearing { use super::*; - pub fn initialize(context: Context, rate: i16) -> Result<()> { + pub fn initialize( + context: &mut Context, + rate: i16, + ) -> Result<()> { instructions::initialize::handler(context, rate) } - pub fn update_rate(context: Context, rate: i16) -> Result<()> { + pub fn update_rate( + context: &mut Context, + rate: i16, + ) -> Result<()> { instructions::update_rate::handler(context, rate) } } -pub fn check_mint_data(mint_account_info: &AccountInfo, authority_key: &Pubkey) -> Result<()> { - let mint_data = mint_account_info.data.borrow(); - let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; - let extension_data = mint_with_extension.get_extension::()?; - +/// Assert the extension names `authority_key` as the account allowed to change +/// the rate. The two callers reach the extension by different routes: see +/// `initialize` for a raw TLV read, and `update_rate` for the accessor +/// anchor-spl puts on a typed mint. +pub fn check_rate_authority(config: &InterestBearingConfig, authority_key: &Address) -> Result<()> { assert_eq!( - extension_data.rate_authority, + config.rate_authority, OptionalNonZeroPubkey::try_from(Some(*authority_key))? ); - msg!("{:?}", extension_data); + msg!("{:?}", config); Ok(()) } diff --git a/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/tests/test_interest_bearing.rs b/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/tests/test_interest_bearing.rs index c9954bccb..e62e0418c 100644 --- a/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/tests/test_interest_bearing.rs +++ b/tokens/token-extensions/interest-bearing/anchor/programs/interest-bearing/tests/test_interest_bearing.rs @@ -1,18 +1,18 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::TOKEN_EXTENSIONS_PROGRAM_ID, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = interest_bearing::id(); let mut svm = LiteSVM::new(); @@ -36,11 +36,17 @@ fn test_initialize_and_update_rate() { payer: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![initialize_ix], &[&payer, &mint_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![initialize_ix], + &[&payer, &mint_keypair], + &payer.pubkey(), + ) + .unwrap(); // Verify mint account exists let mint_account = svm @@ -58,11 +64,12 @@ fn test_initialize_and_update_rate() { authority: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![update_rate_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![update_rate_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify mint still exists after rate update let mint_account = svm diff --git a/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/Cargo.toml b/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/Cargo.toml index 2b821be8d..86f284f02 100644 --- a/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/Cargo.toml +++ b/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/instructions/disable.rs b/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/instructions/disable.rs index f38442c0d..0a8b6999c 100644 --- a/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/instructions/disable.rs +++ b/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/instructions/disable.rs @@ -1,26 +1,26 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::token_interface::{memo_transfer_disable, MemoTransfer, Token2022, TokenAccount}; #[derive(Accounts)] -pub struct DisableAccountConstraints<'info> { +pub struct DisableAccountConstraints { #[account(mut)] - pub owner: Signer<'info>, + pub owner: Signer, #[account( mut, token::authority = owner, )] - pub token_account: InterfaceAccount<'info, TokenAccount>, - pub token_program: Program<'info, Token2022>, + pub token_account: InterfaceAccount, + pub token_program: Program, } -pub fn handler(context: Context) -> Result<()> { +pub fn handler(context: &mut Context) -> Result<()> { memo_transfer_disable(CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), MemoTransfer { - token_program_id: context.accounts.token_program.to_account_info(), - account: context.accounts.token_account.to_account_info(), - owner: context.accounts.owner.to_account_info(), + account: context.accounts.token_account.cpi_handle_mut(), + owner: context.accounts.owner.cpi_handle(), }, ))?; Ok(()) diff --git a/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/instructions/initialize.rs b/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/instructions/initialize.rs index b465e5423..76a30d16c 100644 --- a/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/instructions/initialize.rs +++ b/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/instructions/initialize.rs @@ -10,57 +10,59 @@ use anchor_spl::{ }; #[derive(Accounts)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account(mut)] - pub token_account: Signer<'info>, - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub token_account: Signer, + pub mint_account: InterfaceAccount, + pub token_program: Program, + pub system_program: Program, } -pub fn handler(context: Context) -> Result<()> { +pub fn handler(context: &mut Context) -> Result<()> { + // `AccountView` is Copy, and a copy still points at the same + // account. v2's typed handles make the aliasing a compile error. + let payer_view = *context.accounts.payer.account(); // Calculate space required for token and extension data let token_account_size = ExtensionType::try_calculate_account_len::(&[ExtensionType::MemoTransfer])?; // Calculate minimum lamports required for size of token account with extensions - let lamports = (Rent::get()?).minimum_balance(token_account_size); + let lamports = Rent::get()?.try_minimum_balance(token_account_size)?; // Invoke System Program to create new account with space for token account and extension data create_account( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), CreateAccount { - from: context.accounts.payer.to_account_info(), - to: context.accounts.token_account.to_account_info(), + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.token_account.cpi_handle_mut(), }, ), - lamports, // Lamports - token_account_size as u64, // Space - &context.accounts.token_program.key(), // Owner Program + lamports, // Lamports + token_account_size as u64, // Space + &context.accounts.token_program.address(), // Owner Program )?; // Initialize the standard token account data initialize_account3(CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), InitializeAccount3 { - account: context.accounts.token_account.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - authority: context.accounts.payer.to_account_info(), + account: context.accounts.token_account.cpi_handle_mut(), + mint: context.accounts.mint_account.cpi_handle(), + authority: CpiHandle::readonly(&payer_view), }, ))?; // Initialize the memo transfer extension // This instruction must come after the token account initialization memo_transfer_initialize(CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), MemoTransfer { - token_program_id: context.accounts.token_program.to_account_info(), - account: context.accounts.token_account.to_account_info(), - owner: context.accounts.payer.to_account_info(), + account: context.accounts.token_account.cpi_handle_mut(), + owner: CpiHandle::readonly(&payer_view), }, ))?; Ok(()) diff --git a/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/lib.rs b/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/lib.rs index d9e71f050..d6b31d665 100644 --- a/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/lib.rs +++ b/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/src/lib.rs @@ -9,11 +9,11 @@ declare_id!("5BQyC7y2Pc283woThq11uZRqsgcRbBRLKz4yQ8BJadi2"); pub mod memo_transfer { use super::*; - pub fn initialize(context: Context) -> Result<()> { + pub fn initialize(context: &mut Context) -> Result<()> { instructions::initialize::handler(context) } - pub fn disable(context: Context) -> Result<()> { + pub fn disable(context: &mut Context) -> Result<()> { instructions::disable::handler(context) } } diff --git a/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/tests/test_memo_transfer.rs b/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/tests/test_memo_transfer.rs index e410f2347..666925b60 100644 --- a/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/tests/test_memo_transfer.rs +++ b/tokens/token-extensions/memo-transfer/anchor/programs/memo-transfer/tests/test_memo_transfer.rs @@ -1,13 +1,10 @@ use { anchor_lang::{ - solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::{AccountMeta, Instruction}, + system_program, Address, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ assert_token_account_balance, create_wallet, send_transaction_from_instructions, token_extensions::{ @@ -15,11 +12,10 @@ use { TOKEN_EXTENSIONS_PROGRAM_ID, }, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn memo_program_id() -> Pubkey { +fn memo_program_id() -> Address { "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" .parse() .unwrap() @@ -29,12 +25,12 @@ fn memo_program_id() -> Pubkey { /// Uses explicit keypair - not an ATA - because the test needs multiple /// source accounts for the same owner+mint. fn create_token_account_instructions( - payer: &Pubkey, - token_account: &Pubkey, - mint: &Pubkey, - owner: &Pubkey, + payer: &Address, + token_account: &Address, + mint: &Address, + owner: &Address, ) -> Vec { - let rent_sysvar: Pubkey = "SysvarRent111111111111111111111111111111111" + let rent_sysvar: Address = "SysvarRent111111111111111111111111111111111" .parse() .unwrap(); let create_ix = anchor_lang::solana_program::system_instruction::create_account( @@ -59,9 +55,9 @@ fn create_token_account_instructions( /// Transfer instruction for Token Extensions (instruction 3). fn transfer_instruction( - source: &Pubkey, - dest: &Pubkey, - authority: &Pubkey, + source: &Address, + dest: &Address, + authority: &Address, amount: u64, ) -> Instruction { let mut data = vec![3u8]; @@ -78,7 +74,7 @@ fn transfer_instruction( } /// Memo instruction: just the memo text as bytes. -fn memo_instruction(memo_text: &str, signers: &[&Pubkey]) -> Instruction { +fn memo_instruction(memo_text: &str, signers: &[&Address]) -> Instruction { let accounts: Vec = signers .iter() .map(|s| AccountMeta::new_readonly(**s, true)) @@ -90,7 +86,7 @@ fn memo_instruction(memo_text: &str, signers: &[&Pubkey]) -> Instruction { } } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = memo_transfer::id(); let mut svm = LiteSVM::new(); @@ -124,11 +120,17 @@ fn test_memo_transfer() { token_account: token_keypair.pubkey(), mint_account: mint, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![initialize_ix], &[&payer, &token_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![initialize_ix], + &[&payer, &token_keypair], + &payer.pubkey(), + ) + .unwrap(); // Verify token account exists let token_account = svm @@ -149,16 +151,17 @@ fn test_memo_transfer() { &mint, &payer.pubkey(), ); - send_transaction_from_instructions(&mut svm, create_source_ixs, &[&payer, &source_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + create_source_ixs, + &[&payer, &source_keypair], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); - mint_tokens_to_token_extensions_account( - &mut svm, - &mint, - &source_keypair.pubkey(), - 100, - &payer, - ).unwrap(); + mint_tokens_to_token_extensions_account(&mut svm, &mint, &source_keypair.pubkey(), 100, &payer) + .unwrap(); svm.expire_blockhash(); // Step 4: Transfer without memo - should fail @@ -168,11 +171,9 @@ fn test_memo_transfer() { &payer.pubkey(), 1, ); - let result = send_transaction_from_instructions(&mut svm, vec![transfer_ix], &[&payer], &payer.pubkey()); - assert!( - result.is_err(), - "Transfer without memo should fail" - ); + let result = + send_transaction_from_instructions(&mut svm, vec![transfer_ix], &[&payer], &payer.pubkey()); + assert!(result.is_err(), "Transfer without memo should fail"); svm.expire_blockhash(); // Step 5: Transfer with memo - should succeed @@ -183,10 +184,21 @@ fn test_memo_transfer() { &payer.pubkey(), 1, ); - send_transaction_from_instructions(&mut svm, vec![memo_ix, transfer_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![memo_ix, transfer_ix], + &[&payer], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); - assert_token_account_balance(&svm, &token_keypair.pubkey(), 1, "Should have 1 token after transfer with memo"); + assert_token_account_balance( + &svm, + &token_keypair.pubkey(), + 1, + "Should have 1 token after transfer with memo", + ); // Step 6: Disable RequiredMemo extension let disable_ix = Instruction::new_with_bytes( @@ -199,7 +211,8 @@ fn test_memo_transfer() { } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![disable_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![disable_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 7: Transfer without memo should now succeed (memo disabled) @@ -210,7 +223,13 @@ fn test_memo_transfer() { &mint, &payer.pubkey(), ); - send_transaction_from_instructions(&mut svm, create_source2_ixs, &[&payer, &source2_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + create_source2_ixs, + &[&payer, &source2_keypair], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); mint_tokens_to_token_extensions_account( @@ -219,7 +238,8 @@ fn test_memo_transfer() { &source2_keypair.pubkey(), 100, &payer, - ).unwrap(); + ) + .unwrap(); svm.expire_blockhash(); let transfer_ix2 = transfer_instruction( @@ -228,7 +248,8 @@ fn test_memo_transfer() { &payer.pubkey(), 1, ); - send_transaction_from_instructions(&mut svm, vec![transfer_ix2], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![transfer_ix2], &[&payer], &payer.pubkey()) + .unwrap(); assert_token_account_balance( &svm, diff --git a/tokens/token-extensions/metadata/anchor/programs/metadata/Cargo.toml b/tokens/token-extensions/metadata/anchor/programs/metadata/Cargo.toml index d686173a3..424caed88 100644 --- a/tokens/token-extensions/metadata/anchor/programs/metadata/Cargo.toml +++ b/tokens/token-extensions/metadata/anchor/programs/metadata/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" spl-token-metadata-interface = "0.8.0" spl-type-length-value = "0.9.1" diff --git a/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/emit.rs b/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/emit.rs index b849ab82b..fcb2fe8f7 100644 --- a/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/emit.rs +++ b/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/emit.rs @@ -4,25 +4,25 @@ use anchor_spl::token_interface::{Mint, Token2022}; use spl_token_metadata_interface::instruction::emit; #[derive(Accounts)] -pub struct EmitAccountConstraints<'info> { - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, +pub struct EmitAccountConstraints { + pub mint_account: InterfaceAccount, + pub token_program: Program, } // Invoke the emit instruction from spl_token_metadata_interface directly // There is not an anchor CpiContext for this instruction -pub fn process_emit(context: Context) -> Result<()> { +pub fn process_emit(context: &mut Context) -> Result<()> { invoke( &emit( - &context.accounts.token_program.key(), // token program id - &context.accounts.mint_account.key(), // "metadata" account + &context.accounts.token_program.address(), // token program id + &context.accounts.mint_account.address(), // "metadata" account None, None, ), - &[ - context.accounts.token_program.to_account_info(), - context.accounts.mint_account.to_account_info(), - ], + // Handles line up positionally with the instruction's metas: `emit` + // names only the metadata account (the mint), read-only. The program + // account is not one of them. + &[context.accounts.mint_account.cpi_handle()], )?; Ok(()) } diff --git a/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/initialize.rs b/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/initialize.rs index 740b61bae..86c8b915e 100644 --- a/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/initialize.rs +++ b/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/initialize.rs @@ -1,32 +1,82 @@ use anchor_lang::prelude::*; -use anchor_lang::system_program::{transfer, Transfer}; -use anchor_spl::token_interface::{ - token_metadata_initialize, Mint, Token2022, TokenMetadataInitialize, +use anchor_lang::system_program::{create_account, transfer, CreateAccount, Transfer}; +use anchor_spl::{ + token_2022::{ + initialize_mint2, + spl_token_2022::{extension::ExtensionType, pod::PodMint}, + InitializeMint2, + }, + token_2022_extensions::metadata_pointer::{ + metadata_pointer_initialize, MetadataPointerInitialize, + }, + token_interface::{token_metadata_initialize, Token2022, TokenMetadataInitialize}, }; use spl_token_metadata_interface::state::TokenMetadata; use spl_type_length_value::variable_len_pack::VariableLenPack; #[derive(Accounts)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, - #[account( - init, - payer = payer, - mint::decimals = 2, - mint::authority = payer, - extensions::metadata_pointer::authority = payer, - extensions::metadata_pointer::metadata_address = mint_account, - )] - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + #[account(mut)] + pub mint_account: Signer, + pub token_program: Program, + pub system_program: Program, } -pub fn process_initialize(context: Context, args: TokenMetadataArgs) -> Result<()> { +pub fn process_initialize( + context: &mut Context, + args: TokenMetadataArgs, +) -> Result<()> { let TokenMetadataArgs { name, symbol, uri } = args; + // There is currently not an anchor constraint to automatically initialize + // the MetadataPointer extension, so create and initialize the mint by hand: + // allocate with room for the extension, initialize the extension, then + // initialize the mint data. + let mint_size = + ExtensionType::try_calculate_account_len::(&[ExtensionType::MetadataPointer])?; + let mint_lamports = Rent::get()?.try_minimum_balance(mint_size)?; + let mint_address = *context.accounts.mint_account.address(); + + create_account( + CpiContext::new( + context.accounts.system_program.address(), + CreateAccount { + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + mint_lamports, + mint_size as u64, + context.accounts.token_program.address(), + )?; + + // The metadata lives in the mint account itself, so the pointer points at it. + metadata_pointer_initialize( + CpiContext::new( + context.accounts.token_program.address(), + MetadataPointerInitialize { + mint: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + Some(context.accounts.payer.address()), + Some(&mint_address), + )?; + + initialize_mint2( + CpiContext::new( + context.accounts.token_program.address(), + InitializeMint2 { + mint: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + 2, + context.accounts.payer.address(), + None, + )?; + // Define token metadata let token_metadata = TokenMetadata { name: name.clone(), @@ -39,30 +89,34 @@ pub fn process_initialize(context: Context, args: let data_len = 4 + token_metadata.get_packed_len()?; // Calculate lamports required for the additional metadata - let lamports = Rent::get()?.minimum_balance(data_len); + let lamports = Rent::get()?.try_minimum_balance(data_len)?; // Transfer additional lamports to mint account transfer( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), Transfer { - from: context.accounts.payer.to_account_info(), - to: context.accounts.mint_account.to_account_info(), + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), }, ), lamports, )?; - // Initialize token metadata + // Initialize token metadata. `AccountView` is Copy and a copy still points + // at the same account, so the read-only slots come from copies. v2's typed + // handles enforce borrow exclusivity at compile time. + let payer_view = *context.accounts.payer.account(); + let mint_view = *context.accounts.mint_account.account(); + token_metadata_initialize( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TokenMetadataInitialize { - program_id: context.accounts.token_program.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - metadata: context.accounts.mint_account.to_account_info(), - mint_authority: context.accounts.payer.to_account_info(), - update_authority: context.accounts.payer.to_account_info(), + mint: CpiHandle::readonly(&mint_view), + metadata: context.accounts.mint_account.cpi_handle_mut(), + mint_authority: CpiHandle::readonly(&payer_view), + update_authority: CpiHandle::readonly(&payer_view), }, ), name, @@ -72,7 +126,7 @@ pub fn process_initialize(context: Context, args: Ok(()) } -#[derive(AnchorDeserialize, AnchorSerialize)] +#[derive(IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub struct TokenMetadataArgs { pub name: String, pub symbol: String, diff --git a/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/remove_key.rs b/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/remove_key.rs index 0114a4b26..98cdf70e8 100644 --- a/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/remove_key.rs +++ b/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/remove_key.rs @@ -4,34 +4,37 @@ use anchor_spl::token_interface::{Mint, Token2022}; use spl_token_metadata_interface::instruction::remove_key; #[derive(Accounts)] -pub struct RemoveKeyAccountConstraints<'info> { +pub struct RemoveKeyAccountConstraints { #[account(mut)] - pub update_authority: Signer<'info>, + pub update_authority: Signer, - #[account( - mut, - extensions::metadata_pointer::metadata_address = mint_account, - )] - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + #[account(mut)] + pub mint_account: InterfaceAccount, + pub token_program: Program, + pub system_program: Program, } // Invoke the remove_key instruction from spl_token_metadata_interface directly // There is not an anchor CpiContext for this instruction -pub fn process_remove_key(context: Context, key: String) -> Result<()> { +pub fn process_remove_key( + context: &mut Context, + key: String, +) -> Result<()> { invoke( &remove_key( - &context.accounts.token_program.key(), // token program id - &context.accounts.mint_account.key(), // "metadata" account - &context.accounts.update_authority.key(), // update authority - key, // key to remove + &context.accounts.token_program.address(), // token program id + &context.accounts.mint_account.address(), // "metadata" account + &context.accounts.update_authority.address(), // update authority + key, // key to remove true, // idempotent flag, if true transaction will not fail if key does not exist ), + // Handles line up positionally with the instruction's metas, and a + // writable meta needs a writable handle: `remove_key` names the metadata + // account (the mint, writable) then the update authority. The program + // account is not one of them. &[ - context.accounts.token_program.to_account_info(), - context.accounts.mint_account.to_account_info(), - context.accounts.update_authority.to_account_info(), + context.accounts.mint_account.cpi_handle_mut().into(), + context.accounts.update_authority.cpi_handle(), ], )?; Ok(()) diff --git a/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/update_authority.rs b/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/update_authority.rs index ded7b65f7..c17ff2a84 100644 --- a/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/update_authority.rs +++ b/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/update_authority.rs @@ -1,44 +1,43 @@ use anchor_lang::prelude::*; use anchor_spl::token_interface::{ - spl_pod::optional_keys::OptionalNonZeroPubkey, token_metadata_update_authority, Mint, - Token2022, TokenMetadataUpdateAuthority, + token_metadata_update_authority, Mint, Token2022, TokenMetadataUpdateAuthority, }; #[derive(Accounts)] -pub struct UpdateAuthorityAccountConstraints<'info> { - pub current_authority: Signer<'info>, - pub new_authority: Option>, +pub struct UpdateAuthorityAccountConstraints { + pub current_authority: Signer, + pub new_authority: Option, - #[account( - mut, - extensions::metadata_pointer::metadata_address = mint_account, - )] - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + // v2 has no `extensions::*` validation constraint; Token-2022 checks the + // metadata pointer and the update authority itself when the CPI runs. + #[account(mut)] + pub mint_account: InterfaceAccount, + pub token_program: Program, + pub system_program: Program, } -pub fn process_update_authority(context: Context) -> Result<()> { - let new_authority_key = match &context.accounts.new_authority { - Some(account) => OptionalNonZeroPubkey::try_from(Some(account.key()))?, - None => OptionalNonZeroPubkey::try_from(None)?, - }; +pub fn process_update_authority( + context: &mut Context, +) -> Result<()> { + // v2 takes the new authority as a plain `Option<&Address>` and does the + // `OptionalNonZeroPubkey` conversion itself. + let new_authority = context + .accounts + .new_authority + .as_ref() + .map(|account| *account.address()); - // Change update authority + // Change update authority. v2's struct drops the program-id and + // new-authority slots: neither is passed as an account. token_metadata_update_authority( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TokenMetadataUpdateAuthority { - program_id: context.accounts.token_program.to_account_info(), - metadata: context.accounts.mint_account.to_account_info(), - current_authority: context.accounts.current_authority.to_account_info(), - - // new authority isn't actually needed as account in the CPI - // using current_authority as a placeholder to satisfy the struct - new_authority: context.accounts.current_authority.to_account_info(), + metadata: context.accounts.mint_account.cpi_handle_mut(), + current_authority: context.accounts.current_authority.cpi_handle(), }, ), - new_authority_key, + new_authority.as_ref(), )?; Ok(()) } diff --git a/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/update_field.rs b/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/update_field.rs index 754fb8091..89a1c5588 100644 --- a/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/update_field.rs +++ b/tokens/token-extensions/metadata/anchor/programs/metadata/src/instructions/update_field.rs @@ -10,20 +10,31 @@ use anchor_spl::{ use spl_token_metadata_interface::state::{Field, TokenMetadata}; #[derive(Accounts)] -pub struct UpdateFieldAccountConstraints<'info> { +pub struct UpdateFieldAccountConstraints { #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, - #[account( - mut, - extensions::metadata_pointer::metadata_address = mint_account, - )] - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + /// CHECK: loaded and validated as an `InterfaceAccount` in the handler. + /// + /// It is declared unchecked here only so that the derive does not take the + /// account's exclusive borrow. This instruction has to read the + /// variable-length `TokenMetadata` extension out of the TLV, and a wrapper + /// loaded by `#[account(mut)]` holds an exclusive borrow that makes any + /// `try_borrow()` fail. Loading the same wrapper by hand registers a shared + /// borrow instead, which leaves room for the read. + #[account(mut)] + pub mint_account: UncheckedAccount, + pub token_program: Program, + pub system_program: Program, } -pub fn process_update_field(context: Context, args: UpdateFieldArgs) -> Result<()> { +pub fn process_update_field( + context: &mut Context, + args: UpdateFieldArgs, +) -> Result<()> { + // `AccountView` is Copy, and a copy still points at the same + // account. v2's typed handles make the aliasing a compile error. + let authority_view = *context.accounts.authority.account(); let UpdateFieldArgs { field, value } = args; // Convert to Field type from spl_token_metadata_interface @@ -31,9 +42,16 @@ pub fn process_update_field(context: Context, arg msg!("Field: {:?}, Value: {}", field, value); let (current_lamports, required_lamports) = { - // Get the current state of the mint account - let mint = &context.accounts.mint_account.to_account_info(); - let buffer = mint.try_borrow_data()?; + // Validate the account as a mint. `load` runs the same owner and + // layout checks the derive would have run for an + // `InterfaceAccount` field, and registers a *shared* borrow, so + // the TLV read below is an ordinary `try_borrow()`. + // + // anchor-spl's `TokenInterfaceAccountExtensions::get_extension` would + // be the shorter route, and is what the other extension examples use, + // but it is bounded on `Pod` and `TokenMetadata` is variable-length. + let mint = InterfaceAccount::::load(*context.accounts.mint_account.account())?; + let buffer = mint.account().try_borrow()?; let state = PodStateWithExtensions::::unpack(&buffer)?; // Get and update the token metadata @@ -46,9 +64,9 @@ pub fn process_update_field(context: Context, arg state.try_get_new_account_len_for_variable_len_extension(&token_metadata)?; // Calculate the required lamports for the new account length - let required_lamports = Rent::get()?.minimum_balance(new_account_len); + let required_lamports = Rent::get()?.try_minimum_balance(new_account_len)?; // Get the current lamports of the mint account - let current_lamports = mint.lamports(); + let current_lamports = mint.account().lamports(); msg!("Required lamports: {}", required_lamports); msg!("Current lamports: {}", current_lamports); @@ -61,10 +79,10 @@ pub fn process_update_field(context: Context, arg let lamport_difference = required_lamports - current_lamports; transfer( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), Transfer { - from: context.accounts.authority.to_account_info(), - to: context.accounts.mint_account.to_account_info(), + from: context.accounts.authority.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), }, ), lamport_difference, @@ -78,11 +96,10 @@ pub fn process_update_field(context: Context, arg // Update token metadata token_metadata_update_field( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TokenMetadataUpdateField { - program_id: context.accounts.token_program.to_account_info(), - metadata: context.accounts.mint_account.to_account_info(), - update_authority: context.accounts.authority.to_account_info(), + metadata: context.accounts.mint_account.cpi_handle_mut(), + update_authority: CpiHandle::readonly(&authority_view), }, ), field, @@ -93,7 +110,7 @@ pub fn process_update_field(context: Context, arg // Custom struct to implement AnchorSerialize and AnchorDeserialize // This is required to pass the struct as an argument to the instruction -#[derive(AnchorSerialize, AnchorDeserialize)] +#[derive(IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub struct UpdateFieldArgs { /// Field to update in the metadata pub field: AnchorField, @@ -102,7 +119,7 @@ pub struct UpdateFieldArgs { } // Need to do this so the enum shows up in the IDL -#[derive(AnchorSerialize, AnchorDeserialize, Debug)] +#[derive(Debug, IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub enum AnchorField { /// The name field, corresponding to `TokenMetadata.name` Name, diff --git a/tokens/token-extensions/metadata/anchor/programs/metadata/src/lib.rs b/tokens/token-extensions/metadata/anchor/programs/metadata/src/lib.rs index e086f704c..ac2288d1b 100644 --- a/tokens/token-extensions/metadata/anchor/programs/metadata/src/lib.rs +++ b/tokens/token-extensions/metadata/anchor/programs/metadata/src/lib.rs @@ -11,23 +11,34 @@ declare_id!("BJHEDXSQfD9kBFvhw8ZCGmPFRihzvbMoxoHUKpXdpn4D"); pub mod metadata { use super::*; - pub fn initialize(context: Context, args: TokenMetadataArgs) -> Result<()> { + pub fn initialize( + context: &mut Context, + args: TokenMetadataArgs, + ) -> Result<()> { process_initialize(context, args) } - pub fn update_field(context: Context, args: UpdateFieldArgs) -> Result<()> { + pub fn update_field( + context: &mut Context, + args: UpdateFieldArgs, + ) -> Result<()> { process_update_field(context, args) } - pub fn remove_key(context: Context, key: String) -> Result<()> { + pub fn remove_key( + context: &mut Context, + key: String, + ) -> Result<()> { process_remove_key(context, key) } - pub fn emit(context: Context) -> Result<()> { + pub fn emit(context: &mut Context) -> Result<()> { process_emit(context) } - pub fn update_authority(context: Context) -> Result<()> { + pub fn update_authority( + context: &mut Context, + ) -> Result<()> { process_update_authority(context) } } diff --git a/tokens/token-extensions/metadata/anchor/programs/metadata/tests/test_metadata.rs b/tokens/token-extensions/metadata/anchor/programs/metadata/tests/test_metadata.rs index 8439e9d3c..b545fc66d 100644 --- a/tokens/token-extensions/metadata/anchor/programs/metadata/tests/test_metadata.rs +++ b/tokens/token-extensions/metadata/anchor/programs/metadata/tests/test_metadata.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, @@ -12,7 +12,7 @@ use { solana_signer::Signer, }; -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = metadata::id(); let mut svm = LiteSVM::new(); @@ -43,7 +43,7 @@ fn test_metadata_full_flow() { payer: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -77,7 +77,7 @@ fn test_metadata_full_flow() { authority: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -99,7 +99,7 @@ fn test_metadata_full_flow() { authority: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -123,7 +123,7 @@ fn test_metadata_full_flow() { update_authority: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); @@ -140,7 +140,7 @@ fn test_metadata_full_flow() { new_authority: None, mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); diff --git a/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/Cargo.toml b/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/Cargo.toml index 0c4bc0de7..8c031c522 100644 --- a/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/Cargo.toml +++ b/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/Cargo.toml @@ -14,11 +14,19 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/instructions/close.rs b/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/instructions/close.rs index f87c3ccb3..39c6f7f22 100644 --- a/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/instructions/close.rs +++ b/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/instructions/close.rs @@ -5,27 +5,32 @@ use anchor_spl::{ }; #[derive(Accounts)] -pub struct CloseAccountConstraints<'info> { +pub struct CloseAccountConstraints { #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, - #[account( - mut, - extensions::close_authority::authority = authority, - )] - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, + // Token-2022 checks the mint's close authority against the signer when the + // CloseAccount CPI below runs, so no constraint is needed here. + #[account(mut)] + pub mint_account: InterfaceAccount, + pub token_program: Program, } -pub fn handler(context: Context) -> Result<()> { +pub fn handler(context: &mut Context) -> Result<()> { + // `authority` fills both the destination and authority CPI slots. v2's typed + // handles enforce borrow exclusivity at compile time, so the read-only slot + // is built from a copy of the `AccountView`, and it still points at the same + // underlying account. + let authority_view = *context.accounts.authority.account(); + // cpi to token extensions programs to close mint account // alternatively, this can also be done in the client close_account(CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), CloseAccount { - account: context.accounts.mint_account.to_account_info(), - destination: context.accounts.authority.to_account_info(), - authority: context.accounts.authority.to_account_info(), + account: context.accounts.mint_account.cpi_handle_mut(), + destination: context.accounts.authority.cpi_handle_mut(), + authority: CpiHandle::readonly(&authority_view), }, ))?; Ok(()) diff --git a/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/instructions/initialize.rs b/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/instructions/initialize.rs index ba2f2a47a..27ed82525 100644 --- a/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/instructions/initialize.rs +++ b/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/instructions/initialize.rs @@ -1,48 +1,100 @@ use anchor_lang::prelude::*; -use anchor_spl::token_interface::{ - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_2022::{ - extension::{ - mint_close_authority::MintCloseAuthority, BaseStateWithExtensions, - StateWithExtensions, +use anchor_lang::system_program::{create_account, CreateAccount}; +use anchor_spl::{ + token_2022::{ + initialize_mint2, + spl_token_2022::{ + extension::{ + mint_close_authority::MintCloseAuthority, BaseStateWithExtensions, ExtensionType, + StateWithExtensions, + }, + pod::PodMint, + state::Mint as MintState, }, - state::Mint as MintState, + InitializeMint2, + }, + token_interface::{ + mint_close_authority_initialize, spl_pod::optional_keys::OptionalNonZeroPubkey, + MintCloseAuthorityInitialize, Token2022, }, - Mint, Token2022, }; #[derive(Accounts)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { + #[account(mut)] + pub payer: Signer, + #[account(mut)] - pub payer: Signer<'info>, - - #[account( - init, - payer = payer, - mint::decimals = 2, - mint::authority = payer, - extensions::close_authority::authority = payer, - )] - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub mint_account: Signer, + + pub token_program: Program, + pub system_program: Program, } -pub fn handler(mut context: Context) -> Result<()> { - handle_check_mint_data(&mut context.accounts)?; +// There is currently not an anchor constraint to automatically initialize the +// MintCloseAuthority extension. We can manually create and initialize the mint +// account via CPIs in the instruction handler. +pub fn handler(context: &mut Context) -> Result<()> { + // Calculate space required for mint and extension data + let mint_size = + ExtensionType::try_calculate_account_len::(&[ExtensionType::MintCloseAuthority])?; + + // Calculate minimum lamports required for size of mint account with extensions + let lamports = Rent::get()?.try_minimum_balance(mint_size)?; + + // Invoke System Program to create new account with space for mint and extension data + create_account( + CpiContext::new( + context.accounts.system_program.address(), + CreateAccount { + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + lamports, // Lamports + mint_size as u64, // Space + context.accounts.token_program.address(), // Owner Program + )?; + + // Initialize the MintCloseAuthority extension + // This instruction must come before the instruction to initialize the mint data + mint_close_authority_initialize( + CpiContext::new( + context.accounts.token_program.address(), + MintCloseAuthorityInitialize { + mint: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + Some(context.accounts.payer.address()), + )?; + + // Initialize the standard mint account data + initialize_mint2( + CpiContext::new( + context.accounts.token_program.address(), + InitializeMint2 { + mint: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + 2, // decimals + context.accounts.payer.address(), // mint authority + None, // freeze authority + )?; + + handle_check_mint_data(context)?; Ok(()) } // helper to check mint data, and demonstrate how to read mint extension data within a program -fn handle_check_mint_data(accounts: &mut InitializeAccountConstraints) -> Result<()> { - let mint = &accounts.mint_account.to_account_info(); - let mint_data = mint.data.borrow(); +fn handle_check_mint_data(context: &Context) -> Result<()> { + let mint = context.accounts.mint_account.account(); + let mint_data = mint.try_borrow()?; let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; let extension_data = mint_with_extension.get_extension::()?; assert_eq!( extension_data.close_authority, - OptionalNonZeroPubkey::try_from(Some(accounts.payer.key()))? + OptionalNonZeroPubkey::try_from(Some(*context.accounts.payer.address()))? ); msg!("{:?}", extension_data); diff --git a/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/lib.rs b/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/lib.rs index c5ac6ba42..eabebf8d5 100644 --- a/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/lib.rs +++ b/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/src/lib.rs @@ -9,11 +9,11 @@ declare_id!("AcfQLsYKuzprcCNH1n96pKKgAbAnZchwpbr3gbVN742n"); pub mod mint_close_authority { use super::*; - pub fn initialize(context: Context) -> Result<()> { + pub fn initialize(context: &mut Context) -> Result<()> { instructions::initialize::handler(context) } - pub fn close(context: Context) -> Result<()> { + pub fn close(context: &mut Context) -> Result<()> { instructions::close::handler(context) } } diff --git a/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/tests/test_mint_close_authority.rs b/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/tests/test_mint_close_authority.rs index 156c92040..f6ba86211 100644 --- a/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/tests/test_mint_close_authority.rs +++ b/tokens/token-extensions/mint-close-authority/anchor/programs/mint-close-authority/tests/test_mint_close_authority.rs @@ -1,18 +1,18 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::TOKEN_EXTENSIONS_PROGRAM_ID, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = mint_close_authority::id(); let mut svm = LiteSVM::new(); @@ -36,11 +36,17 @@ fn test_create_and_close_mint() { payer: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![initialize_ix], &[&payer, &mint_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![initialize_ix], + &[&payer, &mint_keypair], + &payer.pubkey(), + ) + .unwrap(); // Verify mint exists let mint_account = svm @@ -61,14 +67,12 @@ fn test_create_and_close_mint() { } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![close_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![close_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify mint no longer exists (lamports returned to authority) let mint_account = svm.get_account(&mint_keypair.pubkey()); - assert!( - mint_account.is_none(), - "Mint account should be closed" - ); + assert!(mint_account.is_none(), "Mint account should be closed"); svm.expire_blockhash(); @@ -80,11 +84,17 @@ fn test_create_and_close_mint() { payer: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![initialize_ix2], &[&payer, &mint_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![initialize_ix2], + &[&payer, &mint_keypair], + &payer.pubkey(), + ) + .unwrap(); // Verify mint exists again let mint_account = svm @@ -105,10 +115,7 @@ fn test_create_and_close_mint() { mint_keypair.pubkey(), false, ), - anchor_lang::solana_program::instruction::AccountMeta::new( - payer.pubkey(), - false, - ), + anchor_lang::solana_program::instruction::AccountMeta::new(payer.pubkey(), false), anchor_lang::solana_program::instruction::AccountMeta::new_readonly( payer.pubkey(), true, @@ -116,7 +123,8 @@ fn test_create_and_close_mint() { ], data: vec![9], // CloseAccount }; - send_transaction_from_instructions(&mut svm, vec![close_direct_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![close_direct_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify mint is closed again let mint_account = svm.get_account(&mint_keypair.pubkey()); diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/Cargo.toml b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/Cargo.toml index 4e71abdec..f0168ff5d 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/Cargo.toml +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/Cargo.toml @@ -14,20 +14,29 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = { version = "1.1.2" } -# session-keys 3.1.1 is the first release that supports Anchor >=0.28,<2.0 +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = { version = "2.0.0-rc.1" } +# session-keys is Anchor v1 only: its `Session` derive requires +# `Option>`, which v2 has no equivalent for. The +# account layout is read directly in src/session.rs instead. # (so it builds against Anchor 1.0). Earlier 2.x releases pin Anchor <=0.30 # and fail to compile against the Anchor 1.0 / Solana 3.x API. Provides the # gasless session-token lesson via `#[session_auth_or]` / `SessionToken`. -session-keys = { version = "3.1.1", features = ["no-entrypoint"] } # Token-2022 + token-metadata access goes through anchor-spl's bundled # re-exports (`anchor_spl::token_interface::spl_token_2022`, which is # `spl-token-2022-interface`, and `anchor_spl::token_2022_extensions:: @@ -38,6 +47,8 @@ session-keys = { version = "3.1.1", features = ["no-entrypoint"] } # re-exports keeps a single, consistent type universe. [dev-dependencies] +# Test-side decode of the player account's borsh payload. +borsh = "1.6.1" litesvm = "0.13.1" solana-keypair = "3.0.1" solana-signer = "3.0.0" diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/chop_tree.rs b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/chop_tree.rs index 597f9f0c6..87075ca33 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/chop_tree.rs +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/chop_tree.rs @@ -5,13 +5,18 @@ use anchor_lang::prelude::*; use anchor_lang::solana_program::program::invoke_signed; use anchor_spl::token_2022_extensions::spl_token_metadata_interface; use anchor_spl::token_interface::{spl_token_2022, Token2022}; -use session_keys::{Session, SessionToken}; -pub fn chop_tree(context: Context, counter: u16, amount: u64) -> Result<()> { +pub fn chop_tree( + context: &mut Context, + counter: u16, + amount: u64, +) -> Result<()> { + let mint_address = *context.accounts.mint.address(); + let nft_authority_address = *context.accounts.nft_authority.address(); // Save game_data bump on first creation (init_if_needed). See init_player.rs // for the same pattern. let game_data_bump = context.bumps.game_data; - let account: &mut ChopTreeAccountConstraints<'_> = context.accounts; + let account = &mut context.accounts; account.player.update_energy()?; account.player.print()?; @@ -39,43 +44,51 @@ pub fn chop_tree(context: Context, counter: u16, amo let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]]; // Update the metadata account with an additional metadata field in this case the player level + // The handles have to line up positionally with the instruction's account + // metas: `update_field` names the metadata account (the mint, writable) and + // its update authority, in that order. + let wood = context.accounts.player.wood.to_string(); + + // `nft_authority` signs the CPI. It is a data account holding a live borrow + // on its buffer, which the runtime would reject when the CPI borrows the + // same account, so hand the borrow back across the call. + context.accounts.nft_authority.release_borrow()?; invoke_signed( &spl_token_metadata_interface::instruction::update_field( &spl_token_2022::id(), - context.accounts.mint.to_account_info().key, - context.accounts.nft_authority.to_account_info().key, + &mint_address, + &nft_authority_address, spl_token_metadata_interface::state::Field::Key("wood".to_string()), - context.accounts.player.wood.to_string(), + wood, ), &[ - context.accounts.mint.to_account_info().clone(), - context.accounts.nft_authority.to_account_info().clone(), + context.accounts.mint.cpi_handle_mut().into(), + context.accounts.nft_authority.cpi_handle(), ], signer, )?; + context.accounts.nft_authority.reacquire_borrow_mut()?; Ok(()) } -#[derive(Accounts, Session)] +#[derive(Accounts)] #[instruction(level_seed: String)] -pub struct ChopTreeAccountConstraints<'info> { - #[session( - // The ephemeral key pair signing the transaction - signer = signer, - // The authority of the user account which must have created the session - authority = player.authority.key() - )] - // Session Tokens are passed as optional accounts - pub session_token: Option>, +pub struct ChopTreeAccountConstraints { + // Session tokens are passed as optional accounts. The token is validated in + // `chop_tree` (see `session::is_valid_session`) rather than by a derive, + // since the session-keys `Session` derive is Anchor v1 only. + /// CHECK: read as gpl-session's SessionToken; validated by seeds, owner and + /// discriminator before it is trusted. + pub session_token: Option, // There is one PlayerData account #[account( mut, - seeds = [b"player".as_ref(), player.authority.key().as_ref()], + seeds = [b"player".as_ref(), player.authority.as_ref()], bump, )] - pub player: Account<'info, PlayerData>, + pub player: BorshAccount, // There can be multiple levels the seed for the level is passed in the instruction // First player starting a new level will pay for the account in the current setup @@ -83,17 +96,17 @@ pub struct ChopTreeAccountConstraints<'info> { init_if_needed, payer = signer, space = GameData::DISCRIMINATOR.len() + GameData::INIT_SPACE, - seeds = [level_seed.as_ref()], + seeds = [level_seed.as_bytes()], bump, )] - pub game_data: Account<'info, GameData>, + pub game_data: BorshAccount, #[account(mut)] - pub signer: Signer<'info>, - pub system_program: Program<'info, System>, + pub signer: Signer, + pub system_program: Program, /// CHECK: Make sure the ata to the mint is actually owned by the signer #[account(mut)] - pub mint: UncheckedAccount<'info>, + pub mint: UncheckedAccount, #[account( init_if_needed, seeds = [b"nft_authority".as_ref()], @@ -101,6 +114,6 @@ pub struct ChopTreeAccountConstraints<'info> { space = NftAuthority::DISCRIMINATOR.len() + NftAuthority::INIT_SPACE, payer = signer, )] - pub nft_authority: Account<'info, NftAuthority>, - pub token_program: Program<'info, Token2022>, + pub nft_authority: BorshAccount, + pub token_program: Program, } diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/init_player.rs b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/init_player.rs index c38c3ba64..8eb75e8c7 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/init_player.rs +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/init_player.rs @@ -3,10 +3,10 @@ use crate::state::player_data::PlayerData; use crate::{constants::MAX_ENERGY, GameData}; use anchor_lang::prelude::*; -pub fn handle_init_player(context: Context) -> Result<()> { +pub fn handle_init_player(context: &mut Context) -> Result<()> { context.accounts.player.energy = MAX_ENERGY; context.accounts.player.last_login = Clock::get()?.unix_timestamp; - context.accounts.player.authority = context.accounts.signer.key(); + context.accounts.player.authority = *context.accounts.signer.address(); context.accounts.player.bump = context.bumps.player; // init_if_needed - only save bump if this is the first init. Subsequent // calls reuse the existing account and must not overwrite the stored bump @@ -20,26 +20,26 @@ pub fn handle_init_player(context: Context) -> Res #[derive(Accounts)] #[instruction(level_seed: String)] -pub struct InitPlayerAccountConstraints<'info> { +pub struct InitPlayerAccountConstraints { #[account( init, payer = signer, space = PlayerData::DISCRIMINATOR.len() + PlayerData::INIT_SPACE, - seeds = [b"player".as_ref(), signer.key().as_ref()], + seeds = [b"player".as_ref(), signer.address().as_ref()], bump, )] - pub player: Account<'info, PlayerData>, + pub player: BorshAccount, #[account( init_if_needed, payer = signer, space = GameData::DISCRIMINATOR.len() + GameData::INIT_SPACE, - seeds = [level_seed.as_ref()], + seeds = [level_seed.as_bytes()], bump, )] - pub game_data: Account<'info, GameData>, + pub game_data: BorshAccount, #[account(mut)] - pub signer: Signer<'info>, - pub system_program: Program<'info, System>, + pub signer: Signer, + pub system_program: Program, } diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/mint_nft.rs b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/mint_nft.rs index 22fa0c986..d3eeb70c8 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/mint_nft.rs +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/mint_nft.rs @@ -13,7 +13,23 @@ use anchor_spl::{ }, }; -pub fn handle_mint_nft(context: Context) -> Result<()> { +pub fn handle_mint_nft(context: &mut Context) -> Result<()> { + // `AccountView` is Copy, and a copy still points at the same account. v2's + // typed handles make the aliasing a compile error. `mint` and `signer` are + // Signers and `nft_authority` releases its data borrow below, so none of + // these copies aliases a live borrow. + let mut mint_view = *context.accounts.mint.account(); + let mint_view_readonly = *context.accounts.mint.account(); + let nft_authority_view = *context.accounts.nft_authority.account(); + let signer_view = *context.accounts.signer.account(); + let mint_address = *context.accounts.mint.address(); + let nft_authority_address = *context.accounts.nft_authority.address(); + + // `nft_authority` signs every CPI below. It is a data account holding a + // live borrow on its buffer, which the runtime would reject when the CPI + // borrows the same account, so hand the borrow back for the duration. + context.accounts.nft_authority.release_borrow()?; + msg!("Mint nft with meta data extension and additional meta data"); let space = @@ -29,7 +45,7 @@ pub fn handle_mint_nft(context: Context) -> Result<() // so we just over-allocate enough room at creation time. let meta_data_space = TOKEN_METADATA_EXTENSION_SPACE; - let lamports_required = Rent::get()?.minimum_balance(space + meta_data_space); + let lamports_required = Rent::get()?.try_minimum_balance(space + meta_data_space)?; msg!( "Create Mint and metadata account size and cost: {} lamports: {}", @@ -39,23 +55,23 @@ pub fn handle_mint_nft(context: Context) -> Result<() system_program::create_account( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.system_program.address(), system_program::CreateAccount { - from: context.accounts.signer.to_account_info(), - to: context.accounts.mint.to_account_info(), + from: context.accounts.signer.cpi_handle_mut(), + to: context.accounts.mint.cpi_handle_mut(), }, ), lamports_required, space as u64, - &context.accounts.token_program.key(), + &context.accounts.token_program.address(), )?; // Assign the mint to the token program system_program::assign( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.system_program.address(), system_program::Assign { - account_to_assign: context.accounts.mint.to_account_info(), + account_to_assign: context.accounts.mint.cpi_handle_mut(), }, ), &token_2022::ID, @@ -65,9 +81,9 @@ pub fn handle_mint_nft(context: Context) -> Result<() let init_meta_data_pointer_ix = match spl_token_2022::extension::metadata_pointer::instruction::initialize( &Token2022::id(), - &context.accounts.mint.key(), - Some(context.accounts.nft_authority.key()), - Some(context.accounts.mint.key()), + &context.accounts.mint.address(), + Some(*context.accounts.nft_authority.address()), + Some(*context.accounts.mint.address()), ) { Ok(ix) => ix, Err(_) => { @@ -75,24 +91,23 @@ pub fn handle_mint_nft(context: Context) -> Result<() } }; + // Handles line up positionally with the instruction's metas, and a writable + // meta needs a writable handle: `initialize` names only the mint, writable. invoke( &init_meta_data_pointer_ix, - &[ - context.accounts.mint.to_account_info(), - context.accounts.nft_authority.to_account_info(), - ], + &[CpiHandleMut::writable(&mut mint_view).into()], )?; // Initialize the mint cpi let mint_cpi_ix = CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), token_2022::InitializeMint2 { - mint: context.accounts.mint.to_account_info(), + mint: context.accounts.mint.cpi_handle_mut(), }, ); - token_2022::initialize_mint2(mint_cpi_ix, 0, &context.accounts.nft_authority.key(), None) - .unwrap(); + token_2022::initialize_mint2(mint_cpi_ix, 0, &nft_authority_address, None) + .unwrap(); // We use a PDA as a mint authority for the metadata account because // we want to be able to update the NFT from the program. @@ -100,28 +115,29 @@ pub fn handle_mint_nft(context: Context) -> Result<() let bump = context.bumps.nft_authority; let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]]; - msg!( - "Init metadata {0}", - context.accounts.nft_authority.to_account_info().key - ); + msg!("Init metadata {0}", nft_authority_address); // Init the metadata account let init_token_meta_data_ix = &spl_token_metadata_interface::instruction::initialize( &spl_token_2022::id(), - context.accounts.mint.key, - context.accounts.nft_authority.to_account_info().key, - context.accounts.mint.key, - context.accounts.nft_authority.to_account_info().key, + &mint_address, + &nft_authority_address, + &mint_address, + &nft_authority_address, "Beaver".to_string(), "BVA".to_string(), "https://arweave.net/MHK3Iopy0GgvDoM7LkkiAdg7pQqExuuWvedApCnzfj0".to_string(), ); + // `initialize` names metadata (the mint, writable), update_authority, mint + // and mint_authority, so the mint and the authority each fill two slots. invoke_signed( init_token_meta_data_ix, &[ - context.accounts.mint.to_account_info().clone(), - context.accounts.nft_authority.to_account_info().clone(), + CpiHandleMut::writable(&mut mint_view).into(), + CpiHandle::readonly(&nft_authority_view), + CpiHandle::readonly(&mint_view_readonly), + CpiHandle::readonly(&nft_authority_view), ], signer, )?; @@ -130,39 +146,39 @@ pub fn handle_mint_nft(context: Context) -> Result<() invoke_signed( &spl_token_metadata_interface::instruction::update_field( &spl_token_2022::id(), - context.accounts.mint.key, - context.accounts.nft_authority.to_account_info().key, + &mint_address, + &nft_authority_address, spl_token_metadata_interface::state::Field::Key("level".to_string()), "1".to_string(), ), &[ - context.accounts.mint.to_account_info().clone(), - context.accounts.nft_authority.to_account_info().clone(), + CpiHandleMut::writable(&mut mint_view).into(), + CpiHandle::readonly(&nft_authority_view), ], signer, )?; // Create the associated token account associated_token::create(CpiContext::new( - context.accounts.associated_token_program.key(), + context.accounts.associated_token_program.address(), associated_token::Create { - payer: context.accounts.signer.to_account_info(), - associated_token: context.accounts.token_account.to_account_info(), - authority: context.accounts.signer.to_account_info(), - mint: context.accounts.mint.to_account_info(), - system_program: context.accounts.system_program.to_account_info(), - token_program: context.accounts.token_program.to_account_info(), + payer: context.accounts.signer.cpi_handle_mut(), + associated_token: context.accounts.token_account.cpi_handle_mut(), + authority: CpiHandle::readonly(&signer_view), + mint: CpiHandle::readonly(&mint_view), + system_program: context.accounts.system_program.cpi_handle(), + token_program: context.accounts.token_program.cpi_handle(), }, ))?; // Mint one token to the associated token account of the player token_2022::mint_to( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), token_2022::MintTo { - mint: context.accounts.mint.to_account_info(), - to: context.accounts.token_account.to_account_info(), - authority: context.accounts.nft_authority.to_account_info(), + mint: context.accounts.mint.cpi_handle_mut(), + to: context.accounts.token_account.cpi_handle_mut(), + authority: CpiHandle::readonly(&nft_authority_view), }, signer, ), @@ -172,10 +188,10 @@ pub fn handle_mint_nft(context: Context) -> Result<() // Freeze the mint authority so no more tokens can be minted to make it an NFT token_2022::set_authority( CpiContext::new_with_signer( - context.accounts.token_program.key(), + context.accounts.token_program.address(), token_2022::SetAuthority { - current_authority: context.accounts.nft_authority.to_account_info(), - account_or_mint: context.accounts.mint.to_account_info(), + current_authority: CpiHandle::readonly(&nft_authority_view), + account_or_mint: context.accounts.mint.cpi_handle_mut(), }, signer, ), @@ -183,22 +199,24 @@ pub fn handle_mint_nft(context: Context) -> Result<() None, )?; + context.accounts.nft_authority.reacquire_borrow_mut()?; + Ok(()) } #[derive(Accounts)] -pub struct MintNftAccountConstraints<'info> { +pub struct MintNftAccountConstraints { #[account(mut)] - pub signer: Signer<'info>, - pub system_program: Program<'info, System>, - pub token_program: Program<'info, Token2022>, + pub signer: Signer, + pub system_program: Program, + pub token_program: Program, /// CHECK: We will create this one for the user #[account(mut)] - pub token_account: UncheckedAccount<'info>, + pub token_account: UncheckedAccount, #[account(mut)] - pub mint: Signer<'info>, - pub rent: Sysvar<'info, Rent>, - pub associated_token_program: Program<'info, AssociatedToken>, + pub mint: Signer, + pub rent: Sysvar, + pub associated_token_program: Program, #[account( init_if_needed, seeds = [b"nft_authority".as_ref()], @@ -206,9 +224,9 @@ pub struct MintNftAccountConstraints<'info> { space = NftAuthority::DISCRIMINATOR.len() + NftAuthority::INIT_SPACE, payer = signer )] - pub nft_authority: Account<'info, NftAuthority>, + pub nft_authority: BorshAccount, } -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct NftAuthority {} diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/lib.rs b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/lib.rs index 6d513f1bc..a6ee48b1d 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/lib.rs +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/lib.rs @@ -4,27 +4,30 @@ pub use crate::errors::GameErrorCode; pub use anchor_lang::prelude::*; -pub use session_keys::{session_auth_or, Session, SessionError}; pub mod constants; pub mod errors; pub mod instructions; +pub mod session; pub mod state; use instructions::*; -// WARNING: This example depends on the `session-keys` crate, which has not -// been independently audited. It is included here purely for educational -// purposes - demonstrating how a game might let a player sign with a -// short-lived session token instead of their main wallet. Do not ship this -// crate (or this program) to mainnet in its current form: review the upstream -// `session-keys` source, get an audit, and harden the session-token issuance -// and expiry handling first. +// WARNING: this example reads gpl-session's `SessionToken` account (see +// `session.rs`), and that program has not been independently audited. It is +// included here purely for educational purposes - demonstrating how a game +// might let a player sign with a short-lived session token instead of their +// main wallet. Do not ship this pattern to mainnet in its current form: review +// the upstream session-keys source, get an audit, and harden the session-token +// issuance and expiry handling first. declare_id!("9aZZ7TJ2fQZxY8hMtWXywp5y6BgqC4N2BPcr9FDT47sW"); #[program] pub mod extension_nft { use super::*; - pub fn init_player(context: Context, _level_seed: String) -> Result<()> { + pub fn init_player( + context: &mut Context, + _level_seed: String, + ) -> Result<()> { init_player::handle_init_player(context) } @@ -32,18 +35,29 @@ pub mod extension_nft { // lets the player either use their session token or their main wallet. (The counter is only // there so that the player can do multiple transactions in the same block. Without it multiple transactions // in the same block would result in the same signature and therefore fail.) - // NOTE: the `#[session_auth_or]` macro injects code that refers to the - // context binding by the literal name `ctx`, so this handler's context - // parameter must be named `ctx` (not `context`) for the macro to expand. - #[session_auth_or( - ctx.accounts.player.authority.key() == ctx.accounts.signer.key(), - GameErrorCode::WrongAuthority - )] - pub fn chop_tree(ctx: Context, _level_seed: String, counter: u16) -> Result<()> { + // The session-keys `#[session_auth_or]` attribute macro is Anchor v1 only, + // so the same check is spelled out: a live session token authorizes the + // call, and otherwise the signer has to be the player's own authority. + pub fn chop_tree( + ctx: &mut Context, + _level_seed: String, + counter: u16, + ) -> Result<()> { + let signer = *ctx.accounts.signer.address(); + let authority = ctx.accounts.player.authority; + let has_session = match ctx.accounts.session_token.as_ref() { + Some(token) => session::is_valid_session(token, &signer, &authority)?, + None => false, + }; + require!( + has_session || authority == signer, + GameErrorCode::WrongAuthority + ); + chop_tree::chop_tree(ctx, counter, 1) } - pub fn mint_nft(context: Context) -> Result<()> { + pub fn mint_nft(context: &mut Context) -> Result<()> { mint_nft::handle_mint_nft(context) } } diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/session.rs b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/session.rs new file mode 100644 index 000000000..75de1e826 --- /dev/null +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/session.rs @@ -0,0 +1,78 @@ +//! A local reader for gpl-session's `SessionToken` account. +//! +//! The `session-keys` crate is Anchor v1 only: its `Session` derive requires a +//! field of type `Option>`, and `SessionToken` is +//! not `Pod`, so v2's zero-copy `Account` cannot hold it either. The account +//! itself is just four `Address`es worth of borsh data owned by the session-keys +//! program, so this module declares that layout, checks the discriminator and +//! PDA, and leaves the gasless-session lesson intact without the dependency. +//! +//! WARNING, unchanged from before: the session-keys program has not been +//! audited. A session token lets a player sign game transactions with a +//! short-lived key instead of their main wallet. Do not ship this pattern +//! without reviewing the session-keys source and hardening issuance. + +use anchor_lang::prelude::*; + +/// `KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5`, the session-keys program. +pub const SESSION_KEYS_ID: Address = + anchor_lang::address!("KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5"); + +/// First seed of the session-token PDA. +pub const SESSION_TOKEN_SEED: &[u8] = b"session_token"; + +/// gpl-session's `SessionToken`, as it is laid out on chain. Anchor's borsh +/// account encoding puts the eight-byte discriminator first, then the fields in +/// declaration order. +#[account(borsh)] +pub struct SessionToken { + pub authority: Address, + pub target_program: Address, + pub session_signer: Address, + pub valid_until: i64, +} + +/// Whether `session_token` is a live session for `authority`, signed by +/// `session_signer`, targeting this program. +/// +/// Returns `false` rather than erroring when the account is absent, malformed, +/// or expired, so the caller can fall back to plain wallet authorization. +pub fn is_valid_session( + session_token: &UncheckedAccount, + session_signer: &Address, + authority: &Address, +) -> Result { + if !session_token.account().owned_by(&SESSION_KEYS_ID) { + return Ok(false); + } + + let data = session_token.account().try_borrow()?; + let disc_len = ::DISCRIMINATOR.len(); + if data.len() <= disc_len + || &data[..disc_len] != ::DISCRIMINATOR + { + return Ok(false); + } + let mut payload = &data[disc_len..]; + let Ok(token) = >::get(&mut payload) + else { + return Ok(false); + }; + + // The PDA binds the token to exactly one (target_program, signer, authority) + // triple, so deriving it is what proves the token is the caller's. + let (expected, _bump) = Pubkey::find_program_address( + &[ + SESSION_TOKEN_SEED, + crate::ID.as_ref(), + session_signer.as_ref(), + authority.as_ref(), + ], + &SESSION_KEYS_ID, + ); + if expected != *session_token.address() { + return Ok(false); + } + + Ok(Clock::get()?.unix_timestamp < token.valid_until) +} diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/state/game_data.rs b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/state/game_data.rs index 03174be68..887da9037 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/state/game_data.rs +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/state/game_data.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use crate::constants::MAX_WOOD_PER_TREE; -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct GameData { pub total_wood_collected: u64, diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/state/player_data.rs b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/state/player_data.rs index 90c6fa64c..663ee3e7f 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/state/player_data.rs +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/state/player_data.rs @@ -1,10 +1,10 @@ use crate::constants::*; use anchor_lang::prelude::*; -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct PlayerData { - pub authority: Pubkey, + pub authority: Address, /// Player name. Capped at 32 bytes - a conservative upper bound for /// display names; bump `#[max_len]` if you need room for emoji-heavy /// or international names (each non-ASCII codepoint costs up to 4 bytes). diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/tests/test_extension_nft.rs b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/tests/test_extension_nft.rs index 74a58e906..18c656b02 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/tests/test_extension_nft.rs +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/tests/test_extension_nft.rs @@ -20,9 +20,9 @@ //! working after the id is regenerated. use { - anchor_lang::{ - prelude::Pubkey, solana_program::system_program, InstructionData, ToAccountMetas, - }, + // `system_program` moved to the crate root in v2, and `Pubkey` is + // compat-only: `Address` is the same 32-byte type. + anchor_lang::{system_program, Address as Pubkey, InstructionData, ToAccountMetas}, litesvm::LiteSVM, solana_instruction::Instruction, solana_keypair::Keypair, @@ -83,7 +83,7 @@ fn init_player_ix(program_id: &Pubkey, signer: &Pubkey) -> Instruction { player: player_pda(program_id, signer), game_data: game_data_pda(program_id, LEVEL_SEED), signer: *signer, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), data: extension_nft::instruction::InitPlayer { @@ -98,7 +98,7 @@ fn mint_nft_ix(program_id: &Pubkey, signer: &Pubkey, mint: &Pubkey) -> Instructi program_id: *program_id, accounts: extension_nft::accounts::MintNftAccountConstraints { signer: *signer, - system_program: system_program::id(), + system_program: system_program::ID, token_program: TOKEN_2022_ID, token_account: associated_token_address(signer, mint), mint: *mint, @@ -121,7 +121,7 @@ fn chop_tree_ix(program_id: &Pubkey, signer: &Pubkey, mint: &Pubkey, counter: u1 player: player_pda(program_id, signer), game_data: game_data_pda(program_id, LEVEL_SEED), signer: *signer, - system_program: system_program::id(), + system_program: system_program::ID, mint: *mint, nft_authority: nft_authority_pda(program_id), token_program: TOKEN_2022_ID, @@ -142,7 +142,7 @@ struct Player { } fn fetch_player(svm: &LiteSVM, player: &Pubkey) -> Player { - use anchor_lang::AnchorDeserialize; + use borsh::BorshDeserialize; let account = svm.get_account(player).expect("player account exists"); // Skip the 8-byte Anchor discriminator. let mut data = &account.data[8..]; @@ -207,7 +207,10 @@ fn test_init_player_mint_and_chop() { // The associated token account should exist and hold the single NFT. let ata = associated_token_address(&signer, &mint.pubkey()); let ata_account = svm.get_account(&ata).expect("ATA created"); - assert_eq!(ata_account.owner, TOKEN_2022_ID, "ATA owned by Token Extensions"); + assert_eq!( + ata_account.owner, TOKEN_2022_ID, + "ATA owned by Token Extensions" + ); // 3. chop_tree - needs the existing mint so it can push the new wood total // into the NFT metadata. Signed by the player's main wallet (no session). diff --git a/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/Cargo.toml b/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/Cargo.toml index fb874ec2c..d468682fb 100644 --- a/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/Cargo.toml +++ b/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/Cargo.toml @@ -14,11 +14,19 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/src/lib.rs b/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/src/lib.rs index 6e3447b08..ff931d076 100644 --- a/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/src/lib.rs +++ b/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/src/lib.rs @@ -17,61 +17,60 @@ pub mod non_transferable { // There is currently not an anchor constraint to automatically initialize the NonTransferable extension // We can manually create and initialize the mint account via CPIs in the instruction handler - pub fn initialize(context: Context) -> Result<()> { + pub fn initialize(context: &mut Context) -> Result<()> { // Calculate space required for mint and extension data let mint_size = ExtensionType::try_calculate_account_len::(&[ExtensionType::NonTransferable])?; // Calculate minimum lamports required for size of mint account with extensions - let lamports = (Rent::get()?).minimum_balance(mint_size); + let lamports = Rent::get()?.try_minimum_balance(mint_size)?; // Invoke System Program to create new account with space for mint and extension data create_account( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), CreateAccount { - from: context.accounts.payer.to_account_info(), - to: context.accounts.mint_account.to_account_info(), + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), }, ), - lamports, // Lamports - mint_size as u64, // Space - &context.accounts.token_program.key(), // Owner Program + lamports, // Lamports + mint_size as u64, // Space + &context.accounts.token_program.address(), // Owner Program )?; // Initialize the NonTransferable extension // This instruction must come before the instruction to initialize the mint data non_transferable_mint_initialize(CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), NonTransferableMintInitialize { - token_program_id: context.accounts.token_program.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), }, ))?; // Initialize the standard mint account data initialize_mint2( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), InitializeMint2 { - mint: context.accounts.mint_account.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), }, ), - 2, // decimals - &context.accounts.payer.key(), // mint authority - Some(&context.accounts.payer.key()), // freeze authority + 2, // decimals + &context.accounts.payer.address(), // mint authority + Some(&context.accounts.payer.address()), // freeze authority )?; Ok(()) } } #[derive(Accounts)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account(mut)] - pub mint_account: Signer<'info>, + pub mint_account: Signer, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub token_program: Program, + pub system_program: Program, } diff --git a/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/tests/test_non_transferable.rs b/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/tests/test_non_transferable.rs index 65f05d6a1..7727dfc11 100644 --- a/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/tests/test_non_transferable.rs +++ b/tokens/token-extensions/non-transferable/anchor/programs/non-transferable/tests/test_non_transferable.rs @@ -1,13 +1,10 @@ use { anchor_lang::{ - solana_program::{ - instruction::Instruction, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::{ @@ -16,11 +13,10 @@ use { TOKEN_EXTENSIONS_PROGRAM_ID, }, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = non_transferable::id(); let mut svm = LiteSVM::new(); @@ -44,11 +40,17 @@ fn test_create_non_transferable_mint_and_attempt_transfer() { payer: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![initialize_ix], &[&payer, &mint_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![initialize_ix], + &[&payer, &mint_keypair], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Verify mint account was created and has extension data @@ -63,12 +65,9 @@ fn test_create_non_transferable_mint_and_attempt_transfer() { // Step 2: Create ATAs for sender and recipient let recipient = Keypair::new(); - let source_ata = create_token_extensions_account( - &mut svm, - &payer.pubkey(), - &mint_keypair.pubkey(), - &payer, - ).unwrap(); + let source_ata = + create_token_extensions_account(&mut svm, &payer.pubkey(), &mint_keypair.pubkey(), &payer) + .unwrap(); svm.expire_blockhash(); let dest_ata = create_token_extensions_account( @@ -76,7 +75,8 @@ fn test_create_non_transferable_mint_and_attempt_transfer() { &recipient.pubkey(), &mint_keypair.pubkey(), &payer, - ).unwrap(); + ) + .unwrap(); svm.expire_blockhash(); // Step 3: Mint 1 token to sender @@ -86,7 +86,8 @@ fn test_create_non_transferable_mint_and_attempt_transfer() { &source_ata, 1, &payer, - ).unwrap(); + ) + .unwrap(); svm.expire_blockhash(); // Step 4: Attempt transfer - should fail because mint is NonTransferable diff --git a/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/Cargo.toml b/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/Cargo.toml index 4872f4272..2b6fb0b21 100644 --- a/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/Cargo.toml +++ b/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/Cargo.toml @@ -14,15 +14,23 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/src/instructions/initialize.rs b/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/src/instructions/initialize.rs index 0879e5e51..8add30eb9 100644 --- a/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/src/instructions/initialize.rs +++ b/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/src/instructions/initialize.rs @@ -1,50 +1,105 @@ use anchor_lang::prelude::*; +use anchor_lang::system_program::{create_account, CreateAccount}; use anchor_spl::{ - token_2022::spl_token_2022::extension::permanent_delegate::PermanentDelegate, + token_2022::{ + initialize_mint2, + spl_token_2022::{ + extension::{permanent_delegate::PermanentDelegate, ExtensionType}, + pod::PodMint, + }, + InitializeMint2, + }, + token_2022_extensions::permanent_delegate::{ + permanent_delegate_initialize, PermanentDelegateInitialize, + }, token_interface::{ spl_pod::optional_keys::OptionalNonZeroPubkey, spl_token_2022::{ extension::{BaseStateWithExtensions, StateWithExtensions}, state::Mint as MintState, }, - Mint, Token2022, + Token2022, }, }; #[derive(Accounts)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { + #[account(mut)] + pub payer: Signer, + #[account(mut)] - pub payer: Signer<'info>, - - #[account( - init, - payer = payer, - mint::decimals = 2, - mint::authority = payer, - extensions::permanent_delegate::delegate = payer, - )] - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub mint_account: Signer, + + pub token_program: Program, + pub system_program: Program, } // helper to check mint data, and demonstrate how to read mint extension data within a program -fn check_mint_data(accounts: &mut InitializeAccountConstraints) -> Result<()> { - let mint = &accounts.mint_account.to_account_info(); - let mint_data = mint.data.borrow(); +fn check_mint_data(context: &Context) -> Result<()> { + let mint = context.accounts.mint_account.account(); + let mint_data = mint.try_borrow()?; let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; let extension_data = mint_with_extension.get_extension::()?; assert_eq!( extension_data.delegate, - OptionalNonZeroPubkey::try_from(Some(accounts.payer.key()))? + OptionalNonZeroPubkey::try_from(Some(*context.accounts.payer.address()))? ); msg!("{:?}", extension_data); Ok(()) } -pub fn handler(mut context: Context) -> Result<()> { - check_mint_data(&mut context.accounts)?; +// There is currently not an anchor constraint to automatically initialize the +// PermanentDelegate extension. We can manually create and initialize the mint +// account via CPIs in the instruction handler. +pub fn handler(context: &mut Context) -> Result<()> { + // Calculate space required for mint and extension data + let mint_size = + ExtensionType::try_calculate_account_len::(&[ExtensionType::PermanentDelegate])?; + + // Calculate minimum lamports required for size of mint account with extensions + let lamports = Rent::get()?.try_minimum_balance(mint_size)?; + + // Invoke System Program to create new account with space for mint and extension data + create_account( + CpiContext::new( + context.accounts.system_program.address(), + CreateAccount { + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + lamports, // Lamports + mint_size as u64, // Space + context.accounts.token_program.address(), // Owner Program + )?; + + // Initialize the PermanentDelegate extension + // This instruction must come before the instruction to initialize the mint data + permanent_delegate_initialize( + CpiContext::new( + context.accounts.token_program.address(), + PermanentDelegateInitialize { + mint: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + context.accounts.payer.address(), + )?; + + // Initialize the standard mint account data + initialize_mint2( + CpiContext::new( + context.accounts.token_program.address(), + InitializeMint2 { + mint: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + 2, // decimals + context.accounts.payer.address(), // mint authority + None, // freeze authority + )?; + + check_mint_data(context)?; Ok(()) } diff --git a/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/src/lib.rs b/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/src/lib.rs index 5c2005fa0..7667da049 100644 --- a/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/src/lib.rs +++ b/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/src/lib.rs @@ -9,7 +9,7 @@ declare_id!("A9rxKS84ZoJVyeTfQbCEfxME2vvAM4uwSMjkmhR5XWb1"); pub mod permanent_delegate { use super::*; - pub fn initialize(context: Context) -> Result<()> { + pub fn initialize(context: &mut Context) -> Result<()> { instructions::initialize::handler(context) } } diff --git a/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/tests/test_permanent_delegate.rs b/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/tests/test_permanent_delegate.rs index 034ac53f7..372a0b5d0 100644 --- a/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/tests/test_permanent_delegate.rs +++ b/tokens/token-extensions/permanent-delegate/anchor/programs/permanent-delegate/tests/test_permanent_delegate.rs @@ -1,24 +1,18 @@ use { anchor_lang::{ - solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::{AccountMeta, Instruction}, + system_program, Address, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ assert_token_account_balance, create_wallet, send_transaction_from_instructions, - token_extensions::{ - mint_tokens_to_token_extensions_account, TOKEN_EXTENSIONS_PROGRAM_ID, - }, + token_extensions::{mint_tokens_to_token_extensions_account, TOKEN_EXTENSIONS_PROGRAM_ID}, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = permanent_delegate::id(); let mut svm = LiteSVM::new(); @@ -32,15 +26,19 @@ fn setup() -> (LiteSVM, Pubkey, Keypair) { /// Create a Token Extensions token account (CreateAccount + InitializeAccount3). /// This creates a non-ATA token account with explicit keypair, which kite doesn't provide. fn create_token_account_instructions( - payer: &Pubkey, - account: &Pubkey, - mint: &Pubkey, - owner: &Pubkey, + payer: &Address, + account: &Address, + mint: &Address, + owner: &Address, ) -> Vec { let space: u64 = 200; let lamports: u64 = 3_000_000; let create_account_ix = anchor_lang::solana_program::system_instruction::create_account( - payer, account, lamports, space, &TOKEN_EXTENSIONS_PROGRAM_ID, + payer, + account, + lamports, + space, + &TOKEN_EXTENSIONS_PROGRAM_ID, ); // InitializeAccount3 (instruction 18): [18, owner(32)] let mut init_data = vec![18u8]; @@ -58,9 +56,9 @@ fn create_token_account_instructions( /// BurnChecked instruction for Token Extensions (instruction 15). fn burn_checked_ix( - account: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, + account: &Address, + mint: &Address, + authority: &Address, amount: u64, decimals: u8, ) -> Instruction { @@ -91,11 +89,17 @@ fn test_create_mint_with_permanent_delegate_and_burn() { payer: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![initialize_ix], &[&payer, &mint_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![initialize_ix], + &[&payer, &mint_keypair], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Step 2: Create a token account owned by a random keypair @@ -107,7 +111,13 @@ fn test_create_mint_with_permanent_delegate_and_burn() { &mint_keypair.pubkey(), &random_owner.pubkey(), ); - send_transaction_from_instructions(&mut svm, create_ata_ixs, &[&payer, &token_account], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + create_ata_ixs, + &[&payer, &token_account], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Step 3: Mint 100 tokens to the token account @@ -117,7 +127,8 @@ fn test_create_mint_with_permanent_delegate_and_burn() { &token_account.pubkey(), 100, &payer, - ).unwrap(); + ) + .unwrap(); svm.expire_blockhash(); // Step 4: Burn all 100 tokens using the permanent delegate (payer) @@ -128,7 +139,13 @@ fn test_create_mint_with_permanent_delegate_and_burn() { 100, 2, // decimals ); - send_transaction_from_instructions(&mut svm, vec![burn_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![burn_ix], &[&payer], &payer.pubkey()) + .unwrap(); - assert_token_account_balance(&svm, &token_account.pubkey(), 0, "Token account balance should be 0 after burn"); + assert_token_account_balance( + &svm, + &token_account.pubkey(), + 0, + "Token account balance should be 0 after burn", + ); } diff --git a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/Cargo.toml b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/Cargo.toml index 8de99e42a..7404633bc 100644 --- a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/Cargo.toml +++ b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/Cargo.toml @@ -14,11 +14,19 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = "1.1.2" +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/harvest.rs b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/harvest.rs index 870e29474..c7b251dd2 100644 --- a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/harvest.rs +++ b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/harvest.rs @@ -4,34 +4,47 @@ use anchor_spl::token_interface::{ }; #[derive(Accounts)] -pub struct HarvestAccountConstraints<'info> { +pub struct HarvestAccountConstraints { #[account(mut)] - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, + pub mint_account: InterfaceAccount, + pub token_program: Program, } // transfer fees are stored directly on the recipient token account and must be "harvested" // "harvesting" transfers fees accumulated on token accounts to the mint account -pub fn process_harvest<'info>(context: Context<'info, HarvestAccountConstraints<'info>>) -> Result<()> { +pub fn process_harvest(context: &mut Context) -> Result<()> { // Using remaining accounts to allow for passing in an unknown number of token accounts to harvest from // Check that remaining accounts are token accounts for the mint to harvest to - let sources = context - .remaining_accounts + // `remaining_accounts()` takes `&mut context` and hands back an owned vec, + // so collect it before anything borrows `context.accounts`. + let mut candidates = context.remaining_accounts()?; + let mint_address = *context.accounts.mint_account.address(); + + // v2 has no `InterfaceAccount::try_from`; `AnchorAccount::load` is the + // equivalent for an account reached through remaining_accounts. The + // wrapper is dropped at the end of each iteration, so it holds no borrow + // across the CPI: only the verdict escapes. + let keep: Vec = candidates .iter() - .filter_map(|account| { - InterfaceAccount::::try_from(account) - .ok() - .filter(|token_account| token_account.mint == context.accounts.mint_account.key()) - .map(|_| account.to_account_info()) + .map(|account| { + InterfaceAccount::::load(*account) + .map(|token_account| *token_account.mint() == mint_address) + .unwrap_or(false) }) - .collect::>(); + .collect(); + + let sources: Vec = candidates + .iter_mut() + .zip(keep) + .filter(|(_, keep)| *keep) + .map(|(account, _)| CpiHandleMut::writable(account)) + .collect(); harvest_withheld_tokens_to_mint( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), HarvestWithheldTokensToMint { - token_program_id: context.accounts.token_program.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), }, ), sources, // token accounts to harvest from diff --git a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/initialize.rs b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/initialize.rs index 1ce456378..9c3a36347 100644 --- a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/initialize.rs +++ b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/initialize.rs @@ -20,20 +20,20 @@ use anchor_spl::{ }; #[derive(Accounts)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account(mut)] - pub mint_account: Signer<'info>, + pub mint_account: Signer, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub token_program: Program, + pub system_program: Program, } // There is currently not an anchor constraint to automatically initialize the TransferFeeConfig extension // We can manually create and initialize the mint account via CPIs in the instruction handler pub fn handle_process_initialize( - context: Context, + context: &mut Context, transfer_fee_basis_points: u16, maximum_fee: u64, ) -> Result<()> { @@ -42,49 +42,48 @@ pub fn handle_process_initialize( ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferFeeConfig])?; // Calculate minimum lamports required for size of mint account with extensions - let lamports = (Rent::get()?).minimum_balance(mint_size); + let lamports = Rent::get()?.try_minimum_balance(mint_size)?; // Invoke System Program to create new account with space for mint and extension data create_account( CpiContext::new( - context.accounts.system_program.key(), + context.accounts.system_program.address(), CreateAccount { - from: context.accounts.payer.to_account_info(), - to: context.accounts.mint_account.to_account_info(), + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), }, ), - lamports, // Lamports - mint_size as u64, // Space - &context.accounts.token_program.key(), // Owner Program + lamports, // Lamports + mint_size as u64, // Space + &context.accounts.token_program.address(), // Owner Program )?; // Initialize the transfer fee extension data // This instruction must come before the instruction to initialize the mint data transfer_fee_initialize( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferFeeInitialize { - token_program_id: context.accounts.token_program.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), }, ), - Some(&context.accounts.payer.key()), // transfer fee config authority (update fee) - Some(&context.accounts.payer.key()), // withdraw authority (withdraw fees) - transfer_fee_basis_points, // transfer fee basis points (% fee per transfer) - maximum_fee, // maximum fee (maximum units of token per transfer) + Some(&context.accounts.payer.address()), // transfer fee config authority (update fee) + Some(&context.accounts.payer.address()), // withdraw authority (withdraw fees) + transfer_fee_basis_points, // transfer fee basis points (% fee per transfer) + maximum_fee, // maximum fee (maximum units of token per transfer) )?; // Initialize the standard mint account data initialize_mint2( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), InitializeMint2 { - mint: context.accounts.mint_account.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), }, ), - 2, // decimals - &context.accounts.payer.key(), // mint authority - Some(&context.accounts.payer.key()), // freeze authority + 2, // decimals + &context.accounts.payer.address(), // mint authority + Some(&context.accounts.payer.address()), // freeze authority )?; handle_check_mint_data(&context.accounts)?; @@ -93,22 +92,22 @@ pub fn handle_process_initialize( // helper to demonstrate how to read mint extension data within a program pub fn handle_check_mint_data(accounts: &InitializeAccountConstraints) -> Result<()> { - let mint = &accounts.mint_account.to_account_info(); - let mint_data = mint.data.borrow(); + // Read-only: the account already holds a shared borrow of its buffer, and a + // second shared borrow is fine where a writable handle would be rejected. + let mint_data = accounts.mint_account.account().try_borrow()?; let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; let extension_data = mint_with_extension.get_extension::()?; assert_eq!( extension_data.transfer_fee_config_authority, - OptionalNonZeroPubkey::try_from(Some(accounts.payer.key()))? + OptionalNonZeroPubkey::try_from(Some(*accounts.payer.address()))? ); assert_eq!( extension_data.withdraw_withheld_authority, - OptionalNonZeroPubkey::try_from(Some(accounts.payer.key()))? + OptionalNonZeroPubkey::try_from(Some(*accounts.payer.address()))? ); msg!("{:?}", extension_data); Ok(()) } - diff --git a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/transfer.rs b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/transfer.rs index 20846e2e7..0d8ad8495 100644 --- a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/transfer.rs +++ b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/transfer.rs @@ -13,20 +13,23 @@ use anchor_spl::{ }; #[derive(Accounts)] -pub struct TransferAccountConstraints<'info> { +pub struct TransferAccountConstraints { #[account(mut)] - pub sender: Signer<'info>, - pub recipient: SystemAccount<'info>, + pub sender: Signer, + pub recipient: SystemAccount, - #[account(mut)] - pub mint_account: InterfaceAccount<'info, Mint>, + // Read-only: `transfer_checked_with_fee` accrues the withheld fee on the + // destination token account, not the mint. It also has to be read-only for + // the extension read below: a mutable data account holds an exclusive + // borrow, so a second `try_borrow()` on it is rejected. + pub mint_account: InterfaceAccount, #[account( mut, associated_token::mint = mint_account, associated_token::authority = sender, associated_token::token_program = token_program )] - pub sender_token_account: InterfaceAccount<'info, TokenAccount>, + pub sender_token_account: InterfaceAccount, #[account( init_if_needed, payer = sender, @@ -34,38 +37,44 @@ pub struct TransferAccountConstraints<'info> { associated_token::authority = recipient, associated_token::token_program = token_program )] - pub recipient_token_account: InterfaceAccount<'info, TokenAccount>, - pub token_program: Program<'info, Token2022>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub recipient_token_account: InterfaceAccount, + pub token_program: Program, + pub associated_token_program: Program, + pub system_program: Program, } // transfer fees are automatically deducted from the transfer amount // recipients receives (transfer amount - fees) // transfer fees are stored directly on the recipient token account and must be "harvested" -pub fn handle_process_transfer(context: Context, amount: u64) -> Result<()> { - // read mint account extension data - let mint = &context.accounts.mint_account.to_account_info(); - let mint_data = mint.data.borrow(); - let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; - let extension_data = mint_with_extension.get_extension::()?; - - // calculate expected fee +pub fn handle_process_transfer( + context: &mut Context, + amount: u64, +) -> Result<()> { + // Read the mint's extension data in its own scope: the `Ref` has to drop + // before the CPI below, or the runtime rejects the CPI's borrow of the same + // account with AccountBorrowFailed. let epoch = Clock::get()?.epoch; - let fee = extension_data.calculate_epoch_fee(epoch, amount).unwrap(); + let fee = { + let mint_data = context.accounts.mint_account.account().try_borrow()?; + let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; + let extension_data = mint_with_extension.get_extension::()?; + extension_data.calculate_epoch_fee(epoch, amount).unwrap() + }; // mint account decimals - let decimals = context.accounts.mint_account.decimals; + let decimals = context.accounts.mint_account.decimals(); transfer_checked_with_fee( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferCheckedWithFee { - token_program_id: context.accounts.token_program.to_account_info(), - source: context.accounts.sender_token_account.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - destination: context.accounts.recipient_token_account.to_account_info(), - authority: context.accounts.sender.to_account_info(), + source: context.accounts.sender_token_account.cpi_handle_mut(), + // Read-only slots take the wrapper's own handle: on a data account + // it relaxes the runtime borrow check that a hand-built handle + // over a copy of the view would still trip. + mint: context.accounts.mint_account.cpi_handle(), + destination: context.accounts.recipient_token_account.cpi_handle_mut(), + authority: context.accounts.sender.cpi_handle(), }, ), amount, // transfer amount diff --git a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/update_fee.rs b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/update_fee.rs index da0646bb5..fbc136c02 100644 --- a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/update_fee.rs +++ b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/update_fee.rs @@ -2,29 +2,28 @@ use anchor_lang::prelude::*; use anchor_spl::token_interface::{transfer_fee_set, Mint, Token2022, TransferFeeSetTransferFee}; #[derive(Accounts)] -pub struct UpdateFeeAccountConstraints<'info> { - pub authority: Signer<'info>, +pub struct UpdateFeeAccountConstraints { + pub authority: Signer, #[account(mut)] - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, + pub mint_account: InterfaceAccount, + pub token_program: Program, } // Note that there is a 2 epoch delay from when new fee updates take effect // This is a safely feature built into the extension // https://github.com/solana-program/token-2022/blob/2d18d97f083627d3f13ce43b16fa4305cbfac4de/program/src/extension/transfer_fee/processor.rs#L92-L109 pub fn handle_process_update_fee( - context: Context, + context: &mut Context, transfer_fee_basis_points: u16, maximum_fee: u64, ) -> Result<()> { transfer_fee_set( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferFeeSetTransferFee { - token_program_id: context.accounts.token_program.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - authority: context.accounts.authority.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), + authority: context.accounts.authority.cpi_handle(), }, ), transfer_fee_basis_points, // transfer fee basis points (% fee per transfer) diff --git a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/withdraw.rs b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/withdraw.rs index ba9fb6659..deb925be9 100644 --- a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/withdraw.rs +++ b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/withdraw.rs @@ -5,26 +5,25 @@ use anchor_spl::token_interface::{ }; #[derive(Accounts)] -pub struct WithdrawAccountConstraints<'info> { - pub authority: Signer<'info>, +pub struct WithdrawAccountConstraints { + pub authority: Signer, #[account(mut)] - pub mint_account: InterfaceAccount<'info, Mint>, + pub mint_account: InterfaceAccount, #[account(mut)] - pub token_account: InterfaceAccount<'info, TokenAccount>, - pub token_program: Program<'info, Token2022>, + pub token_account: InterfaceAccount, + pub token_program: Program, } // transfer fees "harvested" to the mint account can then be withdraw by the withdraw authority // this transfers fees on the mint account to the specified token account -pub fn handle_process_withdraw(context: Context) -> Result<()> { +pub fn handle_process_withdraw(context: &mut Context) -> Result<()> { withdraw_withheld_tokens_from_mint(CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), WithdrawWithheldTokensFromMint { - token_program_id: context.accounts.token_program.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - destination: context.accounts.token_account.to_account_info(), - authority: context.accounts.authority.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), + destination: context.accounts.token_account.cpi_handle_mut(), + authority: context.accounts.authority.cpi_handle(), }, ))?; Ok(()) diff --git a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/lib.rs b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/lib.rs index 811385357..628b32ff6 100644 --- a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/lib.rs +++ b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/lib.rs @@ -10,27 +10,27 @@ pub mod transfer_fee { use super::*; pub fn initialize( - context: Context, + context: &mut Context, transfer_fee_basis_points: u16, maximum_fee: u64, ) -> Result<()> { handle_process_initialize(context, transfer_fee_basis_points, maximum_fee) } - pub fn transfer(context: Context, amount: u64) -> Result<()> { + pub fn transfer(context: &mut Context, amount: u64) -> Result<()> { handle_process_transfer(context, amount) } - pub fn harvest<'info>(context: Context<'info, HarvestAccountConstraints<'info>>) -> Result<()> { + pub fn harvest(context: &mut Context) -> Result<()> { process_harvest(context) } - pub fn withdraw(context: Context) -> Result<()> { + pub fn withdraw(context: &mut Context) -> Result<()> { handle_process_withdraw(context) } pub fn update_fee( - context: Context, + context: &mut Context, transfer_fee_basis_points: u16, maximum_fee: u64, ) -> Result<()> { diff --git a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/tests/test_transfer_fee.rs b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/tests/test_transfer_fee.rs index 087f1ae6d..bb9a2b8ae 100644 --- a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/tests/test_transfer_fee.rs +++ b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/tests/test_transfer_fee.rs @@ -1,13 +1,10 @@ use { anchor_lang::{ - solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::{AccountMeta, Instruction}, + system_program, Address, InstructionData, ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::{ @@ -15,17 +12,16 @@ use { mint_tokens_to_token_extensions_account, TOKEN_EXTENSIONS_PROGRAM_ID, }, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn associated_token_program_id() -> Pubkey { +fn associated_token_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = transfer_fee::id(); let mut svm = LiteSVM::new(); @@ -44,7 +40,8 @@ fn test_transfer_fee_full_flow() { let ata_program = associated_token_program_id(); let sender_ata = get_token_extensions_account_address(&payer.pubkey(), &mint_keypair.pubkey()); - let recipient_ata = get_token_extensions_account_address(&recipient.pubkey(), &mint_keypair.pubkey()); + let recipient_ata = + get_token_extensions_account_address(&recipient.pubkey(), &mint_keypair.pubkey()); // Step 1: Create mint with transfer fee (100 basis points = 1%, max fee = 1) let initialize_ix = Instruction::new_with_bytes( @@ -58,20 +55,22 @@ fn test_transfer_fee_full_flow() { payer: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![initialize_ix], &[&payer, &mint_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![initialize_ix], + &[&payer, &mint_keypair], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Step 2: Create sender ATA and mint 300 tokens - create_token_extensions_account( - &mut svm, - &payer.pubkey(), - &mint_keypair.pubkey(), - &payer, - ).unwrap(); + create_token_extensions_account(&mut svm, &payer.pubkey(), &mint_keypair.pubkey(), &payer) + .unwrap(); svm.expire_blockhash(); mint_tokens_to_token_extensions_account( @@ -80,7 +79,8 @@ fn test_transfer_fee_full_flow() { &sender_ata, 300, &payer, - ).unwrap(); + ) + .unwrap(); svm.expire_blockhash(); // Step 3: Transfer 100 tokens (fee = min(1% * 100 = 1, max_fee = 1) = 1) @@ -95,11 +95,12 @@ fn test_transfer_fee_full_flow() { recipient_token_account: recipient_ata, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, associated_token_program: ata_program, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![transfer_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![transfer_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 4: Transfer 200 tokens (fee = min(1% * 200 = 2, max_fee = 1) = 1, capped by maximumFee) @@ -114,18 +115,17 @@ fn test_transfer_fee_full_flow() { recipient_token_account: recipient_ata, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, associated_token_program: ata_program, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![transfer_ix2], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![transfer_ix2], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 5: Harvest transfer fees from recipient token account to mint - let harvest_ix = Instruction::new_with_bytes( - program_id, - &transfer_fee::instruction::Harvest {}.data(), - { + let harvest_ix = + Instruction::new_with_bytes(program_id, &transfer_fee::instruction::Harvest {}.data(), { let mut metas = transfer_fee::accounts::HarvestAccountConstraints { mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, @@ -133,9 +133,9 @@ fn test_transfer_fee_full_flow() { .to_account_metas(None); metas.push(AccountMeta::new(recipient_ata, false)); metas - }, - ); - send_transaction_from_instructions(&mut svm, vec![harvest_ix], &[&payer], &payer.pubkey()).unwrap(); + }); + send_transaction_from_instructions(&mut svm, vec![harvest_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 6: Withdraw harvested fees from mint to sender's token account @@ -150,7 +150,8 @@ fn test_transfer_fee_full_flow() { } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![withdraw_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![withdraw_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 7: Update transfer fee to 0 @@ -168,5 +169,6 @@ fn test_transfer_fee_full_flow() { } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![update_fee_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![update_fee_ix], &[&payer], &payer.pubkey()) + .unwrap(); } diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/Cargo.toml b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/Cargo.toml index 81d3422a3..098b26a15 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/Cargo.toml +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/Cargo.toml @@ -9,19 +9,31 @@ crate-type = ["cdylib", "lib"] name = "transfer_hook" [features] -default = [] +# `no-entrypoint` is always on: anchor then exports its dispatch as +# `__anchor_dispatch` rather than claiming the `entrypoint` symbol, leaving +# src/entrypoint.rs free to claim it and map the transfer-hook interface's +# discriminators onto this program's handlers. +default = ["no-entrypoint"] cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" spl-discriminator = "0.4.1" spl-tlv-account-resolution = "0.9.0" spl-transfer-hook-interface = "0.9.0" diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/entrypoint.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/entrypoint.rs new file mode 100644 index 000000000..577e8f2b6 --- /dev/null +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/entrypoint.rs @@ -0,0 +1,50 @@ +//! Program entrypoint, hand-written so the SPL transfer-hook interface reaches +//! this program's handlers. +//! +//! Anchor v2 gives every instruction in an executable `#[program]` the eight-byte +//! `sha256("global:")` discriminator, and `#[discrim = N]` there is limited +//! to a single byte. The transfer-hook interface calls its instructions under +//! their own eight-byte values, which leaves no way to declare those handlers +//! directly. `#[program(interface, ...)]` accepts arbitrary discriminator bytes +//! but only generates a CPI client: no dispatch, and so no deployable program. +//! +//! So the crate builds with `no-entrypoint` (which makes anchor export its +//! dispatch as `__anchor_dispatch` instead of claiming the `entrypoint` symbol) +//! and this module claims `entrypoint` itself. All it does is swap an interface +//! discriminator for the matching handler's before delegating; the payload +//! behind it is identical either way. + +use anchor_lang::pinocchio; + +pinocchio::default_allocator!(); +pinocchio::default_panic_handler!(); + +/// Interface discriminator paired with the handler's own, in declaration order. +const DISCRIMINATOR_MAP: [([u8; 8], [u8; 8]); 2] = [ + // initialize_extra_account_meta_list + ([43, 34, 13, 49, 167, 88, 235, 235], [92, 197, 174, 197, 41, 124, 19, 3]), + // transfer_hook + ([105, 37, 101, 197, 75, 251, 102, 26], [220, 57, 220, 152, 126, 125, 97, 168]), +]; + +/// # Safety +/// +/// Called only by the SBF loader, with the register convention anchor's own +/// entrypoint documents: `r1` is the start of the serialized parameter region +/// and `r2` points at the instruction data, whose length sits in the eight +/// bytes below it. +#[cfg(target_os = "solana")] +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data_ptr: *const u8) -> u64 { + let len = *(ix_data_ptr.sub(8) as *const u64) as usize; + if len >= 8 { + let discriminator = core::slice::from_raw_parts_mut(ix_data_ptr as *mut u8, 8); + for (interface, handler) in DISCRIMINATOR_MAP { + if discriminator == interface { + discriminator.copy_from_slice(&handler); + break; + } + } + } + crate::__anchor_dispatch(input, ix_data_ptr) +} diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs index 34c854ce9..a19d9ffa5 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs @@ -1,23 +1,19 @@ use anchor_lang::prelude::*; -use anchor_spl::{ - associated_token::AssociatedToken, - token_2022::Token2022, - token_interface::Mint, -}; +use anchor_spl::{associated_token::AssociatedToken, token_2022::Token2022, token_interface::Mint}; use spl_tlv_account_resolution::state::ExtraAccountMetaList; use spl_transfer_hook_interface::instruction::ExecuteInstruction; use crate::{handle_extra_account_metas, handle_extra_account_metas_count, CounterAccount}; #[derive(Accounts)] -pub struct InitializeExtraAccountMetaListAccountConstraints<'info> { +pub struct InitializeExtraAccountMetaListAccountConstraints { #[account(mut)] - payer: Signer<'info>, + payer: Signer, /// CHECK: ExtraAccountMetaList Account, must use these seeds #[account( init, - seeds = [b"extra-account-metas", mint.key().as_ref()], + seeds = [b"extra-account-metas", mint.address().as_ref()], bump, // size_of returns Result with spl's ProgramError - unwrap is safe for known-good input space = ExtraAccountMetaList::size_of( @@ -25,25 +21,29 @@ pub struct InitializeExtraAccountMetaListAccountConstraints<'info> { ).unwrap(), payer = payer )] - pub extra_account_meta_list: UncheckedAccount<'info>, - pub mint: InterfaceAccount<'info, Mint>, - #[account(init, seeds = [b"counter", payer.key().as_ref()], bump, payer = payer, space = CounterAccount::DISCRIMINATOR.len() + CounterAccount::INIT_SPACE)] - pub counter_account: Account<'info, CounterAccount>, - pub token_program: Program<'info, Token2022>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub extra_account_meta_list: UncheckedAccount, + pub mint: InterfaceAccount, + #[account(init, seeds = [b"counter", payer.address().as_ref()], bump, payer = payer, space = CounterAccount::DISCRIMINATOR.len() + CounterAccount::INIT_SPACE)] + pub counter_account: BorshAccount, + pub token_program: Program, + pub associated_token_program: Program, + pub system_program: Program, } -pub fn handler(mut context: Context) -> Result<()> { +pub fn handler( + mut context: &mut Context, +) -> Result<()> { let extra_account_metas = handle_extra_account_metas()?; // initialize ExtraAccountMetaList account with extra accounts // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types - ExtraAccountMetaList::init::( - &mut context.accounts.extra_account_meta_list.try_borrow_mut_data()?, - &extra_account_metas, - ).map_err(|_| ProgramError::InvalidAccountData)?; + // `AccountView` is Copy, and a copy still points at the same backing + // buffer, so the borrow writes through to the real account. + let mut meta_list_view = *context.accounts.extra_account_meta_list.account(); + let mut meta_list_data = meta_list_view.try_borrow_mut()?; + ExtraAccountMetaList::init::(&mut meta_list_data, &extra_account_metas) + .map_err(|_| ProgramError::InvalidAccountData)?; context.accounts.counter_account.bump = context.bumps.counter_account; diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs index 62aafc317..33128c47e 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::token_interface::{Mint, TokenAccount}; use crate::{check_is_transferring, CounterAccount, TransferError}; @@ -8,22 +9,22 @@ use crate::{check_is_transferring, CounterAccount, TransferError}; // Remaining accounts are the extra accounts required from the ExtraAccountMetaList account // These accounts are provided via CPI to this program from the token2022 program #[derive(Accounts)] -pub struct TransferHookAccountConstraints<'info> { +pub struct TransferHookAccountConstraints { #[account(token::mint = mint, token::authority = owner)] - pub source_token: InterfaceAccount<'info, TokenAccount>, - pub mint: InterfaceAccount<'info, Mint>, + pub source_token: InterfaceAccount, + pub mint: InterfaceAccount, #[account(token::mint = mint)] - pub destination_token: InterfaceAccount<'info, TokenAccount>, + pub destination_token: InterfaceAccount, /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program - pub owner: UncheckedAccount<'info>, + pub owner: UncheckedAccount, /// CHECK: ExtraAccountMetaList Account, - #[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)] - pub extra_account_meta_list: UncheckedAccount<'info>, - #[account(seeds = [b"counter", owner.key().as_ref()], bump)] - pub counter_account: Account<'info, CounterAccount>, + #[account(seeds = [b"extra-account-metas", mint.address().as_ref()], bump)] + pub extra_account_meta_list: UncheckedAccount, + #[account(seeds = [b"counter", owner.address().as_ref()], bump)] + pub counter_account: BorshAccount, } -pub fn handler(context: Context, amount: u64) -> Result<()> { +pub fn handler(context: &mut Context, amount: u64) -> Result<()> { // Fail this instruction if it is not called from within a transfer hook check_is_transferring(&context)?; diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs index c83525603..1a89d0cd2 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs @@ -3,13 +3,12 @@ use std::cell::RefMut; use anchor_lang::prelude::*; use anchor_spl::token_2022::spl_token_2022::{ extension::{ - transfer_hook::TransferHookAccount, BaseStateWithExtensionsMut, - PodStateWithExtensionsMut, + transfer_hook::TransferHookAccount, BaseStateWithExtensions, PodStateWithExtensions, }, pod::PodAccount, }; -use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed}; use spl_discriminator::SplDiscriminate; +use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed}; use spl_transfer_hook_interface::instruction::{ ExecuteInstruction, InitializeExtraAccountMetaListInstruction, }; @@ -27,31 +26,43 @@ pub enum TransferError { IsNotCurrentlyTransferring, } +pub mod entrypoint; + +// v2's `#[program(interface, ...)]` declares an interface for other programs to +// CPI into and emits no entrypoint, and an executable `#[program]` only accepts +// one-byte custom discriminators, so the transfer-hook interface's eight-byte +// discriminators have no direct spelling. `entrypoint` bridges the gap: it maps +// each of them onto a handler before anchor's dispatch runs. #[program] pub mod transfer_hook { use super::*; - #[instruction(discriminator = InitializeExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE)] + // sha256("spl-transfer-hook-interface:initialize-extra-account-metas")[..8] pub fn initialize_extra_account_meta_list( - context: Context, + context: &mut Context, ) -> Result<()> { instructions::initialize_extra_account_meta_list::handler(context) } - #[instruction(discriminator = ExecuteInstruction::SPL_DISCRIMINATOR_SLICE)] - pub fn transfer_hook(context: Context, amount: u64) -> Result<()> { + // sha256("spl-transfer-hook-interface:execute")[..8] + pub fn transfer_hook( + context: &mut Context, + amount: u64, + ) -> Result<()> { instructions::transfer_hook::handler(context, amount) } } pub fn check_is_transferring(context: &Context) -> Result<()> { - let source_token_info = context.accounts.source_token.to_account_info(); - let mut account_data_ref: RefMut<&mut [u8]> = source_token_info.try_borrow_mut_data()?; + // Read-only: the account already holds a shared borrow of its buffer, and a + // second shared borrow is fine where `try_borrow_mut` would be rejected. + let account_data_ref = context.accounts.source_token.account().try_borrow()?; // .map_err() needed because spl-token-2022 uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types - let mut account = PodStateWithExtensionsMut::::unpack(*account_data_ref) + // while anchor-lang uses 3.x - structurally identical but different semver types + let account = PodStateWithExtensions::::unpack(&account_data_ref) .map_err(|_| ProgramError::InvalidAccountData)?; - let account_extension = account.get_extension_mut::() + let account_extension = account + .get_extension::() .map_err(|_| ProgramError::InvalidAccountData)?; if !bool::from(account_extension.transferring) { @@ -78,7 +89,8 @@ pub fn handle_extra_account_metas() -> Result> { ], false, // is_signer true, // is_writable - ).map_err(|_| ProgramError::InvalidArgument)?]) + ) + .map_err(|_| ProgramError::InvalidArgument)?]) } /// Returns the count of extra account metas (avoids the error conversion issue in #[account] attributes) @@ -86,7 +98,7 @@ pub fn handle_extra_account_metas_count() -> usize { 1 // one extra account: the counter PDA } -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct CounterAccount { pub counter: u64, diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/tests/test_transfer_hook.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/tests/test_transfer_hook.rs index 4b083e01c..c60b466ef 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/tests/test_transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/tests/test_transfer_hook.rs @@ -1,13 +1,10 @@ use { anchor_lang::{ - solana_program::{ - instruction::Instruction, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::{ @@ -17,17 +14,16 @@ use { }, transfer_hook::{build_hook_accounts, get_hook_accounts_address, HookAccount}, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn associated_token_program_id() -> Pubkey { +fn associated_token_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = transfer_hook::id(); let mut svm = LiteSVM::new(); @@ -46,7 +42,7 @@ fn test_transfer_hook_account_data_as_seed() { // PDAs let (counter_pda, _) = - Pubkey::find_program_address(&[b"counter", payer.pubkey().as_ref()], &program_id); + Address::find_program_address(&[b"counter", payer.pubkey().as_ref()], &program_id); // Step 1: Create mint with TransferHook extension let mint = create_token_extensions_mint( @@ -61,34 +57,19 @@ fn test_transfer_hook_account_data_as_seed() { .unwrap(); svm.expire_blockhash(); - let extra_account_meta_list = - get_hook_accounts_address(&mint, &program_id); + let extra_account_meta_list = get_hook_accounts_address(&mint, &program_id); // Step 2: Create token accounts and mint tokens let amount: u64 = 100 * 10u64.pow(decimals as u32); - let source_ata = create_token_extensions_account( - &mut svm, - &payer.pubkey(), - &mint, - &payer, - ).unwrap(); + let source_ata = + create_token_extensions_account(&mut svm, &payer.pubkey(), &mint, &payer).unwrap(); svm.expire_blockhash(); - let dest_ata = create_token_extensions_account( - &mut svm, - &recipient.pubkey(), - &mint, - &payer, - ).unwrap(); + let dest_ata = + create_token_extensions_account(&mut svm, &recipient.pubkey(), &mint, &payer).unwrap(); svm.expire_blockhash(); - mint_tokens_to_token_extensions_account( - &mut svm, - &mint, - &source_ata, - amount, - &payer, - ).unwrap(); + mint_tokens_to_token_extensions_account(&mut svm, &mint, &source_ata, amount, &payer).unwrap(); svm.expire_blockhash(); // Step 3: Initialize ExtraAccountMetaList (also creates counter PDA) @@ -102,11 +83,12 @@ fn test_transfer_hook_account_data_as_seed() { counter_account: counter_pda, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, associated_token_program: associated_token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 4: Transfer with hook @@ -129,7 +111,8 @@ fn test_transfer_hook_account_data_as_seed() { transfer_amount, decimals, &extra_accounts, - ).unwrap(); + ) + .unwrap(); svm.expire_blockhash(); // Step 5: Try calling transfer_hook directly (should fail - not transferring) @@ -146,7 +129,12 @@ fn test_transfer_hook_account_data_as_seed() { } .to_account_metas(None), ); - let result = send_transaction_from_instructions(&mut svm, vec![direct_hook_ix], &[&payer], &payer.pubkey()); + let result = send_transaction_from_instructions( + &mut svm, + vec![direct_hook_ix], + &[&payer], + &payer.pubkey(), + ); assert!( result.is_err(), "Calling transfer_hook directly should fail because token is not transferring" diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Cargo.toml b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Cargo.toml index 387b5b193..1a341a025 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Cargo.toml +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Cargo.toml @@ -9,12 +9,16 @@ crate-type = ["cdylib", "lib"] name = "abl_token" [features] -default = [] +# `no-entrypoint` is always on: anchor then exports its dispatch as +# `__anchor_dispatch` rather than claiming the `entrypoint` symbol, leaving +# src/entrypoint.rs free to claim it and route the transfer-hook interface's +# `Execute` discriminator to `tx_hook`. +default = ["no-entrypoint"] cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] @@ -22,11 +26,18 @@ custom-panic = [] [dependencies] # interface-instructions feature removed in Anchor 1.0 -anchor-lang = "1.1.2" -anchor-spl = { version = "1.1.2", features = [ - "token_2022_extensions", - "token_2022", -] } +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +# v2's anchor-spl has only `guardrails` (default) and `metadata`; the +# Token-2022 modules are unconditional now. +anchor-spl = "2.0.0-rc.1" spl-tlv-account-resolution = "0.11.1" spl-transfer-hook-interface = { version = "2.1.0" } diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/entrypoint.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/entrypoint.rs new file mode 100644 index 000000000..5d9e9fd95 --- /dev/null +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/entrypoint.rs @@ -0,0 +1,49 @@ +//! Program entrypoint, hand-written so the SPL transfer-hook interface reaches +//! `tx_hook`. +//! +//! Anchor v2 gives every instruction in an executable `#[program]` the eight-byte +//! `sha256("global:")` discriminator, and `#[discrim = N]` there is limited +//! to a single byte. The transfer-hook interface calls `Execute` under its own +//! eight-byte value, which leaves no way to declare that handler directly. +//! `#[program(interface, ...)]` accepts arbitrary discriminator bytes but only +//! generates a CPI client: no dispatch, and so no deployable program. +//! +//! So the crate builds with `no-entrypoint` (which makes anchor export its +//! dispatch as `__anchor_dispatch` instead of claiming the `entrypoint` symbol) +//! and this module claims `entrypoint` itself. All it does is swap the +//! interface's discriminator for `tx_hook`'s before delegating; the payload +//! behind it (a single `u64` amount) is identical either way. + +use anchor_lang::pinocchio; + +pinocchio::default_allocator!(); +pinocchio::default_panic_handler!(); + +/// `sha256("spl-transfer-hook-interface:execute")[..8]`, the discriminator +/// Token-2022 uses when it calls a mint's transfer hook. +const EXECUTE_DISCRIMINATOR: [u8; 8] = [105, 37, 101, 197, 75, 251, 102, 26]; + +/// `sha256("global:tx_hook")[..8]`, what anchor's dispatch matches on. +const TX_HOOK_DISCRIMINATOR: [u8; 8] = [55, 222, 121, 59, 26, 10, 108, 168]; + +/// # Safety +/// +/// Called only by the SBF loader, with the register convention anchor's own +/// entrypoint documents: `r1` is the start of the serialized parameter region +/// and `r2` points at the instruction data, whose length sits in the eight +/// bytes below it. +#[cfg(target_os = "solana")] +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data_ptr: *const u8) -> u64 { + let len = *(ix_data_ptr.sub(8) as *const u64) as usize; + if len >= EXECUTE_DISCRIMINATOR.len() { + let discriminator = core::slice::from_raw_parts_mut( + ix_data_ptr as *mut u8, + EXECUTE_DISCRIMINATOR.len(), + ); + if discriminator == EXECUTE_DISCRIMINATOR { + discriminator.copy_from_slice(&TX_HOOK_DISCRIMINATOR); + } + } + crate::__anchor_dispatch(input, ix_data_ptr) +} diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/attach_to_mint.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/attach_to_mint.rs index 750c21c57..6ed4a5761 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/attach_to_mint.rs +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/attach_to_mint.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::mint; use anchor_spl::{ token_2022::Token2022, token_interface::{transfer_hook_update, Mint, TransferHookUpdate}, @@ -10,47 +11,46 @@ use spl_transfer_hook_interface::instruction::ExecuteInstruction; use crate::{get_extra_account_metas, get_meta_list_size, META_LIST_ACCOUNT_SEED}; #[derive(Accounts)] -pub struct AttachToMintAccountConstraints<'info> { +pub struct AttachToMintAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account( mut, mint::token_program = token_program, )] - pub mint: Box>, + pub mint: Box>, #[account( init, space = get_meta_list_size()?, - seeds = [META_LIST_ACCOUNT_SEED, mint.key().as_ref()], + seeds = [META_LIST_ACCOUNT_SEED, mint.address().as_ref()], bump, payer = payer, )] /// CHECK: extra metas account - pub extra_metas_account: UncheckedAccount<'info>, + pub extra_metas_account: UncheckedAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, - pub token_program: Program<'info, Token2022>, + pub token_program: Program, } -impl AttachToMintAccountConstraints<'_> { +impl AttachToMintAccountConstraints { pub fn attach_to_mint(&mut self) -> Result<()> { let tx_hook_accs = TransferHookUpdate { - token_program_id: self.token_program.to_account_info(), - mint: self.mint.to_account_info(), - authority: self.payer.to_account_info(), + mint: self.mint.cpi_handle_mut(), + authority: self.payer.cpi_handle(), }; - let context = CpiContext::new(self.token_program.key(), tx_hook_accs); + let context = CpiContext::new(self.token_program.address(), tx_hook_accs); - transfer_hook_update(context, Some(crate::ID_CONST))?; + transfer_hook_update(context, Some(&crate::ID))?; // initialize the extra metas account - let extra_metas_account = &self.extra_metas_account; let metas = get_extra_account_metas()?; - let mut data = extra_metas_account.try_borrow_mut_data()?; + let mut extra_metas_view = *self.extra_metas_account.account(); + let mut data = extra_metas_view.try_borrow_mut()?; ExtraAccountMetaList::init::(&mut data, &metas) .map_err(|_| ProgramError::InvalidAccountData)?; diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/change_mode.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/change_mode.rs index 81fde1820..1b0a1ac0c 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/change_mode.rs +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/change_mode.rs @@ -1,7 +1,8 @@ -use anchor_lang::solana_program::program::invoke; -use anchor_lang::{prelude::*, solana_program::system_instruction::transfer}; +use anchor_lang::prelude::*; +use anchor_lang::system_program::{transfer, Transfer}; use anchor_spl::token_interface::spl_token_metadata_interface::state::TokenMetadata; use anchor_spl::{ + mint, token_2022::{ spl_token_2022::extension::{BaseStateWithExtensions, StateWithExtensions}, spl_token_2022::state::Mint, @@ -16,35 +17,34 @@ use anchor_spl::{ use crate::Mode; #[derive(Accounts)] -pub struct ChangeModeAccountConstraints<'info> { +pub struct ChangeModeAccountConstraints { #[account(mut)] - pub authority: Signer<'info>, + pub authority: Signer, #[account( mut, mint::token_program = token_program, )] - pub mint: InterfaceAccount<'info, MintAccount>, + pub mint: InterfaceAccount, - pub token_program: Program<'info, Token2022>, + pub token_program: Program, - pub system_program: Program<'info, System>, + pub system_program: Program, } -#[derive(AnchorSerialize, AnchorDeserialize)] +#[derive(IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub struct ChangeModeArgs { pub mode: Mode, pub threshold: u64, } -impl ChangeModeAccountConstraints<'_> { +impl ChangeModeAccountConstraints { pub fn change_mode(&mut self, args: ChangeModeArgs) -> Result<()> { let cpi_accounts = TokenMetadataUpdateField { - metadata: self.mint.to_account_info(), - update_authority: self.authority.to_account_info(), - program_id: self.token_program.to_account_info(), + metadata: self.mint.cpi_handle_mut(), + update_authority: self.authority.cpi_handle(), }; - let cpi_program = self.token_program.key(); + let cpi_program = self.token_program.address(); let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); token_metadata_update_field(cpi_ctx, Field::Key("AB".to_string()), args.mode.to_string())?; @@ -57,11 +57,10 @@ impl ChangeModeAccountConstraints<'_> { }; let cpi_accounts = TokenMetadataUpdateField { - metadata: self.mint.to_account_info(), - update_authority: self.authority.to_account_info(), - program_id: self.token_program.to_account_info(), + metadata: self.mint.cpi_handle_mut(), + update_authority: self.authority.cpi_handle(), }; - let cpi_program = self.token_program.key(); + let cpi_program = self.token_program.address(); let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); token_metadata_update_field( @@ -71,20 +70,20 @@ impl ChangeModeAccountConstraints<'_> { )?; } - let data = self.mint.to_account_info().data_len(); - let min_balance = Rent::get()?.minimum_balance(data); - if min_balance > self.mint.to_account_info().get_lamports() { - invoke( - &transfer( - &self.authority.key(), - &self.mint.to_account_info().key(), - min_balance - self.mint.to_account_info().get_lamports(), + // Writing the metadata grew the mint, so top it back up to rent exemption. + let data_len = self.mint.account().data_len(); + let min_balance = Rent::get()?.try_minimum_balance(data_len)?; + let current = self.mint.account().lamports(); + if min_balance > current { + transfer( + CpiContext::new( + self.system_program.address(), + Transfer { + from: self.authority.cpi_handle_mut(), + to: self.mint.cpi_handle_mut(), + }, ), - &[ - self.authority.to_account_info(), - self.mint.to_account_info(), - self.system_program.to_account_info(), - ], + min_balance - current, )?; } @@ -92,8 +91,7 @@ impl ChangeModeAccountConstraints<'_> { } fn has_threshold(&self) -> Result { - let mint_info = self.mint.to_account_info(); - let mint_data = mint_info.data.borrow(); + let mint_data = self.mint.account().try_borrow()?; let mint = StateWithExtensions::::unpack(&mint_data)?; let metadata = mint.get_variable_len_extension::(); Ok(metadata.is_ok() diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_config.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_config.rs index 67142441b..d9608870c 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_config.rs +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_config.rs @@ -2,9 +2,9 @@ use crate::{Config, CONFIG_SEED}; use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct InitConfigAccountConstraints<'info> { +pub struct InitConfigAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account( init, @@ -13,17 +13,17 @@ pub struct InitConfigAccountConstraints<'info> { seeds = [CONFIG_SEED], bump, )] - pub config: Box>, + pub config: Box>, - pub system_program: Program<'info, System>, + pub system_program: Program, } -impl InitConfigAccountConstraints<'_> { +impl InitConfigAccountConstraints { pub fn init_config(&mut self, config_bump: u8) -> Result<()> { - self.config.set_inner(Config { - authority: self.payer.key(), + **self.config = Config { + authority: *self.payer.address(), bump: config_bump, - }); + }; Ok(()) } diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_mint.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_mint.rs index cfbfb90cb..7c5b8faa2 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_mint.rs +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_mint.rs @@ -1,11 +1,19 @@ -use anchor_lang::{ - prelude::*, solana_program::program::invoke, solana_program::system_instruction::transfer, -}; +use anchor_lang::prelude::*; +use anchor_lang::system_program::{create_account, transfer, CreateAccount, Transfer}; use anchor_spl::{ - token_2022::Token2022, + token_2022::{ + initialize_mint2, + spl_token_2022::{extension::ExtensionType, pod::PodMint}, + InitializeMint2, Token2022, + }, + token_2022_extensions::{ + metadata_pointer::{metadata_pointer_initialize, MetadataPointerInitialize}, + permanent_delegate::{permanent_delegate_initialize, PermanentDelegateInitialize}, + transfer_hook::{transfer_hook_initialize, TransferHookInitialize}, + }, token_interface::{ spl_token_metadata_interface::state::Field, token_metadata_initialize, - token_metadata_update_field, Mint, TokenMetadataInitialize, TokenMetadataUpdateField, + token_metadata_update_field, TokenMetadataInitialize, TokenMetadataUpdateField, }, }; @@ -16,69 +24,136 @@ use crate::{get_extra_account_metas, get_meta_list_size, Mode, META_LIST_ACCOUNT #[derive(Accounts)] #[instruction(args: InitMintArgs)] -pub struct InitMintAccountConstraints<'info> { +pub struct InitMintAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, - #[account( - init, - payer = payer, - mint::token_program = token_program, - mint::decimals = args.decimals, - mint::authority = payer.key(), - mint::freeze_authority = args.freeze_authority, - extensions::permanent_delegate::delegate = args.permanent_delegate, - extensions::transfer_hook::authority = args.transfer_hook_authority, - extensions::transfer_hook::program_id = crate::id(), - extensions::metadata_pointer::authority = payer.key(), - extensions::metadata_pointer::metadata_address = mint.key(), - )] - pub mint: Box>, + /// CHECK: created and initialized by this instruction as a Token-2022 mint + /// carrying the PermanentDelegate, TransferHook and MetadataPointer + /// extensions. anchor-spl has no init constraints for those, so the mint is + /// built by hand in `init_mint` below. + #[account(mut)] + pub mint: Signer, #[account( init, space = get_meta_list_size()?, - seeds = [META_LIST_ACCOUNT_SEED, mint.key().as_ref()], + seeds = [META_LIST_ACCOUNT_SEED, mint.address().as_ref()], bump, payer = payer, )] /// CHECK: extra metas account - pub extra_metas_account: UncheckedAccount<'info>, + pub extra_metas_account: UncheckedAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, - pub token_program: Program<'info, Token2022>, + pub token_program: Program, } -impl InitMintAccountConstraints<'_> { +impl InitMintAccountConstraints { pub fn init_mint(&mut self, args: InitMintArgs) -> Result<()> { + let mint_address = *self.mint.address(); + let payer_address = *self.payer.address(); + + // Allocate the mint with room for all three extensions, initialize each + // extension, then initialize the mint data. Extension initialization has + // to happen before InitializeMint2. + let mint_size = ExtensionType::try_calculate_account_len::(&[ + ExtensionType::PermanentDelegate, + ExtensionType::TransferHook, + ExtensionType::MetadataPointer, + ])?; + let lamports = Rent::get()?.try_minimum_balance(mint_size)?; + + create_account( + CpiContext::new( + self.system_program.address(), + CreateAccount { + from: self.payer.cpi_handle_mut(), + to: self.mint.cpi_handle_mut(), + }, + ), + lamports, + mint_size as u64, + self.token_program.address(), + )?; + + permanent_delegate_initialize( + CpiContext::new( + self.token_program.address(), + PermanentDelegateInitialize { + mint: self.mint.cpi_handle_mut(), + }, + ), + &args.permanent_delegate, + )?; + + transfer_hook_initialize( + CpiContext::new( + self.token_program.address(), + TransferHookInitialize { + mint: self.mint.cpi_handle_mut(), + }, + ), + Some(&args.transfer_hook_authority), + Some(&crate::ID), + )?; + + // metadata is stored in the mint itself, so the pointer points at it + metadata_pointer_initialize( + CpiContext::new( + self.token_program.address(), + MetadataPointerInitialize { + mint: self.mint.cpi_handle_mut(), + }, + ), + Some(&payer_address), + Some(&mint_address), + )?; + + initialize_mint2( + CpiContext::new( + self.token_program.address(), + InitializeMint2 { + mint: self.mint.cpi_handle_mut(), + }, + ), + args.decimals, + &payer_address, + Some(&args.freeze_authority), + )?; + + // `payer` and `mint` each fill more than one CPI slot below. v2's typed + // handles enforce borrow exclusivity at compile time, so the read-only + // slots are built from copies of the `AccountView`, and each copy still + // points at the same underlying account. + let payer_view = *self.payer.account(); + let mint_view = *self.mint.account(); + let cpi_accounts = TokenMetadataInitialize { - program_id: self.token_program.to_account_info(), - mint: self.mint.to_account_info(), - metadata: self.mint.to_account_info(), // metadata account is the mint, since data is stored in mint - mint_authority: self.payer.to_account_info(), - update_authority: self.payer.to_account_info(), + // metadata account is the mint, since data is stored in mint + metadata: self.mint.cpi_handle_mut(), + update_authority: CpiHandle::readonly(&payer_view), + mint: CpiHandle::readonly(&mint_view), + mint_authority: CpiHandle::readonly(&payer_view), }; - let cpi_ctx = CpiContext::new(self.token_program.key(), cpi_accounts); + let cpi_ctx = CpiContext::new(self.token_program.address(), cpi_accounts); token_metadata_initialize(cpi_ctx, args.name, args.symbol, args.uri)?; let cpi_accounts = TokenMetadataUpdateField { - metadata: self.mint.to_account_info(), - update_authority: self.payer.to_account_info(), - program_id: self.token_program.to_account_info(), + metadata: self.mint.cpi_handle_mut(), + update_authority: CpiHandle::readonly(&payer_view), }; - - let cpi_ctx = CpiContext::new(self.token_program.key(), cpi_accounts); + let cpi_ctx = CpiContext::new(self.token_program.address(), cpi_accounts); token_metadata_update_field(cpi_ctx, Field::Key("AB".to_string()), args.mode.to_string())?; if args.mode == Mode::Mixed { let cpi_accounts = TokenMetadataUpdateField { - metadata: self.mint.to_account_info(), - update_authority: self.payer.to_account_info(), - program_id: self.token_program.to_account_info(), + metadata: self.mint.cpi_handle_mut(), + update_authority: CpiHandle::readonly(&payer_view), }; - let cpi_ctx = CpiContext::new(self.token_program.key(), cpi_accounts); + let cpi_ctx = CpiContext::new(self.token_program.address(), cpi_accounts); token_metadata_update_field( cpi_ctx, @@ -87,27 +162,27 @@ impl InitMintAccountConstraints<'_> { )?; } - let data = self.mint.to_account_info().data_len(); - let min_balance = Rent::get()?.minimum_balance(data); - if min_balance > self.mint.to_account_info().get_lamports() { - invoke( - &transfer( - &self.payer.key(), - &self.mint.to_account_info().key(), - min_balance - self.mint.to_account_info().get_lamports(), + // Writing the metadata grew the mint, so top it back up to rent exemption. + let data_len = self.mint.account().data_len(); + let min_balance = Rent::get()?.try_minimum_balance(data_len)?; + let current = self.mint.account().lamports(); + if min_balance > current { + transfer( + CpiContext::new( + self.system_program.address(), + Transfer { + from: self.payer.cpi_handle_mut(), + to: self.mint.cpi_handle_mut(), + }, ), - &[ - self.payer.to_account_info(), - self.mint.to_account_info(), - self.system_program.to_account_info(), - ], + min_balance - current, )?; } // initialize the extra metas account - let extra_metas_account = &self.extra_metas_account; let metas = get_extra_account_metas()?; - let mut data = extra_metas_account.try_borrow_mut_data()?; + let mut extra_metas_view = *self.extra_metas_account.account(); + let mut data = extra_metas_view.try_borrow_mut()?; ExtraAccountMetaList::init::(&mut data, &metas) .map_err(|_| ProgramError::InvalidAccountData)?; @@ -115,13 +190,13 @@ impl InitMintAccountConstraints<'_> { } } -#[derive(AnchorSerialize, AnchorDeserialize)] +#[derive(IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub struct InitMintArgs { pub decimals: u8, - pub mint_authority: Pubkey, - pub freeze_authority: Pubkey, - pub permanent_delegate: Pubkey, - pub transfer_hook_authority: Pubkey, + pub mint_authority: Address, + pub freeze_authority: Address, + pub permanent_delegate: Address, + pub transfer_hook_authority: Address, pub mode: Mode, pub threshold: u64, pub name: String, diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_wallet.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_wallet.rs index f1eb1b95a..71a77a382 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_wallet.rs +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_wallet.rs @@ -3,42 +3,39 @@ use anchor_lang::prelude::*; use crate::{ABWallet, Config, AB_WALLET_SEED, CONFIG_SEED}; #[derive(Accounts)] -pub struct InitWalletAccountConstraints<'info> { - #[account(mut)] - pub authority: Signer<'info>, +pub struct InitWalletAccountConstraints { + #[account(mut, address = config.authority)] + pub authority: Signer, - #[account( - seeds = [CONFIG_SEED], - bump = config.bump, - has_one = authority, - )] - pub config: Box>, + #[account(seeds = [CONFIG_SEED], + bump = config.bump)] + pub config: Box>, - pub wallet: SystemAccount<'info>, + pub wallet: SystemAccount, #[account( init, payer = authority, space = ABWallet::DISCRIMINATOR.len() + ABWallet::INIT_SPACE, - seeds = [AB_WALLET_SEED, wallet.key().as_ref()], + seeds = [AB_WALLET_SEED, wallet.address().as_ref()], bump, )] - pub ab_wallet: Account<'info, ABWallet>, + pub ab_wallet: BorshAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } -impl InitWalletAccountConstraints<'_> { +impl InitWalletAccountConstraints { pub fn init_wallet(&mut self, args: InitWalletArgs, bump: u8) -> Result<()> { let ab_wallet = &mut self.ab_wallet; - ab_wallet.wallet = self.wallet.key(); + ab_wallet.wallet = *self.wallet.address(); ab_wallet.allowed = args.allowed; ab_wallet.bump = bump; Ok(()) } } -#[derive(AnchorSerialize, AnchorDeserialize)] +#[derive(IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub struct InitWalletArgs { pub allowed: bool, } diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/remove_wallet.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/remove_wallet.rs index a505f085e..d8630b19e 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/remove_wallet.rs +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/remove_wallet.rs @@ -3,27 +3,24 @@ use anchor_lang::prelude::*; use crate::{ABWallet, Config}; #[derive(Accounts)] -pub struct RemoveWalletAccountConstraints<'info> { - #[account(mut)] - pub authority: Signer<'info>, +pub struct RemoveWalletAccountConstraints { + #[account(mut, address = config.authority)] + pub authority: Signer, - #[account( - seeds = [b"config"], - bump = config.bump, - has_one = authority, - )] - pub config: Box>, + #[account(seeds = [b"config"], + bump = config.bump)] + pub config: Box>, #[account( mut, close = authority, )] - pub ab_wallet: Account<'info, ABWallet>, + pub ab_wallet: BorshAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } -impl RemoveWalletAccountConstraints<'_> { +impl RemoveWalletAccountConstraints { pub fn remove_wallet(&mut self) -> Result<()> { Ok(()) } diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/tx_hook.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/tx_hook.rs index 0c3f6291e..29739da0e 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/tx_hook.rs +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/tx_hook.rs @@ -12,25 +12,24 @@ use anchor_spl::{ use crate::{ABListError, ABWallet, Mode}; #[derive(Accounts)] -pub struct TxHookAccountConstraints<'info> { +pub struct TxHookAccountConstraints { /// CHECK: - pub source_token_account: UncheckedAccount<'info>, + pub source_token_account: UncheckedAccount, /// CHECK: - pub mint: UncheckedAccount<'info>, + pub mint: UncheckedAccount, /// CHECK: - pub destination_token_account: UncheckedAccount<'info>, + pub destination_token_account: UncheckedAccount, /// CHECK: - pub owner_delegate: UncheckedAccount<'info>, + pub owner_delegate: UncheckedAccount, /// CHECK: - pub meta_list: UncheckedAccount<'info>, + pub meta_list: UncheckedAccount, /// CHECK: - pub ab_wallet: UncheckedAccount<'info>, + pub ab_wallet: UncheckedAccount, } -impl TxHookAccountConstraints<'_> { +impl TxHookAccountConstraints { pub fn tx_hook(&self, amount: u64) -> Result<()> { - let mint_info = self.mint.to_account_info(); - let mint_data = mint_info.data.borrow(); + let mint_data = self.mint.account().try_borrow()?; let mint = StateWithExtensions::::unpack(&mint_data)?; let metadata = mint.get_variable_len_extension::()?; @@ -55,12 +54,23 @@ impl TxHookAccountConstraints<'_> { } fn decode_wallet_mode(&self) -> Result { - if self.ab_wallet.data_is_empty() { + let wallet_data = self.ab_wallet.account().try_borrow()?; + if wallet_data.is_empty() { return Ok(DecodedWalletMode::None); } - let wallet_data = &mut self.ab_wallet.data.borrow(); - let wallet = ABWallet::try_deserialize(&mut &wallet_data[..])?; + // v2 has no `Account::try_from`, so the discriminator is checked and + // the payload read by hand. + let disc_len = ::DISCRIMINATOR.len(); + require!( + wallet_data.len() > disc_len + && &wallet_data[..disc_len] + == ::DISCRIMINATOR, + ABListError::InvalidMetadata + ); + let mut payload = &wallet_data[disc_len..]; + let wallet = >::get(&mut payload) + .map_err(|_| ABListError::InvalidMetadata)?; if wallet.allowed { Ok(DecodedWalletMode::Allow) diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/lib.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/lib.rs index 517ef28d2..fbdc1893e 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/lib.rs @@ -15,38 +15,53 @@ pub use utils::*; declare_id!("3ku1ZEGvBEEfhaYsAzBZuecTPEa58ZRhoVqHVGpGxVGi"); +pub mod entrypoint; + +// v2's `#[program(interface, ...)]` declares an interface for other programs to +// CPI into and emits no entrypoint, and an executable `#[program]` only accepts +// one-byte custom discriminators, so the transfer-hook interface's eight-byte +// `Execute` discriminator has no direct spelling. `entrypoint` bridges the gap: +// it routes `Execute` to `tx_hook` and hands everything else to anchor. #[program] pub mod abl_token { use super::*; - pub fn init_mint(context: Context, args: InitMintArgs) -> Result<()> { + pub fn init_mint( + context: &mut Context, + args: InitMintArgs, + ) -> Result<()> { context.accounts.init_mint(args) } - pub fn init_config(context: Context) -> Result<()> { + pub fn init_config(context: &mut Context) -> Result<()> { context.accounts.init_config(context.bumps.config) } - pub fn attach_to_mint(context: Context) -> Result<()> { + pub fn attach_to_mint(context: &mut Context) -> Result<()> { context.accounts.attach_to_mint() } - #[instruction(discriminator = ExecuteInstruction::SPL_DISCRIMINATOR_SLICE)] - pub fn tx_hook(context: Context, amount: u64) -> Result<()> { + pub fn tx_hook(context: &mut Context, amount: u64) -> Result<()> { context.accounts.tx_hook(amount) } - pub fn init_wallet(context: Context, args: InitWalletArgs) -> Result<()> { + pub fn init_wallet( + context: &mut Context, + args: InitWalletArgs, + ) -> Result<()> { let bump = context.bumps.ab_wallet; context.accounts.init_wallet(args, bump) } - pub fn remove_wallet(context: Context) -> Result<()> { + pub fn remove_wallet(context: &mut Context) -> Result<()> { context.accounts.remove_wallet() } - pub fn change_mode(context: Context, args: ChangeModeArgs) -> Result<()> { + pub fn change_mode( + context: &mut Context, + args: ChangeModeArgs, + ) -> Result<()> { context.accounts.change_mode(args) } } diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/state.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/state.rs index bf2df39df..e37f7ca34 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/state.rs +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/state.rs @@ -5,23 +5,23 @@ use std::{ use anchor_lang::prelude::*; -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct ABWallet { - pub wallet: Pubkey, + pub wallet: Address, pub allowed: bool, /// Canonical bump for this PDA. pub bump: u8, } -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Config { - pub authority: Pubkey, + pub authority: Address, pub bump: u8, } -#[derive(AnchorSerialize, AnchorDeserialize, PartialEq)] +#[derive(PartialEq, IdlType, wincode::SchemaRead, wincode::SchemaWrite)] pub enum Mode { Allow, Block, diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/utils.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/utils.rs index 7ffc952ad..cd51f2db6 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/utils.rs +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/utils.rs @@ -26,6 +26,7 @@ pub fn get_extra_account_metas() -> Result> { ], false, false, - ).map_err(|_| ProgramError::InvalidArgument)?, // [2] destination token account + ) + .map_err(|_| ProgramError::InvalidArgument)?, // [2] destination token account ]) } diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/tests/test_abl_token.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/tests/test_abl_token.rs index ccc5712cd..bc3b7e4e2 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/tests/test_abl_token.rs +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/tests/test_abl_token.rs @@ -1,22 +1,18 @@ use { anchor_lang::{ - solana_program::{ - instruction::Instruction, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::TOKEN_EXTENSIONS_PROGRAM_ID, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = abl_token::id(); let mut svm = LiteSVM::new(); @@ -33,9 +29,8 @@ fn test_init_config_and_init_mint() { let mint_keypair = Keypair::new(); // Derive PDAs - let (config_pda, _) = - Pubkey::find_program_address(&[b"config"], &program_id); - let (extra_account_meta_list, _) = Pubkey::find_program_address( + let (config_pda, _) = Address::find_program_address(&[b"config"], &program_id); + let (extra_account_meta_list, _) = Address::find_program_address( &[b"extra-account-metas", mint_keypair.pubkey().as_ref()], &program_id, ); @@ -47,11 +42,12 @@ fn test_init_config_and_init_mint() { abl_token::accounts::InitConfigAccountConstraints { payer: payer.pubkey(), config: config_pda, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![init_config_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![init_config_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 2: Initialize mint with transfer hook and metadata @@ -77,10 +73,16 @@ fn test_init_config_and_init_mint() { payer: payer.pubkey(), mint: mint_keypair.pubkey(), extra_metas_account: extra_account_meta_list, - system_program: system_program::id(), + system_program: system_program::ID, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![init_mint_ix], &[&payer, &mint_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![init_mint_ix], + &[&payer, &mint_keypair], + &payer.pubkey(), + ) + .unwrap(); } diff --git a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/Cargo.toml b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/Cargo.toml index 81d3422a3..098b26a15 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/Cargo.toml +++ b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/Cargo.toml @@ -9,19 +9,31 @@ crate-type = ["cdylib", "lib"] name = "transfer_hook" [features] -default = [] +# `no-entrypoint` is always on: anchor then exports its dispatch as +# `__anchor_dispatch` rather than claiming the `entrypoint` symbol, leaving +# src/entrypoint.rs free to claim it and map the transfer-hook interface's +# discriminators onto this program's handlers. +default = ["no-entrypoint"] cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" spl-discriminator = "0.4.1" spl-tlv-account-resolution = "0.9.0" spl-transfer-hook-interface = "0.9.0" diff --git a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/entrypoint.rs b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/entrypoint.rs new file mode 100644 index 000000000..577e8f2b6 --- /dev/null +++ b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/entrypoint.rs @@ -0,0 +1,50 @@ +//! Program entrypoint, hand-written so the SPL transfer-hook interface reaches +//! this program's handlers. +//! +//! Anchor v2 gives every instruction in an executable `#[program]` the eight-byte +//! `sha256("global:")` discriminator, and `#[discrim = N]` there is limited +//! to a single byte. The transfer-hook interface calls its instructions under +//! their own eight-byte values, which leaves no way to declare those handlers +//! directly. `#[program(interface, ...)]` accepts arbitrary discriminator bytes +//! but only generates a CPI client: no dispatch, and so no deployable program. +//! +//! So the crate builds with `no-entrypoint` (which makes anchor export its +//! dispatch as `__anchor_dispatch` instead of claiming the `entrypoint` symbol) +//! and this module claims `entrypoint` itself. All it does is swap an interface +//! discriminator for the matching handler's before delegating; the payload +//! behind it is identical either way. + +use anchor_lang::pinocchio; + +pinocchio::default_allocator!(); +pinocchio::default_panic_handler!(); + +/// Interface discriminator paired with the handler's own, in declaration order. +const DISCRIMINATOR_MAP: [([u8; 8], [u8; 8]); 2] = [ + // initialize_extra_account_meta_list + ([43, 34, 13, 49, 167, 88, 235, 235], [92, 197, 174, 197, 41, 124, 19, 3]), + // transfer_hook + ([105, 37, 101, 197, 75, 251, 102, 26], [220, 57, 220, 152, 126, 125, 97, 168]), +]; + +/// # Safety +/// +/// Called only by the SBF loader, with the register convention anchor's own +/// entrypoint documents: `r1` is the start of the serialized parameter region +/// and `r2` points at the instruction data, whose length sits in the eight +/// bytes below it. +#[cfg(target_os = "solana")] +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data_ptr: *const u8) -> u64 { + let len = *(ix_data_ptr.sub(8) as *const u64) as usize; + if len >= 8 { + let discriminator = core::slice::from_raw_parts_mut(ix_data_ptr as *mut u8, 8); + for (interface, handler) in DISCRIMINATOR_MAP { + if discriminator == interface { + discriminator.copy_from_slice(&handler); + break; + } + } + } + crate::__anchor_dispatch(input, ix_data_ptr) +} diff --git a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs index 7aef4af81..236ee3ccd 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs +++ b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs @@ -1,23 +1,19 @@ use anchor_lang::prelude::*; -use anchor_spl::{ - associated_token::AssociatedToken, - token_2022::Token2022, - token_interface::Mint, -}; +use anchor_spl::{associated_token::AssociatedToken, token_2022::Token2022, token_interface::Mint}; use spl_tlv_account_resolution::state::ExtraAccountMetaList; use spl_transfer_hook_interface::instruction::ExecuteInstruction; use crate::{handle_extra_account_metas, handle_extra_account_metas_count, CounterAccount}; #[derive(Accounts)] -pub struct InitializeExtraAccountMetaListAccountConstraints<'info> { +pub struct InitializeExtraAccountMetaListAccountConstraints { #[account(mut)] - payer: Signer<'info>, + payer: Signer, /// CHECK: ExtraAccountMetaList Account, must use these seeds #[account( init, - seeds = [b"extra-account-metas", mint.key().as_ref()], + seeds = [b"extra-account-metas", mint.address().as_ref()], bump, // size_of returns Result with spl's ProgramError - unwrap is safe for known-good input space = ExtraAccountMetaList::size_of( @@ -25,25 +21,29 @@ pub struct InitializeExtraAccountMetaListAccountConstraints<'info> { ).unwrap(), payer = payer )] - pub extra_account_meta_list: AccountInfo<'info>, - pub mint: InterfaceAccount<'info, Mint>, + pub extra_account_meta_list: UncheckedAccount, + pub mint: InterfaceAccount, #[account(init, seeds = [b"counter"], bump, payer = payer, space = CounterAccount::DISCRIMINATOR.len() + CounterAccount::INIT_SPACE)] - pub counter_account: Account<'info, CounterAccount>, - pub token_program: Program<'info, Token2022>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub counter_account: BorshAccount, + pub token_program: Program, + pub associated_token_program: Program, + pub system_program: Program, } -pub fn handler(mut context: Context) -> Result<()> { +pub fn handler( + mut context: &mut Context, +) -> Result<()> { let extra_account_metas = handle_extra_account_metas()?; // initialize ExtraAccountMetaList account with extra accounts // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types - ExtraAccountMetaList::init::( - &mut context.accounts.extra_account_meta_list.try_borrow_mut_data()?, - &extra_account_metas, - ).map_err(|_| ProgramError::InvalidAccountData)?; + // `AccountView` is Copy, and a copy still points at the same backing + // buffer, so the borrow writes through to the real account. + let mut meta_list_view = *context.accounts.extra_account_meta_list.account(); + let mut meta_list_data = meta_list_view.try_borrow_mut()?; + ExtraAccountMetaList::init::(&mut meta_list_data, &extra_account_metas) + .map_err(|_| ProgramError::InvalidAccountData)?; context.accounts.counter_account.bump = context.bumps.counter_account; diff --git a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs index 31ee75fc4..8f173c59e 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::token_interface::{Mint, TokenAccount}; use crate::{check_is_transferring, CounterAccount, TransferError}; @@ -8,22 +9,22 @@ use crate::{check_is_transferring, CounterAccount, TransferError}; // Remaining accounts are the extra accounts required from the ExtraAccountMetaList account // These accounts are provided via CPI to this program from the token2022 program #[derive(Accounts)] -pub struct TransferHookAccountConstraints<'info> { +pub struct TransferHookAccountConstraints { #[account(token::mint = mint, token::authority = owner)] - pub source_token: InterfaceAccount<'info, TokenAccount>, - pub mint: InterfaceAccount<'info, Mint>, + pub source_token: InterfaceAccount, + pub mint: InterfaceAccount, #[account(token::mint = mint)] - pub destination_token: InterfaceAccount<'info, TokenAccount>, + pub destination_token: InterfaceAccount, /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program - pub owner: UncheckedAccount<'info>, + pub owner: UncheckedAccount, /// CHECK: ExtraAccountMetaList Account, - #[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)] - pub extra_account_meta_list: UncheckedAccount<'info>, + #[account(seeds = [b"extra-account-metas", mint.address().as_ref()], bump)] + pub extra_account_meta_list: UncheckedAccount, #[account(seeds = [b"counter"], bump)] - pub counter_account: Account<'info, CounterAccount>, + pub counter_account: BorshAccount, } -pub fn handler(context: Context, amount: u64) -> Result<()> { +pub fn handler(context: &mut Context, amount: u64) -> Result<()> { // Fail this instruction if it is not called from within a transfer hook check_is_transferring(&context)?; diff --git a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/lib.rs b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/lib.rs index 7fabbb4b0..4aabf0894 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/lib.rs @@ -3,13 +3,12 @@ use std::cell::RefMut; use anchor_lang::prelude::*; use anchor_spl::token_2022::spl_token_2022::{ extension::{ - transfer_hook::TransferHookAccount, BaseStateWithExtensionsMut, - PodStateWithExtensionsMut, + transfer_hook::TransferHookAccount, BaseStateWithExtensions, PodStateWithExtensions, }, pod::PodAccount, }; -use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed}; use spl_discriminator::SplDiscriminate; +use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed}; use spl_transfer_hook_interface::instruction::{ ExecuteInstruction, InitializeExtraAccountMetaListInstruction, }; @@ -27,28 +26,44 @@ pub enum TransferError { IsNotCurrentlyTransferring, } +pub mod entrypoint; + +// v2's `#[program(interface, ...)]` declares an interface for other programs to +// CPI into and emits no entrypoint, and an executable `#[program]` only accepts +// one-byte custom discriminators, so the transfer-hook interface's eight-byte +// discriminators have no direct spelling. `entrypoint` bridges the gap: it maps +// each of them onto a handler before anchor's dispatch runs. #[program] pub mod transfer_hook { use super::*; - #[instruction(discriminator = InitializeExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE)] + // sha256("spl-transfer-hook-interface:initialize-extra-account-metas")[..8] pub fn initialize_extra_account_meta_list( - context: Context, + context: &mut Context, ) -> Result<()> { instructions::initialize_extra_account_meta_list::handler(context) } - #[instruction(discriminator = ExecuteInstruction::SPL_DISCRIMINATOR_SLICE)] - pub fn transfer_hook(context: Context, amount: u64) -> Result<()> { + // sha256("spl-transfer-hook-interface:execute")[..8] + pub fn transfer_hook( + context: &mut Context, + amount: u64, + ) -> Result<()> { instructions::transfer_hook::handler(context, amount) } } pub fn check_is_transferring(context: &Context) -> Result<()> { - let source_token_info = context.accounts.source_token.to_account_info(); - let mut account_data_ref: RefMut<&mut [u8]> = source_token_info.try_borrow_mut_data()?; - let mut account = PodStateWithExtensionsMut::::unpack(*account_data_ref)?; - let account_extension = account.get_extension_mut::()?; + // Read-only: the account already holds a shared borrow of its buffer, and a + // second shared borrow is fine where `try_borrow_mut` would be rejected. + let account_data_ref = context.accounts.source_token.account().try_borrow()?; + // .map_err() needed because spl-token-2022 uses solana-program-error 2.x + // while anchor-lang uses 3.x - structurally identical but different semver types + let account = PodStateWithExtensions::::unpack(&account_data_ref) + .map_err(|_| ProgramError::InvalidAccountData)?; + let account_extension = account + .get_extension::() + .map_err(|_| ProgramError::InvalidAccountData)?; if !bool::from(account_extension.transferring) { return err!(TransferError::IsNotCurrentlyTransferring); @@ -67,7 +82,8 @@ pub fn handle_extra_account_metas() -> Result> { }], false, // is_signer true, // is_writable - ).map_err(|_| ProgramError::InvalidArgument)?]) + ) + .map_err(|_| ProgramError::InvalidArgument)?]) } /// Returns the count of extra account metas (avoids the error conversion issue in #[account] attributes) @@ -75,7 +91,7 @@ pub fn handle_extra_account_metas_count() -> usize { 1 // one extra account: the counter PDA } -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct CounterAccount { pub counter: u64, diff --git a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/tests/test_transfer_hook_counter.rs b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/tests/test_transfer_hook_counter.rs index ae927cd15..a7fa1c4d7 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/tests/test_transfer_hook_counter.rs +++ b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/tests/test_transfer_hook_counter.rs @@ -1,13 +1,10 @@ use { anchor_lang::{ - solana_program::{ - instruction::Instruction, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::{ @@ -17,17 +14,16 @@ use { }, transfer_hook::{build_hook_accounts, get_hook_accounts_address, HookAccount}, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn associated_token_program_id() -> Pubkey { +fn associated_token_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = transfer_hook::id(); let mut svm = LiteSVM::new(); @@ -44,7 +40,7 @@ fn test_transfer_hook_counter() { let decimals: u8 = 9; // PDAs - let (counter_pda, _) = Pubkey::find_program_address(&[b"counter"], &program_id); + let (counter_pda, _) = Address::find_program_address(&[b"counter"], &program_id); // Step 1: Create mint with TransferHook extension via kite let mint = create_token_extensions_mint( @@ -59,36 +55,21 @@ fn test_transfer_hook_counter() { .unwrap(); svm.expire_blockhash(); - let extra_account_meta_list = - get_hook_accounts_address(&mint, &program_id); + let extra_account_meta_list = get_hook_accounts_address(&mint, &program_id); // Step 2: Create token accounts and mint tokens let recipient = Keypair::new(); let amount: u64 = 100 * 10u64.pow(decimals as u32); - let source_ata = create_token_extensions_account( - &mut svm, - &payer.pubkey(), - &mint, - &payer, - ).unwrap(); + let source_ata = + create_token_extensions_account(&mut svm, &payer.pubkey(), &mint, &payer).unwrap(); svm.expire_blockhash(); - let dest_ata = create_token_extensions_account( - &mut svm, - &recipient.pubkey(), - &mint, - &payer, - ).unwrap(); + let dest_ata = + create_token_extensions_account(&mut svm, &recipient.pubkey(), &mint, &payer).unwrap(); svm.expire_blockhash(); - mint_tokens_to_token_extensions_account( - &mut svm, - &mint, - &source_ata, - amount, - &payer, - ).unwrap(); + mint_tokens_to_token_extensions_account(&mut svm, &mint, &source_ata, amount, &payer).unwrap(); svm.expire_blockhash(); // Step 3: Initialize ExtraAccountMetaList (also creates counter PDA) @@ -102,11 +83,12 @@ fn test_transfer_hook_counter() { counter_account: counter_pda, token_program: TOKEN_EXTENSIONS_PROGRAM_ID, associated_token_program: associated_token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 4: Transfer with hook (this triggers the counter increment) @@ -129,7 +111,8 @@ fn test_transfer_hook_counter() { transfer_amount, decimals, &extra_accounts, - ).unwrap(); + ) + .unwrap(); svm.expire_blockhash(); // Step 5: Try calling transfer_hook directly (should fail - not transferring) @@ -146,7 +129,12 @@ fn test_transfer_hook_counter() { } .to_account_metas(None), ); - let result = send_transaction_from_instructions(&mut svm, vec![direct_hook_ix], &[&payer], &payer.pubkey()); + let result = send_transaction_from_instructions( + &mut svm, + vec![direct_hook_ix], + &[&payer], + &payer.pubkey(), + ); assert!( result.is_err(), "Calling transfer_hook directly should fail because token is not transferring" diff --git a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/Cargo.toml b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/Cargo.toml index 81d3422a3..098b26a15 100644 --- a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/Cargo.toml +++ b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/Cargo.toml @@ -9,19 +9,31 @@ crate-type = ["cdylib", "lib"] name = "transfer_hook" [features] -default = [] +# `no-entrypoint` is always on: anchor then exports its dispatch as +# `__anchor_dispatch` rather than claiming the `entrypoint` symbol, leaving +# src/entrypoint.rs free to claim it and map the transfer-hook interface's +# discriminators onto this program's handlers. +default = ["no-entrypoint"] cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" spl-discriminator = "0.4.1" spl-tlv-account-resolution = "0.9.0" spl-transfer-hook-interface = "0.9.0" diff --git a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/entrypoint.rs b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/entrypoint.rs new file mode 100644 index 000000000..577e8f2b6 --- /dev/null +++ b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/entrypoint.rs @@ -0,0 +1,50 @@ +//! Program entrypoint, hand-written so the SPL transfer-hook interface reaches +//! this program's handlers. +//! +//! Anchor v2 gives every instruction in an executable `#[program]` the eight-byte +//! `sha256("global:")` discriminator, and `#[discrim = N]` there is limited +//! to a single byte. The transfer-hook interface calls its instructions under +//! their own eight-byte values, which leaves no way to declare those handlers +//! directly. `#[program(interface, ...)]` accepts arbitrary discriminator bytes +//! but only generates a CPI client: no dispatch, and so no deployable program. +//! +//! So the crate builds with `no-entrypoint` (which makes anchor export its +//! dispatch as `__anchor_dispatch` instead of claiming the `entrypoint` symbol) +//! and this module claims `entrypoint` itself. All it does is swap an interface +//! discriminator for the matching handler's before delegating; the payload +//! behind it is identical either way. + +use anchor_lang::pinocchio; + +pinocchio::default_allocator!(); +pinocchio::default_panic_handler!(); + +/// Interface discriminator paired with the handler's own, in declaration order. +const DISCRIMINATOR_MAP: [([u8; 8], [u8; 8]); 2] = [ + // initialize_extra_account_meta_list + ([43, 34, 13, 49, 167, 88, 235, 235], [92, 197, 174, 197, 41, 124, 19, 3]), + // transfer_hook + ([105, 37, 101, 197, 75, 251, 102, 26], [220, 57, 220, 152, 126, 125, 97, 168]), +]; + +/// # Safety +/// +/// Called only by the SBF loader, with the register convention anchor's own +/// entrypoint documents: `r1` is the start of the serialized parameter region +/// and `r2` points at the instruction data, whose length sits in the eight +/// bytes below it. +#[cfg(target_os = "solana")] +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data_ptr: *const u8) -> u64 { + let len = *(ix_data_ptr.sub(8) as *const u64) as usize; + if len >= 8 { + let discriminator = core::slice::from_raw_parts_mut(ix_data_ptr as *mut u8, 8); + for (interface, handler) in DISCRIMINATOR_MAP { + if discriminator == interface { + discriminator.copy_from_slice(&handler); + break; + } + } + } + crate::__anchor_dispatch(input, ix_data_ptr) +} diff --git a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize.rs b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize.rs index 7cc60c7bf..51f780fac 100644 --- a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize.rs +++ b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize.rs @@ -1,62 +1,104 @@ use anchor_lang::prelude::*; -use anchor_spl::token_interface::{ - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_2022::{ - extension::{ - transfer_hook::TransferHook as TransferHookExtension, BaseStateWithExtensions, - StateWithExtensions, +use anchor_lang::system_program::{create_account, CreateAccount}; +use anchor_spl::{ + token_2022::{ + initialize_mint2, + spl_token_2022::{extension::ExtensionType, pod::PodMint}, + InitializeMint2, + }, + token_2022_extensions::transfer_hook::{transfer_hook_initialize, TransferHookInitialize}, + token_interface::{ + spl_pod::optional_keys::OptionalNonZeroPubkey, + spl_token_2022::{ + extension::{ + transfer_hook::TransferHook as TransferHookExtension, BaseStateWithExtensions, + StateWithExtensions, + }, + state::Mint as MintState, }, - state::Mint as MintState, + Token2022, }, - Mint, Token2022, }; #[derive(Accounts)] -#[instruction(_decimals: u8)] -pub struct InitializeAccountConstraints<'info> { +pub struct InitializeAccountConstraints { + #[account(mut)] + pub payer: Signer, + #[account(mut)] - pub payer: Signer<'info>, + pub mint_account: Signer, - #[account( - init, - payer = payer, - mint::decimals = _decimals, - mint::authority = payer, - extensions::transfer_hook::authority = payer, - extensions::transfer_hook::program_id = crate::ID - )] - pub mint_account: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub system_program: Program<'info, System>, + pub token_program: Program, + pub system_program: Program, } // create a mint account that specifies this program as the transfer hook program -pub fn handler(mut context: Context, _decimals: u8) -> Result<()> { - handle_check_mint_data(&mut context.accounts)?; +// +// There is currently not an anchor constraint to automatically initialize the +// TransferHook extension. We can manually create and initialize the mint +// account via CPIs in the instruction handler. +pub fn handler(context: &mut Context, decimals: u8) -> Result<()> { + // Calculate space required for mint and extension data + let mint_size = + ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferHook])?; + + // Calculate minimum lamports required for size of mint account with extensions + let lamports = Rent::get()?.try_minimum_balance(mint_size)?; + + // Invoke System Program to create new account with space for mint and extension data + create_account( + CpiContext::new( + context.accounts.system_program.address(), + CreateAccount { + from: context.accounts.payer.cpi_handle_mut(), + to: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + lamports, // Lamports + mint_size as u64, // Space + context.accounts.token_program.address(), // Owner Program + )?; + + // Initialize the TransferHook extension, pointing at this program + // This instruction must come before the instruction to initialize the mint data + transfer_hook_initialize( + CpiContext::new( + context.accounts.token_program.address(), + TransferHookInitialize { + mint: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + Some(context.accounts.payer.address()), + Some(&crate::ID), + )?; + + // Initialize the standard mint account data + initialize_mint2( + CpiContext::new( + context.accounts.token_program.address(), + InitializeMint2 { + mint: context.accounts.mint_account.cpi_handle_mut(), + }, + ), + decimals, // decimals + context.accounts.payer.address(), // mint authority + None, // freeze authority + )?; + + handle_check_mint_data(context)?; Ok(()) } // helper to check mint data, and demonstrate how to read mint extension data within a program -fn handle_check_mint_data(accounts: &mut InitializeAccountConstraints) -> Result<()> { - let mint = &accounts.mint_account.to_account_info(); - let mint_data = mint.data.borrow(); - // .map_err() needed because spl-token-2022 uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types - let mint_with_extension = StateWithExtensions::::unpack(&mint_data) - .map_err(|_| ProgramError::InvalidAccountData)?; - let extension_data = mint_with_extension.get_extension::() - .map_err(|_| ProgramError::InvalidAccountData)?; - - assert_eq!( - extension_data.authority, - OptionalNonZeroPubkey::try_from(Some(accounts.payer.key())) - .map_err(|_| ProgramError::InvalidArgument)? - ); +fn handle_check_mint_data(context: &Context) -> Result<()> { + let mint = context.accounts.mint_account.account(); + let mint_data = mint.try_borrow()?; + let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; + let extension_data = mint_with_extension.get_extension::()?; assert_eq!( extension_data.program_id, - OptionalNonZeroPubkey::try_from(Some(crate::ID)) - .map_err(|_| ProgramError::InvalidArgument)? + OptionalNonZeroPubkey::try_from(Some(crate::ID))? ); msg!("{:?}", extension_data); diff --git a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs index 5a858e593..b22372d0e 100644 --- a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs +++ b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs @@ -9,14 +9,14 @@ use spl_transfer_hook_interface::instruction::ExecuteInstruction; use crate::{handle_extra_account_metas, handle_extra_account_metas_count}; #[derive(Accounts)] -pub struct InitializeExtraAccountMetaListAccountConstraints<'info> { +pub struct InitializeExtraAccountMetaListAccountConstraints { #[account(mut)] - payer: Signer<'info>, + payer: Signer, /// CHECK: ExtraAccountMetaList Account, must use these seeds #[account( init, - seeds = [b"extra-account-metas", mint.key().as_ref()], + seeds = [b"extra-account-metas", mint.address().as_ref()], bump, // size_of returns Result with spl's ProgramError - unwrap is safe for known-good input space = ExtraAccountMetaList::size_of( @@ -24,23 +24,27 @@ pub struct InitializeExtraAccountMetaListAccountConstraints<'info> { ).unwrap(), payer = payer )] - pub extra_account_meta_list: UncheckedAccount<'info>, - pub mint: InterfaceAccount<'info, Mint>, - pub token_program: Program<'info, Token2022>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub extra_account_meta_list: UncheckedAccount, + pub mint: InterfaceAccount, + pub token_program: Program, + pub associated_token_program: Program, + pub system_program: Program, } -pub fn handler(mut context: Context) -> Result<()> { +pub fn handler( + mut context: &mut Context, +) -> Result<()> { let extra_account_metas = handle_extra_account_metas()?; // initialize ExtraAccountMetaList account with extra accounts // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types - ExtraAccountMetaList::init::( - &mut context.accounts.extra_account_meta_list.try_borrow_mut_data()?, - &extra_account_metas, - ).map_err(|_| ProgramError::InvalidAccountData)?; + // `AccountView` is Copy, and a copy still points at the same backing + // buffer, so the borrow writes through to the real account. + let mut meta_list_view = *context.accounts.extra_account_meta_list.account(); + let mut meta_list_data = meta_list_view.try_borrow_mut()?; + ExtraAccountMetaList::init::(&mut meta_list_data, &extra_account_metas) + .map_err(|_| ProgramError::InvalidAccountData)?; Ok(()) } diff --git a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs index e7b3a5be8..329d602a5 100644 --- a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::token_interface::{Mint, TokenAccount}; use crate::check_is_transferring; @@ -8,20 +9,20 @@ use crate::check_is_transferring; // Remaining accounts are the extra accounts required from the ExtraAccountMetaList account // These accounts are provided via CPI to this program from the token2022 program #[derive(Accounts)] -pub struct TransferHookAccountConstraints<'info> { +pub struct TransferHookAccountConstraints { #[account(token::mint = mint, token::authority = owner)] - pub source_token: InterfaceAccount<'info, TokenAccount>, - pub mint: InterfaceAccount<'info, Mint>, + pub source_token: InterfaceAccount, + pub mint: InterfaceAccount, #[account(token::mint = mint)] - pub destination_token: InterfaceAccount<'info, TokenAccount>, + pub destination_token: InterfaceAccount, /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program - pub owner: UncheckedAccount<'info>, + pub owner: UncheckedAccount, /// CHECK: ExtraAccountMetaList Account, - #[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)] - pub extra_account_meta_list: UncheckedAccount<'info>, + #[account(seeds = [b"extra-account-metas", mint.address().as_ref()], bump)] + pub extra_account_meta_list: UncheckedAccount, } -pub fn handler(context: Context, _amount: u64) -> Result<()> { +pub fn handler(context: &mut Context, _amount: u64) -> Result<()> { // Fail this instruction if it is not called from within a transfer hook check_is_transferring(&context)?; diff --git a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/lib.rs b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/lib.rs index 9ef5d2be4..503b2ff52 100644 --- a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/lib.rs @@ -3,13 +3,12 @@ use std::cell::RefMut; use anchor_lang::prelude::*; use anchor_spl::token_2022::spl_token_2022::{ extension::{ - transfer_hook::TransferHookAccount, BaseStateWithExtensionsMut, - PodStateWithExtensionsMut, + transfer_hook::TransferHookAccount, BaseStateWithExtensions, PodStateWithExtensions, }, pod::PodAccount, }; -use spl_tlv_account_resolution::account::ExtraAccountMeta; use spl_discriminator::SplDiscriminate; +use spl_tlv_account_resolution::account::ExtraAccountMeta; use spl_transfer_hook_interface::instruction::{ ExecuteInstruction, InitializeExtraAccountMetaListInstruction, }; @@ -25,35 +24,50 @@ pub enum TransferError { IsNotCurrentlyTransferring, } +pub mod entrypoint; + +// v2's `#[program(interface, ...)]` declares an interface for other programs to +// CPI into and emits no entrypoint, and an executable `#[program]` only accepts +// one-byte custom discriminators, so the transfer-hook interface's eight-byte +// discriminators have no direct spelling. `entrypoint` bridges the gap: it maps +// each of them onto a handler before anchor's dispatch runs. #[program] pub mod transfer_hook { use super::*; - pub fn initialize(context: Context, decimals: u8) -> Result<()> { + pub fn initialize( + context: &mut Context, + decimals: u8, + ) -> Result<()> { instructions::initialize::handler(context, decimals) } - #[instruction(discriminator = InitializeExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE)] + // sha256("spl-transfer-hook-interface:initialize-extra-account-metas")[..8] pub fn initialize_extra_account_meta_list( - context: Context, + context: &mut Context, ) -> Result<()> { instructions::initialize_extra_account_meta_list::handler(context) } - #[instruction(discriminator = ExecuteInstruction::SPL_DISCRIMINATOR_SLICE)] - pub fn transfer_hook(context: Context, amount: u64) -> Result<()> { + // sha256("spl-transfer-hook-interface:execute")[..8] + pub fn transfer_hook( + context: &mut Context, + amount: u64, + ) -> Result<()> { instructions::transfer_hook::handler(context, amount) } } pub fn check_is_transferring(context: &Context) -> Result<()> { - let source_token_info = context.accounts.source_token.to_account_info(); - let mut account_data_ref: RefMut<&mut [u8]> = source_token_info.try_borrow_mut_data()?; + // Read-only: the account already holds a shared borrow of its buffer, and a + // second shared borrow is fine where `try_borrow_mut` would be rejected. + let account_data_ref = context.accounts.source_token.account().try_borrow()?; // .map_err() needed because spl-token-2022 uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types - let mut account = PodStateWithExtensionsMut::::unpack(*account_data_ref) + // while anchor-lang uses 3.x - structurally identical but different semver types + let account = PodStateWithExtensions::::unpack(&account_data_ref) .map_err(|_| ProgramError::InvalidAccountData)?; - let account_extension = account.get_extension_mut::() + let account_extension = account + .get_extension::() .map_err(|_| ProgramError::InvalidAccountData)?; if !bool::from(account_extension.transferring) { diff --git a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/tests/test_transfer_hook.rs b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/tests/test_transfer_hook.rs index 7471118f0..f839dcea8 100644 --- a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/tests/test_transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/tests/test_transfer_hook.rs @@ -1,13 +1,10 @@ use { anchor_lang::{ - solana_program::{ - instruction::Instruction, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::{ @@ -17,17 +14,16 @@ use { }, transfer_hook::{build_hook_accounts, get_hook_accounts_address}, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn associated_token_program_id() -> Pubkey { +fn associated_token_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = transfer_hook::id(); let mut svm = LiteSVM::new(); @@ -47,37 +43,36 @@ fn test_transfer_hook_hello_world() { let decimals: u8 = 2; // ExtraAccountMetaList PDA - let extra_account_meta_list = - get_hook_accounts_address(&mint_keypair.pubkey(), &program_id); + let extra_account_meta_list = get_hook_accounts_address(&mint_keypair.pubkey(), &program_id); // Step 1: Create mint with transfer hook extension pointing to our program // (uses the program's own Initialize instruction, not kite, since it sets up // the mint with the program as the hook authority) let initialize_ix = Instruction::new_with_bytes( program_id, - &transfer_hook::instruction::Initialize { - decimals, - } - .data(), + &transfer_hook::instruction::Initialize { decimals }.data(), transfer_hook::accounts::InitializeAccountConstraints { payer: payer.pubkey(), mint_account: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![initialize_ix], &[&payer, &mint_keypair], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![initialize_ix], + &[&payer, &mint_keypair], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Step 2: Create token accounts and mint tokens let amount: u64 = 100 * 10u64.pow(decimals as u32); - let source_ata = create_token_extensions_account( - &mut svm, - &payer.pubkey(), - &mint_keypair.pubkey(), - &payer, - ).unwrap(); + let source_ata = + create_token_extensions_account(&mut svm, &payer.pubkey(), &mint_keypair.pubkey(), &payer) + .unwrap(); svm.expire_blockhash(); let dest_ata = create_token_extensions_account( @@ -85,7 +80,8 @@ fn test_transfer_hook_hello_world() { &recipient.pubkey(), &mint_keypair.pubkey(), &payer, - ).unwrap(); + ) + .unwrap(); svm.expire_blockhash(); mint_tokens_to_token_extensions_account( @@ -94,7 +90,8 @@ fn test_transfer_hook_hello_world() { &source_ata, amount, &payer, - ).unwrap(); + ) + .unwrap(); svm.expire_blockhash(); // Step 3: Create ExtraAccountMetaList account @@ -107,11 +104,12 @@ fn test_transfer_hook_hello_world() { mint: mint_keypair.pubkey(), token_program: TOKEN_EXTENSIONS_PROGRAM_ID, associated_token_program: ata_program, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 4: Transfer with transfer hook @@ -130,7 +128,8 @@ fn test_transfer_hook_hello_world() { transfer_amount, decimals, &extra_accounts, - ).unwrap(); + ) + .unwrap(); svm.expire_blockhash(); // Step 5: Try calling transfer_hook directly (should fail - not transferring) @@ -146,7 +145,12 @@ fn test_transfer_hook_hello_world() { } .to_account_metas(None), ); - let result = send_transaction_from_instructions(&mut svm, vec![direct_hook_ix], &[&payer], &payer.pubkey()); + let result = send_transaction_from_instructions( + &mut svm, + vec![direct_hook_ix], + &[&payer], + &payer.pubkey(), + ); assert!( result.is_err(), "Calling transfer_hook directly should fail because token is not transferring" diff --git a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/Cargo.toml b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/Cargo.toml index 53c6e80ed..9f340987c 100644 --- a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/Cargo.toml +++ b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/Cargo.toml @@ -9,19 +9,31 @@ crate-type = ["cdylib", "lib"] name = "transfer_hook" [features] -default = [] +# `no-entrypoint` is always on: anchor then exports its dispatch as +# `__anchor_dispatch` rather than claiming the `entrypoint` symbol, leaving +# src/entrypoint.rs free to claim it and map the transfer-hook interface's +# discriminators onto this program's handlers. +default = ["no-entrypoint"] cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = "1.1.2" -anchor-spl = "1.1.2" +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" # SPL crates v3.x-compatible - uses solana-program-error 3.x matching anchor-lang 1.0 spl-discriminator = "0.5.2" spl-tlv-account-resolution = "0.11.1" diff --git a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/entrypoint.rs b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/entrypoint.rs new file mode 100644 index 000000000..577e8f2b6 --- /dev/null +++ b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/entrypoint.rs @@ -0,0 +1,50 @@ +//! Program entrypoint, hand-written so the SPL transfer-hook interface reaches +//! this program's handlers. +//! +//! Anchor v2 gives every instruction in an executable `#[program]` the eight-byte +//! `sha256("global:")` discriminator, and `#[discrim = N]` there is limited +//! to a single byte. The transfer-hook interface calls its instructions under +//! their own eight-byte values, which leaves no way to declare those handlers +//! directly. `#[program(interface, ...)]` accepts arbitrary discriminator bytes +//! but only generates a CPI client: no dispatch, and so no deployable program. +//! +//! So the crate builds with `no-entrypoint` (which makes anchor export its +//! dispatch as `__anchor_dispatch` instead of claiming the `entrypoint` symbol) +//! and this module claims `entrypoint` itself. All it does is swap an interface +//! discriminator for the matching handler's before delegating; the payload +//! behind it is identical either way. + +use anchor_lang::pinocchio; + +pinocchio::default_allocator!(); +pinocchio::default_panic_handler!(); + +/// Interface discriminator paired with the handler's own, in declaration order. +const DISCRIMINATOR_MAP: [([u8; 8], [u8; 8]); 2] = [ + // initialize_extra_account_meta_list + ([43, 34, 13, 49, 167, 88, 235, 235], [92, 197, 174, 197, 41, 124, 19, 3]), + // transfer_hook + ([105, 37, 101, 197, 75, 251, 102, 26], [220, 57, 220, 152, 126, 125, 97, 168]), +]; + +/// # Safety +/// +/// Called only by the SBF loader, with the register convention anchor's own +/// entrypoint documents: `r1` is the start of the serialized parameter region +/// and `r2` points at the instruction data, whose length sits in the eight +/// bytes below it. +#[cfg(target_os = "solana")] +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data_ptr: *const u8) -> u64 { + let len = *(ix_data_ptr.sub(8) as *const u64) as usize; + if len >= 8 { + let discriminator = core::slice::from_raw_parts_mut(ix_data_ptr as *mut u8, 8); + for (interface, handler) in DISCRIMINATOR_MAP { + if discriminator == interface { + discriminator.copy_from_slice(&handler); + break; + } + } + } + crate::__anchor_dispatch(input, ix_data_ptr) +} diff --git a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs index d19d662fe..b2adb571b 100644 --- a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs +++ b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs @@ -6,14 +6,14 @@ use spl_transfer_hook_interface::instruction::ExecuteInstruction; use crate::{handle_extra_account_metas, handle_extra_account_metas_count, CounterAccount}; #[derive(Accounts)] -pub struct InitializeExtraAccountMetaListAccountConstraints<'info> { +pub struct InitializeExtraAccountMetaListAccountConstraints { #[account(mut)] - payer: Signer<'info>, + payer: Signer, /// CHECK: ExtraAccountMetaList Account, must use these seeds #[account( init, - seeds = [b"extra-account-metas", mint.key().as_ref()], + seeds = [b"extra-account-metas", mint.address().as_ref()], bump, // size_of returns Result with spl's ProgramError - unwrap is safe for known-good input space = ExtraAccountMetaList::size_of( @@ -21,22 +21,25 @@ pub struct InitializeExtraAccountMetaListAccountConstraints<'info> { ).unwrap(), payer = payer )] - pub extra_account_meta_list: UncheckedAccount<'info>, - pub mint: InterfaceAccount<'info, Mint>, + pub extra_account_meta_list: UncheckedAccount, + pub mint: InterfaceAccount, #[account(init, seeds = [b"counter"], bump, payer = payer, space = CounterAccount::DISCRIMINATOR.len() + CounterAccount::INIT_SPACE)] - pub counter_account: Account<'info, CounterAccount>, - pub system_program: Program<'info, System>, + pub counter_account: BorshAccount, + pub system_program: Program, } -pub fn handler(mut context: Context) -> Result<()> { +pub fn handler( + mut context: &mut Context, +) -> Result<()> { let extra_account_metas = handle_extra_account_metas()?; // initialize ExtraAccountMetaList account with extra accounts - ExtraAccountMetaList::init::( - &mut context.accounts.extra_account_meta_list.try_borrow_mut_data()?, - &extra_account_metas, - ) - .map_err(|_| ProgramError::InvalidAccountData)?; + // `AccountView` is Copy, and a copy still points at the same backing + // buffer, so the borrow writes through to the real account. + let mut meta_list_view = *context.accounts.extra_account_meta_list.account(); + let mut meta_list_data = meta_list_view.try_borrow_mut()?; + ExtraAccountMetaList::init::(&mut meta_list_data, &extra_account_metas) + .map_err(|_| ProgramError::InvalidAccountData)?; context.accounts.counter_account.bump = context.bumps.counter_account; diff --git a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs index c7f5b9ec1..3e83b2d07 100644 --- a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, - token::Token, + token::{self, Token}, token_interface::{transfer_checked, Mint, TokenAccount, TransferChecked}, }; @@ -17,43 +17,43 @@ use crate::{check_is_transferring, CounterAccount, TransferError}; // the 4096-byte BPF stack frame limit in try_accounts deserialization. // This struct has 12 accounts - without Box, the generated code uses ~4160 bytes of stack. #[derive(Accounts)] -pub struct TransferHookAccountConstraints<'info> { +pub struct TransferHookAccountConstraints { #[account(token::mint = mint, token::authority = owner)] - pub source_token: Box>, - pub mint: Box>, + pub source_token: Box>, + pub mint: Box>, #[account(token::mint = mint)] - pub destination_token: Box>, + pub destination_token: Box>, /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program - pub owner: UncheckedAccount<'info>, + pub owner: UncheckedAccount, /// CHECK: ExtraAccountMetaList Account, - #[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)] - pub extra_account_meta_list: UncheckedAccount<'info>, - pub wsol_mint: Box>, - pub token_program: Program<'info, Token>, - pub associated_token_program: Program<'info, AssociatedToken>, + #[account(seeds = [b"extra-account-metas", mint.address().as_ref()], bump)] + pub extra_account_meta_list: UncheckedAccount, + pub wsol_mint: Box>, + pub token_program: Program, + pub associated_token_program: Program, #[account( mut, seeds = [b"delegate"], bump )] - pub delegate: SystemAccount<'info>, + pub delegate: SystemAccount, #[account( mut, token::mint = wsol_mint, token::authority = delegate, )] - pub delegate_wsol_token_account: Box>, + pub delegate_wsol_token_account: Box>, #[account( mut, token::mint = wsol_mint, token::authority = owner, )] - pub sender_wsol_token_account: Box>, + pub sender_wsol_token_account: Box>, #[account(seeds = [b"counter"], bump)] - pub counter_account: Account<'info, CounterAccount>, + pub counter_account: BorshAccount, } -pub fn handler(context: Context, amount: u64) -> Result<()> { +pub fn handler(context: &mut Context, amount: u64) -> Result<()> { // Fail this instruction if it is not called from within a transfer hook check_is_transferring(&context)?; @@ -68,17 +68,19 @@ pub fn handler(context: Context, amount: u64) -> context.accounts.counter_account.counter ); + // Read through the AccountView: these accounts are not declared `mut`, and + // asking a read-only account for a writable handle panics. msg!( "Is writable mint {0}", - context.accounts.mint.to_account_info().is_writable + context.accounts.mint.account().is_writable() ); msg!( "Is destination mint {0}", - context.accounts.destination_token.to_account_info().is_writable + context.accounts.destination_token.account().is_writable() ); msg!( "Is source mint {0}", - context.accounts.source_token.to_account_info().is_writable + context.accounts.source_token.account().is_writable() ); let signer_seeds: &[&[&[u8]]] = &[&[b"delegate", &[context.bumps.delegate]]]; @@ -87,17 +89,20 @@ pub fn handler(context: Context, amount: u64) -> // transfer lamports amount equal to token transfer amount transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.sender_wsol_token_account.to_account_info(), - mint: context.accounts.wsol_mint.to_account_info(), - to: context.accounts.delegate_wsol_token_account.to_account_info(), - authority: context.accounts.delegate.to_account_info(), + from: context.accounts.sender_wsol_token_account.cpi_handle_mut(), + mint: context.accounts.wsol_mint.cpi_handle(), + to: context + .accounts + .delegate_wsol_token_account + .cpi_handle_mut(), + authority: context.accounts.delegate.cpi_handle(), }, ) .with_signer(signer_seeds), amount, - context.accounts.wsol_mint.decimals, + context.accounts.wsol_mint.decimals(), )?; Ok(()) } diff --git a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/lib.rs b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/lib.rs index 7c17b8ecb..f6e121bfe 100644 --- a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/lib.rs @@ -1,11 +1,11 @@ -use anchor_lang::{prelude::*, solana_program::pubkey::Pubkey}; +use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, token::Token, token_2022::spl_token_2022::{ extension::{ - transfer_hook::TransferHookAccount, BaseStateWithExtensionsMut, - PodStateWithExtensionsMut, + transfer_hook::TransferHookAccount, BaseStateWithExtensions, + PodStateWithExtensions, }, pod::PodAccount, }, @@ -36,30 +36,43 @@ pub enum TransferError { IsNotCurrentlyTransferring, } +pub mod entrypoint; + +// v2's `#[program(interface, ...)]` declares an interface for other programs to +// CPI into and emits no entrypoint, and an executable `#[program]` only accepts +// one-byte custom discriminators, so the transfer-hook interface's eight-byte +// discriminators have no direct spelling. `entrypoint` bridges the gap: it maps +// each of them onto a handler before anchor's dispatch runs. #[program] pub mod transfer_hook { use super::*; - #[instruction(discriminator = InitializeExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE)] + // sha256("spl-transfer-hook-interface:initialize-extra-account-metas")[..8] pub fn initialize_extra_account_meta_list( - context: Context, + context: &mut Context, ) -> Result<()> { instructions::initialize_extra_account_meta_list::handler(context) } - #[instruction(discriminator = ExecuteInstruction::SPL_DISCRIMINATOR_SLICE)] - pub fn transfer_hook(context: Context, amount: u64) -> Result<()> { + // sha256("spl-transfer-hook-interface:execute")[..8] + pub fn transfer_hook( + context: &mut Context, + amount: u64, + ) -> Result<()> { instructions::transfer_hook::handler(context, amount) } } pub fn check_is_transferring(context: &Context) -> Result<()> { - let source_token_info = context.accounts.source_token.to_account_info(); - let mut account_data_ref: RefMut<&mut [u8]> = source_token_info.try_borrow_mut_data()?; - let mut account = PodStateWithExtensionsMut::::unpack(*account_data_ref) + // Read-only: the account already holds a shared borrow of its buffer, and a + // second shared borrow is fine where `try_borrow_mut` would be rejected. + let account_data_ref = context.accounts.source_token.account().try_borrow()?; + // .map_err() needed because spl-token-2022 uses solana-program-error 2.x + // while anchor-lang uses 3.x - structurally identical but different semver types + let account = PodStateWithExtensions::::unpack(&account_data_ref) .map_err(|_| ProgramError::InvalidAccountData)?; let account_extension = account - .get_extension_mut::() + .get_extension::() .map_err(|_| ProgramError::InvalidAccountData)?; if !bool::from(account_extension.transferring) { @@ -77,7 +90,7 @@ pub fn handle_extra_account_metas() -> Result> { // index 0-3 are the accounts required for token transfer (source, mint, destination, owner) // index 4 is address of ExtraAccountMetaList account - let wsol_mint = Pubkey::from_str("So11111111111111111111111111111111111111112").unwrap(); + let wsol_mint = Address::from_str("So11111111111111111111111111111111111111112").unwrap(); let token_program_id = Token::id(); let ata_program_id = AssociatedToken::id(); @@ -140,7 +153,7 @@ pub fn handle_extra_account_metas_count() -> usize { 7 // wsol_mint, token_program, ata_program, delegate, delegate_wsol, sender_wsol, counter } -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct CounterAccount { pub counter: u8, diff --git a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/tests/test_transfer_hook.rs b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/tests/test_transfer_hook.rs index 3912dd921..f67139775 100644 --- a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/tests/test_transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/tests/test_transfer_hook.rs @@ -1,13 +1,10 @@ use { anchor_lang::{ - solana_program::{ - instruction::Instruction, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::{ @@ -15,11 +12,10 @@ use { }, transfer_hook::get_hook_accounts_address, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = transfer_hook::id(); let mut svm = LiteSVM::new(); @@ -49,9 +45,8 @@ fn test_initialize_extra_account_meta_list() { svm.expire_blockhash(); // PDAs - let extra_account_meta_list = - get_hook_accounts_address(&mint, &program_id); - let (counter_pda, _) = Pubkey::find_program_address(&[b"counter"], &program_id); + let extra_account_meta_list = get_hook_accounts_address(&mint, &program_id); + let (counter_pda, _) = Address::find_program_address(&[b"counter"], &program_id); // Step 2: Initialize ExtraAccountMetaList (also creates counter PDA) let init_extra_ix = Instruction::new_with_bytes( @@ -62,11 +57,12 @@ fn test_initialize_extra_account_meta_list() { extra_account_meta_list, mint, counter_account: counter_pda, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify the ExtraAccountMetaList account was created let account = svm.get_account(&extra_account_meta_list); diff --git a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/Cargo.toml b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/Cargo.toml index bc41fd69c..363c1f6d8 100644 --- a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/Cargo.toml +++ b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/Cargo.toml @@ -9,19 +9,31 @@ crate-type = ["cdylib", "lib"] name = "transfer_switch" [features] -default = [] +# `no-entrypoint` is always on: anchor then exports its dispatch as +# `__anchor_dispatch` rather than claiming the `entrypoint` symbol, leaving +# src/entrypoint.rs free to claim it and map the transfer-hook interface's +# discriminators onto this program's handlers. +default = ["no-entrypoint"] cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = "1.1.2" +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" spl-discriminator = "0.4.1" spl-tlv-account-resolution = "0.9.0" spl-transfer-hook-interface = "0.9.0" diff --git a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/entrypoint.rs b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/entrypoint.rs new file mode 100644 index 000000000..eb0294f7c --- /dev/null +++ b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/entrypoint.rs @@ -0,0 +1,50 @@ +//! Program entrypoint, hand-written so the SPL transfer-hook interface reaches +//! this program's handlers. +//! +//! Anchor v2 gives every instruction in an executable `#[program]` the eight-byte +//! `sha256("global:")` discriminator, and `#[discrim = N]` there is limited +//! to a single byte. The transfer-hook interface calls its instructions under +//! their own eight-byte values, which leaves no way to declare those handlers +//! directly. `#[program(interface, ...)]` accepts arbitrary discriminator bytes +//! but only generates a CPI client: no dispatch, and so no deployable program. +//! +//! So the crate builds with `no-entrypoint` (which makes anchor export its +//! dispatch as `__anchor_dispatch` instead of claiming the `entrypoint` symbol) +//! and this module claims `entrypoint` itself. All it does is swap an interface +//! discriminator for the matching handler's before delegating; the payload +//! behind it is identical either way. + +use anchor_lang::pinocchio; + +pinocchio::default_allocator!(); +pinocchio::default_panic_handler!(); + +/// Interface discriminator paired with the handler's own, in declaration order. +const DISCRIMINATOR_MAP: [([u8; 8], [u8; 8]); 2] = [ + // initialize_extra_account_metas_list + ([43, 34, 13, 49, 167, 88, 235, 235], [253, 251, 47, 0, 182, 68, 159, 62]), + // transfer_hook + ([105, 37, 101, 197, 75, 251, 102, 26], [220, 57, 220, 152, 126, 125, 97, 168]), +]; + +/// # Safety +/// +/// Called only by the SBF loader, with the register convention anchor's own +/// entrypoint documents: `r1` is the start of the serialized parameter region +/// and `r2` points at the instruction data, whose length sits in the eight +/// bytes below it. +#[cfg(target_os = "solana")] +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data_ptr: *const u8) -> u64 { + let len = *(ix_data_ptr.sub(8) as *const u64) as usize; + if len >= 8 { + let discriminator = core::slice::from_raw_parts_mut(ix_data_ptr as *mut u8, 8); + for (interface, handler) in DISCRIMINATOR_MAP { + if discriminator == interface { + discriminator.copy_from_slice(&handler); + break; + } + } + } + crate::__anchor_dispatch(input, ix_data_ptr) +} diff --git a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/configure_admin.rs b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/configure_admin.rs index b9fc5800e..5ba16083a 100644 --- a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/configure_admin.rs +++ b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/configure_admin.rs @@ -1,13 +1,18 @@ use {crate::state::AdminConfig, anchor_lang::prelude::*}; #[derive(Accounts)] -pub struct ConfigureAdminAccountConstraints<'info> { - #[account(mut)] - pub admin: Signer<'info>, +pub struct ConfigureAdminAccountConstraints { + // Bootstrapping the config passes the same key as both `admin` and + // `new_admin`, so the two slots legitimately alias. v2 rejects an account + // that appears twice while any of its slots is in the mutable mask, and it + // flags *both* indices, so both carry `unsafe(dup)`, which keeps them + // writable while taking them out of that mask. + #[account(unsafe(dup))] + pub admin: Signer, /// CHECK: the new admin - #[account(mut)] - pub new_admin: UncheckedAccount<'info>, + #[account(unsafe(dup))] + pub new_admin: UncheckedAccount, /// To hold the address of the admin that controls switches #[account( @@ -17,33 +22,35 @@ pub struct ConfigureAdminAccountConstraints<'info> { seeds = [b"admin-config"], bump )] - pub admin_config: Account<'info, AdminConfig>, + pub admin_config: BorshAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } pub fn handle_is_admin(accounts: &mut ConfigureAdminAccountConstraints) -> Result<()> { - // check if we are not creating the account for the first time, - // ensure it's the admin that is making the change + // check if we are not creating the account for the first time, + // ensure it's the admin that is making the change + // + if accounts.admin_config.is_initialised { + // make sure it's the admin // - if accounts.admin_config.is_initialised { - // make sure it's the admin - // - require_keys_eq!(accounts.admin.key(), accounts.admin_config.admin,); + require_keys_eq!(*accounts.admin.address(), accounts.admin_config.admin,); - // make sure the admin is not reentering their key - // - require_keys_neq!(accounts.admin.key(), accounts.new_admin.key()); - } - Ok(()) - } - -pub fn handle_configure_admin(accounts: &mut ConfigureAdminAccountConstraints, bump: u8) -> Result<()> { - accounts.admin_config.set_inner(AdminConfig { - admin: accounts.new_admin.key(), // set the admin pubkey that can switch transfers on/off - is_initialised: true, // let us know an admin has been set - bump, // canonical bump for the admin-config PDA - }); - Ok(()) + // make sure the admin is not reentering their key + // + require_keys_neq!(accounts.admin.address(), accounts.new_admin.address()); } + Ok(()) +} +pub fn handle_configure_admin( + accounts: &mut ConfigureAdminAccountConstraints, + bump: u8, +) -> Result<()> { + *accounts.admin_config = AdminConfig { + admin: *accounts.new_admin.address(), // set the admin pubkey that can switch transfers on/off + is_initialised: true, // let us know an admin has been set + bump, // canonical bump for the admin-config PDA + }; + Ok(()) +} diff --git a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/initialise_extra_account_metas_list.rs b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/initialise_extra_account_metas_list.rs index e97028e60..c014196d6 100644 --- a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/initialise_extra_account_metas_list.rs +++ b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/initialise_extra_account_metas_list.rs @@ -11,72 +11,77 @@ use { }; #[derive(Accounts)] -pub struct InitializeExtraAccountMetasAccountConstraints<'info> { +pub struct InitializeExtraAccountMetasAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account()] - pub token_mint: InterfaceAccount<'info, Mint>, + pub token_mint: InterfaceAccount, /// CHECK: extra accoumt metas list #[account( mut, - seeds = [b"extra-account-metas", token_mint.key().as_ref()], + seeds = [b"extra-account-metas", token_mint.address().as_ref()], bump, )] - pub extra_account_metas_list: UncheckedAccount<'info>, + pub extra_account_metas_list: UncheckedAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } -pub fn handle_initialize_extra_account_metas_list(accounts: &mut InitializeExtraAccountMetasAccountConstraints, bumps: InitializeExtraAccountMetasAccountConstraintsBumps) -> Result<()> { - // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types - let account_metas = vec![ - // 5 - wallet (sender) config account - ExtraAccountMeta::new_with_seeds( - &[ - Seed::AccountKey { index: 3 }, // sender index - ], - false, // is_signer - false, // is_writable - ).map_err(|_| ProgramError::InvalidArgument)?, - ]; +pub fn handle_initialize_extra_account_metas_list( + accounts: &mut InitializeExtraAccountMetasAccountConstraints, + bumps: &InitializeExtraAccountMetasAccountConstraintsBumps, +) -> Result<()> { + // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x + // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types + let account_metas = vec![ + // 5 - wallet (sender) config account + ExtraAccountMeta::new_with_seeds( + &[ + Seed::AccountKey { index: 3 }, // sender index + ], + false, // is_signer + false, // is_writable + ) + .map_err(|_| ProgramError::InvalidArgument)?, + ]; - // calculate account size - // unwrap is safe for known-good input (count of metas we just created) - let account_size = ExtraAccountMetaList::size_of(account_metas.len()).unwrap() as u64; + // calculate account size + // unwrap is safe for known-good input (count of metas we just created) + let account_size = ExtraAccountMetaList::size_of(account_metas.len()).unwrap() as u64; - // calculate minimum required lamports - let lamports = Rent::get()?.minimum_balance(account_size as usize); + // calculate minimum required lamports + let lamports = Rent::get()?.try_minimum_balance(account_size as usize)?; - let mint = accounts.token_mint.key(); - let signer_seeds: &[&[&[u8]]] = &[&[ - b"extra-account-metas", - mint.as_ref(), - &[bumps.extra_account_metas_list], - ]]; + let mint = accounts.token_mint.address(); + let signer_seeds: &[&[&[u8]]] = &[&[ + b"extra-account-metas", + mint.as_ref(), + &[bumps.extra_account_metas_list], + ]]; - create_account( - CpiContext::new( - accounts.system_program.key(), - CreateAccount { - from: accounts.payer.to_account_info(), - to: accounts.extra_account_metas_list.to_account_info(), - }, - ) - .with_signer(signer_seeds), - lamports, - account_size, - &crate::ID, - )?; + create_account( + CpiContext::new( + accounts.system_program.address(), + CreateAccount { + from: accounts.payer.cpi_handle_mut(), + to: accounts.extra_account_metas_list.cpi_handle_mut(), + }, + ) + .with_signer(signer_seeds), + lamports, + account_size, + &crate::ID, + )?; - // Initialize the account data to store the list of ExtraAccountMetas - ExtraAccountMetaList::init::( - &mut accounts.extra_account_metas_list.try_borrow_mut_data()?, - &account_metas, - ).map_err(|_| ProgramError::InvalidAccountData)?; - - Ok(()) - } + // Initialize the account data to store the list of ExtraAccountMetas + let mut list_view = *accounts.extra_account_metas_list.account(); + ExtraAccountMetaList::init::( + &mut list_view.try_borrow_mut()?, + &account_metas, + ) + .map_err(|_| ProgramError::InvalidAccountData)?; + Ok(()) +} diff --git a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/switch.rs b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/switch.rs index f33d67d3b..15bd1838b 100644 --- a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/switch.rs +++ b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/switch.rs @@ -4,46 +4,43 @@ use { }; #[derive(Accounts)] -pub struct SwitchAccountConstraints<'info> { +pub struct SwitchAccountConstraints { /// admin that controls the switch - #[account(mut)] - pub admin: Signer<'info>, + #[account(mut, address = admin_config.admin)] + pub admin: Signer, /// CHECK: wallet - transfer sender #[account(mut)] - pub wallet: UncheckedAccount<'info>, + pub wallet: UncheckedAccount, /// admin config - #[account( - has_one=admin, - seeds=[b"admin-config"], - bump, - )] - pub admin_config: Account<'info, AdminConfig>, + #[account(seeds=[b"admin-config"], + bump)] + pub admin_config: BorshAccount, /// the wallet (sender) transfer switch #[account( init_if_needed, payer=admin, space = TransferSwitch::DISCRIMINATOR.len() + TransferSwitch::INIT_SPACE, - seeds = [wallet.key().as_ref()], + seeds = [wallet.address().as_ref()], bump, )] - pub wallet_switch: Account<'info, TransferSwitch>, + pub wallet_switch: BorshAccount, - pub system_program: Program<'info, System>, + pub system_program: Program, } pub fn handle_switch(accounts: &mut SwitchAccountConstraints, on: bool, bump: u8) -> Result<()> { - // toggle switch on/off for the given wallet - // - accounts.wallet_switch.set_inner(TransferSwitch { - wallet: accounts.wallet.key(), - on, - bump, // canonical bump for this wallet's PDA - }); - Ok(()) - } + // toggle switch on/off for the given wallet + // + *accounts.wallet_switch = TransferSwitch { + wallet: *accounts.wallet.address(), + on, + bump, // canonical bump for this wallet's PDA + }; + Ok(()) +} // admin_config is validated via `seeds=[b"admin-config"], bump` - Anchor // re-derives it and fails if it doesn't match, so storing AdminConfig.bump @@ -51,4 +48,3 @@ pub fn handle_switch(accounts: &mut SwitchAccountConstraints, on: bool, bump: u8 // bump field on AdminConfig is still populated on creation to satisfy the // 'every PDA struct stores its bump' rule and save derivation cost in any // future call sites). - diff --git a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/transfer_hook.rs b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/transfer_hook.rs index c35671596..7140f956b 100644 --- a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/transfer_hook.rs @@ -4,8 +4,8 @@ use { anchor_spl::{ token_2022::spl_token_2022::{ extension::{ - transfer_hook::TransferHookAccount, BaseStateWithExtensionsMut, - PodStateWithExtensionsMut, + transfer_hook::TransferHookAccount, BaseStateWithExtensions, + PodStateWithExtensions, }, pod::PodAccount, }, @@ -14,59 +14,60 @@ use { }; #[derive(Accounts)] -pub struct TransferHookAccountConstraints<'info> { +pub struct TransferHookAccountConstraints { /// CHECK: Sender token account #[account()] - pub source_token_account: UncheckedAccount<'info>, + pub source_token_account: UncheckedAccount, /// The mint of the token transferring #[account()] - pub token_mint: InterfaceAccount<'info, Mint>, + pub token_mint: InterfaceAccount, /// CHECK: Recipient token account #[account()] - pub receiver_token_account: UncheckedAccount<'info>, + pub receiver_token_account: UncheckedAccount, /// CHECK: the transfer sender #[account()] - pub wallet: UncheckedAccount<'info>, + pub wallet: UncheckedAccount, /// CHECK: extra account metas #[account( - seeds = [b"extra-account-metas", token_mint.key().as_ref()], + seeds = [b"extra-account-metas", token_mint.address().as_ref()], bump, )] - pub extra_account_metas_list: UncheckedAccount<'info>, + pub extra_account_metas_list: UncheckedAccount, /// sender transfer switch #[account( - seeds=[wallet.key().as_ref()], + seeds=[wallet.address().as_ref()], bump, )] - pub wallet_switch: Account<'info, TransferSwitch>, + pub wallet_switch: BorshAccount, } pub fn handle_assert_switch_is_on(accounts: &mut TransferHookAccountConstraints) -> Result<()> { - if !accounts.wallet_switch.on { - return err!(TransferError::SwitchNotOn); - } - Ok(()) + if !accounts.wallet_switch.on { + return err!(TransferError::SwitchNotOn); } + Ok(()) +} pub fn handle_assert_is_transferring(accounts: &mut TransferHookAccountConstraints) -> Result<()> { - let source_token_info = accounts.source_token_account.to_account_info(); - let mut account_data_ref = source_token_info.try_borrow_mut_data()?; - // .map_err() needed because spl-token-2022 uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types - let mut account = PodStateWithExtensionsMut::::unpack(*account_data_ref) - .map_err(|_| ProgramError::InvalidAccountData)?; - let account_extension = account.get_extension_mut::() - .map_err(|_| ProgramError::InvalidAccountData)?; - - if !bool::from(account_extension.transferring) { - return err!(TransferError::IsNotCurrentlyTransferring); - } + // Read-only: the account already holds a shared borrow of its buffer, and a + // second shared borrow is fine where `try_borrow_mut` would be rejected. + let account_data_ref = accounts.source_token_account.account().try_borrow()?; + // .map_err() needed because spl-token-2022 uses solana-program-error 2.x + // while anchor-lang uses 3.x - structurally identical but different semver types + let account = PodStateWithExtensions::::unpack(&account_data_ref) + .map_err(|_| ProgramError::InvalidAccountData)?; + let account_extension = account + .get_extension::() + .map_err(|_| ProgramError::InvalidAccountData)?; - Ok(()) + if !bool::from(account_extension.transferring) { + return err!(TransferError::IsNotCurrentlyTransferring); } + Ok(()) +} diff --git a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/lib.rs b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/lib.rs index c392c703c..c80aa0918 100644 --- a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/lib.rs @@ -11,30 +11,42 @@ use spl_transfer_hook_interface::instruction::{ declare_id!("FjcHckEgXcBhFmSGai3FRpDLiT6hbpV893n8iTxVd81g"); +pub mod entrypoint; + +// v2's `#[program(interface, ...)]` declares an interface for other programs to +// CPI into and emits no entrypoint, and an executable `#[program]` only accepts +// one-byte custom discriminators, so the transfer-hook interface's eight-byte +// discriminators have no direct spelling. `entrypoint` bridges the gap: it maps +// each of them onto a handler before anchor's dispatch runs. #[program] pub mod transfer_switch { use super::*; - pub fn configure_admin(mut context: Context) -> Result<()> { + pub fn configure_admin( + mut context: &mut Context, + ) -> Result<()> { let bump = context.bumps.admin_config; handle_is_admin(&mut context.accounts)?; handle_configure_admin(&mut context.accounts, bump) } - #[instruction(discriminator = InitializeExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE)] + // sha256("spl-transfer-hook-interface:initialize-extra-account-metas")[..8] pub fn initialize_extra_account_metas_list( - mut context: Context, + mut context: &mut Context, ) -> Result<()> { - handle_initialize_extra_account_metas_list(&mut context.accounts, context.bumps) + handle_initialize_extra_account_metas_list(&mut context.accounts, &context.bumps) } - pub fn switch(mut context: Context, on: bool) -> Result<()> { + pub fn switch(mut context: &mut Context, on: bool) -> Result<()> { let bump = context.bumps.wallet_switch; handle_switch(&mut context.accounts, on, bump) } - #[instruction(discriminator = ExecuteInstruction::SPL_DISCRIMINATOR_SLICE)] - pub fn transfer_hook(mut context: Context, _amount: u64) -> Result<()> { + // sha256("spl-transfer-hook-interface:execute")[..8] + pub fn transfer_hook( + mut context: &mut Context, + _amount: u64, + ) -> Result<()> { handle_assert_is_transferring(&mut context.accounts)?; handle_assert_switch_is_on(&mut context.accounts) } diff --git a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/state.rs b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/state.rs index 3bbcddd12..41e036256 100644 --- a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/state.rs +++ b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/state.rs @@ -1,19 +1,19 @@ use anchor_lang::prelude::*; -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct TransferSwitch { - pub wallet: Pubkey, + pub wallet: Address, pub on: bool, /// Canonical bump for this PDA. pub bump: u8, } -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct AdminConfig { pub is_initialised: bool, - pub admin: Pubkey, + pub admin: Address, /// Canonical bump for this PDA. pub bump: u8, } diff --git a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/tests/test_transfer_switch.rs b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/tests/test_transfer_switch.rs index 5696596ed..f81d50ba1 100644 --- a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/tests/test_transfer_switch.rs +++ b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/tests/test_transfer_switch.rs @@ -1,13 +1,10 @@ use { anchor_lang::{ - solana_program::{ - instruction::Instruction, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::{ @@ -17,11 +14,10 @@ use { }, transfer_hook::{build_hook_accounts, get_hook_accounts_address, HookAccount}, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = transfer_switch::id(); let mut svm = LiteSVM::new(); @@ -40,9 +36,9 @@ fn test_transfer_switch() { let decimals: u8 = 9; // Derive PDAs - let (admin_config, _) = Pubkey::find_program_address(&[b"admin-config"], &program_id); + let (admin_config, _) = Address::find_program_address(&[b"admin-config"], &program_id); let (sender_switch, _) = - Pubkey::find_program_address(&[sender.pubkey().as_ref()], &program_id); + Address::find_program_address(&[sender.pubkey().as_ref()], &program_id); // Step 1: Create mint with TransferHook extension let mint = create_token_extensions_mint( @@ -57,34 +53,19 @@ fn test_transfer_switch() { .unwrap(); svm.expire_blockhash(); - let extra_account_meta_list = - get_hook_accounts_address(&mint, &program_id); + let extra_account_meta_list = get_hook_accounts_address(&mint, &program_id); // Step 2: Create token accounts and mint tokens let amount: u64 = 100 * 10u64.pow(decimals as u32); - let source_ata = create_token_extensions_account( - &mut svm, - &sender.pubkey(), - &mint, - &payer, - ).unwrap(); + let source_ata = + create_token_extensions_account(&mut svm, &sender.pubkey(), &mint, &payer).unwrap(); svm.expire_blockhash(); - let dest_ata = create_token_extensions_account( - &mut svm, - &recipient.pubkey(), - &mint, - &payer, - ).unwrap(); + let dest_ata = + create_token_extensions_account(&mut svm, &recipient.pubkey(), &mint, &payer).unwrap(); svm.expire_blockhash(); - mint_tokens_to_token_extensions_account( - &mut svm, - &mint, - &source_ata, - amount, - &payer, - ).unwrap(); + mint_tokens_to_token_extensions_account(&mut svm, &mint, &source_ata, amount, &payer).unwrap(); svm.expire_blockhash(); // Step 3: Configure admin @@ -95,11 +76,17 @@ fn test_transfer_switch() { admin: payer.pubkey(), new_admin: payer.pubkey(), admin_config, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![configure_admin_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![configure_admin_ix], + &[&payer], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Step 4: Initialize extra account metas list @@ -110,11 +97,12 @@ fn test_transfer_switch() { payer: payer.pubkey(), token_mint: mint, extra_account_metas_list: extra_account_meta_list, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 5: Turn transfers OFF for sender @@ -126,11 +114,12 @@ fn test_transfer_switch() { wallet: sender.pubkey(), admin_config, wallet_switch: sender_switch, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![switch_off_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![switch_off_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 6: Try transfer - should FAIL (switch is off) @@ -154,10 +143,7 @@ fn test_transfer_switch() { decimals, &extra_accounts, ); - assert!( - result.is_err(), - "Transfer should fail when switch is off" - ); + assert!(result.is_err(), "Transfer should fail when switch is off"); svm.expire_blockhash(); // Step 7: Turn transfers ON for sender @@ -169,11 +155,12 @@ fn test_transfer_switch() { wallet: sender.pubkey(), admin_config, wallet_switch: sender_switch, - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![switch_on_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![switch_on_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 8: Transfer - should SUCCEED (switch is on) @@ -186,5 +173,6 @@ fn test_transfer_switch() { transfer_amount, decimals, &extra_accounts, - ).unwrap(); + ) + .unwrap(); } diff --git a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/Cargo.toml b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/Cargo.toml index 57df243ed..098b26a15 100644 --- a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/Cargo.toml +++ b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/Cargo.toml @@ -9,19 +9,31 @@ crate-type = ["cdylib", "lib"] name = "transfer_hook" [features] -default = [] +# `no-entrypoint` is always on: anchor then exports its dispatch as +# `__anchor_dispatch` rather than claiming the `entrypoint` symbol, leaving +# src/entrypoint.rs free to claim it and map the transfer-hook interface's +# discriminators onto this program's handlers. +default = ["no-entrypoint"] cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = "1.1.2" +anchor-lang = { version = "2.0.0-rc.1", features = ["compat"] } +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = "2.0.0-rc.1" spl-discriminator = "0.4.1" spl-tlv-account-resolution = "0.9.0" spl-transfer-hook-interface = "0.9.0" diff --git a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/entrypoint.rs b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/entrypoint.rs new file mode 100644 index 000000000..577e8f2b6 --- /dev/null +++ b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/entrypoint.rs @@ -0,0 +1,50 @@ +//! Program entrypoint, hand-written so the SPL transfer-hook interface reaches +//! this program's handlers. +//! +//! Anchor v2 gives every instruction in an executable `#[program]` the eight-byte +//! `sha256("global:")` discriminator, and `#[discrim = N]` there is limited +//! to a single byte. The transfer-hook interface calls its instructions under +//! their own eight-byte values, which leaves no way to declare those handlers +//! directly. `#[program(interface, ...)]` accepts arbitrary discriminator bytes +//! but only generates a CPI client: no dispatch, and so no deployable program. +//! +//! So the crate builds with `no-entrypoint` (which makes anchor export its +//! dispatch as `__anchor_dispatch` instead of claiming the `entrypoint` symbol) +//! and this module claims `entrypoint` itself. All it does is swap an interface +//! discriminator for the matching handler's before delegating; the payload +//! behind it is identical either way. + +use anchor_lang::pinocchio; + +pinocchio::default_allocator!(); +pinocchio::default_panic_handler!(); + +/// Interface discriminator paired with the handler's own, in declaration order. +const DISCRIMINATOR_MAP: [([u8; 8], [u8; 8]); 2] = [ + // initialize_extra_account_meta_list + ([43, 34, 13, 49, 167, 88, 235, 235], [92, 197, 174, 197, 41, 124, 19, 3]), + // transfer_hook + ([105, 37, 101, 197, 75, 251, 102, 26], [220, 57, 220, 152, 126, 125, 97, 168]), +]; + +/// # Safety +/// +/// Called only by the SBF loader, with the register convention anchor's own +/// entrypoint documents: `r1` is the start of the serialized parameter region +/// and `r2` points at the instruction data, whose length sits in the eight +/// bytes below it. +#[cfg(target_os = "solana")] +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data_ptr: *const u8) -> u64 { + let len = *(ix_data_ptr.sub(8) as *const u64) as usize; + if len >= 8 { + let discriminator = core::slice::from_raw_parts_mut(ix_data_ptr as *mut u8, 8); + for (interface, handler) in DISCRIMINATOR_MAP { + if discriminator == interface { + discriminator.copy_from_slice(&handler); + break; + } + } + } + crate::__anchor_dispatch(input, ix_data_ptr) +} diff --git a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/add_to_whitelist.rs b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/add_to_whitelist.rs index 86e100ef5..4b7d6803b 100644 --- a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/add_to_whitelist.rs +++ b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/add_to_whitelist.rs @@ -3,32 +3,33 @@ use anchor_lang::prelude::*; use crate::WhiteList; #[derive(Accounts)] -pub struct AddToWhiteListAccountConstraints<'info> { +pub struct AddToWhiteListAccountConstraints { /// CHECK: New account to add to white list #[account()] - pub new_account: UncheckedAccount<'info>, + pub new_account: UncheckedAccount, #[account( mut, seeds = [b"white_list"], bump )] - pub white_list: Account<'info, WhiteList>, + pub white_list: BorshAccount, #[account(mut)] - pub signer: Signer<'info>, + pub signer: Signer, } -pub fn handler(context: Context) -> Result<()> { - if context.accounts.white_list.authority != context.accounts.signer.key() { +pub fn handler(context: &mut Context) -> Result<()> { + if context.accounts.white_list.authority != *context.accounts.signer.address() { panic!("Only the authority can add to the white list!"); } - context.accounts + context + .accounts .white_list .white_list - .push(context.accounts.new_account.key()); + .push(*context.accounts.new_account.address()); msg!( "New account white listed! {0}", - context.accounts.new_account.key().to_string() + context.accounts.new_account.address().to_string() ); msg!( "White list length! {0}", diff --git a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs index a8c5f6def..aa692605b 100644 --- a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs +++ b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs @@ -6,14 +6,14 @@ use spl_transfer_hook_interface::instruction::ExecuteInstruction; use crate::{handle_extra_account_metas, handle_extra_account_metas_count, WhiteList}; #[derive(Accounts)] -pub struct InitializeExtraAccountMetaListAccountConstraints<'info> { +pub struct InitializeExtraAccountMetaListAccountConstraints { #[account(mut)] - payer: Signer<'info>, + payer: Signer, /// CHECK: ExtraAccountMetaList Account, must use these seeds #[account( init, - seeds = [b"extra-account-metas", mint.key().as_ref()], + seeds = [b"extra-account-metas", mint.address().as_ref()], bump, // size_of returns Result with spl's ProgramError - unwrap is safe for known-good input space = ExtraAccountMetaList::size_of( @@ -21,16 +21,18 @@ pub struct InitializeExtraAccountMetaListAccountConstraints<'info> { ).unwrap(), payer = payer )] - pub extra_account_meta_list: UncheckedAccount<'info>, - pub mint: InterfaceAccount<'info, Mint>, - pub system_program: Program<'info, System>, + pub extra_account_meta_list: UncheckedAccount, + pub mint: InterfaceAccount, + pub system_program: Program, #[account(init_if_needed, seeds = [b"white_list"], bump, payer = payer, space = WhiteList::DISCRIMINATOR.len() + WhiteList::INIT_SPACE)] - pub white_list: Account<'info, WhiteList>, + pub white_list: BorshAccount, } -pub fn handler(mut context: Context) -> Result<()> { +pub fn handler( + mut context: &mut Context, +) -> Result<()> { // set authority field on white_list account as payer address - context.accounts.white_list.authority = context.accounts.payer.key(); + context.accounts.white_list.authority = *context.accounts.payer.address(); context.accounts.white_list.bump = context.bumps.white_list; let extra_account_metas = handle_extra_account_metas()?; @@ -38,9 +40,11 @@ pub fn handler(mut context: Context( - &mut context.accounts.extra_account_meta_list.try_borrow_mut_data()?, - &extra_account_metas, - ).map_err(|_| ProgramError::InvalidAccountData)?; + // `AccountView` is Copy, and a copy still points at the same backing + // buffer, so the borrow writes through to the real account. + let mut meta_list_view = *context.accounts.extra_account_meta_list.account(); + let mut meta_list_data = meta_list_view.try_borrow_mut()?; + ExtraAccountMetaList::init::(&mut meta_list_data, &extra_account_metas) + .map_err(|_| ProgramError::InvalidAccountData)?; Ok(()) } diff --git a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs index d0279a605..4fb4d9cae 100644 --- a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::token; use anchor_spl::token_interface::{Mint, TokenAccount}; use crate::{check_is_transferring, WhiteList}; @@ -8,22 +9,22 @@ use crate::{check_is_transferring, WhiteList}; // Remaining accounts are the extra accounts required from the ExtraAccountMetaList account // These accounts are provided via CPI to this program from the token2022 program #[derive(Accounts)] -pub struct TransferHookAccountConstraints<'info> { +pub struct TransferHookAccountConstraints { #[account(token::mint = mint, token::authority = owner)] - pub source_token: InterfaceAccount<'info, TokenAccount>, - pub mint: InterfaceAccount<'info, Mint>, + pub source_token: InterfaceAccount, + pub mint: InterfaceAccount, #[account(token::mint = mint)] - pub destination_token: InterfaceAccount<'info, TokenAccount>, + pub destination_token: InterfaceAccount, /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program - pub owner: UncheckedAccount<'info>, + pub owner: UncheckedAccount, /// CHECK: ExtraAccountMetaList Account, - #[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)] - pub extra_account_meta_list: UncheckedAccount<'info>, + #[account(seeds = [b"extra-account-metas", mint.address().as_ref()], bump)] + pub extra_account_meta_list: UncheckedAccount, #[account(seeds = [b"white_list"], bump)] - pub white_list: Account<'info, WhiteList>, + pub white_list: BorshAccount, } -pub fn handler(context: Context, _amount: u64) -> Result<()> { +pub fn handler(context: &mut Context, _amount: u64) -> Result<()> { // Fail this instruction if it is not called from within a transfer hook check_is_transferring(&context)?; @@ -31,7 +32,7 @@ pub fn handler(context: Context, _amount: u64) - .accounts .white_list .white_list - .contains(&context.accounts.destination_token.key()) + .contains(&context.accounts.destination_token.address()) { panic!("Account not in white list!"); } diff --git a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/lib.rs b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/lib.rs index 08089435b..315d898da 100644 --- a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/lib.rs @@ -3,13 +3,12 @@ use std::cell::RefMut; use anchor_lang::prelude::*; use anchor_spl::token_2022::spl_token_2022::{ extension::{ - transfer_hook::TransferHookAccount, BaseStateWithExtensionsMut, - PodStateWithExtensionsMut, + transfer_hook::TransferHookAccount, BaseStateWithExtensions, PodStateWithExtensions, }, pod::PodAccount, }; -use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed}; use spl_discriminator::SplDiscriminate; +use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed}; use spl_transfer_hook_interface::instruction::{ ExecuteInstruction, InitializeExtraAccountMetaListInstruction, }; @@ -25,35 +24,47 @@ pub enum TransferError { IsNotCurrentlyTransferring, } +pub mod entrypoint; + +// v2's `#[program(interface, ...)]` declares an interface for other programs to +// CPI into and emits no entrypoint, and an executable `#[program]` only accepts +// one-byte custom discriminators, so the transfer-hook interface's eight-byte +// discriminators have no direct spelling. `entrypoint` bridges the gap: it maps +// each of them onto a handler before anchor's dispatch runs. #[program] pub mod transfer_hook { use super::*; - #[instruction(discriminator = InitializeExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE)] + // sha256("spl-transfer-hook-interface:initialize-extra-account-metas")[..8] pub fn initialize_extra_account_meta_list( - context: Context, + context: &mut Context, ) -> Result<()> { instructions::initialize_extra_account_meta_list::handler(context) } - #[instruction(discriminator = ExecuteInstruction::SPL_DISCRIMINATOR_SLICE)] - pub fn transfer_hook(context: Context, amount: u64) -> Result<()> { + // sha256("spl-transfer-hook-interface:execute")[..8] + pub fn transfer_hook( + context: &mut Context, + amount: u64, + ) -> Result<()> { instructions::transfer_hook::handler(context, amount) } - pub fn add_to_whitelist(context: Context) -> Result<()> { + pub fn add_to_whitelist(context: &mut Context) -> Result<()> { instructions::add_to_whitelist::handler(context) } } pub fn check_is_transferring(context: &Context) -> Result<()> { - let source_token_info = context.accounts.source_token.to_account_info(); - let mut account_data_ref: RefMut<&mut [u8]> = source_token_info.try_borrow_mut_data()?; + // Read-only: the account already holds a shared borrow of its buffer, and a + // second shared borrow is fine where `try_borrow_mut` would be rejected. + let account_data_ref = context.accounts.source_token.account().try_borrow()?; // .map_err() needed because spl-token-2022 uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types - let mut account = PodStateWithExtensionsMut::::unpack(*account_data_ref) + // while anchor-lang uses 3.x - structurally identical but different semver types + let account = PodStateWithExtensions::::unpack(&account_data_ref) .map_err(|_| ProgramError::InvalidAccountData)?; - let account_extension = account.get_extension_mut::() + let account_extension = account + .get_extension::() .map_err(|_| ProgramError::InvalidAccountData)?; if !bool::from(account_extension.transferring) { @@ -73,7 +84,8 @@ pub fn handle_extra_account_metas() -> Result> { }], false, // is_signer true, // is_writable - ).map_err(|_| ProgramError::InvalidArgument)?]) + ) + .map_err(|_| ProgramError::InvalidArgument)?]) } /// Returns the count of extra account metas (avoids the error conversion issue in #[account] attributes) @@ -81,12 +93,12 @@ pub fn handle_extra_account_metas_count() -> usize { 1 // one extra account: the whitelist PDA } -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct WhiteList { - pub authority: Pubkey, + pub authority: Address, #[max_len(11)] - pub white_list: Vec, + pub white_list: Vec
, /// Canonical bump for this PDA. pub bump: u8, } diff --git a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/tests/test_transfer_hook.rs b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/tests/test_transfer_hook.rs index 8911293e8..ece754b6d 100644 --- a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/tests/test_transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/tests/test_transfer_hook.rs @@ -1,13 +1,10 @@ use { anchor_lang::{ - solana_program::{ - instruction::Instruction, - pubkey::Pubkey, - system_program, - }, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, + solana_keypair::Keypair, solana_kite::{ create_wallet, send_transaction_from_instructions, token_extensions::{ @@ -17,11 +14,10 @@ use { }, transfer_hook::{build_hook_accounts, get_hook_accounts_address, HookAccount}, }, - solana_keypair::Keypair, solana_signer::Signer, }; -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = transfer_hook::id(); let mut svm = LiteSVM::new(); @@ -39,7 +35,7 @@ fn test_whitelist_transfer_hook() { let decimals: u8 = 9; // Derive PDAs - let (white_list_pda, _) = Pubkey::find_program_address(&[b"white_list"], &program_id); + let (white_list_pda, _) = Address::find_program_address(&[b"white_list"], &program_id); // Step 1: Create mint with TransferHook extension let mint = create_token_extensions_mint( @@ -54,34 +50,19 @@ fn test_whitelist_transfer_hook() { .unwrap(); svm.expire_blockhash(); - let extra_account_meta_list = - get_hook_accounts_address(&mint, &program_id); + let extra_account_meta_list = get_hook_accounts_address(&mint, &program_id); // Step 2: Create token accounts and mint tokens let amount: u64 = 100 * 10u64.pow(decimals as u32); - let source_ata = create_token_extensions_account( - &mut svm, - &payer.pubkey(), - &mint, - &payer, - ).unwrap(); + let source_ata = + create_token_extensions_account(&mut svm, &payer.pubkey(), &mint, &payer).unwrap(); svm.expire_blockhash(); - let dest_ata = create_token_extensions_account( - &mut svm, - &recipient.pubkey(), - &mint, - &payer, - ).unwrap(); + let dest_ata = + create_token_extensions_account(&mut svm, &recipient.pubkey(), &mint, &payer).unwrap(); svm.expire_blockhash(); - mint_tokens_to_token_extensions_account( - &mut svm, - &mint, - &source_ata, - amount, - &payer, - ).unwrap(); + mint_tokens_to_token_extensions_account(&mut svm, &mint, &source_ata, amount, &payer).unwrap(); svm.expire_blockhash(); // Step 3: Initialize ExtraAccountMetaList (also creates whitelist) @@ -92,12 +73,13 @@ fn test_whitelist_transfer_hook() { payer: payer.pubkey(), extra_account_meta_list, mint, - system_program: system_program::id(), + system_program: system_program::ID, white_list: white_list_pda, } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions(&mut svm, vec![init_extra_ix], &[&payer], &payer.pubkey()) + .unwrap(); svm.expire_blockhash(); // Step 4: Add destination token account to whitelist @@ -111,7 +93,13 @@ fn test_whitelist_transfer_hook() { } .to_account_metas(None), ); - send_transaction_from_instructions(&mut svm, vec![add_to_whitelist_ix], &[&payer], &payer.pubkey()).unwrap(); + send_transaction_from_instructions( + &mut svm, + vec![add_to_whitelist_ix], + &[&payer], + &payer.pubkey(), + ) + .unwrap(); svm.expire_blockhash(); // Step 5: Transfer - should succeed (destination is whitelisted) @@ -134,5 +122,6 @@ fn test_whitelist_transfer_hook() { transfer_amount, decimals, &extra_accounts, - ).unwrap(); + ) + .unwrap(); } diff --git a/tokens/token-minter/anchor/programs/token-minter/Cargo.toml b/tokens/token-minter/anchor/programs/token-minter/Cargo.toml index aabe1312a..b35258de5 100644 --- a/tokens/token-minter/anchor/programs/token-minter/Cargo.toml +++ b/tokens/token-minter/anchor/programs/token-minter/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = { version = "1.1.2", features = ["metadata"] } +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = { version = "2.0.0-rc.1", features = ["metadata"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/token-minter/anchor/programs/token-minter/src/instructions/create.rs b/tokens/token-minter/anchor/programs/token-minter/src/instructions/create.rs index 95be34e0d..bc423c828 100644 --- a/tokens/token-minter/anchor/programs/token-minter/src/instructions/create.rs +++ b/tokens/token-minter/anchor/programs/token-minter/src/instructions/create.rs @@ -5,60 +5,64 @@ use { create_metadata_accounts_v3, mpl_token_metadata::types::DataV2, CreateMetadataAccountsV3, Metadata, }, - token::{Mint, Token}, + mint::{self, Mint}, + token::{self, Token}, }, }; #[derive(Accounts)] -pub struct CreateTokenAccountConstraints<'info> { +pub struct CreateTokenAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account( init, payer = payer, mint::decimals = 9, - mint::authority = payer.key(), - mint::freeze_authority = payer.key(), + mint::authority = payer, + mint::freeze_authority = payer, )] - pub mint_account: Account<'info, Mint>, + pub mint_account: Account, /// CHECK: Validate address by deriving pda #[account( mut, - seeds = [b"metadata", token_metadata_program.key().as_ref(), mint_account.key().as_ref()], + seeds = [b"metadata", token_metadata_program.address().as_ref(), mint_account.address().as_ref()], bump, - seeds::program = token_metadata_program.key(), + seeds::program = token_metadata_program.address(), )] - pub metadata_account: UncheckedAccount<'info>, + pub metadata_account: UncheckedAccount, - pub token_program: Program<'info, Token>, - pub token_metadata_program: Program<'info, Metadata>, - pub system_program: Program<'info, System>, - pub rent: Sysvar<'info, Rent>, + pub token_program: Program, + pub token_metadata_program: Program, + pub system_program: Program, + pub rent: Sysvar, } pub fn handle_create_token( - context: Context, + context: &mut Context, token_name: String, token_symbol: String, token_uri: String, ) -> Result<()> { + // `AccountView` is Copy, and a copy still points at the same + // account. v2's typed handles make the aliasing a compile error. + let payer_view = *context.accounts.payer.account(); msg!("Creating metadata account"); // Cross Program Invocation (CPI) // Invoking the create_metadata_account_v3 instruction on the token metadata program create_metadata_accounts_v3( CpiContext::new( - context.accounts.token_metadata_program.key(), + context.accounts.token_metadata_program.address(), CreateMetadataAccountsV3 { - metadata: context.accounts.metadata_account.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - mint_authority: context.accounts.payer.to_account_info(), - update_authority: context.accounts.payer.to_account_info(), - payer: context.accounts.payer.to_account_info(), - system_program: context.accounts.system_program.to_account_info(), - rent: context.accounts.rent.to_account_info(), + metadata: context.accounts.metadata_account.cpi_handle_mut(), + mint: context.accounts.mint_account.cpi_handle(), + mint_authority: CpiHandle::readonly(&payer_view), + update_authority: CpiHandle::readonly(&payer_view), + payer: context.accounts.payer.cpi_handle_mut(), + system_program: context.accounts.system_program.cpi_handle(), + update_authority_is_signer: true, }, ), DataV2 { @@ -71,7 +75,6 @@ pub fn handle_create_token( uses: None, }, false, // Is mutable - true, // Update authority is signer None, // Collection details )?; diff --git a/tokens/token-minter/anchor/programs/token-minter/src/instructions/mint.rs b/tokens/token-minter/anchor/programs/token-minter/src/instructions/mint.rs index 7d731da01..68a650163 100644 --- a/tokens/token-minter/anchor/programs/token-minter/src/instructions/mint.rs +++ b/tokens/token-minter/anchor/programs/token-minter/src/instructions/mint.rs @@ -7,14 +7,18 @@ use { }; #[derive(Accounts)] -pub struct MintTokenAccountConstraints<'info> { - #[account(mut)] - pub mint_authority: Signer<'info>, +pub struct MintTokenAccountConstraints { + // Minting to yourself is ordinary, so `mint_authority` and `recipient` may + // be the same account. v2 rejects an account that appears twice while any + // of its slots is in the mutable mask, and `unsafe(dup)` takes this one out + // of that mask while keeping it writable (it is still the init payer). + #[account(unsafe(dup))] + pub mint_authority: Signer, - pub recipient: SystemAccount<'info>, + pub recipient: SystemAccount, #[account(mut)] - pub mint_account: Account<'info, Mint>, + pub mint_account: Account, #[account( init_if_needed, @@ -22,11 +26,11 @@ pub struct MintTokenAccountConstraints<'info> { associated_token::mint = mint_account, associated_token::authority = recipient, )] - pub associated_token_account: Account<'info, TokenAccount>, + pub associated_token_account: Account, - pub token_program: Program<'info, Token>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Program, + pub associated_token_program: Program, + pub system_program: Program, } /// Mints `amount` tokens to the recipient's associated token account. @@ -35,24 +39,24 @@ pub struct MintTokenAccountConstraints<'info> { /// on). Clients convert from major units, e.g. 1 token with 9 decimals is /// `1 * 10u64.pow(9)` minor units. pub fn handle_mint_token( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { msg!("Minting tokens to associated token account..."); - msg!("Mint: {}", &context.accounts.mint_account.key()); + msg!("Mint: {}", &context.accounts.mint_account.address()); msg!( "Token Address: {}", - &context.accounts.associated_token_account.key() + &context.accounts.associated_token_account.address() ); // Invoke the mint_to instruction on the token program mint_to( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), MintTo { - mint: context.accounts.mint_account.to_account_info(), - to: context.accounts.associated_token_account.to_account_info(), - authority: context.accounts.mint_authority.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), + to: context.accounts.associated_token_account.cpi_handle_mut(), + authority: context.accounts.mint_authority.cpi_handle(), }, ), amount, diff --git a/tokens/token-minter/anchor/programs/token-minter/src/lib.rs b/tokens/token-minter/anchor/programs/token-minter/src/lib.rs index 8033bddbb..ff143d221 100644 --- a/tokens/token-minter/anchor/programs/token-minter/src/lib.rs +++ b/tokens/token-minter/anchor/programs/token-minter/src/lib.rs @@ -10,7 +10,7 @@ pub mod token_minter { use super::*; pub fn create_token( - context: Context, + context: &mut Context, token_name: String, token_symbol: String, token_uri: String, @@ -20,7 +20,7 @@ pub mod token_minter { /// Mint `amount` minor units of the token to the recipient. pub fn mint_token( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { mint::handle_mint_token(context, amount) diff --git a/tokens/token-minter/anchor/programs/token-minter/tests/test_token_minter.rs b/tokens/token-minter/anchor/programs/token-minter/tests/test_token_minter.rs index 487a185f3..389cff2b0 100644 --- a/tokens/token-minter/anchor/programs/token-minter/tests/test_token_minter.rs +++ b/tokens/token-minter/anchor/programs/token-minter/tests/test_token_minter.rs @@ -1,13 +1,11 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, - solana_kite::{ - create_wallet, get_token_account_balance, send_transaction_from_instructions, - }, + solana_kite::{create_wallet, get_token_account_balance, send_transaction_from_instructions}, solana_signer::Signer, }; @@ -21,52 +19,48 @@ fn to_minor_units(major_units: u64) -> u64 { major_units.checked_mul(10u64.pow(MINT_DECIMALS)).unwrap() } -fn metadata_program_id() -> Pubkey { +fn metadata_program_id() -> Address { "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" .parse() .unwrap() } -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn associated_token_program_id() -> Pubkey { +fn associated_token_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn rent_sysvar_id() -> Pubkey { +fn rent_sysvar_id() -> Address { "SysvarRent111111111111111111111111111111111" .parse() .unwrap() } -fn derive_metadata_pda(mint: &Pubkey) -> Pubkey { +fn derive_metadata_pda(mint: &Address) -> Address { let metadata_pid = metadata_program_id(); - let (pda, _bump) = Pubkey::find_program_address( + let (pda, _bump) = Address::find_program_address( &[b"metadata", metadata_pid.as_ref(), mint.as_ref()], &metadata_pid, ); pda } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - let (ata, _bump) = Pubkey::find_program_address( - &[ - wallet.as_ref(), - token_program_id().as_ref(), - mint.as_ref(), - ], +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + let (ata, _bump) = Address::find_program_address( + &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &associated_token_program_id(), ); ata } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = token_minter::id(); let mut svm = LiteSVM::new(); @@ -101,7 +95,7 @@ fn test_create_token() { metadata_account, token_program: token_program_id(), token_metadata_program: metadata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, rent: rent_sysvar_id(), } .to_account_metas(None), @@ -148,7 +142,7 @@ fn test_create_and_mint_tokens() { metadata_account, token_program: token_program_id(), token_metadata_program: metadata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, rent: rent_sysvar_id(), } .to_account_metas(None), @@ -178,17 +172,12 @@ fn test_create_and_mint_tokens() { associated_token_account: ata, token_program: token_program_id(), associated_token_program: associated_token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions( - &mut svm, - vec![mint_ix], - &[&payer], - &payer.pubkey(), - ) - .unwrap(); + send_transaction_from_instructions(&mut svm, vec![mint_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify 100 tokens minted (in minor units) let balance = get_token_account_balance(&svm, &ata).unwrap(); diff --git a/tokens/transfer-tokens/anchor/programs/transfer-tokens/Cargo.toml b/tokens/transfer-tokens/anchor/programs/transfer-tokens/Cargo.toml index e846a7232..52dbac2a8 100644 --- a/tokens/transfer-tokens/anchor/programs/transfer-tokens/Cargo.toml +++ b/tokens/transfer-tokens/anchor/programs/transfer-tokens/Cargo.toml @@ -14,14 +14,22 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] -anchor-lang = { version = "1.1.2", features = ["init-if-needed"] } -anchor-spl = { version = "1.1.2", features = ["metadata"] } +anchor-lang = "2.0.0-rc.1" +# The `#[program]` macro expands to `wincode` paths for instruction-data +# (de)serialization, so the crate has to be a direct dependency. +wincode = { version = "0.5", features = ["derive"] } +# anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 +# moved to wincode 0.6. With both in the graph, `Address`'s wincode impls +# belong to the version the account derives are not using and every +# `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. +solana-address = ">=2.6, <2.7" +anchor-spl = { version = "2.0.0-rc.1", features = ["metadata"] } [dev-dependencies] litesvm = "0.13.1" diff --git a/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/create.rs b/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/create.rs index 6670117d2..f2865ad74 100644 --- a/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/create.rs +++ b/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/create.rs @@ -5,61 +5,65 @@ use { create_metadata_accounts_v3, mpl_token_metadata::types::DataV2, CreateMetadataAccountsV3, Metadata, }, + mint, token_interface::{Mint, TokenInterface}, }, }; #[derive(Accounts)] -pub struct CreateTokenAccountConstraints<'info> { +pub struct CreateTokenAccountConstraints { #[account(mut)] - pub payer: Signer<'info>, + pub payer: Signer, #[account( init, payer = payer, mint::decimals = 9, - mint::authority = payer.key(), - mint::freeze_authority = payer.key(), + mint::authority = payer, + mint::freeze_authority = payer, mint::token_program = token_program, )] - pub mint_account: InterfaceAccount<'info, Mint>, + pub mint_account: InterfaceAccount, /// CHECK: Validate address by deriving pda #[account( mut, - seeds = [b"metadata", token_metadata_program.key().as_ref(), mint_account.key().as_ref()], + seeds = [b"metadata", token_metadata_program.address().as_ref(), mint_account.address().as_ref()], bump, - seeds::program = token_metadata_program.key(), + seeds::program = token_metadata_program.address(), )] - pub metadata_account: UncheckedAccount<'info>, + pub metadata_account: UncheckedAccount, - pub token_program: Interface<'info, TokenInterface>, - pub token_metadata_program: Program<'info, Metadata>, - pub system_program: Program<'info, System>, - pub rent: Sysvar<'info, Rent>, + pub token_program: Interface<'static, TokenInterface>, + pub token_metadata_program: Program, + pub system_program: Program, + pub rent: Sysvar, } pub fn handle_create_token( - context: Context, + context: &mut Context, token_name: String, token_symbol: String, token_uri: String, ) -> Result<()> { + // `AccountView` is Copy, and a copy still points at the same + // account. v2's typed handles make the aliasing a compile error. + let payer_view = *context.accounts.payer.account(); msg!("Creating metadata account"); // Cross Program Invocation (CPI) // Invoking the create_metadata_account_v3 instruction on the token metadata program create_metadata_accounts_v3( CpiContext::new( - context.accounts.token_metadata_program.key(), + context.accounts.token_metadata_program.address(), CreateMetadataAccountsV3 { - metadata: context.accounts.metadata_account.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - mint_authority: context.accounts.payer.to_account_info(), - update_authority: context.accounts.payer.to_account_info(), - payer: context.accounts.payer.to_account_info(), - system_program: context.accounts.system_program.to_account_info(), - rent: context.accounts.rent.to_account_info(), + metadata: context.accounts.metadata_account.cpi_handle_mut(), + mint: context.accounts.mint_account.cpi_handle(), + mint_authority: CpiHandle::readonly(&payer_view), + update_authority: CpiHandle::readonly(&payer_view), + payer: context.accounts.payer.cpi_handle_mut(), + system_program: context.accounts.system_program.cpi_handle(), + update_authority_is_signer: true, }, ), DataV2 { @@ -72,7 +76,6 @@ pub fn handle_create_token( uses: None, }, false, // Is mutable - true, // Update authority is signer None, // Collection details )?; diff --git a/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/mint.rs b/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/mint.rs index f57df043a..894f735cc 100644 --- a/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/mint.rs +++ b/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/mint.rs @@ -7,14 +7,18 @@ use { }; #[derive(Accounts)] -pub struct MintTokenAccountConstraints<'info> { - #[account(mut)] - pub mint_authority: Signer<'info>, +pub struct MintTokenAccountConstraints { + // The mint authority and the recipient may be the same account (minting or + // sending to yourself). v2 rejects an account that appears twice while any + // of its slots is in the mutable mask, and `unsafe(dup)` takes this one out + // of that mask while keeping it writable. + #[account(unsafe(dup))] + pub mint_authority: Signer, - pub recipient: SystemAccount<'info>, + pub recipient: SystemAccount, #[account(mut)] - pub mint_account: InterfaceAccount<'info, Mint>, + pub mint_account: InterfaceAccount, #[account( init_if_needed, @@ -23,11 +27,11 @@ pub struct MintTokenAccountConstraints<'info> { associated_token::authority = recipient, associated_token::token_program = token_program, )] - pub associated_token_account: InterfaceAccount<'info, TokenAccount>, + pub associated_token_account: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } /// Mints `amount` tokens to the recipient's associated token account. @@ -36,24 +40,24 @@ pub struct MintTokenAccountConstraints<'info> { /// on). Clients convert from major units, e.g. 1 token with 9 decimals is /// `1 * 10u64.pow(9)` minor units. pub fn handle_mint_token( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { msg!("Minting tokens to associated token account..."); - msg!("Mint: {}", &context.accounts.mint_account.key()); + msg!("Mint: {}", &context.accounts.mint_account.address()); msg!( "Token Address: {}", - &context.accounts.associated_token_account.key() + &context.accounts.associated_token_account.address() ); // Invoke the mint_to instruction on the token program mint_to( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), MintTo { - mint: context.accounts.mint_account.to_account_info(), - to: context.accounts.associated_token_account.to_account_info(), - authority: context.accounts.mint_authority.to_account_info(), + mint: context.accounts.mint_account.cpi_handle_mut(), + to: context.accounts.associated_token_account.cpi_handle_mut(), + authority: context.accounts.mint_authority.cpi_handle(), }, ), amount, diff --git a/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/transfer.rs b/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/transfer.rs index b471124df..ee4039127 100644 --- a/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/transfer.rs +++ b/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/instructions/transfer.rs @@ -7,14 +7,14 @@ use { }; #[derive(Accounts)] -pub struct TransferTokensAccountConstraints<'info> { +pub struct TransferTokensAccountConstraints { #[account(mut)] - pub sender: Signer<'info>, + pub sender: Signer, - pub recipient: SystemAccount<'info>, + pub recipient: SystemAccount, #[account(mut)] - pub mint_account: InterfaceAccount<'info, Mint>, + pub mint_account: InterfaceAccount, #[account( mut, @@ -22,7 +22,7 @@ pub struct TransferTokensAccountConstraints<'info> { associated_token::authority = sender, associated_token::token_program = token_program, )] - pub sender_token_account: InterfaceAccount<'info, TokenAccount>, + pub sender_token_account: InterfaceAccount, #[account( init_if_needed, @@ -31,11 +31,11 @@ pub struct TransferTokensAccountConstraints<'info> { associated_token::authority = recipient, associated_token::token_program = token_program, )] - pub recipient_token_account: InterfaceAccount<'info, TokenAccount>, + pub recipient_token_account: InterfaceAccount, - pub token_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'static, TokenInterface>, + pub associated_token_program: Program, + pub system_program: Program, } /// Transfers `amount` tokens from the sender's to the recipient's associated @@ -47,36 +47,36 @@ pub struct TransferTokensAccountConstraints<'info> { /// decimals through the CPI so a wrong-mint or wrong-decimals account fails /// the CPI instead of silently moving the wrong quantity. pub fn handle_transfer_tokens( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { msg!("Transferring tokens..."); - msg!( - "Mint: {}", - &context.accounts.mint_account.to_account_info().key() - ); + msg!("Mint: {}", context.accounts.mint_account.address()); msg!( "From Token Address: {}", - &context.accounts.sender_token_account.key() + &context.accounts.sender_token_account.address() ); msg!( "To Token Address: {}", - &context.accounts.recipient_token_account.key() + &context.accounts.recipient_token_account.address() ); // Invoke the transfer_checked instruction on the token program transfer_checked( CpiContext::new( - context.accounts.token_program.key(), + context.accounts.token_program.address(), TransferChecked { - from: context.accounts.sender_token_account.to_account_info(), - mint: context.accounts.mint_account.to_account_info(), - to: context.accounts.recipient_token_account.to_account_info(), - authority: context.accounts.sender.to_account_info(), + from: context.accounts.sender_token_account.cpi_handle_mut(), + // Read-only slots take the wrapper's own handle: on a data account + // it relaxes the runtime borrow check that a hand-built handle + // over a copy of the view would still trip. + mint: context.accounts.mint_account.cpi_handle(), + to: context.accounts.recipient_token_account.cpi_handle_mut(), + authority: context.accounts.sender.cpi_handle(), }, ), amount, - context.accounts.mint_account.decimals, + context.accounts.mint_account.decimals(), )?; msg!("Tokens transferred successfully."); diff --git a/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/lib.rs b/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/lib.rs index 3d008421e..06ba2f8b9 100644 --- a/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/lib.rs +++ b/tokens/transfer-tokens/anchor/programs/transfer-tokens/src/lib.rs @@ -11,7 +11,7 @@ pub mod transfer_tokens { use super::*; pub fn create_token( - context: Context, + context: &mut Context, token_title: String, token_symbol: String, token_uri: String, @@ -21,7 +21,7 @@ pub mod transfer_tokens { /// Mint `amount` minor units of the token to the recipient. pub fn mint_token( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { mint::handle_mint_token(context, amount) @@ -29,7 +29,7 @@ pub mod transfer_tokens { /// Transfer `amount` minor units of the token from sender to recipient. pub fn transfer_tokens( - context: Context, + context: &mut Context, amount: u64, ) -> Result<()> { transfer::handle_transfer_tokens(context, amount) diff --git a/tokens/transfer-tokens/anchor/programs/transfer-tokens/tests/test_transfer_tokens.rs b/tokens/transfer-tokens/anchor/programs/transfer-tokens/tests/test_transfer_tokens.rs index bf0052e93..55a1c6e35 100644 --- a/tokens/transfer-tokens/anchor/programs/transfer-tokens/tests/test_transfer_tokens.rs +++ b/tokens/transfer-tokens/anchor/programs/transfer-tokens/tests/test_transfer_tokens.rs @@ -1,7 +1,7 @@ use { anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, system_program}, - InstructionData, ToAccountMetas, + solana_program::instruction::Instruction, system_program, Address, InstructionData, + ToAccountMetas, }, litesvm::LiteSVM, solana_keypair::Keypair, @@ -19,52 +19,48 @@ fn to_minor_units(major_units: u64) -> u64 { major_units.checked_mul(10u64.pow(MINT_DECIMALS)).unwrap() } -fn metadata_program_id() -> Pubkey { +fn metadata_program_id() -> Address { "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" .parse() .unwrap() } -fn token_program_id() -> Pubkey { +fn token_program_id() -> Address { "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" .parse() .unwrap() } -fn associated_token_program_id() -> Pubkey { +fn associated_token_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() .unwrap() } -fn rent_sysvar_id() -> Pubkey { +fn rent_sysvar_id() -> Address { "SysvarRent111111111111111111111111111111111" .parse() .unwrap() } -fn derive_metadata_pda(mint: &Pubkey) -> Pubkey { +fn derive_metadata_pda(mint: &Address) -> Address { let metadata_pid = metadata_program_id(); - let (pda, _bump) = Pubkey::find_program_address( + let (pda, _bump) = Address::find_program_address( &[b"metadata", metadata_pid.as_ref(), mint.as_ref()], &metadata_pid, ); pda } -fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { - let (ata, _bump) = Pubkey::find_program_address( - &[ - wallet.as_ref(), - token_program_id().as_ref(), - mint.as_ref(), - ], +fn derive_ata(wallet: &Address, mint: &Address) -> Address { + let (ata, _bump) = Address::find_program_address( + &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()], &associated_token_program_id(), ); ata } -fn setup() -> (LiteSVM, Pubkey, Keypair) { +fn setup() -> (LiteSVM, Address, Keypair) { let program_id = transfer_tokens::id(); let mut svm = LiteSVM::new(); @@ -100,7 +96,7 @@ fn test_create_mint_and_transfer() { metadata_account, token_program: token_program_id(), token_metadata_program: metadata_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, rent: rent_sysvar_id(), } .to_account_metas(None), @@ -136,17 +132,12 @@ fn test_create_mint_and_transfer() { associated_token_account: sender_ata, token_program: token_program_id(), associated_token_program: associated_token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions( - &mut svm, - vec![mint_ix], - &[&payer], - &payer.pubkey(), - ) - .unwrap(); + send_transaction_from_instructions(&mut svm, vec![mint_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify 100 tokens minted (in minor units) assert_eq!( @@ -173,17 +164,12 @@ fn test_create_mint_and_transfer() { recipient_token_account: recipient_ata, token_program: token_program_id(), associated_token_program: associated_token_program_id(), - system_program: system_program::id(), + system_program: system_program::ID, } .to_account_metas(None), ); - send_transaction_from_instructions( - &mut svm, - vec![transfer_ix], - &[&payer], - &payer.pubkey(), - ) - .unwrap(); + send_transaction_from_instructions(&mut svm, vec![transfer_ix], &[&payer], &payer.pubkey()) + .unwrap(); // Verify: sender 50 tokens, recipient 50 tokens (in minor units) assert_eq!(