mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-02 21:19:44 +00:00
feat(token): add mint authority model to token program
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<AccountId>` (`Some(id)` = mintable by `id`, `None` =
fixed supply). Keeping it a plain `Option<AccountId>` 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.
This commit is contained in:
parent
751d4ac530
commit
fe4c7a96da
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -2265,9 +2265,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.17"
|
version = "0.1.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
|
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@ -3364,9 +3364,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.14.1"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"web-time",
|
"web-time",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@ -3935,9 +3935,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.51"
|
version = "0.3.53"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327"
|
checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
@ -3955,9 +3955,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.30"
|
version = "0.2.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935"
|
checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
|||||||
@ -666,6 +666,12 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"option": "account_id"
|
"option": "account_id"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "authority",
|
||||||
|
"type": {
|
||||||
|
"option": "account_id"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -120,6 +120,12 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"option": "account_id"
|
"option": "account_id"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "authority",
|
||||||
|
"type": {
|
||||||
|
"option": "account_id"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -160,6 +160,12 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"option": "account_id"
|
"option": "account_id"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "authority",
|
||||||
|
"type": {
|
||||||
|
"option": "account_id"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -49,6 +49,12 @@
|
|||||||
{
|
{
|
||||||
"name": "total_supply",
|
"name": "total_supply",
|
||||||
"type": "u128"
|
"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",
|
"name": "print_nft",
|
||||||
"accounts": [
|
"accounts": [
|
||||||
@ -194,6 +273,12 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"option": "account_id"
|
"option": "account_id"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "authority",
|
||||||
|
"type": {
|
||||||
|
"option": "account_id"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
459
programs/amm/methods/guest/Cargo.lock
generated
459
programs/amm/methods/guest/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -193,6 +193,7 @@ pub fn new_definition(
|
|||||||
&token_core::Instruction::NewFungibleDefinition {
|
&token_core::Instruction::NewFungibleDefinition {
|
||||||
name: String::from("LP Token"),
|
name: String::from("LP Token"),
|
||||||
total_supply: MINIMUM_LIQUIDITY,
|
total_supply: MINIMUM_LIQUIDITY,
|
||||||
|
mint_authority: Some(pool_definition_lp.account_id),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_pda_seeds(vec![
|
.with_pda_seeds(vec![
|
||||||
@ -206,8 +207,10 @@ pub fn new_definition(
|
|||||||
name: String::from("LP Token"),
|
name: String::from("LP Token"),
|
||||||
total_supply: MINIMUM_LIQUIDITY,
|
total_supply: MINIMUM_LIQUIDITY,
|
||||||
metadata_id: None,
|
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(
|
let call_token_lp_user = ChainedCall::new(
|
||||||
token_program_id,
|
token_program_id,
|
||||||
vec![pool_lp_after_lock, user_holding_lp.clone()],
|
vec![pool_lp_after_lock, user_holding_lp.clone()],
|
||||||
|
|||||||
@ -538,10 +538,11 @@ impl ChainedCallForTests {
|
|||||||
|
|
||||||
ChainedCall::new(
|
ChainedCall::new(
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
vec![pool_lp_auth, lp_lock_holding_auth],
|
vec![pool_lp_auth.clone(), lp_lock_holding_auth],
|
||||||
&token_core::Instruction::NewFungibleDefinition {
|
&token_core::Instruction::NewFungibleDefinition {
|
||||||
name: String::from("LP Token"),
|
name: String::from("LP Token"),
|
||||||
total_supply: MINIMUM_LIQUIDITY,
|
total_supply: MINIMUM_LIQUIDITY,
|
||||||
|
mint_authority: Some(pool_lp_auth.account_id),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_pda_seeds(vec![
|
.with_pda_seeds(vec![
|
||||||
@ -872,6 +873,7 @@ impl AccountWithMetadataForTests {
|
|||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: BalanceForTests::lp_supply_init(),
|
total_supply: BalanceForTests::lp_supply_init(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(IdForTests::token_lp_definition_id()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
@ -897,6 +899,7 @@ impl AccountWithMetadataForTests {
|
|||||||
name: String::from("LP Token"),
|
name: String::from("LP Token"),
|
||||||
total_supply: MINIMUM_LIQUIDITY,
|
total_supply: MINIMUM_LIQUIDITY,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(IdForTests::token_lp_definition_id()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
@ -914,6 +917,7 @@ impl AccountWithMetadataForTests {
|
|||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: BalanceForTests::lp_supply_init(),
|
total_supply: BalanceForTests::lp_supply_init(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(IdForTests::token_lp_definition_id()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
@ -3263,6 +3267,7 @@ fn test_new_definition_lp_symmetric_amounts() {
|
|||||||
&token_core::Instruction::NewFungibleDefinition {
|
&token_core::Instruction::NewFungibleDefinition {
|
||||||
name: String::from("LP Token"),
|
name: String::from("LP Token"),
|
||||||
total_supply: MINIMUM_LIQUIDITY,
|
total_supply: MINIMUM_LIQUIDITY,
|
||||||
|
mint_authority: Some(pool_lp_auth.account_id),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_pda_seeds(vec![
|
.with_pda_seeds(vec![
|
||||||
@ -3365,6 +3370,7 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() {
|
|||||||
&token_core::Instruction::NewFungibleDefinition {
|
&token_core::Instruction::NewFungibleDefinition {
|
||||||
name: String::from("LP Token"),
|
name: String::from("LP Token"),
|
||||||
total_supply: MINIMUM_LIQUIDITY,
|
total_supply: MINIMUM_LIQUIDITY,
|
||||||
|
mint_authority: Some(pool_lp_auth.account_id),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_pda_seeds(vec![
|
.with_pda_seeds(vec![
|
||||||
|
|||||||
@ -41,6 +41,7 @@ fn definition_account() -> AccountWithMetadata {
|
|||||||
name: "TEST".to_string(),
|
name: "TEST".to_string(),
|
||||||
total_supply: 1000,
|
total_supply: 1000,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: nssa_core::account::Nonce(0),
|
nonce: nssa_core::account::Nonce(0),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -401,6 +401,7 @@ impl Accounts {
|
|||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: Balances::token_a_supply(),
|
total_supply: Balances::token_a_supply(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -414,6 +415,7 @@ impl Accounts {
|
|||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: Balances::token_b_supply(),
|
total_supply: Balances::token_b_supply(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -427,6 +429,7 @@ impl Accounts {
|
|||||||
name: String::from("LP Token"),
|
name: String::from("LP Token"),
|
||||||
total_supply: Balances::token_lp_supply(),
|
total_supply: Balances::token_lp_supply(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(Ids::token_lp_definition()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -705,6 +708,7 @@ impl Accounts {
|
|||||||
name: String::from("LP Token"),
|
name: String::from("LP Token"),
|
||||||
total_supply: Balances::token_lp_supply_add(),
|
total_supply: Balances::token_lp_supply_add(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(Ids::token_lp_definition()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -797,6 +801,7 @@ impl Accounts {
|
|||||||
name: String::from("LP Token"),
|
name: String::from("LP Token"),
|
||||||
total_supply: Balances::token_lp_supply_remove(),
|
total_supply: Balances::token_lp_supply_remove(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(Ids::token_lp_definition()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -810,6 +815,7 @@ impl Accounts {
|
|||||||
name: String::from("LP Token"),
|
name: String::from("LP Token"),
|
||||||
total_supply: 0,
|
total_supply: 0,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(Ids::token_lp_definition()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -902,6 +908,7 @@ impl Accounts {
|
|||||||
name: String::from("LP Token"),
|
name: String::from("LP Token"),
|
||||||
total_supply: Balances::lp_supply_init(),
|
total_supply: Balances::lp_supply_init(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(Ids::token_lp_definition()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -1390,6 +1397,7 @@ fn fungible_total_supply(account: &Account) -> u128 {
|
|||||||
name: _,
|
name: _,
|
||||||
total_supply,
|
total_supply,
|
||||||
metadata_id: _,
|
metadata_id: _,
|
||||||
|
authority: _,
|
||||||
} = definition
|
} = definition
|
||||||
else {
|
else {
|
||||||
panic!("expected fungible token definition")
|
panic!("expected fungible token definition")
|
||||||
|
|||||||
@ -84,6 +84,7 @@ impl Accounts {
|
|||||||
name: String::from("Gold"),
|
name: String::from("Gold"),
|
||||||
total_supply: 1_000_000_u128,
|
total_supply: 1_000_000_u128,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -121,6 +122,7 @@ impl Accounts {
|
|||||||
name: String::from("Foreign Gold"),
|
name: String::from("Foreign Gold"),
|
||||||
total_supply: 1_000_000_u128,
|
total_supply: 1_000_000_u128,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -495,6 +497,7 @@ fn ata_burn() {
|
|||||||
name: String::from("Gold"),
|
name: String::from("Gold"),
|
||||||
total_supply: 700_000_u128,
|
total_supply: 700_000_u128,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -108,6 +108,7 @@ impl Accounts {
|
|||||||
name: String::from("Gold"),
|
name: String::from("Gold"),
|
||||||
total_supply: Balances::user_holding_init(),
|
total_supply: Balances::user_holding_init(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -133,6 +134,7 @@ impl Accounts {
|
|||||||
name: String::from("DAI"),
|
name: String::from("DAI"),
|
||||||
total_supply: Balances::stablecoin_supply_init(),
|
total_supply: Balances::stablecoin_supply_init(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,10 @@ impl Keys {
|
|||||||
fn recipient_key() -> PrivateKey {
|
fn recipient_key() -> PrivateKey {
|
||||||
PrivateKey::try_new([12; 32]).expect("valid private key")
|
PrivateKey::try_new([12; 32]).expect("valid private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn authority_key() -> PrivateKey {
|
||||||
|
PrivateKey::try_new([13; 32]).expect("valid private key")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ids {
|
impl Ids {
|
||||||
@ -50,6 +54,10 @@ impl Ids {
|
|||||||
fn recipient() -> AccountId {
|
fn recipient() -> AccountId {
|
||||||
AccountId::from(&PublicKey::new_from_private_key(&Keys::recipient_key()))
|
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 {
|
impl Accounts {
|
||||||
@ -61,6 +69,7 @@ impl Accounts {
|
|||||||
name: String::from("Gold"),
|
name: String::from("Gold"),
|
||||||
total_supply: 1_000_000_u128,
|
total_supply: 1_000_000_u128,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(Ids::token_definition()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -74,6 +83,7 @@ impl Accounts {
|
|||||||
name: String::from("Gold"),
|
name: String::from("Gold"),
|
||||||
total_supply: 1_000_000_u128,
|
total_supply: 1_000_000_u128,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(Ids::token_definition()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -102,6 +112,15 @@ impl Accounts {
|
|||||||
nonce: Nonce(0),
|
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) {
|
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::token_definition(), Accounts::token_definition_init());
|
||||||
state.force_insert_account(Ids::holder(), Accounts::holder_init());
|
state.force_insert_account(Ids::holder(), Accounts::holder_init());
|
||||||
state.force_insert_account(Ids::recipient(), Accounts::recipient_init());
|
state.force_insert_account(Ids::recipient(), Accounts::recipient_init());
|
||||||
|
state.force_insert_account(Ids::authority(), Accounts::authority_init());
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,6 +146,7 @@ fn state_for_token_tests_without_recipient() -> V03State {
|
|||||||
deploy_token(&mut state);
|
deploy_token(&mut state);
|
||||||
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
|
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::holder(), Accounts::holder_init());
|
||||||
|
state.force_insert_account(Ids::authority(), Accounts::authority_init());
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +158,7 @@ fn token_new_fungible_definition() {
|
|||||||
let instruction = token_core::Instruction::NewFungibleDefinition {
|
let instruction = token_core::Instruction::NewFungibleDefinition {
|
||||||
name: String::from("Gold"),
|
name: String::from("Gold"),
|
||||||
total_supply: 1_000_000_u128,
|
total_supply: 1_000_000_u128,
|
||||||
|
mint_authority: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let message = public_transaction::Message::try_new(
|
let message = public_transaction::Message::try_new(
|
||||||
@ -164,6 +186,7 @@ fn token_new_fungible_definition() {
|
|||||||
name: String::from("Gold"),
|
name: String::from("Gold"),
|
||||||
total_supply: 1_000_000_u128,
|
total_supply: 1_000_000_u128,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(1),
|
nonce: Nonce(1),
|
||||||
}
|
}
|
||||||
@ -415,6 +438,7 @@ fn token_burn() {
|
|||||||
name: String::from("Gold"),
|
name: String::from("Gold"),
|
||||||
total_supply: 800_000_u128,
|
total_supply: 800_000_u128,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(Ids::token_definition()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
@ -464,6 +488,7 @@ fn token_mint() {
|
|||||||
name: String::from("Gold"),
|
name: String::from("Gold"),
|
||||||
total_supply: 1_500_000_u128,
|
total_supply: 1_500_000_u128,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(Ids::token_definition()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(1),
|
nonce: Nonce(1),
|
||||||
}
|
}
|
||||||
@ -585,6 +610,7 @@ fn token_mint_fresh_authorized_public_recipient() {
|
|||||||
name: String::from("Gold"),
|
name: String::from("Gold"),
|
||||||
total_supply: 1_500_000_u128,
|
total_supply: 1_500_000_u128,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(Ids::token_definition()),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(1),
|
nonce: Nonce(1),
|
||||||
}
|
}
|
||||||
@ -915,3 +941,319 @@ fn token_deshielded_transfer() {
|
|||||||
.get_proof_for_commitment(&Commitment::new(&sender_id, &new_sender_account))
|
.get_proof_for_commitment(&Commitment::new(&sender_id, &new_sender_account))
|
||||||
.is_some());
|
.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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -79,6 +79,7 @@ fn collateral_definition_account() -> AccountWithMetadata {
|
|||||||
name: "SNT".to_owned(),
|
name: "SNT".to_owned(),
|
||||||
total_supply: 1_000_000,
|
total_supply: 1_000_000,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
@ -156,6 +157,7 @@ fn stablecoin_definition_account() -> AccountWithMetadata {
|
|||||||
name: "DAI".to_owned(),
|
name: "DAI".to_owned(),
|
||||||
total_supply: 1_000_000,
|
total_supply: 1_000_000,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
@ -389,6 +391,7 @@ fn open_position_rejects_mismatched_token_definition() {
|
|||||||
name: "OTHER".to_owned(),
|
name: "OTHER".to_owned(),
|
||||||
total_supply: 1,
|
total_supply: 1,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -18,10 +18,18 @@ pub enum Instruction {
|
|||||||
|
|
||||||
/// Create a new fungible token definition without metadata.
|
/// 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:
|
/// Required accounts:
|
||||||
/// - Token Definition account (uninitialized, authorized),
|
/// - Token Definition account (uninitialized, authorized),
|
||||||
/// - Token Holding account (uninitialized, authorized).
|
/// - Token Holding account (uninitialized, authorized).
|
||||||
NewFungibleDefinition { name: String, total_supply: u128 },
|
NewFungibleDefinition {
|
||||||
|
name: String,
|
||||||
|
total_supply: u128,
|
||||||
|
mint_authority: Option<AccountId>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Create a new fungible or non-fungible token definition with metadata.
|
/// Create a new fungible or non-fungible token definition with metadata.
|
||||||
///
|
///
|
||||||
@ -49,20 +57,53 @@ pub enum Instruction {
|
|||||||
/// - Token Holding account (authorized).
|
/// - Token Holding account (authorized).
|
||||||
Burn { amount_to_burn: u128 },
|
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:
|
/// Required accounts:
|
||||||
/// - Token Definition account (initialized, authorized),
|
/// - Token Definition account (initialized, authorized as the current mint authority),
|
||||||
/// - Token Holding account (initialized, or uninitialized with holder authorization in the
|
/// - Token Holding account (uninitialized or authorized and initialized).
|
||||||
/// same transaction).
|
|
||||||
Mint { amount_to_mint: u128 },
|
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.
|
/// Print a new NFT from the master copy.
|
||||||
///
|
///
|
||||||
/// Required accounts:
|
/// Required accounts:
|
||||||
/// - NFT Master Token Holding account (authorized),
|
/// - NFT Master Token Holding account (authorized),
|
||||||
/// - NFT Printed Copy Token Holding account (uninitialized, authorized).
|
/// - NFT Printed Copy Token Holding account (uninitialized, authorized).
|
||||||
PrintNft,
|
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<AccountId> },
|
||||||
|
|
||||||
|
/// 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<AccountId> },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@ -70,6 +111,9 @@ pub enum NewTokenDefinition {
|
|||||||
Fungible {
|
Fungible {
|
||||||
name: String,
|
name: String,
|
||||||
total_supply: u128,
|
total_supply: u128,
|
||||||
|
/// Mint authority. `Some(id)` makes the token mintable by `id`; `None`
|
||||||
|
/// fixes the supply.
|
||||||
|
mint_authority: Option<AccountId>,
|
||||||
},
|
},
|
||||||
NonFungible {
|
NonFungible {
|
||||||
name: String,
|
name: String,
|
||||||
@ -84,6 +128,14 @@ pub enum TokenDefinition {
|
|||||||
name: String,
|
name: String,
|
||||||
total_supply: u128,
|
total_supply: u128,
|
||||||
metadata_id: Option<AccountId>,
|
metadata_id: Option<AccountId>,
|
||||||
|
/// Mint authority slot. `Some(id)` may mint and rotate/renounce;
|
||||||
|
/// `None` means the supply is permanently fixed.
|
||||||
|
///
|
||||||
|
/// Stored directly as `Option<AccountId>` (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<AccountId>,
|
||||||
},
|
},
|
||||||
NonFungible {
|
NonFungible {
|
||||||
name: String,
|
name: String,
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
#![cfg_attr(not(test), no_main)]
|
#![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 spel_framework::context::ProgramContext;
|
||||||
use nssa_core::account::AccountWithMetadata;
|
use spel_framework::prelude::*;
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
risc0_zkvm::guest::entry!(main);
|
risc0_zkvm::guest::entry!(main);
|
||||||
@ -25,15 +29,15 @@ mod token {
|
|||||||
recipient: AccountWithMetadata,
|
recipient: AccountWithMetadata,
|
||||||
amount_to_transfer: u128,
|
amount_to_transfer: u128,
|
||||||
) -> SpelResult {
|
) -> SpelResult {
|
||||||
Ok(spel_framework::SpelOutput::execute(token_program::transfer::transfer(
|
Ok(spel_framework::SpelOutput::execute(
|
||||||
sender,
|
token_program::transfer::transfer(sender, recipient, amount_to_transfer),
|
||||||
recipient,
|
vec![],
|
||||||
amount_to_transfer,
|
))
|
||||||
), vec![]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new fungible token definition without metadata.
|
/// Create a new fungible token definition without metadata.
|
||||||
/// Definition and holding targets must be uninitialized and authorized.
|
/// Definition and holding targets must be uninitialized and authorized.
|
||||||
|
/// `mint_authority` is `Some(id)` for a mintable token or `None` for fixed supply.
|
||||||
#[instruction]
|
#[instruction]
|
||||||
pub fn new_fungible_definition(
|
pub fn new_fungible_definition(
|
||||||
#[account(init, signer)]
|
#[account(init, signer)]
|
||||||
@ -42,6 +46,7 @@ mod token {
|
|||||||
holding_target_account: AccountWithMetadata,
|
holding_target_account: AccountWithMetadata,
|
||||||
name: String,
|
name: String,
|
||||||
total_supply: u128,
|
total_supply: u128,
|
||||||
|
mint_authority: Option<AccountId>,
|
||||||
) -> SpelResult {
|
) -> SpelResult {
|
||||||
Ok(spel_framework::SpelOutput::execute(
|
Ok(spel_framework::SpelOutput::execute(
|
||||||
token_program::new_definition::new_fungible_definition(
|
token_program::new_definition::new_fungible_definition(
|
||||||
@ -49,6 +54,7 @@ mod token {
|
|||||||
holding_target_account,
|
holding_target_account,
|
||||||
name,
|
name,
|
||||||
total_supply,
|
total_supply,
|
||||||
|
mint_authority,
|
||||||
),
|
),
|
||||||
vec![],
|
vec![],
|
||||||
))
|
))
|
||||||
@ -111,15 +117,16 @@ mod token {
|
|||||||
user_holding_account: AccountWithMetadata,
|
user_holding_account: AccountWithMetadata,
|
||||||
amount_to_burn: u128,
|
amount_to_burn: u128,
|
||||||
) -> SpelResult {
|
) -> SpelResult {
|
||||||
Ok(spel_framework::SpelOutput::execute(token_program::burn::burn(
|
Ok(spel_framework::SpelOutput::execute(
|
||||||
definition_account,
|
token_program::burn::burn(definition_account, user_holding_account, amount_to_burn),
|
||||||
user_holding_account,
|
vec![],
|
||||||
amount_to_burn,
|
))
|
||||||
), vec![]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mint new tokens to the holder's account.
|
/// Mint new tokens under self/PDA authority: the definition account itself is
|
||||||
/// Fresh public holders must be explicitly authorized in the same transaction.
|
/// 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]
|
#[instruction]
|
||||||
pub fn mint(
|
pub fn mint(
|
||||||
ctx: ProgramContext,
|
ctx: ProgramContext,
|
||||||
@ -129,12 +136,87 @@ mod token {
|
|||||||
user_holding_account: AccountWithMetadata,
|
user_holding_account: AccountWithMetadata,
|
||||||
amount_to_mint: u128,
|
amount_to_mint: u128,
|
||||||
) -> SpelResult {
|
) -> SpelResult {
|
||||||
Ok(spel_framework::SpelOutput::execute(token_program::mint::mint(
|
Ok(spel_framework::SpelOutput::execute(
|
||||||
definition_account,
|
token_program::mint::mint(
|
||||||
user_holding_account,
|
definition_account,
|
||||||
amount_to_mint,
|
user_holding_account,
|
||||||
ctx.self_program_id,
|
amount_to_mint,
|
||||||
), vec![]))
|
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<AccountId>,
|
||||||
|
) -> 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<AccountId>,
|
||||||
|
) -> 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.
|
/// Print a new NFT from the master copy.
|
||||||
@ -146,9 +228,9 @@ mod token {
|
|||||||
#[account(init, signer)]
|
#[account(init, signer)]
|
||||||
printed_account: AccountWithMetadata,
|
printed_account: AccountWithMetadata,
|
||||||
) -> SpelResult {
|
) -> SpelResult {
|
||||||
Ok(spel_framework::SpelOutput::execute(token_program::print_nft::print_nft(
|
Ok(spel_framework::SpelOutput::execute(
|
||||||
master_account,
|
token_program::print_nft::print_nft(master_account, printed_account),
|
||||||
printed_account,
|
vec![],
|
||||||
), vec![]))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ pub fn burn(
|
|||||||
name: _,
|
name: _,
|
||||||
metadata_id: _,
|
metadata_id: _,
|
||||||
total_supply,
|
total_supply,
|
||||||
|
authority: _,
|
||||||
},
|
},
|
||||||
TokenHolding::Fungible {
|
TokenHolding::Fungible {
|
||||||
definition_id: _,
|
definition_id: _,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ pub mod initialize;
|
|||||||
pub mod mint;
|
pub mod mint;
|
||||||
pub mod new_definition;
|
pub mod new_definition;
|
||||||
pub mod print_nft;
|
pub mod print_nft;
|
||||||
|
pub mod set_authority;
|
||||||
pub mod transfer;
|
pub mod transfer;
|
||||||
|
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@ -4,16 +4,56 @@ use nssa_core::{
|
|||||||
};
|
};
|
||||||
use token_core::{TokenDefinition, TokenHolding};
|
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(
|
pub fn mint(
|
||||||
definition_account: AccountWithMetadata,
|
definition_account: AccountWithMetadata,
|
||||||
user_holding_account: AccountWithMetadata,
|
user_holding_account: AccountWithMetadata,
|
||||||
amount_to_mint: u128,
|
amount_to_mint: u128,
|
||||||
token_program_id: ProgramId,
|
token_program_id: ProgramId,
|
||||||
) -> Vec<AccountPostState> {
|
) -> Vec<AccountPostState> {
|
||||||
assert!(
|
mint_inner(
|
||||||
definition_account.is_authorized,
|
definition_account,
|
||||||
"Definition authorization is missing"
|
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<AccountPostState> {
|
||||||
|
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<AccountWithMetadata>,
|
||||||
|
amount_to_mint: u128,
|
||||||
|
token_program_id: ProgramId,
|
||||||
|
) -> Vec<AccountPostState> {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
definition_account.account.program_owner, token_program_id,
|
definition_account.account.program_owner, token_program_id,
|
||||||
"Token definition must be owned by token program"
|
"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)
|
let mut definition = TokenDefinition::try_from(&definition_account.account.data)
|
||||||
.expect("Token Definition account must be valid");
|
.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() {
|
let mut holding = if user_holding_account.account == Account::default() {
|
||||||
TokenHolding::zeroized_from_definition(definition_account.account_id, &definition)
|
TokenHolding::zeroized_from_definition(definition_account.account_id, &definition)
|
||||||
} else {
|
} else {
|
||||||
@ -40,6 +100,7 @@ pub fn mint(
|
|||||||
name: _,
|
name: _,
|
||||||
metadata_id: _,
|
metadata_id: _,
|
||||||
total_supply,
|
total_supply,
|
||||||
|
authority: _,
|
||||||
},
|
},
|
||||||
TokenHolding::Fungible {
|
TokenHolding::Fungible {
|
||||||
definition_id: _,
|
definition_id: _,
|
||||||
@ -69,8 +130,16 @@ pub fn mint(
|
|||||||
let mut holding_post = user_holding_account.account;
|
let mut holding_post = user_holding_account.account;
|
||||||
holding_post.data = Data::from(&holding);
|
holding_post.data = Data::from(&holding);
|
||||||
|
|
||||||
vec![
|
// Post-states must match pre-state order and count: [definition, holding]
|
||||||
AccountPostState::new(definition_post),
|
// for self authority, plus the read-only authority account when external.
|
||||||
AccountPostState::new_claimed_if_default(holding_post, Claim::Authorized),
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,31 @@
|
|||||||
use nssa_core::{
|
use nssa_core::{
|
||||||
account::{Account, AccountWithMetadata, Data},
|
account::{Account, AccountId, AccountWithMetadata, Data},
|
||||||
program::{AccountPostState, Claim},
|
program::{AccountPostState, Claim},
|
||||||
};
|
};
|
||||||
use token_core::{
|
use token_core::{
|
||||||
NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, TokenMetadata,
|
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<AccountId>) -> Option<AccountId> {
|
||||||
|
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(
|
pub fn new_fungible_definition(
|
||||||
definition_target_account: AccountWithMetadata,
|
definition_target_account: AccountWithMetadata,
|
||||||
holding_target_account: AccountWithMetadata,
|
holding_target_account: AccountWithMetadata,
|
||||||
name: String,
|
name: String,
|
||||||
total_supply: u128,
|
total_supply: u128,
|
||||||
|
mint_authority: Option<AccountId>,
|
||||||
) -> Vec<AccountPostState> {
|
) -> Vec<AccountPostState> {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
definition_target_account.account,
|
definition_target_account.account,
|
||||||
@ -36,6 +51,7 @@ pub fn new_fungible_definition(
|
|||||||
name,
|
name,
|
||||||
total_supply,
|
total_supply,
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: validate_mint_authority(mint_authority),
|
||||||
};
|
};
|
||||||
let token_holding = TokenHolding::Fungible {
|
let token_holding = TokenHolding::Fungible {
|
||||||
definition_id: definition_target_account.account_id,
|
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 {
|
let (token_definition, token_holding) = match new_definition {
|
||||||
NewTokenDefinition::Fungible { name, total_supply } => (
|
NewTokenDefinition::Fungible {
|
||||||
|
name,
|
||||||
|
total_supply,
|
||||||
|
mint_authority,
|
||||||
|
} => (
|
||||||
TokenDefinition::Fungible {
|
TokenDefinition::Fungible {
|
||||||
name,
|
name,
|
||||||
total_supply,
|
total_supply,
|
||||||
metadata_id: Some(metadata_target_account.account_id),
|
metadata_id: Some(metadata_target_account.account_id),
|
||||||
|
authority: validate_mint_authority(mint_authority),
|
||||||
},
|
},
|
||||||
TokenHolding::Fungible {
|
TokenHolding::Fungible {
|
||||||
definition_id: definition_target_account.account_id,
|
definition_id: definition_target_account.account_id,
|
||||||
@ -124,7 +145,7 @@ pub fn new_definition_with_metadata(
|
|||||||
standard: metadata.standard,
|
standard: metadata.standard,
|
||||||
uri: metadata.uri,
|
uri: metadata.uri,
|
||||||
creators: metadata.creators,
|
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();
|
let mut definition_target_account_post = definition_target_account.account.clone();
|
||||||
|
|||||||
97
programs/token/src/set_authority.rs
Normal file
97
programs/token/src/set_authority.rs
Normal file
@ -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<AccountId>,
|
||||||
|
token_program_id: ProgramId,
|
||||||
|
) -> Vec<AccountPostState> {
|
||||||
|
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<AccountId>,
|
||||||
|
token_program_id: ProgramId,
|
||||||
|
) -> Vec<AccountPostState> {
|
||||||
|
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<AccountWithMetadata>,
|
||||||
|
new_authority: Option<AccountId>,
|
||||||
|
token_program_id: ProgramId,
|
||||||
|
) -> Vec<AccountPostState> {
|
||||||
|
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
|
||||||
|
}
|
||||||
@ -15,9 +15,10 @@ use token_core::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
burn::burn,
|
burn::burn,
|
||||||
initialize::initialize_account,
|
initialize::initialize_account,
|
||||||
mint::mint,
|
mint::{mint, mint_with_authority},
|
||||||
new_definition::{new_definition_with_metadata, new_fungible_definition},
|
new_definition::{new_definition_with_metadata, new_fungible_definition},
|
||||||
print_nft::print_nft,
|
print_nft::print_nft,
|
||||||
|
set_authority::{set_authority, set_authority_with_authority},
|
||||||
transfer::transfer,
|
transfer::transfer,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ impl AccountForTests {
|
|||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: BalanceForTests::init_supply(),
|
total_supply: BalanceForTests::init_supply(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(AccountId::new([15_u8; 32])),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
@ -59,6 +61,7 @@ impl AccountForTests {
|
|||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: BalanceForTests::init_supply(),
|
total_supply: BalanceForTests::init_supply(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
@ -76,6 +79,7 @@ impl AccountForTests {
|
|||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: BalanceForTests::init_supply(),
|
total_supply: BalanceForTests::init_supply(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
@ -157,6 +161,7 @@ impl AccountForTests {
|
|||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: BalanceForTests::init_supply_burned(),
|
total_supply: BalanceForTests::init_supply_burned(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(AccountId::new([15_u8; 32])),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
@ -238,6 +243,7 @@ impl AccountForTests {
|
|||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: BalanceForTests::init_supply_mint(),
|
total_supply: BalanceForTests::init_supply_mint(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: Some(AccountId::new([15_u8; 32])),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
@ -328,6 +334,7 @@ impl AccountForTests {
|
|||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: BalanceForTests::init_supply(),
|
total_supply: BalanceForTests::init_supply(),
|
||||||
metadata_id: None,
|
metadata_id: None,
|
||||||
|
authority: None,
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
},
|
},
|
||||||
@ -594,6 +601,7 @@ fn test_new_definition_non_default_first_account_should_fail() {
|
|||||||
holding_account,
|
holding_account,
|
||||||
String::from("test"),
|
String::from("test"),
|
||||||
10,
|
10,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,6 +626,7 @@ fn test_new_definition_non_default_second_account_should_fail() {
|
|||||||
holding_account,
|
holding_account,
|
||||||
String::from("test"),
|
String::from("test"),
|
||||||
10,
|
10,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -631,6 +640,7 @@ fn test_new_definition_requires_authorized_definition_target() {
|
|||||||
holding_account,
|
holding_account,
|
||||||
String::from("test"),
|
String::from("test"),
|
||||||
10,
|
10,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -644,6 +654,7 @@ fn test_new_definition_requires_authorized_holding_target() {
|
|||||||
holding_account,
|
holding_account,
|
||||||
String::from("test"),
|
String::from("test"),
|
||||||
10,
|
10,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,6 +668,7 @@ fn test_new_definition_with_valid_inputs_succeeds() {
|
|||||||
holding_account,
|
holding_account,
|
||||||
String::from("test"),
|
String::from("test"),
|
||||||
BalanceForTests::init_supply(),
|
BalanceForTests::init_supply(),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let [definition_account, holding_account] = post_states.try_into().unwrap();
|
let [definition_account, holding_account] = post_states.try_into().unwrap();
|
||||||
@ -918,9 +930,11 @@ fn test_mint_not_valid_definition_account() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "Definition authorization is missing")]
|
#[should_panic(expected = "Mint authority must authorize the transaction")]
|
||||||
fn test_mint_missing_authorization() {
|
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 holding_account = AccountForTests::holding_same_definition_without_authorization();
|
||||||
let _post_states = mint(
|
let _post_states = mint(
|
||||||
definition_account,
|
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]
|
#[test]
|
||||||
#[should_panic(expected = "Mismatch Token Definition and Token Holding")]
|
#[should_panic(expected = "Mismatch Token Definition and Token Holding")]
|
||||||
fn test_mint_mismatched_token_definition() {
|
fn test_mint_mismatched_token_definition() {
|
||||||
|
//
|
||||||
let definition_account = AccountForTests::definition_account_auth();
|
let definition_account = AccountForTests::definition_account_auth();
|
||||||
let holding_account = AccountForTests::holding_different_definition();
|
let holding_account = AccountForTests::holding_different_definition();
|
||||||
let _post_states = mint(
|
let _post_states = mint(
|
||||||
@ -1053,6 +1081,7 @@ fn test_new_definition_with_metadata_success() {
|
|||||||
let new_definition = NewTokenDefinition::Fungible {
|
let new_definition = NewTokenDefinition::Fungible {
|
||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: 15u128,
|
total_supply: 15u128,
|
||||||
|
mint_authority: None,
|
||||||
};
|
};
|
||||||
let metadata = NewTokenMetadata {
|
let metadata = NewTokenMetadata {
|
||||||
standard: MetadataStandard::Simple,
|
standard: MetadataStandard::Simple,
|
||||||
@ -1074,6 +1103,42 @@ fn test_new_definition_with_metadata_success() {
|
|||||||
assert_eq!(metadata_post.required_claim(), Some(Claim::Authorized));
|
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")]
|
#[should_panic(expected = "Definition target account must be authorized")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_call_new_definition_metadata_requires_authorized_definition() {
|
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 {
|
let new_definition = NewTokenDefinition::Fungible {
|
||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: 15u128,
|
total_supply: 15u128,
|
||||||
|
mint_authority: None,
|
||||||
};
|
};
|
||||||
let metadata = NewTokenMetadata {
|
let metadata = NewTokenMetadata {
|
||||||
standard: MetadataStandard::Simple,
|
standard: MetadataStandard::Simple,
|
||||||
@ -1107,6 +1173,7 @@ fn test_call_new_definition_metadata_requires_authorized_holding() {
|
|||||||
let new_definition = NewTokenDefinition::Fungible {
|
let new_definition = NewTokenDefinition::Fungible {
|
||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: 15u128,
|
total_supply: 15u128,
|
||||||
|
mint_authority: None,
|
||||||
};
|
};
|
||||||
let metadata = NewTokenMetadata {
|
let metadata = NewTokenMetadata {
|
||||||
standard: MetadataStandard::Simple,
|
standard: MetadataStandard::Simple,
|
||||||
@ -1135,6 +1202,7 @@ fn test_call_new_definition_metadata_requires_authorized_metadata() {
|
|||||||
let new_definition = NewTokenDefinition::Fungible {
|
let new_definition = NewTokenDefinition::Fungible {
|
||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: 15u128,
|
total_supply: 15u128,
|
||||||
|
mint_authority: None,
|
||||||
};
|
};
|
||||||
let metadata = NewTokenMetadata {
|
let metadata = NewTokenMetadata {
|
||||||
standard: MetadataStandard::Simple,
|
standard: MetadataStandard::Simple,
|
||||||
@ -1167,6 +1235,7 @@ fn test_call_new_definition_metadata_with_init_definition() {
|
|||||||
let new_definition = NewTokenDefinition::Fungible {
|
let new_definition = NewTokenDefinition::Fungible {
|
||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: 15u128,
|
total_supply: 15u128,
|
||||||
|
mint_authority: None,
|
||||||
};
|
};
|
||||||
let metadata = NewTokenMetadata {
|
let metadata = NewTokenMetadata {
|
||||||
standard: MetadataStandard::Simple,
|
standard: MetadataStandard::Simple,
|
||||||
@ -1199,6 +1268,7 @@ fn test_call_new_definition_metadata_with_init_metadata() {
|
|||||||
let new_definition = NewTokenDefinition::Fungible {
|
let new_definition = NewTokenDefinition::Fungible {
|
||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: 15u128,
|
total_supply: 15u128,
|
||||||
|
mint_authority: None,
|
||||||
};
|
};
|
||||||
let metadata = NewTokenMetadata {
|
let metadata = NewTokenMetadata {
|
||||||
standard: MetadataStandard::Simple,
|
standard: MetadataStandard::Simple,
|
||||||
@ -1231,6 +1301,7 @@ fn test_call_new_definition_metadata_with_init_holding() {
|
|||||||
let new_definition = NewTokenDefinition::Fungible {
|
let new_definition = NewTokenDefinition::Fungible {
|
||||||
name: String::from("test"),
|
name: String::from("test"),
|
||||||
total_supply: 15u128,
|
total_supply: 15u128,
|
||||||
|
mint_authority: None,
|
||||||
};
|
};
|
||||||
let metadata = NewTokenMetadata {
|
let metadata = NewTokenMetadata {
|
||||||
standard: MetadataStandard::Simple,
|
standard: MetadataStandard::Simple,
|
||||||
@ -1313,3 +1384,374 @@ fn test_print_nft_success() {
|
|||||||
assert_eq!(post_master_nft.required_claim(), None);
|
assert_eq!(post_master_nft.required_claim(), None);
|
||||||
assert_eq!(post_printed.required_claim(), Some(Claim::Authorized));
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user