fix(token): owner-guard set_authority; align demo script to token IDL

Second review round on PR #125 (LP-0013):

- set_authority now rejects foreign-owned definitions. It takes the
  ProgramContext and asserts definition_account.program_owner ==
  self_program_id, matching mint and initialize_account. Without this a
  foreign-owned account with token-shaped data could have its authority
  field rewritten. Added test_set_authority_rejects_foreign_owned_definition.

- demo-full-flow.sh now calls instruction and flag names that exist in
  the regenerated token IDL: new-fungible-definition (was the nonexistent
  new-fungible-definition-with-authority), --total-supply (was
  --initial-supply), and drops --authority-account for the self-authority
  mint/set-authority path (the rest account is --authority-accounts and is
  empty when the definition is its own authority).

- Stripped a trailing-space lint nit in docs/LP-0013-README.md.
This commit is contained in:
bristinWild 2026-06-19 23:07:59 +05:30
parent 160ff8ee4a
commit 686a7d066a
5 changed files with 53 additions and 12 deletions

View File

@ -15,7 +15,7 @@ The `lez-authority` crate provides a reusable, program-agnostic authority librar
## Architecture
### Authority Model
### Authority Model
`mint_authority: Option<[u8; 32]>` is added to `TokenDefinition::Fungible`:
- `Some(key)` — the key holder can mint and rotate/revoke

View File

@ -148,6 +148,7 @@ mod token {
/// The definition account must be authorized as the current mint authority.
#[instruction]
pub fn set_authority(
ctx: ProgramContext,
definition_account: AccountWithMetadata,
authority_accounts: Vec<AccountWithMetadata>,
new_authority: Option<AccountId>,
@ -157,6 +158,7 @@ mod token {
definition_account,
new_authority,
authority_accounts,
ctx.self_program_id,
),
vec![],
))

View File

@ -1,7 +1,7 @@
use lez_authority::Ownable;
use nssa_core::{
account::{AccountId, AccountWithMetadata, Data},
program::AccountPostState,
program::{AccountPostState, ProgramId},
};
use token_core::TokenDefinition;
@ -9,7 +9,13 @@ pub fn set_authority(
definition_account: AccountWithMetadata,
new_authority: Option<AccountId>,
authority_accounts: Vec<AccountWithMetadata>,
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");

View File

@ -18,6 +18,7 @@ use crate::{
mint::mint,
new_definition::{new_definition_with_metadata, new_fungible_definition},
print_nft::print_nft,
set_authority::set_authority,
transfer::transfer,
};
@ -960,6 +961,20 @@ 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])),
vec![],
TOKEN_PROGRAM_ID,
);
}
#[test]
#[should_panic(expected = "Mismatch Token Definition and Token Holding")]
fn test_mint_mismatched_token_definition() {
@ -1534,13 +1549,19 @@ mod authority_tests {
def_with_authority(),
Some(AccountId::new([0u8; 32])),
vec![],
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), vec![]);
let post_states = set_authority(
def_with_authority(),
Some(new_key),
vec![],
TOKEN_PROGRAM_ID,
);
let [def_post] = post_states.try_into().unwrap();
let def = TokenDefinition::try_from(&def_post.account().data).unwrap();
@ -1553,7 +1574,7 @@ mod authority_tests {
#[test]
fn set_authority_revokes_permanently() {
let post_states = set_authority(def_with_authority(), None, vec![]);
let post_states = set_authority(def_with_authority(), None, vec![], TOKEN_PROGRAM_ID);
let [def_post] = post_states.try_into().unwrap();
let def = TokenDefinition::try_from(&def_post.account().data).unwrap();
@ -1571,6 +1592,7 @@ mod authority_tests {
def_with_authority_revoked(),
Some(AccountId::new([7_u8; 32])),
vec![],
TOKEN_PROGRAM_ID,
);
}
@ -1579,7 +1601,12 @@ mod authority_tests {
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])), vec![]);
let _ = set_authority(
def,
Some(AccountId::new([7_u8; 32])),
vec![],
TOKEN_PROGRAM_ID,
);
}
#[test]
@ -1589,13 +1616,19 @@ mod authority_tests {
def_wrong_authority(),
Some(AccountId::new([7_u8; 32])),
vec![],
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), vec![]);
let post_states = set_authority(
def_with_authority(),
Some(new_key),
vec![],
TOKEN_PROGRAM_ID,
);
let [def_post] = post_states.try_into().unwrap();
let def = TokenDefinition::try_from(&def_post.account().data).unwrap();
@ -1626,6 +1659,7 @@ mod authority_tests {
def_with_authority(),
Some(AccountId::new([7_u8; 32])),
vec![],
TOKEN_PROGRAM_ID,
);
let [def_post] = rotate_post.try_into().unwrap();
@ -1670,6 +1704,7 @@ mod authority_tests {
def_with_authority(),
Some(AccountId::new([7_u8; 32])),
vec![],
TOKEN_PROGRAM_ID,
);
let [def_post] = rotate_post.try_into().unwrap();

View File

@ -18,7 +18,7 @@
# 1. Start a local LEZ sequencer
# 2. Fund the wallet
# 3. Create token accounts
# 4. Submit NewFungibleDefinitionWithAuthority transaction
# 4. Submit NewFungibleDefinition transaction (with mint authority)
# 5. Submit Mint transaction (authority-gated)
# 6. Submit SetAuthority (revoke) transaction
# 7. Run unit tests to verify authority logic (60 tests)
@ -101,11 +101,11 @@ echo "[4/7] Creating token with mint authority..."
DEF_ID_HEX=$(b58_to_hex "$DEF_ID")
NSSA_WALLET_HOME_DIR="$WALLET_DIR" \
${TIMEOUT:+$TIMEOUT 30} "$SPEL" --idl "$IDL" --program "$TOKEN_BIN" \
-- new-fungible-definition-with-authority \
-- new-fungible-definition \
--definition-target-account "$DEF_ID" \
--holding-target-account "$SUPPLY_ID" \
--name "DemoCoin" \
--initial-supply 1000000 \
--total-supply 1000000 \
--mint-authority "$DEF_ID_HEX"
echo " Token 'DemoCoin' submitted. Initial supply: 1,000,000"
sleep 2
@ -115,7 +115,6 @@ NSSA_WALLET_HOME_DIR="$WALLET_DIR" \
${TIMEOUT:+$TIMEOUT 30} "$SPEL" --idl "$IDL" --program "$TOKEN_BIN" \
-- mint \
--definition-account "$DEF_ID" \
--authority-account "$DEF_ID" \
--user-holding-account "$RECIPIENT_ID" \
--amount-to-mint 500000
echo " Mint transaction submitted. New total supply: 1,500,000"
@ -126,7 +125,6 @@ NSSA_WALLET_HOME_DIR="$WALLET_DIR" \
${TIMEOUT:+$TIMEOUT 30} "$SPEL" --idl "$IDL" --program "$TOKEN_BIN" \
-- set-authority \
--definition-account "$DEF_ID" \
--authority-account "$DEF_ID" \
--new-authority none
echo " Authority revoked. Supply permanently fixed at 1,500,000"
sleep 2
@ -139,7 +137,7 @@ echo ""
echo "================================================================"
echo " LP-0013 Demo Complete"
echo " Summary:"
echo " [1/4] NewFungibleDefinitionWithAuthority → supply=1,000,000"
echo " [1/4] NewFungibleDefinition (with authority) → supply=1,000,000"
echo " [2/4] Mint 500,000 → supply=1,500,000"
echo " [3/4] SetAuthority (revoke) → supply fixed"
echo " [4/4] Unit tests passing → all authority cases verified"