From fe4c7a96da393808946d0ffdb9ef44a5da9d8ef0 Mon Sep 17 00:00:00 2001 From: bristinWild Date: Wed, 27 May 2026 15:04:28 +0530 Subject: [PATCH] feat(token): add mint authority model to token program MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional mint authority to fungible tokens for controlled supply: create with a designated minter, mint additional supply, rotate the authority to a new key, or permanently revoke it to fix the supply. The authority is stored inline on `TokenDefinition::Fungible` as `authority: Option` (`Some(id)` = mintable by `id`, `None` = fixed supply). Keeping it a plain `Option` rather than a custom wrapper type leaves account state decodable by `spel inspect`; the require/rotate/revoke guard logic lives inline in the handlers. LEZ rejects a transaction that lists the same account id twice, so one instruction cannot statically express both "the definition account is the authority and signs" (self/PDA authority) and "a distinct rotated account signs" (external authority) — they need opposite signer markers. Each privileged operation is therefore split into a self and an external variant: - `Mint` / `SetAuthority` — the definition account is the signer. - `MintWithAuthority` / `SetAuthorityWithAuthority` — a distinct authority account is the signer; the definition account does not sign. Creation via `NewFungibleDefinition { mint_authority, .. }`; an all-zero authority id is rejected. The AMM's LP token uses self/PDA authority — its stored authority is the LP definition PDA, minted only by the pool via chained calls. Covered by token unit tests and zkVM integration tests: creation with and without an authority, self- and external-authority mint, rotation, and external rotate/revoke. IDLs regenerated. --- Cargo.lock | 16 +- artifacts/amm-idl.json | 6 + artifacts/ata-idl.json | 6 + artifacts/stablecoin-idl.json | 6 + artifacts/token-idl.json | 85 ++++ programs/amm/methods/guest/Cargo.lock | 459 ++++++------------ programs/amm/src/new_definition.rs | 5 +- programs/amm/src/tests.rs | 8 +- programs/ata/src/tests.rs | 1 + programs/integration_tests/tests/amm.rs | 8 + programs/integration_tests/tests/ata.rs | 3 + .../integration_tests/tests/stablecoin.rs | 2 + programs/integration_tests/tests/token.rs | 342 +++++++++++++ programs/stablecoin/src/tests.rs | 3 + programs/token/core/src/lib.rs | 62 ++- programs/token/methods/guest/src/bin/token.rs | 130 ++++- programs/token/src/burn.rs | 1 + programs/token/src/lib.rs | 1 + programs/token/src/mint.rs | 85 +++- programs/token/src/new_definition.rs | 27 +- programs/token/src/set_authority.rs | 97 ++++ programs/token/src/tests.rs | 448 ++++++++++++++++- 22 files changed, 1448 insertions(+), 353 deletions(-) create mode 100644 programs/token/src/set_authority.rs diff --git a/Cargo.lock b/Cargo.lock index b7888ad..a4aec91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2265,9 +2265,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] @@ -3364,9 +3364,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -3935,9 +3935,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", "num-conv", @@ -3955,9 +3955,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.30" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", diff --git a/artifacts/amm-idl.json b/artifacts/amm-idl.json index a27829e..dbe1260 100644 --- a/artifacts/amm-idl.json +++ b/artifacts/amm-idl.json @@ -666,6 +666,12 @@ "type": { "option": "account_id" } + }, + { + "name": "authority", + "type": { + "option": "account_id" + } } ] }, diff --git a/artifacts/ata-idl.json b/artifacts/ata-idl.json index 1222abe..2ca5b4a 100644 --- a/artifacts/ata-idl.json +++ b/artifacts/ata-idl.json @@ -120,6 +120,12 @@ "type": { "option": "account_id" } + }, + { + "name": "authority", + "type": { + "option": "account_id" + } } ] }, diff --git a/artifacts/stablecoin-idl.json b/artifacts/stablecoin-idl.json index 2488061..2d57776 100644 --- a/artifacts/stablecoin-idl.json +++ b/artifacts/stablecoin-idl.json @@ -160,6 +160,12 @@ "type": { "option": "account_id" } + }, + { + "name": "authority", + "type": { + "option": "account_id" + } } ] }, diff --git a/artifacts/token-idl.json b/artifacts/token-idl.json index 290e832..18510e4 100644 --- a/artifacts/token-idl.json +++ b/artifacts/token-idl.json @@ -49,6 +49,12 @@ { "name": "total_supply", "type": "u128" + }, + { + "name": "mint_authority", + "type": { + "option": "account_id" + } } ] }, @@ -153,6 +159,79 @@ } ] }, + { + "name": "mint_with_authority", + "accounts": [ + { + "name": "definition_account", + "writable": true, + "signer": false, + "init": false + }, + { + "name": "user_holding_account", + "writable": true, + "signer": false, + "init": false + }, + { + "name": "authority_account", + "writable": false, + "signer": true, + "init": false + } + ], + "args": [ + { + "name": "amount_to_mint", + "type": "u128" + } + ] + }, + { + "name": "set_authority", + "accounts": [ + { + "name": "definition_account", + "writable": true, + "signer": true, + "init": false + } + ], + "args": [ + { + "name": "new_authority", + "type": { + "option": "account_id" + } + } + ] + }, + { + "name": "set_authority_with_authority", + "accounts": [ + { + "name": "definition_account", + "writable": true, + "signer": false, + "init": false + }, + { + "name": "authority_account", + "writable": false, + "signer": true, + "init": false + } + ], + "args": [ + { + "name": "new_authority", + "type": { + "option": "account_id" + } + } + ] + }, { "name": "print_nft", "accounts": [ @@ -194,6 +273,12 @@ "type": { "option": "account_id" } + }, + { + "name": "authority", + "type": { + "option": "account_id" + } } ] }, diff --git a/programs/amm/methods/guest/Cargo.lock b/programs/amm/methods/guest/Cargo.lock index f7a1ffe..8018c5a 100644 --- a/programs/amm/methods/guest/Cargo.lock +++ b/programs/amm/methods/guest/Cargo.lock @@ -31,7 +31,7 @@ dependencies = [ "cfg-if", "const-hex", "derive_more", - "foldhash 0.2.0", + "foldhash", "hashbrown 0.17.1", "indexmap 2.14.0", "itoa", @@ -108,9 +108,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "ark-bn254" @@ -154,7 +154,7 @@ checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -263,7 +263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -301,7 +301,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -405,7 +405,7 @@ checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -452,9 +452,9 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "auto_impl" @@ -464,7 +464,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -513,19 +513,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] -name = "bitcoin-io" -version = "0.1.100" +name = "bitcoin-consensus-encoding" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11301df0b06f22dea7bb1916403fdd88a371031e495c49b8f96931b28189e175" +checksum = "b2d6094e2a1ba3c93b5a596fe5a10d1a10c3c6e06785cde89f693a044c01aa40" +dependencies = [ + "bitcoin-internals", +] + +[[package]] +name = "bitcoin-internals" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" +dependencies = [ + "hex-conservative 0.3.2", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] [[package]] name = "bitcoin_hashes" -version = "0.14.100" +version = "0.14.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" dependencies = [ "bitcoin-io", - "hex-conservative", + "hex-conservative 0.2.2", ] [[package]] @@ -536,15 +557,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -578,18 +599,18 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] [[package]] name = "borsh" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" dependencies = [ "borsh-derive", "bytes", @@ -598,15 +619,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -647,7 +668,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -658,24 +679,24 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "bytesize" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" +checksum = "49e78e506b9d7633710dab98996f22f95f3d0f488e8f1aa162830556ed9fc14d" dependencies = [ "serde_core", ] [[package]] name = "cc" -version = "1.2.62" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "shlex", @@ -695,9 +716,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cipher", @@ -706,9 +727,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -722,7 +743,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "crypto-common 0.2.2", "inout", ] @@ -870,9 +891,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -884,7 +905,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "hybrid-array", "rand_core 0.10.1", ] @@ -918,7 +939,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -929,7 +950,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -958,7 +979,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -992,7 +1012,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.117", + "syn 2.0.118", "unicode-xid", ] @@ -1013,7 +1033,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid 0.9.6", - "crypto-common 0.1.6", + "crypto-common 0.1.7", "subtle", ] @@ -1023,7 +1043,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "crypto-common 0.2.2", ] @@ -1062,7 +1082,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1110,22 +1130,22 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "enum-ordinalize" -version = "4.3.2" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb" dependencies = [ "enum-ordinalize-derive", ] [[package]] name = "enum-ordinalize-derive" -version = "4.3.2" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1216,12 +1236,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1246,7 +1260,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1287,9 +1301,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1321,16 +1335,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", ] [[package]] @@ -1357,7 +1369,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", - "foldhash 0.1.5", ] [[package]] @@ -1366,15 +1377,9 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "foldhash 0.2.0", + "foldhash", ] -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hex" version = "0.4.3" @@ -1390,6 +1395,15 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hex-conservative" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830e599c2904b08f0834ee6337d8fe8f0ed4a63b5d9e7a7f49c0ffa06d08d360" +dependencies = [ + "arrayvec", +] + [[package]] name = "hex-literal" version = "0.4.1" @@ -1407,9 +1421,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "ctutils", "typenum", @@ -1439,12 +1453,6 @@ dependencies = [ "cc", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1468,7 +1476,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1541,13 +1549,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1627,12 +1634,6 @@ dependencies = [ "spin", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "lee_core" version = "0.1.0" @@ -1670,9 +1671,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" -version = "0.4.30" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "malloc_buf" @@ -1685,9 +1686,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "merlin" @@ -1707,7 +1708,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block", "core-graphics-types", "foreign-types", @@ -1800,7 +1801,7 @@ checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1843,7 +1844,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1915,16 +1916,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "primitive-types" version = "0.12.2" @@ -1962,7 +1953,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.11.1", + "bitflags 2.13.0", "num-traits", "rand 0.9.4", "rand_chacha 0.9.0", @@ -1981,9 +1972,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -2082,9 +2073,9 @@ dependencies = [ [[package]] name = "rapidhash" -version = "4.4.1" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +checksum = "32b266a82f4aa99bb5c25e28d11cc44ace63d91adbcbcee4d323e2ae3d49ef37" dependencies = [ "rustversion", ] @@ -2106,14 +2097,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rfc6979" @@ -2398,7 +2389,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -2536,7 +2527,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2563,9 +2554,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64", "bs58", @@ -2583,14 +2574,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2626,9 +2617,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signature" @@ -2670,7 +2661,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.117", + "syn 2.0.118", "thiserror 1.0.69", "toml", ] @@ -2685,7 +2676,7 @@ dependencies = [ "serde_json", "sha2", "spel-framework-core", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2721,7 +2712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2755,9 +2746,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -2777,7 +2768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys", @@ -2809,7 +2800,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2820,17 +2811,16 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -2840,15 +2830,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -2970,7 +2960,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3008,9 +2998,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -3094,27 +3084,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -3125,9 +3106,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3135,60 +3116,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver 1.0.28", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -3210,7 +3157,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3221,7 +3168,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3275,100 +3222,12 @@ dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap 2.14.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.14.0", - "log", - "semver 1.0.28", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "wyz" version = "0.5.1" @@ -3380,42 +3239,42 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.49" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.49" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] diff --git a/programs/amm/src/new_definition.rs b/programs/amm/src/new_definition.rs index 02ce020..7afe9b2 100644 --- a/programs/amm/src/new_definition.rs +++ b/programs/amm/src/new_definition.rs @@ -193,6 +193,7 @@ pub fn new_definition( &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, + mint_authority: Some(pool_definition_lp.account_id), }, ) .with_pda_seeds(vec![ @@ -206,8 +207,10 @@ pub fn new_definition( name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, metadata_id: None, + // Self-authority: the LP token is mintable only by the pool, which + // presents this PDA as the authorized minter in the chained Mint call. + authority: Some(pool_definition_lp.account_id), }); - let call_token_lp_user = ChainedCall::new( token_program_id, vec![pool_lp_after_lock, user_holding_lp.clone()], diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs index 3878b5e..bb37c7c 100644 --- a/programs/amm/src/tests.rs +++ b/programs/amm/src/tests.rs @@ -538,10 +538,11 @@ impl ChainedCallForTests { ChainedCall::new( TOKEN_PROGRAM_ID, - vec![pool_lp_auth, lp_lock_holding_auth], + vec![pool_lp_auth.clone(), lp_lock_holding_auth], &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, + mint_authority: Some(pool_lp_auth.account_id), }, ) .with_pda_seeds(vec![ @@ -872,6 +873,7 @@ impl AccountWithMetadataForTests { name: String::from("test"), total_supply: BalanceForTests::lp_supply_init(), metadata_id: None, + authority: Some(IdForTests::token_lp_definition_id()), }), nonce: Nonce(0), }, @@ -897,6 +899,7 @@ impl AccountWithMetadataForTests { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, metadata_id: None, + authority: Some(IdForTests::token_lp_definition_id()), }), nonce: Nonce(0), }, @@ -914,6 +917,7 @@ impl AccountWithMetadataForTests { name: String::from("test"), total_supply: BalanceForTests::lp_supply_init(), metadata_id: None, + authority: Some(IdForTests::token_lp_definition_id()), }), nonce: Nonce(0), }, @@ -3263,6 +3267,7 @@ fn test_new_definition_lp_symmetric_amounts() { &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, + mint_authority: Some(pool_lp_auth.account_id), }, ) .with_pda_seeds(vec![ @@ -3365,6 +3370,7 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() { &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, + mint_authority: Some(pool_lp_auth.account_id), }, ) .with_pda_seeds(vec![ diff --git a/programs/ata/src/tests.rs b/programs/ata/src/tests.rs index 595cfdd..09f1792 100644 --- a/programs/ata/src/tests.rs +++ b/programs/ata/src/tests.rs @@ -41,6 +41,7 @@ fn definition_account() -> AccountWithMetadata { name: "TEST".to_string(), total_supply: 1000, metadata_id: None, + authority: None, }), nonce: nssa_core::account::Nonce(0), }, diff --git a/programs/integration_tests/tests/amm.rs b/programs/integration_tests/tests/amm.rs index c76502c..af711aa 100644 --- a/programs/integration_tests/tests/amm.rs +++ b/programs/integration_tests/tests/amm.rs @@ -401,6 +401,7 @@ impl Accounts { name: String::from("test"), total_supply: Balances::token_a_supply(), metadata_id: None, + authority: None, }), nonce: Nonce(0), } @@ -414,6 +415,7 @@ impl Accounts { name: String::from("test"), total_supply: Balances::token_b_supply(), metadata_id: None, + authority: None, }), nonce: Nonce(0), } @@ -427,6 +429,7 @@ impl Accounts { name: String::from("LP Token"), total_supply: Balances::token_lp_supply(), metadata_id: None, + authority: Some(Ids::token_lp_definition()), }), nonce: Nonce(0), } @@ -705,6 +708,7 @@ impl Accounts { name: String::from("LP Token"), total_supply: Balances::token_lp_supply_add(), metadata_id: None, + authority: Some(Ids::token_lp_definition()), }), nonce: Nonce(0), } @@ -797,6 +801,7 @@ impl Accounts { name: String::from("LP Token"), total_supply: Balances::token_lp_supply_remove(), metadata_id: None, + authority: Some(Ids::token_lp_definition()), }), nonce: Nonce(0), } @@ -810,6 +815,7 @@ impl Accounts { name: String::from("LP Token"), total_supply: 0, metadata_id: None, + authority: Some(Ids::token_lp_definition()), }), nonce: Nonce(0), } @@ -902,6 +908,7 @@ impl Accounts { name: String::from("LP Token"), total_supply: Balances::lp_supply_init(), metadata_id: None, + authority: Some(Ids::token_lp_definition()), }), nonce: Nonce(0), } @@ -1390,6 +1397,7 @@ fn fungible_total_supply(account: &Account) -> u128 { name: _, total_supply, metadata_id: _, + authority: _, } = definition else { panic!("expected fungible token definition") diff --git a/programs/integration_tests/tests/ata.rs b/programs/integration_tests/tests/ata.rs index 243ccd3..6e5d016 100644 --- a/programs/integration_tests/tests/ata.rs +++ b/programs/integration_tests/tests/ata.rs @@ -84,6 +84,7 @@ impl Accounts { name: String::from("Gold"), total_supply: 1_000_000_u128, metadata_id: None, + authority: None, }), nonce: Nonce(0), } @@ -121,6 +122,7 @@ impl Accounts { name: String::from("Foreign Gold"), total_supply: 1_000_000_u128, metadata_id: None, + authority: None, }), nonce: Nonce(0), } @@ -495,6 +497,7 @@ fn ata_burn() { name: String::from("Gold"), total_supply: 700_000_u128, metadata_id: None, + authority: None, }), nonce: Nonce(0), } diff --git a/programs/integration_tests/tests/stablecoin.rs b/programs/integration_tests/tests/stablecoin.rs index c4cc276..d72ad67 100644 --- a/programs/integration_tests/tests/stablecoin.rs +++ b/programs/integration_tests/tests/stablecoin.rs @@ -108,6 +108,7 @@ impl Accounts { name: String::from("Gold"), total_supply: Balances::user_holding_init(), metadata_id: None, + authority: None, }), nonce: Nonce(0), } @@ -133,6 +134,7 @@ impl Accounts { name: String::from("DAI"), total_supply: Balances::stablecoin_supply_init(), metadata_id: None, + authority: None, }), nonce: Nonce(0), } diff --git a/programs/integration_tests/tests/token.rs b/programs/integration_tests/tests/token.rs index fd7bbf8..bb276d2 100644 --- a/programs/integration_tests/tests/token.rs +++ b/programs/integration_tests/tests/token.rs @@ -28,6 +28,10 @@ impl Keys { fn recipient_key() -> PrivateKey { PrivateKey::try_new([12; 32]).expect("valid private key") } + + fn authority_key() -> PrivateKey { + PrivateKey::try_new([13; 32]).expect("valid private key") + } } impl Ids { @@ -50,6 +54,10 @@ impl Ids { fn recipient() -> AccountId { AccountId::from(&PublicKey::new_from_private_key(&Keys::recipient_key())) } + + fn authority() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&Keys::authority_key())) + } } impl Accounts { @@ -61,6 +69,7 @@ impl Accounts { name: String::from("Gold"), total_supply: 1_000_000_u128, metadata_id: None, + authority: Some(Ids::token_definition()), }), nonce: Nonce(0), } @@ -74,6 +83,7 @@ impl Accounts { name: String::from("Gold"), total_supply: 1_000_000_u128, metadata_id: None, + authority: Some(Ids::token_definition()), }), nonce: Nonce(0), } @@ -102,6 +112,15 @@ impl Accounts { nonce: Nonce(0), } } + + fn authority_init() -> Account { + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::default(), + nonce: Nonce(0), + } + } } fn deploy_token(state: &mut V03State) { @@ -118,6 +137,7 @@ fn state_for_token_tests() -> V03State { state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init()); state.force_insert_account(Ids::holder(), Accounts::holder_init()); state.force_insert_account(Ids::recipient(), Accounts::recipient_init()); + state.force_insert_account(Ids::authority(), Accounts::authority_init()); state } @@ -126,6 +146,7 @@ fn state_for_token_tests_without_recipient() -> V03State { deploy_token(&mut state); state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init()); state.force_insert_account(Ids::holder(), Accounts::holder_init()); + state.force_insert_account(Ids::authority(), Accounts::authority_init()); state } @@ -137,6 +158,7 @@ fn token_new_fungible_definition() { let instruction = token_core::Instruction::NewFungibleDefinition { name: String::from("Gold"), total_supply: 1_000_000_u128, + mint_authority: None, }; let message = public_transaction::Message::try_new( @@ -164,6 +186,7 @@ fn token_new_fungible_definition() { name: String::from("Gold"), total_supply: 1_000_000_u128, metadata_id: None, + authority: None, }), nonce: Nonce(1), } @@ -415,6 +438,7 @@ fn token_burn() { name: String::from("Gold"), total_supply: 800_000_u128, metadata_id: None, + authority: Some(Ids::token_definition()), }), nonce: Nonce(0), } @@ -464,6 +488,7 @@ fn token_mint() { name: String::from("Gold"), total_supply: 1_500_000_u128, metadata_id: None, + authority: Some(Ids::token_definition()), }), nonce: Nonce(1), } @@ -585,6 +610,7 @@ fn token_mint_fresh_authorized_public_recipient() { name: String::from("Gold"), total_supply: 1_500_000_u128, metadata_id: None, + authority: Some(Ids::token_definition()), }), nonce: Nonce(1), } @@ -915,3 +941,319 @@ fn token_deshielded_transfer() { .get_proof_for_commitment(&Commitment::new(&sender_id, &new_sender_account)) .is_some()); } + +#[test] +fn token_new_fungible_definition_with_authority() { + let mut state = V03State::new(); + deploy_token(&mut state); + let authority_key: [u8; 32] = Ids::token_definition() + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"); + let instruction = token_core::Instruction::NewFungibleDefinition { + name: String::from("AuthCoin"), + total_supply: 1_000_000_u128, + mint_authority: Some(AccountId::new(authority_key)), + }; + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::holder()], + vec![Nonce(0), Nonce(0)], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[&Keys::def_key(), &Keys::holder_key()], + ); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + assert_eq!( + state.get_account_by_id(Ids::token_definition()), + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("AuthCoin"), + total_supply: 1_000_000_u128, + metadata_id: None, + authority: Some(AccountId::new(authority_key)), + }), + nonce: Nonce(1), + } + ); +} + +#[test] +fn token_set_authority_revoke() { + let mut state = V03State::new(); + deploy_token(&mut state); + let authority_key: [u8; 32] = Ids::token_definition() + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"); + // Create token with authority + let instruction = token_core::Instruction::NewFungibleDefinition { + name: String::from("AuthCoin"), + total_supply: 1_000_000_u128, + mint_authority: Some(AccountId::new(authority_key)), + }; + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::holder()], + vec![Nonce(0), Nonce(0)], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[&Keys::def_key(), &Keys::holder_key()], + ); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + // Seed the authority account so it can sign the revoke + state.force_insert_account(Ids::authority(), Accounts::authority_init()); + + // Revoke authority + let instruction = token_core::Instruction::SetAuthority { + new_authority: None, + }; + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition()], + vec![Nonce(1)], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + assert_eq!( + state.get_account_by_id(Ids::token_definition()), + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("AuthCoin"), + total_supply: 1_000_000_u128, + metadata_id: None, + authority: None, + }), + nonce: Nonce(2), + } + ); +} + +/// After the authority is rotated to an external key, that external key can rotate +/// or revoke again via `SetAuthorityWithAuthority` — signing as a distinct authority +/// account while the definition account does not sign. +#[test] +fn token_set_authority_with_authority_revokes() { + let mut state = V03State::new(); + deploy_token(&mut state); + + // Create with self-authority (definition is the initial mint authority). + let instruction = token_core::Instruction::NewFungibleDefinition { + name: String::from("RotCoin"), + total_supply: 1_000_000_u128, + mint_authority: Some(Ids::token_definition()), + }; + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::holder()], + vec![Nonce(0), Nonce(0)], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[&Keys::def_key(), &Keys::holder_key()], + ); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + // Rotate to the external authority via self-authority (def_key signs). + let instruction = token_core::Instruction::SetAuthority { + new_authority: Some(Ids::authority()), + }; + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition()], + vec![Nonce(1)], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + // Seed the external authority so it can sign. + state.force_insert_account(Ids::authority(), Accounts::authority_init()); + + // The external authority revokes via SetAuthorityWithAuthority. Accounts: + // [definition, authority]; only the authority signs. + let instruction = token_core::Instruction::SetAuthorityWithAuthority { + new_authority: None, + }; + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::authority()], + vec![Nonce(0)], + instruction, + ) + .unwrap(); + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&Keys::authority_key()]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + let def = state.get_account_by_id(Ids::token_definition()); + let stored = match TokenDefinition::try_from(&def.data).unwrap() { + TokenDefinition::Fungible { authority, .. } => authority, + _ => None, + }; + assert_eq!(stored, None, "authority must be permanently revoked"); +} + +/// Integration test for RFP-001 authority rotation flow: +/// 1. Create a token where `Ids::token_definition()` is the initial mint authority +/// (self-authority). +/// 2. Rotate the mint authority to `Ids::authority()` (an external key). +/// 3. Verify that the new external authority can mint by presenting itself as a rest account. +/// 4. Verify that the OLD authority (def key) can no longer mint after rotation. +#[test] +fn token_rotate_authority_then_new_authority_can_mint() { + let mut state = V03State::new(); + deploy_token(&mut state); + + let authority_key: [u8; 32] = Ids::authority() + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"); + + // Step 1: Create token with self-authority (def account is initial mint authority). + let instruction = token_core::Instruction::NewFungibleDefinition { + name: String::from("RotCoin"), + total_supply: 1_000_000_u128, + mint_authority: Some(AccountId::new( + Ids::token_definition() + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"), + )), + }; + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::holder()], + vec![Nonce(0), Nonce(0)], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[&Keys::def_key(), &Keys::holder_key()], + ); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + // Step 2: Rotate mint authority from def_key to Ids::authority() (external key). + // Self-authority path: no rest accounts; def_key signs. + let instruction = token_core::Instruction::SetAuthority { + new_authority: Some(AccountId::new(authority_key)), + }; + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition()], + vec![Nonce(1)], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + // Verify the authority slot now holds Ids::authority(). + assert_eq!( + state.get_account_by_id(Ids::token_definition()), + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("RotCoin"), + total_supply: 1_000_000_u128, + metadata_id: None, + authority: Some(AccountId::new(authority_key)), + }), + nonce: Nonce(2), + } + ); + + // Seed the external authority account and the holder so they exist in state. + state.force_insert_account(Ids::authority(), Accounts::authority_init()); + state.force_insert_account(Ids::holder(), Accounts::holder_init()); + + // Step 3: New external authority mints via MintWithAuthority, signing as a + // distinct authority account. Accounts: [definition, holder, authority]. + let instruction = token_core::Instruction::MintWithAuthority { + amount_to_mint: 500_000_u128, + }; + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::holder(), Ids::authority()], + vec![Nonce(0)], + instruction, + ) + .unwrap(); + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&Keys::authority_key()]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + // Verify total_supply increased and holder balance reflects the mint. + assert_eq!( + state.get_account_by_id(Ids::token_definition()), + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("RotCoin"), + total_supply: 1_500_000_u128, + metadata_id: None, + authority: Some(AccountId::new(authority_key)), + }), + nonce: Nonce(2), + } + ); + assert_eq!( + state.get_account_by_id(Ids::holder()), + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: Ids::token_definition(), + balance: 1_500_000_u128, + }), + nonce: Nonce(0), + } + ); + + // Step 4: OLD authority (def_key self-authority path) must be rejected after rotation. + let instruction = token_core::Instruction::Mint { + amount_to_mint: 1_u128, + }; + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::holder()], + vec![Nonce(0)], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]); + let tx = PublicTransaction::new(message, witness_set); + let result = state.transition_from_public_transaction(&tx, 0, 0); + assert!( + result.is_err(), + "Old authority must be rejected after rotation" + ); +} diff --git a/programs/stablecoin/src/tests.rs b/programs/stablecoin/src/tests.rs index 41e0154..57a64ed 100644 --- a/programs/stablecoin/src/tests.rs +++ b/programs/stablecoin/src/tests.rs @@ -79,6 +79,7 @@ fn collateral_definition_account() -> AccountWithMetadata { name: "SNT".to_owned(), total_supply: 1_000_000, metadata_id: None, + authority: None, }), nonce: Nonce(0), }, @@ -156,6 +157,7 @@ fn stablecoin_definition_account() -> AccountWithMetadata { name: "DAI".to_owned(), total_supply: 1_000_000, metadata_id: None, + authority: None, }), nonce: Nonce(0), }, @@ -389,6 +391,7 @@ fn open_position_rejects_mismatched_token_definition() { name: "OTHER".to_owned(), total_supply: 1, metadata_id: None, + authority: None, }), nonce: Nonce(0), }, diff --git a/programs/token/core/src/lib.rs b/programs/token/core/src/lib.rs index 3954537..f8c629c 100644 --- a/programs/token/core/src/lib.rs +++ b/programs/token/core/src/lib.rs @@ -18,10 +18,18 @@ pub enum Instruction { /// Create a new fungible token definition without metadata. /// + /// `mint_authority` decides the supply model: + /// - `Some(id)` — `id` may mint additional supply and rotate/renounce the authority, + /// - `None` — supply is permanently fixed at `total_supply`. + /// /// Required accounts: /// - Token Definition account (uninitialized, authorized), /// - Token Holding account (uninitialized, authorized). - NewFungibleDefinition { name: String, total_supply: u128 }, + NewFungibleDefinition { + name: String, + total_supply: u128, + mint_authority: Option, + }, /// Create a new fungible or non-fungible token definition with metadata. /// @@ -49,20 +57,53 @@ pub enum Instruction { /// - Token Holding account (authorized). Burn { amount_to_burn: u128 }, - /// Mint new tokens to the holder's account. + /// Mint new tokens to the holder's account under **self/PDA authority**: the + /// Token Definition account itself is the current mint authority and must be + /// authorized in this transaction (signer, or a PDA authorized under its + /// seeds). A definition with no authority has a fixed supply and rejects + /// minting. /// /// Required accounts: - /// - Token Definition account (initialized, authorized), - /// - Token Holding account (initialized, or uninitialized with holder authorization in the - /// same transaction). + /// - Token Definition account (initialized, authorized as the current mint authority), + /// - Token Holding account (uninitialized or authorized and initialized). Mint { amount_to_mint: u128 }, + /// Mint new tokens under an **external authority**: a distinct authority + /// account (the account the mint authority was rotated to) authorizes the + /// mint by signing, while the Token Definition account is mutated but does + /// not sign. Its account id must match the definition's stored authority. + /// + /// Required accounts: + /// - Token Definition account (initialized), + /// - Token Holding account (uninitialized or authorized and initialized), + /// - Authority account (authorized as the current mint authority). + MintWithAuthority { amount_to_mint: u128 }, + /// Print a new NFT from the master copy. /// /// Required accounts: /// - NFT Master Token Holding account (authorized), /// - NFT Printed Copy Token Holding account (uninitialized, authorized). PrintNft, + + /// Rotate or renounce the mint authority under **self/PDA authority**: the + /// Token Definition account itself is the current authority and must be + /// authorized in this transaction. Pass `new_authority: None` to permanently + /// renounce minting (fixed supply). + /// + /// Required accounts: + /// - Token Definition account (initialized, authorized as the current mint authority). + SetAuthority { new_authority: Option }, + + /// Rotate or renounce the mint authority under an **external authority**: a + /// distinct authority account (the account the authority was rotated to) + /// authorizes the change by signing, while the Token Definition account is + /// mutated but does not sign. Pass `new_authority: None` to permanently renounce. + /// + /// Required accounts: + /// - Token Definition account (initialized), + /// - Authority account (authorized as the current mint authority). + SetAuthorityWithAuthority { new_authority: Option }, } #[derive(Serialize, Deserialize)] @@ -70,6 +111,9 @@ pub enum NewTokenDefinition { Fungible { name: String, total_supply: u128, + /// Mint authority. `Some(id)` makes the token mintable by `id`; `None` + /// fixes the supply. + mint_authority: Option, }, NonFungible { name: String, @@ -84,6 +128,14 @@ pub enum TokenDefinition { name: String, total_supply: u128, metadata_id: Option, + /// Mint authority slot. `Some(id)` may mint and rotate/renounce; + /// `None` means the supply is permanently fixed. + /// + /// Stored directly as `Option` (Borsh-identical to a custom + /// authority newtype) so account state stays decodable by `spel inspect`. + /// The require/rotate/renounce guard logic lives inline in the `mint` and + /// `set_authority` handlers. + authority: Option, }, NonFungible { name: String, diff --git a/programs/token/methods/guest/src/bin/token.rs b/programs/token/methods/guest/src/bin/token.rs index 4b0e350..5024c72 100644 --- a/programs/token/methods/guest/src/bin/token.rs +++ b/programs/token/methods/guest/src/bin/token.rs @@ -1,8 +1,12 @@ #![cfg_attr(not(test), no_main)] +#![allow( + clippy::cloned_ref_to_slice_refs, + reason = "SPEL macro emits cloned validation slices for one-account instructions" +)] -use spel_framework::prelude::*; +use nssa_core::account::{AccountId, AccountWithMetadata}; use spel_framework::context::ProgramContext; -use nssa_core::account::AccountWithMetadata; +use spel_framework::prelude::*; #[cfg(not(test))] risc0_zkvm::guest::entry!(main); @@ -25,15 +29,15 @@ mod token { recipient: AccountWithMetadata, amount_to_transfer: u128, ) -> SpelResult { - Ok(spel_framework::SpelOutput::execute(token_program::transfer::transfer( - sender, - recipient, - amount_to_transfer, - ), vec![])) + Ok(spel_framework::SpelOutput::execute( + token_program::transfer::transfer(sender, recipient, amount_to_transfer), + vec![], + )) } /// Create a new fungible token definition without metadata. /// Definition and holding targets must be uninitialized and authorized. + /// `mint_authority` is `Some(id)` for a mintable token or `None` for fixed supply. #[instruction] pub fn new_fungible_definition( #[account(init, signer)] @@ -42,6 +46,7 @@ mod token { holding_target_account: AccountWithMetadata, name: String, total_supply: u128, + mint_authority: Option, ) -> SpelResult { Ok(spel_framework::SpelOutput::execute( token_program::new_definition::new_fungible_definition( @@ -49,6 +54,7 @@ mod token { holding_target_account, name, total_supply, + mint_authority, ), vec![], )) @@ -111,15 +117,16 @@ mod token { user_holding_account: AccountWithMetadata, amount_to_burn: u128, ) -> SpelResult { - Ok(spel_framework::SpelOutput::execute(token_program::burn::burn( - definition_account, - user_holding_account, - amount_to_burn, - ), vec![])) + Ok(spel_framework::SpelOutput::execute( + token_program::burn::burn(definition_account, user_holding_account, amount_to_burn), + vec![], + )) } - /// Mint new tokens to the holder's account. - /// Fresh public holders must be explicitly authorized in the same transaction. + /// Mint new tokens under self/PDA authority: the definition account itself is + /// the current mint authority and signs (or is PDA-authorized, e.g. the AMM + /// minting its own LP token). Fresh public holders must be explicitly + /// authorized in the same transaction. #[instruction] pub fn mint( ctx: ProgramContext, @@ -129,12 +136,87 @@ mod token { user_holding_account: AccountWithMetadata, amount_to_mint: u128, ) -> SpelResult { - Ok(spel_framework::SpelOutput::execute(token_program::mint::mint( - definition_account, - user_holding_account, - amount_to_mint, - ctx.self_program_id, - ), vec![])) + Ok(spel_framework::SpelOutput::execute( + token_program::mint::mint( + definition_account, + user_holding_account, + amount_to_mint, + ctx.self_program_id, + ), + vec![], + )) + } + + /// Mint new tokens under an external authority: a distinct `authority_account` + /// (the account the mint authority was rotated to) signs, while the definition + /// account is mutated but does not sign. This is the path a rotated authority + /// uses to mint. Fresh public holders must be explicitly authorized in the + /// same transaction. + #[instruction] + pub fn mint_with_authority( + ctx: ProgramContext, + #[account(mut)] + definition_account: AccountWithMetadata, + #[account(mut)] + user_holding_account: AccountWithMetadata, + #[account(signer)] + authority_account: AccountWithMetadata, + amount_to_mint: u128, + ) -> SpelResult { + Ok(spel_framework::SpelOutput::execute( + token_program::mint::mint_with_authority( + definition_account, + user_holding_account, + authority_account, + amount_to_mint, + ctx.self_program_id, + ), + vec![], + )) + } + + /// Rotate or renounce the mint authority under self/PDA authority: the definition + /// account itself is the current authority and signs (or is PDA-authorized). + /// Pass `new_authority: None` to permanently renounce minting (fixed supply). + #[instruction] + pub fn set_authority( + ctx: ProgramContext, + #[account(mut, signer)] + definition_account: AccountWithMetadata, + new_authority: Option, + ) -> SpelResult { + Ok(spel_framework::SpelOutput::execute( + token_program::set_authority::set_authority( + definition_account, + new_authority, + ctx.self_program_id, + ), + vec![], + )) + } + + /// Rotate or renounce the mint authority under an external authority: a distinct + /// `authority_account` (the account the authority was rotated to) signs, while the + /// definition account is mutated but does not sign. This lets a rotated authority + /// rotate or revoke again. Pass `new_authority: None` to permanently renounce. + #[instruction] + pub fn set_authority_with_authority( + ctx: ProgramContext, + #[account(mut)] + definition_account: AccountWithMetadata, + #[account(signer)] + authority_account: AccountWithMetadata, + new_authority: Option, + ) -> SpelResult { + Ok(spel_framework::SpelOutput::execute( + token_program::set_authority::set_authority_with_authority( + definition_account, + authority_account, + new_authority, + ctx.self_program_id, + ), + vec![], + )) } /// Print a new NFT from the master copy. @@ -146,9 +228,9 @@ mod token { #[account(init, signer)] printed_account: AccountWithMetadata, ) -> SpelResult { - Ok(spel_framework::SpelOutput::execute(token_program::print_nft::print_nft( - master_account, - printed_account, - ), vec![])) + Ok(spel_framework::SpelOutput::execute( + token_program::print_nft::print_nft(master_account, printed_account), + vec![], + )) } } diff --git a/programs/token/src/burn.rs b/programs/token/src/burn.rs index 94637d9..e984745 100644 --- a/programs/token/src/burn.rs +++ b/programs/token/src/burn.rs @@ -31,6 +31,7 @@ pub fn burn( name: _, metadata_id: _, total_supply, + authority: _, }, TokenHolding::Fungible { definition_id: _, diff --git a/programs/token/src/lib.rs b/programs/token/src/lib.rs index 8b0698c..b0d1361 100644 --- a/programs/token/src/lib.rs +++ b/programs/token/src/lib.rs @@ -7,6 +7,7 @@ pub mod initialize; pub mod mint; pub mod new_definition; pub mod print_nft; +pub mod set_authority; pub mod transfer; mod tests; diff --git a/programs/token/src/mint.rs b/programs/token/src/mint.rs index 0c638d1..b96e917 100644 --- a/programs/token/src/mint.rs +++ b/programs/token/src/mint.rs @@ -4,16 +4,56 @@ use nssa_core::{ }; use token_core::{TokenDefinition, TokenHolding}; +/// Mint additional supply under **self/PDA authority**: the definition account +/// itself is the current mint authority and proves it by being authorized in +/// this transaction (a signer, or a PDA authorized under its seeds — e.g. the +/// AMM minting its own LP token via a chained call). pub fn mint( definition_account: AccountWithMetadata, user_holding_account: AccountWithMetadata, amount_to_mint: u128, token_program_id: ProgramId, ) -> Vec { - assert!( - definition_account.is_authorized, - "Definition authorization is missing" - ); + mint_inner( + definition_account, + user_holding_account, + None, + amount_to_mint, + token_program_id, + ) +} + +/// Mint additional supply under an **external authority**: a distinct account +/// (e.g. an owner key the authority was rotated to) proves authority by signing. +/// The definition account is still mutated but does not authorize the mint, +/// which is what lets a rotated authority mint without the definition's key. +pub fn mint_with_authority( + definition_account: AccountWithMetadata, + user_holding_account: AccountWithMetadata, + authority_account: AccountWithMetadata, + amount_to_mint: u128, + token_program_id: ProgramId, +) -> Vec { + mint_inner( + definition_account, + user_holding_account, + Some(authority_account), + amount_to_mint, + token_program_id, + ) +} + +/// Shared minting core for both authority modes. `authority_account` is the +/// external authority when `Some`; when `None` the definition account itself is +/// treated as the authority (self/PDA authority). Post-state order mirrors the +/// pre-state account order for each mode. +fn mint_inner( + definition_account: AccountWithMetadata, + user_holding_account: AccountWithMetadata, + authority_account: Option, + amount_to_mint: u128, + token_program_id: ProgramId, +) -> Vec { assert_eq!( definition_account.account.program_owner, token_program_id, "Token definition must be owned by token program" @@ -21,6 +61,26 @@ pub fn mint( let mut definition = TokenDefinition::try_from(&definition_account.account.data) .expect("Token Definition account must be valid"); + + // Minting is gated on the definition's stored mint authority: the account + // that proves authority must be authorized AND its id must match the stored + // authority. That account is the explicit external authority when present, + // otherwise the definition account itself (self/PDA authority). + if let TokenDefinition::Fungible { authority, .. } = &definition { + // `None` means the supply is permanently fixed (renounced) — minting is rejected. + let mint_authority = + authority.expect("Mint authority check failed: authority revoked, supply is fixed"); + let authority_ref = authority_account.as_ref().unwrap_or(&definition_account); + assert!( + authority_ref.is_authorized, + "Mint authority must authorize the transaction" + ); + assert_eq!( + authority_ref.account_id, mint_authority, + "Mint authority check failed: signer is not the current authority" + ); + } + let mut holding = if user_holding_account.account == Account::default() { TokenHolding::zeroized_from_definition(definition_account.account_id, &definition) } else { @@ -40,6 +100,7 @@ pub fn mint( name: _, metadata_id: _, total_supply, + authority: _, }, TokenHolding::Fungible { definition_id: _, @@ -69,8 +130,16 @@ pub fn mint( let mut holding_post = user_holding_account.account; holding_post.data = Data::from(&holding); - vec![ - AccountPostState::new(definition_post), - AccountPostState::new_claimed_if_default(holding_post, Claim::Authorized), - ] + // Post-states must match pre-state order and count: [definition, holding] + // for self authority, plus the read-only authority account when external. + let mut post_states = Vec::with_capacity(3); + post_states.push(AccountPostState::new(definition_post)); + post_states.push(AccountPostState::new_claimed_if_default( + holding_post, + Claim::Authorized, + )); + if let Some(authority) = authority_account { + post_states.push(AccountPostState::new(authority.account)); + } + post_states } diff --git a/programs/token/src/new_definition.rs b/programs/token/src/new_definition.rs index 91967a0..731c383 100644 --- a/programs/token/src/new_definition.rs +++ b/programs/token/src/new_definition.rs @@ -1,16 +1,31 @@ use nssa_core::{ - account::{Account, AccountWithMetadata, Data}, + account::{Account, AccountId, AccountWithMetadata, Data}, program::{AccountPostState, Claim}, }; use token_core::{ NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, TokenMetadata, }; +/// Validate the mint authority for a freshly created fungible definition. +/// +/// `Some(id)` makes the token mintable by `id`; `None` fixes the supply. +/// An all-zero authority id is rejected as it cannot be a real signer. +fn validate_mint_authority(mint_authority: Option) -> Option { + if let Some(id) = &mint_authority { + assert!( + id.value() != &[0u8; 32], + "Mint authority must be a valid non-zero account ID" + ); + } + mint_authority +} + pub fn new_fungible_definition( definition_target_account: AccountWithMetadata, holding_target_account: AccountWithMetadata, name: String, total_supply: u128, + mint_authority: Option, ) -> Vec { assert_eq!( definition_target_account.account, @@ -36,6 +51,7 @@ pub fn new_fungible_definition( name, total_supply, metadata_id: None, + authority: validate_mint_authority(mint_authority), }; let token_holding = TokenHolding::Fungible { definition_id: definition_target_account.account_id, @@ -92,11 +108,16 @@ pub fn new_definition_with_metadata( ); let (token_definition, token_holding) = match new_definition { - NewTokenDefinition::Fungible { name, total_supply } => ( + NewTokenDefinition::Fungible { + name, + total_supply, + mint_authority, + } => ( TokenDefinition::Fungible { name, total_supply, metadata_id: Some(metadata_target_account.account_id), + authority: validate_mint_authority(mint_authority), }, TokenHolding::Fungible { definition_id: definition_target_account.account_id, @@ -124,7 +145,7 @@ pub fn new_definition_with_metadata( standard: metadata.standard, uri: metadata.uri, creators: metadata.creators, - primary_sale_date: 0u64, // TODO #261: future works to implement this + primary_sale_date: 0u64, }; let mut definition_target_account_post = definition_target_account.account.clone(); diff --git a/programs/token/src/set_authority.rs b/programs/token/src/set_authority.rs new file mode 100644 index 0000000..5be0e90 --- /dev/null +++ b/programs/token/src/set_authority.rs @@ -0,0 +1,97 @@ +use nssa_core::{ + account::{AccountId, AccountWithMetadata, Data}, + program::{AccountPostState, ProgramId}, +}; +use token_core::TokenDefinition; + +/// Rotate or revoke the mint authority under **self/PDA authority**: the definition +/// account itself is the current authority and proves it by being authorized in this +/// transaction (a signer, or a PDA authorized under its seeds). +pub fn set_authority( + definition_account: AccountWithMetadata, + new_authority: Option, + token_program_id: ProgramId, +) -> Vec { + set_authority_inner(definition_account, None, new_authority, token_program_id) +} + +/// Rotate or revoke the mint authority under an **external authority**: a distinct +/// account (the account the authority was previously rotated to) proves authority by +/// signing, so a rotated authority can rotate or revoke again without the definition's +/// key. The definition account is mutated but does not authorize the change. +pub fn set_authority_with_authority( + definition_account: AccountWithMetadata, + authority_account: AccountWithMetadata, + new_authority: Option, + token_program_id: ProgramId, +) -> Vec { + set_authority_inner( + definition_account, + Some(authority_account), + new_authority, + token_program_id, + ) +} + +/// Shared rotation/revocation core for both authority modes. `authority_account` is +/// the external authority when `Some`; when `None` the definition account itself is +/// treated as the authority (self/PDA authority). Only mutates state after all checks +/// pass, so a rejected call leaves the prior authority intact. Post-state order mirrors +/// the pre-state account order for each mode. +fn set_authority_inner( + definition_account: AccountWithMetadata, + authority_account: Option, + new_authority: Option, + token_program_id: ProgramId, +) -> Vec { + assert_eq!( + definition_account.account.program_owner, token_program_id, + "Token definition must be owned by token program" + ); + + let mut definition = TokenDefinition::try_from(&definition_account.account.data) + .expect("Token Definition account must be valid"); + + match &mut definition { + TokenDefinition::Fungible { authority, .. } => { + // The account that proves authority must be authorized AND its id must + // match the stored authority. That account is the explicit external + // authority when present, otherwise the definition account itself. + // `None` means the authority was renounced and can no longer be set. + let current = authority.expect("SetAuthority failed: authority already revoked"); + let authority_ref = authority_account.as_ref().unwrap_or(&definition_account); + assert!( + authority_ref.is_authorized, + "Mint authority must authorize the transaction" + ); + assert_eq!( + authority_ref.account_id, current, + "SetAuthority failed: signer is not the current authority" + ); + + if let Some(new) = &new_authority { + assert!( + new.value() != &[0u8; 32], + "New mint authority must be a valid non-zero account ID" + ); + } + // Rotate to the new authority, or renounce with `None`. + *authority = new_authority; + } + TokenDefinition::NonFungible { .. } => { + panic!("SetAuthority is not supported for Non-Fungible Tokens"); + } + } + + let mut definition_post = definition_account.account; + definition_post.data = Data::from(&definition); + + // Post-states must match pre-state order and count: [definition] for self + // authority, plus the read-only authority account when external. + let mut post_states = Vec::with_capacity(2); + post_states.push(AccountPostState::new(definition_post)); + if let Some(authority) = authority_account { + post_states.push(AccountPostState::new(authority.account)); + } + post_states +} diff --git a/programs/token/src/tests.rs b/programs/token/src/tests.rs index 2df8e5c..02e56d5 100644 --- a/programs/token/src/tests.rs +++ b/programs/token/src/tests.rs @@ -15,9 +15,10 @@ use token_core::{ use crate::{ burn::burn, initialize::initialize_account, - mint::mint, + mint::{mint, mint_with_authority}, new_definition::{new_definition_with_metadata, new_fungible_definition}, print_nft::print_nft, + set_authority::{set_authority, set_authority_with_authority}, transfer::transfer, }; @@ -42,6 +43,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply(), metadata_id: None, + authority: Some(AccountId::new([15_u8; 32])), }), nonce: Nonce(0), }, @@ -59,6 +61,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply(), metadata_id: None, + authority: None, }), nonce: Nonce(0), }, @@ -76,6 +79,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply(), metadata_id: None, + authority: None, }), nonce: Nonce(0), }, @@ -157,6 +161,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply_burned(), metadata_id: None, + authority: Some(AccountId::new([15_u8; 32])), }), nonce: Nonce(0), }, @@ -238,6 +243,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply_mint(), metadata_id: None, + authority: Some(AccountId::new([15_u8; 32])), }), nonce: Nonce(0), }, @@ -328,6 +334,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply(), metadata_id: None, + authority: None, }), nonce: Nonce(0), }, @@ -594,6 +601,7 @@ fn test_new_definition_non_default_first_account_should_fail() { holding_account, String::from("test"), 10, + None, ); } @@ -618,6 +626,7 @@ fn test_new_definition_non_default_second_account_should_fail() { holding_account, String::from("test"), 10, + None, ); } @@ -631,6 +640,7 @@ fn test_new_definition_requires_authorized_definition_target() { holding_account, String::from("test"), 10, + None, ); } @@ -644,6 +654,7 @@ fn test_new_definition_requires_authorized_holding_target() { holding_account, String::from("test"), 10, + None, ); } @@ -657,6 +668,7 @@ fn test_new_definition_with_valid_inputs_succeeds() { holding_account, String::from("test"), BalanceForTests::init_supply(), + None, ); let [definition_account, holding_account] = post_states.try_into().unwrap(); @@ -918,9 +930,11 @@ fn test_mint_not_valid_definition_account() { } #[test] -#[should_panic(expected = "Definition authorization is missing")] +#[should_panic(expected = "Mint authority must authorize the transaction")] fn test_mint_missing_authorization() { - let definition_account = AccountForTests::definition_account_without_auth(); + // The definition account itself is the authority; mark it unauthorized. + let mut definition_account = AccountForTests::definition_account_auth(); + definition_account.is_authorized = false; let holding_account = AccountForTests::holding_same_definition_without_authorization(); let _post_states = mint( definition_account, @@ -943,9 +957,23 @@ fn test_mint_rejects_foreign_owned_definition() { ); } +#[test] +#[should_panic(expected = "Token definition must be owned by token program")] +fn test_set_authority_rejects_foreign_owned_definition() { + // A foreign-owned account carrying token-shaped data must not be able to + // rotate or revoke its authority through the token program. + let definition_account = AccountForTests::definition_account_foreign_owner(); + let _post_states = set_authority( + definition_account, + Some(AccountId::new([7_u8; 32])), + TOKEN_PROGRAM_ID, + ); +} + #[test] #[should_panic(expected = "Mismatch Token Definition and Token Holding")] fn test_mint_mismatched_token_definition() { + // let definition_account = AccountForTests::definition_account_auth(); let holding_account = AccountForTests::holding_different_definition(); let _post_states = mint( @@ -1053,6 +1081,7 @@ fn test_new_definition_with_metadata_success() { let new_definition = NewTokenDefinition::Fungible { name: String::from("test"), total_supply: 15u128, + mint_authority: None, }; let metadata = NewTokenMetadata { standard: MetadataStandard::Simple, @@ -1074,6 +1103,42 @@ fn test_new_definition_with_metadata_success() { assert_eq!(metadata_post.required_claim(), Some(Claim::Authorized)); } +/// Comment #2: a metadata-backed fungible created with `mint_authority: Some(..)` +/// carries a real, non-renounced authority and is therefore mintable — no longer +/// force-fixed-supply the way the hardcoded `Authority::renounced()` made it. +#[test] +fn test_metadata_fungible_with_authority_is_mintable() { + let definition_account = AccountForTests::definition_account_uninit_auth(); + let holding_account = AccountForTests::holding_account_uninit_auth(); + let metadata_account = AccountForTests::metadata_account_uninit_auth(); + let new_definition = NewTokenDefinition::Fungible { + name: String::from("test"), + total_supply: 15u128, + mint_authority: Some(AccountId::new([15_u8; 32])), + }; + let metadata = NewTokenMetadata { + standard: MetadataStandard::Simple, + uri: "test_uri".to_string(), + creators: "test_creators".to_string(), + }; + let post_states = new_definition_with_metadata( + definition_account, + holding_account, + metadata_account, + new_definition, + metadata, + ); + let [definition_post, _holding_post, _metadata_post] = post_states.try_into().unwrap(); + + // The stored authority must be the requested key, NOT renounced. + let def = TokenDefinition::try_from(&definition_post.account().data).unwrap(); + let stored = match def { + TokenDefinition::Fungible { authority, .. } => authority, + _ => None, + }; + assert_eq!(stored, Some(AccountId::new([15_u8; 32]))); +} + #[should_panic(expected = "Definition target account must be authorized")] #[test] fn test_call_new_definition_metadata_requires_authorized_definition() { @@ -1083,6 +1148,7 @@ fn test_call_new_definition_metadata_requires_authorized_definition() { let new_definition = NewTokenDefinition::Fungible { name: String::from("test"), total_supply: 15u128, + mint_authority: None, }; let metadata = NewTokenMetadata { standard: MetadataStandard::Simple, @@ -1107,6 +1173,7 @@ fn test_call_new_definition_metadata_requires_authorized_holding() { let new_definition = NewTokenDefinition::Fungible { name: String::from("test"), total_supply: 15u128, + mint_authority: None, }; let metadata = NewTokenMetadata { standard: MetadataStandard::Simple, @@ -1135,6 +1202,7 @@ fn test_call_new_definition_metadata_requires_authorized_metadata() { let new_definition = NewTokenDefinition::Fungible { name: String::from("test"), total_supply: 15u128, + mint_authority: None, }; let metadata = NewTokenMetadata { standard: MetadataStandard::Simple, @@ -1167,6 +1235,7 @@ fn test_call_new_definition_metadata_with_init_definition() { let new_definition = NewTokenDefinition::Fungible { name: String::from("test"), total_supply: 15u128, + mint_authority: None, }; let metadata = NewTokenMetadata { standard: MetadataStandard::Simple, @@ -1199,6 +1268,7 @@ fn test_call_new_definition_metadata_with_init_metadata() { let new_definition = NewTokenDefinition::Fungible { name: String::from("test"), total_supply: 15u128, + mint_authority: None, }; let metadata = NewTokenMetadata { standard: MetadataStandard::Simple, @@ -1231,6 +1301,7 @@ fn test_call_new_definition_metadata_with_init_holding() { let new_definition = NewTokenDefinition::Fungible { name: String::from("test"), total_supply: 15u128, + mint_authority: None, }; let metadata = NewTokenMetadata { standard: MetadataStandard::Simple, @@ -1313,3 +1384,374 @@ fn test_print_nft_success() { assert_eq!(post_master_nft.required_claim(), None); assert_eq!(post_printed.required_claim(), Some(Claim::Authorized)); } + +#[cfg(test)] +mod authority_tests { + use super::*; + use crate::{mint::mint, set_authority::set_authority}; + + const AUTHORITY: [u8; 32] = [15_u8; 32]; + const TOKEN_PROGRAM_ID: [u32; 8] = [5_u32; 8]; + + /// A fungible definition whose own account id ([15;32]) equals its stored + /// mint authority, authorized in the transaction. This models both an external + /// owner signing the definition key and a PDA authorized via its seeds. + fn def_with_authority() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5_u32; 8], + balance: 0_u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), + total_supply: 100_000_u128, + metadata_id: None, + authority: Some(AccountId::new(AUTHORITY)), + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: AccountId::new([15; 32]), + } + } + + /// A definition whose authority has been renounced (fixed supply). + fn def_with_authority_revoked() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5_u32; 8], + balance: 0_u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), + total_supply: 100_000_u128, + metadata_id: None, + authority: None, + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: AccountId::new([15; 32]), + } + } + + /// A definition whose account id ([99;32]) does NOT match its stored + /// authority ([15;32]) — models a caller that isn't the current authority. + fn def_wrong_authority() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5_u32; 8], + balance: 0_u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), + total_supply: 100_000_u128, + metadata_id: None, + authority: Some(AccountId::new(AUTHORITY)), + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: AccountId::new([99; 32]), + } + } + + fn holding_account() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5_u32; 8], + balance: 0_u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: AccountId::new([15; 32]), + balance: 1_000_u128, + }), + nonce: 0_u128.into(), + }, + is_authorized: false, + account_id: AccountId::new([17; 32]), + } + } + + #[test] + fn mint_with_authority_succeeds() { + let post_states = mint( + def_with_authority(), + holding_account(), + 50_000, + TOKEN_PROGRAM_ID, + ); + let [def_post, holding_post] = post_states.try_into().unwrap(); + + let def = TokenDefinition::try_from(&def_post.account().data).unwrap(); + let holding = TokenHolding::try_from(&holding_post.account().data).unwrap(); + + assert!(matches!( + def, + TokenDefinition::Fungible { + total_supply: 150_000, + .. + } + )); + assert!(matches!( + holding, + TokenHolding::Fungible { + balance: 51_000, + .. + } + )); + } + + #[test] + #[should_panic(expected = "Mint authority check failed")] + fn mint_with_revoked_authority_fails() { + let _ = mint( + def_with_authority_revoked(), + holding_account(), + 50_000, + TOKEN_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "Mint authority must authorize the transaction")] + fn mint_without_is_authorized_fails() { + let mut def = def_with_authority(); + def.is_authorized = false; + let _ = mint(def, holding_account(), 50_000, TOKEN_PROGRAM_ID); + } + + #[test] + #[should_panic(expected = "Mint authority check failed")] + fn mint_with_wrong_signer_fails() { + let _ = mint( + def_wrong_authority(), + holding_account(), + 50_000, + TOKEN_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "New mint authority must be a valid non-zero account ID")] + fn set_authority_rejects_zero_new_authority() { + let _ = set_authority( + def_with_authority(), + Some(AccountId::new([0u8; 32])), + TOKEN_PROGRAM_ID, + ); + } + + #[test] + fn set_authority_rotates_to_new_key() { + let new_key = AccountId::new([7_u8; 32]); + let post_states = set_authority(def_with_authority(), Some(new_key), TOKEN_PROGRAM_ID); + let [def_post] = post_states.try_into().unwrap(); + + let def = TokenDefinition::try_from(&def_post.account().data).unwrap(); + let auth = match def { + TokenDefinition::Fungible { authority, .. } => authority, + _ => None, + }; + assert_eq!(auth, Some(AccountId::new([7_u8; 32]))); + } + + #[test] + fn set_authority_revokes_permanently() { + let post_states = set_authority(def_with_authority(), None, TOKEN_PROGRAM_ID); + let [def_post] = post_states.try_into().unwrap(); + + let def = TokenDefinition::try_from(&def_post.account().data).unwrap(); + let renounced = match def { + TokenDefinition::Fungible { authority, .. } => authority.is_none(), + _ => false, + }; + assert!(renounced); + } + + #[test] + #[should_panic(expected = "SetAuthority failed")] + fn set_authority_on_revoked_fails() { + let _ = set_authority( + def_with_authority_revoked(), + Some(AccountId::new([7_u8; 32])), + TOKEN_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "Mint authority must authorize the transaction")] + fn set_authority_without_is_authorized_fails() { + let mut def = def_with_authority(); + def.is_authorized = false; + let _ = set_authority(def, Some(AccountId::new([7_u8; 32])), TOKEN_PROGRAM_ID); + } + + #[test] + #[should_panic(expected = "SetAuthority failed")] + fn set_authority_wrong_signer_fails() { + let _ = set_authority( + def_wrong_authority(), + Some(AccountId::new([7_u8; 32])), + TOKEN_PROGRAM_ID, + ); + } + + /// After rotating A ([15;32]) -> B ([7;32]) via self-authority, B can rotate + /// again to C ([9;32]) by presenting itself as the external authority. + #[test] + fn set_authority_with_authority_rotates_again() { + let rotate_post = set_authority( + def_with_authority(), + Some(AccountId::new([7_u8; 32])), + TOKEN_PROGRAM_ID, + ); + let [def_post] = rotate_post.try_into().unwrap(); + + let mut rotated_def = def_with_authority(); + rotated_def.account = def_post.account().clone(); + + // B ([7;32]) rotates to C ([9;32]) as the external authority. + let post_states = set_authority_with_authority( + rotated_def, + new_authority_signer(), + Some(AccountId::new([9_u8; 32])), + TOKEN_PROGRAM_ID, + ); + let [def_after, _auth] = post_states.try_into().unwrap(); + let auth = match TokenDefinition::try_from(&def_after.account().data).unwrap() { + TokenDefinition::Fungible { authority, .. } => authority, + _ => None, + }; + assert_eq!(auth, Some(AccountId::new([9_u8; 32]))); + } + + /// A rotated external authority B ([7;32]) can permanently revoke. + #[test] + fn set_authority_with_authority_revokes() { + let rotate_post = set_authority( + def_with_authority(), + Some(AccountId::new([7_u8; 32])), + TOKEN_PROGRAM_ID, + ); + let [def_post] = rotate_post.try_into().unwrap(); + + let mut rotated_def = def_with_authority(); + rotated_def.account = def_post.account().clone(); + + let post_states = set_authority_with_authority( + rotated_def, + new_authority_signer(), + None, + TOKEN_PROGRAM_ID, + ); + let [def_after, _auth] = post_states.try_into().unwrap(); + let renounced = match TokenDefinition::try_from(&def_after.account().data).unwrap() { + TokenDefinition::Fungible { authority, .. } => authority.is_none(), + _ => false, + }; + assert!(renounced); + } + + /// An external account that is not the current authority cannot rotate/revoke. + #[test] + #[should_panic(expected = "SetAuthority failed: signer is not the current authority")] + fn set_authority_with_authority_wrong_signer_fails() { + // Stored authority is A ([15;32]); present a different authorized account. + let wrong_authority = AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([8_u8; 32]), + }; + let _ = set_authority_with_authority( + def_with_authority(), + wrong_authority, + Some(AccountId::new([9_u8; 32])), + TOKEN_PROGRAM_ID, + ); + } + + #[test] + fn set_authority_rotate_then_old_cannot_mint() { + let new_key = AccountId::new([7_u8; 32]); + let post_states = set_authority(def_with_authority(), Some(new_key), TOKEN_PROGRAM_ID); + let [def_post] = post_states.try_into().unwrap(); + + let def = TokenDefinition::try_from(&def_post.account().data).unwrap(); + let auth = match def { + TokenDefinition::Fungible { authority, .. } => authority, + _ => None, + }; + // Rotated to the new key; the old authority no longer controls it. + assert_eq!(auth, Some(AccountId::new([7_u8; 32]))); + assert_ne!(auth, Some(AccountId::new(AUTHORITY))); + } + + /// Authority signer for the rotated key B ([7;32]), authorized. + fn new_authority_signer() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([7_u8; 32]), + } + } + + /// RFP-001 end-to-end (comment #1): after rotating authority A -> B, the new + /// authority B can actually mint by presenting itself in `authority_accounts`. + #[test] + fn rotated_authority_can_mint() { + // Rotate A ([15;32]) -> B ([7;32]), signed by A via self-authority. + let rotate_post = set_authority( + def_with_authority(), + Some(AccountId::new([7_u8; 32])), + TOKEN_PROGRAM_ID, + ); + let [def_post] = rotate_post.try_into().unwrap(); + + // Rebuild the definition carrying the rotated authority, re-authorized. + let mut rotated_def = def_with_authority(); + rotated_def.account = def_post.account().clone(); + + // B mints by presenting itself as the external authority. + let mint_post = mint_with_authority( + rotated_def, + holding_account(), + new_authority_signer(), + 10_000, + TOKEN_PROGRAM_ID, + ); + let [def_after, holding_after, _auth] = mint_post.try_into().unwrap(); + let minted = TokenDefinition::try_from(&def_after.account().data).unwrap(); + assert!(matches!( + minted, + TokenDefinition::Fungible { + total_supply: 110_000, + .. + } + )); + let holding = TokenHolding::try_from(&holding_after.account().data).unwrap(); + assert!(matches!( + holding, + TokenHolding::Fungible { + balance: 11_000, + .. + } + )); + } + + /// Comment #1 negative: after rotation to B, the OLD authority A can no + /// longer mint. Here A attempts self-authority (empty `authority_accounts`), + /// but the definition's own id no longer matches the stored authority B. + #[test] + #[should_panic(expected = "Mint authority check failed")] + fn rotated_authority_old_key_cannot_mint() { + let rotate_post = set_authority( + def_with_authority(), + Some(AccountId::new([7_u8; 32])), + TOKEN_PROGRAM_ID, + ); + let [def_post] = rotate_post.try_into().unwrap(); + + let mut rotated_def = def_with_authority(); + rotated_def.account = def_post.account().clone(); + + // A ([15;32]) is no longer the authority; self-authority must fail. + let _ = mint(rotated_def, holding_account(), 10_000, TOKEN_PROGRAM_ID); + } +}