diff --git a/Cargo.lock b/Cargo.lock index cc0c9f0..9732e53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -598,6 +598,15 @@ dependencies = [ "inout", ] +[[package]] +name = "clock_core" +version = "0.1.0" +source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0-rc1#35d8df0d031315219f94d1546ceb862b0e5b208f" +dependencies = [ + "borsh", + "nssa_core", +] + [[package]] name = "cobs" version = "0.3.0" @@ -1821,10 +1830,11 @@ checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e" [[package]] name = "nssa" version = "0.1.0" -source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?rev=ffcbc15972adbf557939bf3e2852af276422631b#ffcbc15972adbf557939bf3e2852af276422631b" +source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0-rc1#35d8df0d031315219f94d1546ceb862b0e5b208f" dependencies = [ "anyhow", "borsh", + "clock_core", "hex", "k256", "log", @@ -1842,7 +1852,7 @@ dependencies = [ [[package]] name = "nssa_core" version = "0.1.0" -source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?rev=ffcbc15972adbf557939bf3e2852af276422631b#ffcbc15972adbf557939bf3e2852af276422631b" +source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0-rc1#35d8df0d031315219f94d1546ceb862b0e5b208f" dependencies = [ "base58", "borsh", @@ -2962,8 +2972,8 @@ dependencies = [ [[package]] name = "spel-framework-core" -version = "0.1.0" -source = "git+https://github.com/logos-co/spel.git?rev=57201f64b4542bb3f592bc9a0aa654c47aa908a6#57201f64b4542bb3f592bc9a0aa654c47aa908a6" +version = "0.2.0" +source = "git+https://github.com/logos-co/spel.git?tag=v0.2.0-rc.2#9005e9fbbd78b0530412f9987273f753ed32eb2d" dependencies = [ "borsh", "nssa_core", diff --git a/Cargo.toml b/Cargo.toml index a5c2f2d..7021e21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,8 @@ exclude = [ resolver = "2" [workspace.dependencies] -nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b", features = ["host"] } -nssa = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b", features = ["test-utils"] } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc1", features = ["host"] } +nssa = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc1", features = ["test-utils"] } token_core = { path = "token/core" } token_program = { path = "token" } amm_core = { path = "amm/core" } @@ -33,4 +33,3 @@ borsh = { version = "1.0", features = ["derive"] } risc0-zkvm = { version = "=3.0.5" } serde_json = "1.0" tokio = { version = "1.28.2", features = ["net", "rt-multi-thread", "sync", "macros"] } - diff --git a/amm/Cargo.toml b/amm/Cargo.toml index fe96d0c..04be123 100644 --- a/amm/Cargo.toml +++ b/amm/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b", features = ["host"] } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc1", features = ["host"] } amm_core = { path = "core" } token_core = { path = "../token/core" } diff --git a/amm/core/Cargo.toml b/amm/core/Cargo.toml index 9e33d34..16b08c6 100644 --- a/amm/core/Cargo.toml +++ b/amm/core/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b", features = ["host"] } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc1", features = ["host"] } token_core = { path = "../../token/core" } borsh = { version = "1.5", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } diff --git a/amm/core/src/lib.rs b/amm/core/src/lib.rs index eab2db0..eeac604 100644 --- a/amm/core/src/lib.rs +++ b/amm/core/src/lib.rs @@ -29,7 +29,7 @@ pub enum Instruction { /// pool.account_id)` /// - User Holding Account for Token A (authorized) /// - User Holding Account for Token B (authorized) - /// - User Holding Account for Pool Liquidity + /// - User Holding Account for Pool Liquidity (authorized when uninitialized) NewDefinition { token_a_amount: u128, token_b_amount: u128, diff --git a/amm/methods/guest/Cargo.lock b/amm/methods/guest/Cargo.lock index 3324889..cc8d9a8 100644 --- a/amm/methods/guest/Cargo.lock +++ b/amm/methods/guest/Cargo.lock @@ -1778,7 +1778,7 @@ checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e" [[package]] name = "nssa_core" version = "0.1.0" -source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?rev=ffcbc15972adbf557939bf3e2852af276422631b#ffcbc15972adbf557939bf3e2852af276422631b" +source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0-rc1#35d8df0d031315219f94d1546ceb862b0e5b208f" dependencies = [ "base58", "borsh", @@ -2897,8 +2897,8 @@ dependencies = [ [[package]] name = "spel-framework" -version = "0.1.0" -source = "git+https://github.com/logos-co/spel.git?rev=57201f64b4542bb3f592bc9a0aa654c47aa908a6#57201f64b4542bb3f592bc9a0aa654c47aa908a6" +version = "0.2.0" +source = "git+https://github.com/logos-co/spel.git?tag=v0.2.0-rc.2#9005e9fbbd78b0530412f9987273f753ed32eb2d" dependencies = [ "borsh", "nssa_core", @@ -2908,8 +2908,8 @@ dependencies = [ [[package]] name = "spel-framework-core" -version = "0.1.0" -source = "git+https://github.com/logos-co/spel.git?rev=57201f64b4542bb3f592bc9a0aa654c47aa908a6#57201f64b4542bb3f592bc9a0aa654c47aa908a6" +version = "0.2.0" +source = "git+https://github.com/logos-co/spel.git?tag=v0.2.0-rc.2#9005e9fbbd78b0530412f9987273f753ed32eb2d" dependencies = [ "borsh", "nssa_core", @@ -2921,8 +2921,8 @@ dependencies = [ [[package]] name = "spel-framework-macros" -version = "0.1.0" -source = "git+https://github.com/logos-co/spel.git?rev=57201f64b4542bb3f592bc9a0aa654c47aa908a6#57201f64b4542bb3f592bc9a0aa654c47aa908a6" +version = "0.2.0" +source = "git+https://github.com/logos-co/spel.git?tag=v0.2.0-rc.2#9005e9fbbd78b0530412f9987273f753ed32eb2d" dependencies = [ "proc-macro2", "quote", diff --git a/amm/methods/guest/Cargo.toml b/amm/methods/guest/Cargo.toml index a710dc9..385c4ac 100644 --- a/amm/methods/guest/Cargo.toml +++ b/amm/methods/guest/Cargo.toml @@ -10,8 +10,8 @@ name = "amm" path = "src/bin/amm.rs" [dependencies] -spel-framework = { git = "https://github.com/logos-co/spel.git", rev = "57201f64b4542bb3f592bc9a0aa654c47aa908a6", package = "spel-framework" } -nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b" } +spel-framework = { git = "https://github.com/logos-co/spel.git", tag = "v0.2.0-rc.2", package = "spel-framework" } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc1" } risc0-zkvm = { version = "=3.0.5", default-features = false } amm_core = { path = "../../core" } amm_program = { path = "../..", package = "amm_program" } diff --git a/amm/methods/guest/src/bin/amm.rs b/amm/methods/guest/src/bin/amm.rs index 5d7d618..17db95d 100644 --- a/amm/methods/guest/src/bin/amm.rs +++ b/amm/methods/guest/src/bin/amm.rs @@ -16,6 +16,7 @@ mod amm { use super::*; /// Initializes a new Pool (or re-initializes an existing zero-supply Pool). + /// A fresh user LP holding must be explicitly authorized by the caller. #[instruction] pub fn new_definition( pool: AccountWithMetadata, diff --git a/amm/src/new_definition.rs b/amm/src/new_definition.rs index 52b637e..b7a45cc 100644 --- a/amm/src/new_definition.rs +++ b/amm/src/new_definition.rs @@ -2,12 +2,13 @@ use std::num::NonZeroU128; use amm_core::{ assert_supported_fee_tier, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, - compute_lp_lock_holding_pda, compute_pool_pda, compute_vault_pda, PoolDefinition, + compute_lp_lock_holding_pda, compute_lp_lock_holding_pda_seed, compute_pool_pda, + compute_pool_pda_seed, compute_vault_pda, compute_vault_pda_seed, PoolDefinition, MINIMUM_LIQUIDITY, }; use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::{AccountPostState, ChainedCall, ProgramId}, + program::{AccountPostState, ChainedCall, Claim, ProgramId}, }; use token_core::TokenDefinition; @@ -78,6 +79,10 @@ pub fn new_definition( Account::default(), "Pool account must be uninitialized" ); + assert!( + user_holding_lp.account != Account::default() || user_holding_lp.is_authorized, + "Fresh user LP holding requires user authorization" + ); // LP Token minting calculation let initial_lp = token_a_amount @@ -106,40 +111,63 @@ pub fn new_definition( }; pool_post.data = Data::from(&pool_post_definition); - let pool_post: AccountPostState = AccountPostState::new_claimed(pool_post.clone()); + let pool_post: AccountPostState = AccountPostState::new_claimed( + pool_post.clone(), + Claim::Pda(compute_pool_pda_seed( + definition_token_a_id, + definition_token_b_id, + )), + ); let token_program_id = user_holding_a.account.program_owner; // Chain call for Token A (user_holding_a -> Vault_A) + let mut vault_a_authorized = vault_a.clone(); + vault_a_authorized.is_authorized = true; let call_token_a = ChainedCall::new( token_program_id, - vec![user_holding_a.clone(), vault_a.clone()], + vec![user_holding_a.clone(), vault_a_authorized], &token_core::Instruction::Transfer { amount_to_transfer: token_a_amount.into(), }, - ); + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + pool.account_id, + definition_token_a_id, + )]); // Chain call for Token B (user_holding_b -> Vault_B) + let mut vault_b_authorized = vault_b.clone(); + vault_b_authorized.is_authorized = true; let call_token_b = ChainedCall::new( token_program_id, - vec![user_holding_b.clone(), vault_b.clone()], + vec![user_holding_b.clone(), vault_b_authorized], &token_core::Instruction::Transfer { amount_to_transfer: token_b_amount.into(), }, - ); + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + pool.account_id, + definition_token_b_id, + )]); // Chain call for liquidity token lock holding let mut pool_lp_auth = pool_definition_lp.clone(); pool_lp_auth.is_authorized = true; + let mut lp_lock_holding_auth = lp_lock_holding.clone(); + lp_lock_holding_auth.is_authorized = true; let call_token_lp_lock = ChainedCall::new( token_program_id, - vec![pool_lp_auth.clone(), lp_lock_holding.clone()], + vec![pool_lp_auth.clone(), lp_lock_holding_auth], &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, }, ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); + .with_pda_seeds(vec![ + compute_liquidity_token_pda_seed(pool.account_id), + compute_lp_lock_holding_pda_seed(pool.account_id), + ]); let mut pool_lp_after_lock = pool_lp_auth.clone(); pool_lp_after_lock.account.program_owner = token_program_id; diff --git a/amm/src/tests.rs b/amm/src/tests.rs index 96cb61b..5f7f433 100644 --- a/amm/src/tests.rs +++ b/amm/src/tests.rs @@ -4,13 +4,13 @@ use std::num::NonZero; use amm_core::{ compute_liquidity_token_pda, compute_liquidity_token_pda_seed, compute_lp_lock_holding_pda, - compute_pool_pda, compute_vault_pda, compute_vault_pda_seed, PoolDefinition, - FEE_BPS_DENOMINATOR, FEE_TIER_BPS_1, FEE_TIER_BPS_100, FEE_TIER_BPS_30, FEE_TIER_BPS_5, - MINIMUM_LIQUIDITY, + compute_lp_lock_holding_pda_seed, compute_pool_pda, compute_pool_pda_seed, compute_vault_pda, + compute_vault_pda_seed, PoolDefinition, FEE_BPS_DENOMINATOR, FEE_TIER_BPS_1, FEE_TIER_BPS_100, + FEE_TIER_BPS_30, FEE_TIER_BPS_5, MINIMUM_LIQUIDITY, }; use nssa_core::{ account::{Account, AccountId, AccountWithMetadata, Data, Nonce}, - program::{ChainedCall, ProgramId}, + program::{ChainedCall, Claim, ProgramId}, }; use token_core::{TokenDefinition, TokenHolding}; @@ -489,46 +489,57 @@ impl ChainedCallForTests { } fn cc_new_definition_token_a() -> ChainedCall { + let mut vault_a_auth = AccountWithMetadataForTests::vault_a_init(); + vault_a_auth.is_authorized = true; + ChainedCall::new( TOKEN_PROGRAM_ID, - vec![ - AccountWithMetadataForTests::user_holding_a(), - AccountWithMetadataForTests::vault_a_init(), - ], + vec![AccountWithMetadataForTests::user_holding_a(), vault_a_auth], &token_core::Instruction::Transfer { amount_to_transfer: BalanceForTests::vault_a_reserve_init(), }, ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + )]) } fn cc_new_definition_token_b() -> ChainedCall { + let mut vault_b_auth = AccountWithMetadataForTests::vault_b_init(); + vault_b_auth.is_authorized = true; + ChainedCall::new( TOKEN_PROGRAM_ID, - vec![ - AccountWithMetadataForTests::user_holding_b(), - AccountWithMetadataForTests::vault_b_init(), - ], + vec![AccountWithMetadataForTests::user_holding_b(), vault_b_auth], &token_core::Instruction::Transfer { amount_to_transfer: BalanceForTests::vault_b_reserve_init(), }, ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + )]) } fn cc_new_definition_token_lp_lock() -> ChainedCall { let mut pool_lp_auth = AccountForTests::pool_lp_uninit(); pool_lp_auth.is_authorized = true; + let mut lp_lock_holding_auth = AccountForTests::lp_lock_holding_uninit(); + lp_lock_holding_auth.is_authorized = true; ChainedCall::new( TOKEN_PROGRAM_ID, - vec![pool_lp_auth, AccountForTests::lp_lock_holding_uninit()], + vec![pool_lp_auth, lp_lock_holding_auth], &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, }, ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed( - IdForTests::pool_definition_id(), - )]) + .with_pda_seeds(vec![ + compute_liquidity_token_pda_seed(IdForTests::pool_definition_id()), + compute_lp_lock_holding_pda_seed(IdForTests::pool_definition_id()), + ]) } fn cc_new_definition_token_lp_user() -> ChainedCall { @@ -2068,6 +2079,13 @@ fn test_call_new_definition_chained_call_successful() { let pool_post = post_states[0].clone(); assert!(AccountWithMetadataForTests::pool_definition_init().account == *pool_post.account()); + assert_eq!( + pool_post.required_claim(), + Some(Claim::Pda(compute_pool_pda_seed( + IdForTests::token_a_definition_id(), + IdForTests::token_b_definition_id(), + ))) + ); let chained_call_lp_lock = chained_calls[0].clone(); let chained_call_lp_user = chained_calls[1].clone(); @@ -2752,20 +2770,20 @@ fn test_new_definition_lp_symmetric_amounts() { let mut pool_lp_auth = AccountForTests::pool_lp_uninit(); pool_lp_auth.is_authorized = true; + let mut lp_lock_holding_auth = AccountForTests::lp_lock_holding_uninit(); + lp_lock_holding_auth.is_authorized = true; let expected_lp_lock_call = ChainedCall::new( TOKEN_PROGRAM_ID, - vec![ - pool_lp_auth.clone(), - AccountForTests::lp_lock_holding_uninit(), - ], + vec![pool_lp_auth.clone(), lp_lock_holding_auth], &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, }, ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed( - IdForTests::pool_definition_id(), - )]); + .with_pda_seeds(vec![ + compute_liquidity_token_pda_seed(IdForTests::pool_definition_id()), + compute_lp_lock_holding_pda_seed(IdForTests::pool_definition_id()), + ]); let expected_lp_user_call = ChainedCall::new( TOKEN_PROGRAM_ID, @@ -2814,21 +2832,21 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() { let mut pool_lp_auth = AccountForTests::pool_lp_uninit(); pool_lp_auth.is_authorized = true; + let mut lp_lock_holding_auth = AccountForTests::lp_lock_holding_uninit(); + lp_lock_holding_auth.is_authorized = true; let expected_lock_call = ChainedCall::new( TOKEN_PROGRAM_ID, - vec![ - pool_lp_auth.clone(), - AccountForTests::lp_lock_holding_uninit(), - ], + vec![pool_lp_auth.clone(), lp_lock_holding_auth], &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, }, ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed( - IdForTests::pool_definition_id(), - )]); + .with_pda_seeds(vec![ + compute_liquidity_token_pda_seed(IdForTests::pool_definition_id()), + compute_lp_lock_holding_pda_seed(IdForTests::pool_definition_id()), + ]); let expected_user_call = ChainedCall::new( TOKEN_PROGRAM_ID, vec![ diff --git a/ata/Cargo.toml b/ata/Cargo.toml index 6b9d4ee..51babec 100644 --- a/ata/Cargo.toml +++ b/ata/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b", features = ["host"] } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc1", features = ["host"] } ata_core = { path = "core" } token_core = { path = "../token/core" } diff --git a/ata/core/Cargo.toml b/ata/core/Cargo.toml index 87a3a86..0e2fd3e 100644 --- a/ata/core/Cargo.toml +++ b/ata/core/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b", features = ["host"] } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc1", features = ["host"] } borsh = { version = "1.5", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } risc0-zkvm = { version = "=3.0.5", default-features = false } diff --git a/ata/core/src/lib.rs b/ata/core/src/lib.rs index 994c632..4ea46bf 100644 --- a/ata/core/src/lib.rs +++ b/ata/core/src/lib.rs @@ -18,13 +18,13 @@ pub enum Instruction { /// `token_program_id` is derived from `token_definition.account.program_owner`. Create { ata_program_id: ProgramId }, - /// Transfer tokens FROM owner's ATA to a recipient holding account. - /// Uses PDA seeds to authorize the ATA in the chained Token::Transfer call. + /// Transfer tokens FROM owner's ATA to a recipient token holding account. + /// Uses ATA PDA seeds to authorize the chained Token::Transfer call. /// /// Required accounts (3): /// - Owner account (authorized) /// - Sender ATA (owner's token holding) - /// - Recipient token holding (any account; auto-created if default) + /// - Recipient token holding (must be initialized) /// /// `token_program_id` is derived from `sender_ata.account.program_owner`. Transfer { diff --git a/ata/methods/guest/Cargo.lock b/ata/methods/guest/Cargo.lock index a7872b4..8c55a8a 100644 --- a/ata/methods/guest/Cargo.lock +++ b/ata/methods/guest/Cargo.lock @@ -1777,7 +1777,7 @@ checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e" [[package]] name = "nssa_core" version = "0.1.0" -source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?rev=ffcbc15972adbf557939bf3e2852af276422631b#ffcbc15972adbf557939bf3e2852af276422631b" +source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0-rc1#35d8df0d031315219f94d1546ceb862b0e5b208f" dependencies = [ "base58", "borsh", @@ -2896,8 +2896,8 @@ dependencies = [ [[package]] name = "spel-framework" -version = "0.1.0" -source = "git+https://github.com/logos-co/spel.git?rev=57201f64b4542bb3f592bc9a0aa654c47aa908a6#57201f64b4542bb3f592bc9a0aa654c47aa908a6" +version = "0.2.0" +source = "git+https://github.com/logos-co/spel.git?tag=v0.2.0-rc.2#9005e9fbbd78b0530412f9987273f753ed32eb2d" dependencies = [ "borsh", "nssa_core", @@ -2907,8 +2907,8 @@ dependencies = [ [[package]] name = "spel-framework-core" -version = "0.1.0" -source = "git+https://github.com/logos-co/spel.git?rev=57201f64b4542bb3f592bc9a0aa654c47aa908a6#57201f64b4542bb3f592bc9a0aa654c47aa908a6" +version = "0.2.0" +source = "git+https://github.com/logos-co/spel.git?tag=v0.2.0-rc.2#9005e9fbbd78b0530412f9987273f753ed32eb2d" dependencies = [ "borsh", "nssa_core", @@ -2920,8 +2920,8 @@ dependencies = [ [[package]] name = "spel-framework-macros" -version = "0.1.0" -source = "git+https://github.com/logos-co/spel.git?rev=57201f64b4542bb3f592bc9a0aa654c47aa908a6#57201f64b4542bb3f592bc9a0aa654c47aa908a6" +version = "0.2.0" +source = "git+https://github.com/logos-co/spel.git?tag=v0.2.0-rc.2#9005e9fbbd78b0530412f9987273f753ed32eb2d" dependencies = [ "proc-macro2", "quote", diff --git a/ata/methods/guest/Cargo.toml b/ata/methods/guest/Cargo.toml index ad6d45a..22e8f42 100644 --- a/ata/methods/guest/Cargo.toml +++ b/ata/methods/guest/Cargo.toml @@ -10,8 +10,8 @@ name = "ata" path = "src/bin/ata.rs" [dependencies] -spel-framework = { git = "https://github.com/logos-co/spel.git", rev = "57201f64b4542bb3f592bc9a0aa654c47aa908a6", package = "spel-framework" } -nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b" } +spel-framework = { git = "https://github.com/logos-co/spel.git", tag = "v0.2.0-rc.2", package = "spel-framework" } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc1" } risc0-zkvm = { version = "=3.0.5", default-features = false } ata_core = { path = "../../core" } ata_program = { path = "../..", package = "ata_program" } diff --git a/ata/methods/guest/src/bin/ata.rs b/ata/methods/guest/src/bin/ata.rs index a75edfd..ad8a950 100644 --- a/ata/methods/guest/src/bin/ata.rs +++ b/ata/methods/guest/src/bin/ata.rs @@ -28,7 +28,8 @@ mod ata { Ok(SpelOutput::with_chained_calls(post_states, chained_calls)) } - /// Transfer tokens FROM owner's ATA to a recipient holding account. + /// Transfer tokens FROM owner's ATA to a recipient token holding account. + /// The recipient holding account must already be initialized. #[instruction] pub fn transfer( owner: AccountWithMetadata, diff --git a/ata/src/create.rs b/ata/src/create.rs index 8610995..060f0c7 100644 --- a/ata/src/create.rs +++ b/ata/src/create.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, - program::{AccountPostState, ChainedCall, ProgramId}, + program::{AccountPostState, ChainedCall, Claim, ProgramId}, }; pub fn create_associated_token_account( @@ -9,9 +9,12 @@ pub fn create_associated_token_account( ata_account: AccountWithMetadata, ata_program_id: ProgramId, ) -> (Vec, Vec) { - // No authorization check needed: create is idempotent, so anyone can call it safely. + // No explicit owner authorization check is needed here: ATA creation is idempotent, so the + // call itself may proceed without `owner.is_authorized`. If the owner account is still + // default, the returned post-state will still carry `Claim::Authorized` so the runtime can + // claim that owner account when needed. let token_program_id = token_definition.account.program_owner; - ata_core::verify_ata_and_get_seed( + let seed = ata_core::verify_ata_and_get_seed( &ata_account, &owner, token_definition.account_id, @@ -22,7 +25,7 @@ pub fn create_associated_token_account( if ata_account.account != Account::default() { return ( vec![ - AccountPostState::new_claimed_if_default(owner.account.clone()), + AccountPostState::new_claimed_if_default(owner.account.clone(), Claim::Authorized), AccountPostState::new(token_definition.account.clone()), AccountPostState::new(ata_account.account.clone()), ], @@ -31,14 +34,17 @@ pub fn create_associated_token_account( } let post_states = vec![ - AccountPostState::new_claimed_if_default(owner.account.clone()), + AccountPostState::new_claimed_if_default(owner.account.clone(), Claim::Authorized), AccountPostState::new(token_definition.account.clone()), AccountPostState::new(ata_account.account.clone()), ]; + let mut ata_account_auth = ata_account.clone(); + ata_account_auth.is_authorized = true; let chained_call = ChainedCall::new( token_program_id, - vec![token_definition.clone(), ata_account.clone()], + vec![token_definition.clone(), ata_account_auth], &token_core::Instruction::InitializeAccount, - ); + ) + .with_pda_seeds(vec![seed]); (post_states, vec![chained_call]) } diff --git a/ata/src/tests.rs b/ata/src/tests.rs index f5d654d..07a2185 100644 --- a/ata/src/tests.rs +++ b/ata/src/tests.rs @@ -1,5 +1,8 @@ use ata_core::{compute_ata_seed, get_associated_token_account_id}; -use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data}; +use nssa_core::{ + account::{Account, AccountId, AccountWithMetadata, Data}, + program::{ChainedCall, Claim}, +}; use token_core::{TokenDefinition, TokenHolding}; const ATA_PROGRAM_ID: nssa_core::program::ProgramId = [1u32; 8]; @@ -79,8 +82,18 @@ fn create_emits_chained_call_for_uninitialized_ata() { ); assert_eq!(post_states.len(), 3); - assert_eq!(chained_calls.len(), 1); - assert_eq!(chained_calls[0].program_id, TOKEN_PROGRAM_ID); + assert_eq!(post_states[0].required_claim(), Some(Claim::Authorized)); + + let mut authorized_ata = uninitialized_ata_account(); + authorized_ata.is_authorized = true; + let expected_call = ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![definition_account(), authorized_ata], + &token_core::Instruction::InitializeAccount, + ) + .with_pda_seeds(vec![compute_ata_seed(owner_id(), definition_id())]); + + assert_eq!(chained_calls, vec![expected_call]); } #[test] diff --git a/ata/src/transfer.rs b/ata/src/transfer.rs index 89d7013..c3198fa 100644 --- a/ata/src/transfer.rs +++ b/ata/src/transfer.rs @@ -16,7 +16,7 @@ pub fn transfer_from_associated_token_account( let definition_id = TokenHolding::try_from(&sender_ata.account.data) .expect("Sender ATA must hold a valid token") .definition_id(); - let seed = + let sender_seed = ata_core::verify_ata_and_get_seed(&sender_ata, &owner, definition_id, ata_program_id); let post_states = vec![ @@ -29,11 +29,11 @@ pub fn transfer_from_associated_token_account( let chained_call = ChainedCall::new( token_program_id, - vec![sender_ata_auth, recipient.clone()], + vec![sender_ata_auth, recipient], &token_core::Instruction::Transfer { amount_to_transfer: amount, }, ) - .with_pda_seeds(vec![seed]); + .with_pda_seeds(vec![sender_seed]); (post_states, vec![chained_call]) } diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index 9378a8d..d121029 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -809,6 +809,18 @@ impl Accounts { } fn user_lp_holding_new_init() -> Account { + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: Ids::token_lp_definition(), + balance: Balances::lp_user_init(), + }), + nonce: Nonce(1), + } + } + + fn user_lp_holding_new_init_precreated() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, @@ -895,7 +907,7 @@ fn deploy_programs(state: &mut V03State) { } fn state_for_amm_tests() -> V03State { - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); deploy_programs(&mut state); state.force_insert_account(Ids::pool_definition(), Accounts::pool_definition_init()); state.force_insert_account( @@ -919,7 +931,7 @@ fn state_for_amm_tests() -> V03State { } fn state_for_amm_tests_with_new_def() -> V03State { - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); deploy_programs(&mut state); state.force_insert_account( Ids::token_a_definition(), @@ -938,7 +950,17 @@ fn current_nonce(state: &V03State, account_id: AccountId) -> Nonce { state.get_account_by_id(account_id).nonce } -fn try_execute_new_definition(state: &mut V03State, fees: u128) -> Result<(), NssaError> { +fn state_for_amm_tests_with_precreated_user_lp_for_new_def() -> V03State { + let mut state = state_for_amm_tests_with_new_def(); + state.force_insert_account(Ids::user_lp(), Accounts::user_lp_holding_init_zero()); + state +} + +fn try_execute_new_definition( + state: &mut V03State, + fees: u128, + authorize_user_lp: bool, +) -> Result<(), NssaError> { let instruction = amm_core::Instruction::NewDefinition { token_a_amount: Balances::vault_a_init(), token_b_amount: Balances::vault_b_init(), @@ -958,23 +980,37 @@ fn try_execute_new_definition(state: &mut V03State, fees: u128) -> Result<(), Ns Ids::user_b(), Ids::user_lp(), ], - vec![ - current_nonce(state, Ids::user_a()), - current_nonce(state, Ids::user_b()), - ], + if authorize_user_lp { + vec![ + current_nonce(state, Ids::user_a()), + current_nonce(state, Ids::user_b()), + current_nonce(state, Ids::user_lp()), + ] + } else { + vec![ + current_nonce(state, Ids::user_a()), + current_nonce(state, Ids::user_b()), + ] + }, instruction, ) .unwrap(); - let witness_set = - public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]); + let witness_set = if authorize_user_lp { + public_transaction::WitnessSet::for_message( + &message, + &[&Keys::user_a(), &Keys::user_b(), &Keys::user_lp()], + ) + } else { + public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]) + }; let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0) + state.transition_from_public_transaction(&tx, 0, 0) } fn execute_new_definition(state: &mut V03State, fees: u128) { - try_execute_new_definition(state, fees).unwrap(); + try_execute_new_definition(state, fees, true).unwrap(); } fn execute_swap_a_to_b(state: &mut V03State, swap_amount_in: u128, min_amount_out: u128) { @@ -1001,7 +1037,7 @@ fn execute_swap_a_to_b(state: &mut V03State, swap_amount_in: u128, min_amount_ou let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); } fn execute_swap_b_to_a(state: &mut V03State, swap_amount_in: u128, min_amount_out: u128) { @@ -1028,7 +1064,7 @@ fn execute_swap_b_to_a(state: &mut V03State, swap_amount_in: u128, min_amount_ou let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); } fn execute_add_liquidity( @@ -1066,7 +1102,7 @@ fn execute_add_liquidity( public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); } fn execute_remove_liquidity( @@ -1100,7 +1136,7 @@ fn execute_remove_liquidity( let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_lp()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); } fn fungible_balance(account: &Account) -> u128 { @@ -1163,7 +1199,7 @@ fn amm_remove_liquidity() { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_lp()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), @@ -1225,7 +1261,7 @@ fn amm_remove_liquidity_insufficient_user_lp_fails() { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_lp()]); let tx = PublicTransaction::new(message, witness_set); - assert!(state.transition_from_public_transaction(&tx, 0).is_err()); + assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err()); } #[test] @@ -1270,6 +1306,88 @@ fn amm_new_definition_uninitialized_pool() { ); } +#[test] +fn amm_new_definition_without_user_lp_authorization_fails() { + let mut state = state_for_amm_tests_with_new_def(); + state.force_insert_account(Ids::vault_a(), Accounts::vault_a_reinitializable()); + state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable()); + + let result = try_execute_new_definition(&mut state, Balances::fee_tier(), false); + + assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_)))); + assert_eq!( + state.get_account_by_id(Ids::pool_definition()), + Account::default() + ); + assert_eq!( + state.get_account_by_id(Ids::vault_a()), + Accounts::vault_a_reinitializable() + ); + assert_eq!( + state.get_account_by_id(Ids::vault_b()), + Accounts::vault_b_reinitializable() + ); + assert_eq!( + state.get_account_by_id(Ids::token_lp_definition()), + Account::default() + ); + assert_eq!( + state.get_account_by_id(Ids::lp_lock_holding()), + Account::default() + ); + assert_eq!( + state.get_account_by_id(Ids::user_a()), + Accounts::user_a_holding() + ); + assert_eq!( + state.get_account_by_id(Ids::user_b()), + Accounts::user_b_holding() + ); + assert_eq!(state.get_account_by_id(Ids::user_lp()), Account::default()); +} + +#[test] +fn amm_new_definition_precreated_zero_balance_user_lp() { + let mut state = state_for_amm_tests_with_precreated_user_lp_for_new_def(); + state.force_insert_account(Ids::vault_a(), Accounts::vault_a_reinitializable()); + state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable()); + + try_execute_new_definition(&mut state, Balances::fee_tier(), false).unwrap(); + + assert_eq!( + state.get_account_by_id(Ids::pool_definition()), + Accounts::pool_definition_new_init() + ); + assert_eq!( + state.get_account_by_id(Ids::vault_a()), + Accounts::vault_a_init() + ); + assert_eq!( + state.get_account_by_id(Ids::vault_b()), + Accounts::vault_b_init() + ); + assert_eq!( + state.get_account_by_id(Ids::token_lp_definition()), + Accounts::token_lp_definition_new_init() + ); + assert_eq!( + state.get_account_by_id(Ids::lp_lock_holding()), + Accounts::lp_lock_holding_new_init() + ); + assert_eq!( + state.get_account_by_id(Ids::user_a()), + Accounts::user_a_holding_new_init() + ); + assert_eq!( + state.get_account_by_id(Ids::user_b()), + Accounts::user_b_holding_new_init() + ); + assert_eq!( + state.get_account_by_id(Ids::user_lp()), + Accounts::user_lp_holding_new_init_precreated() + ); +} + #[test] fn amm_new_definition_supports_all_fee_tiers() { for fees in [ @@ -1293,7 +1411,7 @@ fn amm_new_definition_supports_all_fee_tiers() { #[test] fn amm_new_definition_rejects_unsupported_fee_tier_transaction() { - let mut state = state_for_amm_tests_with_new_def(); + let mut state = state_for_amm_tests_with_precreated_user_lp_for_new_def(); state.force_insert_account(Ids::vault_a(), Accounts::vault_a_reinitializable()); state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable()); state.force_insert_account( @@ -1304,9 +1422,8 @@ fn amm_new_definition_rejects_unsupported_fee_tier_transaction() { Ids::token_lp_definition(), Accounts::token_lp_definition_reinitializable(), ); - state.force_insert_account(Ids::user_lp(), Accounts::user_lp_holding_init_zero()); - let result = try_execute_new_definition(&mut state, 2); + let result = try_execute_new_definition(&mut state, 2, false); assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_)))); assert_eq!( @@ -1369,7 +1486,7 @@ fn amm_add_liquidity() { public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), @@ -1428,7 +1545,7 @@ fn amm_swap_b_to_a() { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), @@ -1479,7 +1596,7 @@ fn amm_swap_a_to_b() { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index faf3fdc..dd2eef7 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -93,6 +93,18 @@ impl Accounts { nonce: Nonce(0), } } + + fn recipient_ata_init() -> Account { + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: Ids::token_definition(), + balance: 0_u128, + }), + nonce: Nonce(0), + } + } } fn deploy_programs(state: &mut V03State) { @@ -113,16 +125,22 @@ fn deploy_programs(state: &mut V03State) { } fn state_for_ata_tests() -> V03State { - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); deploy_programs(&mut state); state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init()); state.force_insert_account(Ids::owner_ata(), Accounts::owner_ata_init()); state } +fn state_for_ata_tests_with_precreated_recipient_ata() -> V03State { + let mut state = state_for_ata_tests(); + state.force_insert_account(Ids::recipient_ata(), Accounts::recipient_ata_init()); + state +} + #[test] fn ata_create() { - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); deploy_programs(&mut state); state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init()); @@ -141,7 +159,7 @@ fn ata_create() { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_key()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::owner_ata()), @@ -176,7 +194,7 @@ fn ata_create_is_idempotent() { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_key()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); // Already initialized — should remain unchanged assert_eq!( @@ -195,7 +213,7 @@ fn ata_create_is_idempotent() { #[test] fn ata_transfer() { - let mut state = state_for_ata_tests(); + let mut state = state_for_ata_tests_with_precreated_recipient_ata(); let instruction = ata_core::Instruction::Transfer { ata_program_id: Ids::ata_program(), @@ -213,7 +231,7 @@ fn ata_transfer() { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_key()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::owner_ata()), @@ -262,7 +280,7 @@ fn ata_burn() { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_key()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::owner_ata()), @@ -294,7 +312,7 @@ fn ata_burn() { #[test] fn ata_create_from_private_owner() { - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); deploy_programs(&mut state); state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init()); @@ -359,7 +377,7 @@ fn ata_create_from_private_owner() { let witness_set = WitnessSet::for_message(&message, proof, &[]); let tx = PrivacyPreservingTransaction::new(message, witness_set); state - .transition_from_privacy_preserving_transaction(&tx, 0) + .transition_from_privacy_preserving_transaction(&tx, 0, 0) .unwrap(); assert_eq!( diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index 901b607..aed4d8a 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -74,6 +74,18 @@ impl Accounts { nonce: Nonce(0), } } + + fn recipient_init() -> Account { + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: Ids::token_definition(), + balance: 0_u128, + }), + nonce: Nonce(0), + } + } } fn deploy_token(state: &mut V03State) { @@ -85,7 +97,16 @@ fn deploy_token(state: &mut V03State) { } fn state_for_token_tests() -> V03State { - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); + deploy_token(&mut state); + state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init()); + state.force_insert_account(Ids::holder(), Accounts::holder_init()); + state.force_insert_account(Ids::recipient(), Accounts::recipient_init()); + state +} + +fn state_for_token_tests_without_recipient() -> V03State { + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); deploy_token(&mut state); state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init()); state.force_insert_account(Ids::holder(), Accounts::holder_init()); @@ -94,7 +115,7 @@ fn state_for_token_tests() -> V03State { #[test] fn token_new_fungible_definition() { - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); deploy_token(&mut state); let instruction = token_core::Instruction::NewFungibleDefinition { @@ -116,7 +137,7 @@ fn token_new_fungible_definition() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::token_definition()), @@ -165,7 +186,7 @@ fn token_transfer() { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::holder_key()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::holder()), @@ -194,6 +215,88 @@ fn token_transfer() { ); } +#[test] +fn token_transfer_fresh_public_recipient_requires_authorization() { + let mut state = state_for_token_tests_without_recipient(); + + let instruction = token_core::Instruction::Transfer { + amount_to_transfer: 500_000_u128, + }; + + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::holder(), Ids::recipient()], + vec![Nonce(0)], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::holder_key()]); + + let tx = PublicTransaction::new(message, witness_set); + assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err()); + + assert_eq!( + state.get_account_by_id(Ids::holder()), + Accounts::holder_init() + ); + assert_eq!( + state.get_account_by_id(Ids::recipient()), + Account::default() + ); +} + +#[test] +fn token_transfer_fresh_authorized_public_recipient() { + let mut state = state_for_token_tests_without_recipient(); + + let instruction = token_core::Instruction::Transfer { + amount_to_transfer: 500_000_u128, + }; + + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::holder(), Ids::recipient()], + vec![Nonce(0), Nonce(0)], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[&Keys::holder_key(), &Keys::recipient_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::holder()), + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: Ids::token_definition(), + balance: 500_000_u128, + }), + nonce: Nonce(1), + } + ); + + assert_eq!( + state.get_account_by_id(Ids::recipient()), + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: Ids::token_definition(), + balance: 500_000_u128, + }), + nonce: Nonce(1), + } + ); +} + #[test] fn token_burn() { let mut state = state_for_token_tests(); @@ -213,7 +316,7 @@ fn token_burn() { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::holder_key()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::token_definition()), @@ -262,7 +365,7 @@ fn token_mint() { 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).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::token_definition()), @@ -292,6 +395,89 @@ fn token_mint() { ); } +#[test] +fn token_mint_fresh_public_recipient_requires_authorization() { + let mut state = state_for_token_tests_without_recipient(); + + let instruction = token_core::Instruction::Mint { + amount_to_mint: 500_000_u128, + }; + + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::recipient()], + vec![Nonce(0)], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]); + + let tx = PublicTransaction::new(message, witness_set); + assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err()); + + assert_eq!( + state.get_account_by_id(Ids::token_definition()), + Accounts::token_definition_init() + ); + assert_eq!( + state.get_account_by_id(Ids::recipient()), + Account::default() + ); +} + +#[test] +fn token_mint_fresh_authorized_public_recipient() { + let mut state = state_for_token_tests_without_recipient(); + + let instruction = token_core::Instruction::Mint { + amount_to_mint: 500_000_u128, + }; + + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::recipient()], + vec![Nonce(0), Nonce(0)], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[&Keys::def_key(), &Keys::recipient_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("Gold"), + total_supply: 1_500_000_u128, + metadata_id: None, + }), + nonce: Nonce(1), + } + ); + + assert_eq!( + state.get_account_by_id(Ids::recipient()), + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: Ids::token_definition(), + balance: 500_000_u128, + }), + nonce: Nonce(1), + } + ); +} + struct PrivateKeys; impl PrivateKeys { @@ -377,7 +563,7 @@ fn shielded_token_transfer(amount: u128, state: &mut V03State) -> Account { let witness_set = WitnessSet::for_message(&message, proof, &[&Keys::holder_key()]); let tx = PrivacyPreservingTransaction::new(message, witness_set); state - .transition_from_privacy_preserving_transaction(&tx, 0) + .transition_from_privacy_preserving_transaction(&tx, 0, 0) .unwrap(); Account { @@ -476,7 +662,7 @@ fn token_private_transfer() { let witness_set = WitnessSet::for_message(&message, proof, &[]); let tx = PrivacyPreservingTransaction::new(message, witness_set); state - .transition_from_privacy_preserving_transaction(&tx, 0) + .transition_from_privacy_preserving_transaction(&tx, 0, 0) .unwrap(); let sender_nonce_after = @@ -559,7 +745,7 @@ fn token_deshielded_transfer() { let witness_set = WitnessSet::for_message(&message, proof, &[]); let tx = PrivacyPreservingTransaction::new(message, witness_set); state - .transition_from_privacy_preserving_transaction(&tx, 0) + .transition_from_privacy_preserving_transaction(&tx, 0, 0) .unwrap(); assert_eq!( diff --git a/token/Cargo.toml b/token/Cargo.toml index 1e83aa4..bce7726 100644 --- a/token/Cargo.toml +++ b/token/Cargo.toml @@ -4,5 +4,5 @@ version = "0.1.0" edition = "2021" [dependencies] -nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b", features = ["host"] } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc1", features = ["host"] } token_core = { path = "core" } diff --git a/token/core/Cargo.toml b/token/core/Cargo.toml index f5a64d1..5eb50b3 100644 --- a/token/core/Cargo.toml +++ b/token/core/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b", features = ["host"] } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc1", features = ["host"] } borsh = { version = "1.5", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } diff --git a/token/core/src/lib.rs b/token/core/src/lib.rs index 140ae38..6bc04dc 100644 --- a/token/core/src/lib.rs +++ b/token/core/src/lib.rs @@ -10,23 +10,24 @@ pub enum Instruction { /// Transfer tokens from sender to recipient. /// /// Required accounts: - /// - Sender's Token Holding account (authorized), - /// - Recipient's Token Holding account. + /// - Sender's Token Holding account (initialized, authorized), + /// - Recipient's Token Holding account (initialized, or uninitialized with recipient + /// authorization in the same transaction). Transfer { amount_to_transfer: u128 }, /// Create a new fungible token definition without metadata. /// /// Required accounts: - /// - Token Definition account (uninitialized), - /// - Token Holding account (uninitialized). + /// - Token Definition account (uninitialized, authorized), + /// - Token Holding account (uninitialized, authorized). NewFungibleDefinition { name: String, total_supply: u128 }, /// Create a new fungible or non-fungible token definition with metadata. /// /// Required accounts: - /// - Token Definition account (uninitialized), - /// - Token Holding account (uninitialized), - /// - Token Metadata account (uninitialized). + /// - Token Definition account (uninitialized, authorized), + /// - Token Holding account (uninitialized, authorized), + /// - Token Metadata account (uninitialized, authorized). NewDefinitionWithMetadata { new_definition: NewTokenDefinition, /// Boxed to avoid large enum variant size @@ -37,7 +38,7 @@ pub enum Instruction { /// /// Required accounts: /// - Token Definition account (initialized), - /// - Token Holding account (uninitialized), + /// - Token Holding account (uninitialized, authorized), InitializeAccount, /// Burn tokens from the holder's account. @@ -50,15 +51,16 @@ pub enum Instruction { /// Mint new tokens to the holder's account. /// /// Required accounts: - /// - Token Definition account (authorized), - /// - Token Holding account (uninitialized or initialized). + /// - Token Definition account (initialized, authorized), + /// - Token Holding account (initialized, or uninitialized with holder authorization in the + /// same transaction). Mint { amount_to_mint: u128 }, /// Print a new NFT from the master copy. /// /// Required accounts: /// - NFT Master Token Holding account (authorized), - /// - NFT Printed Copy Token Holding account (uninitialized). + /// - NFT Printed Copy Token Holding account (uninitialized, authorized). PrintNft, } diff --git a/token/methods/guest/Cargo.lock b/token/methods/guest/Cargo.lock index 41b5909..198a356 100644 --- a/token/methods/guest/Cargo.lock +++ b/token/methods/guest/Cargo.lock @@ -1744,7 +1744,7 @@ checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e" [[package]] name = "nssa_core" version = "0.1.0" -source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?rev=ffcbc15972adbf557939bf3e2852af276422631b#ffcbc15972adbf557939bf3e2852af276422631b" +source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0-rc1#35d8df0d031315219f94d1546ceb862b0e5b208f" dependencies = [ "base58", "borsh", @@ -2863,8 +2863,8 @@ dependencies = [ [[package]] name = "spel-framework" -version = "0.1.0" -source = "git+https://github.com/logos-co/spel.git?rev=57201f64b4542bb3f592bc9a0aa654c47aa908a6#57201f64b4542bb3f592bc9a0aa654c47aa908a6" +version = "0.2.0" +source = "git+https://github.com/logos-co/spel.git?tag=v0.2.0-rc.2#9005e9fbbd78b0530412f9987273f753ed32eb2d" dependencies = [ "borsh", "nssa_core", @@ -2874,8 +2874,8 @@ dependencies = [ [[package]] name = "spel-framework-core" -version = "0.1.0" -source = "git+https://github.com/logos-co/spel.git?rev=57201f64b4542bb3f592bc9a0aa654c47aa908a6#57201f64b4542bb3f592bc9a0aa654c47aa908a6" +version = "0.2.0" +source = "git+https://github.com/logos-co/spel.git?tag=v0.2.0-rc.2#9005e9fbbd78b0530412f9987273f753ed32eb2d" dependencies = [ "borsh", "nssa_core", @@ -2887,8 +2887,8 @@ dependencies = [ [[package]] name = "spel-framework-macros" -version = "0.1.0" -source = "git+https://github.com/logos-co/spel.git?rev=57201f64b4542bb3f592bc9a0aa654c47aa908a6#57201f64b4542bb3f592bc9a0aa654c47aa908a6" +version = "0.2.0" +source = "git+https://github.com/logos-co/spel.git?tag=v0.2.0-rc.2#9005e9fbbd78b0530412f9987273f753ed32eb2d" dependencies = [ "proc-macro2", "quote", diff --git a/token/methods/guest/Cargo.toml b/token/methods/guest/Cargo.toml index 3fd3530..fe9a7bf 100644 --- a/token/methods/guest/Cargo.toml +++ b/token/methods/guest/Cargo.toml @@ -10,8 +10,8 @@ name = "token" path = "src/bin/token.rs" [dependencies] -spel-framework = { git = "https://github.com/logos-co/spel.git", rev = "57201f64b4542bb3f592bc9a0aa654c47aa908a6", package = "spel-framework" } -nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b" } +spel-framework = { git = "https://github.com/logos-co/spel.git", tag = "v0.2.0-rc.2", package = "spel-framework" } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc1" } risc0-zkvm = { version = "=3.0.5", default-features = false } token_core = { path = "../../core" } token_program = { path = "../..", package = "token_program" } diff --git a/token/methods/guest/src/bin/token.rs b/token/methods/guest/src/bin/token.rs index 1330815..73ae46a 100644 --- a/token/methods/guest/src/bin/token.rs +++ b/token/methods/guest/src/bin/token.rs @@ -11,6 +11,7 @@ mod token { use super::*; /// Transfer tokens from sender to recipient. + /// Fresh public recipients must be explicitly authorized in the same transaction. #[instruction] pub fn transfer( sender: AccountWithMetadata, @@ -25,6 +26,7 @@ mod token { } /// Create a new fungible token definition without metadata. + /// Definition and holding targets must be uninitialized and authorized. #[instruction] pub fn new_fungible_definition( definition_target_account: AccountWithMetadata, @@ -43,6 +45,7 @@ mod token { } /// Create a new fungible or non-fungible token definition with metadata. + /// Definition, holding, and metadata targets must be uninitialized and authorized. #[instruction] pub fn new_definition_with_metadata( definition_target_account: AccountWithMetadata, @@ -63,6 +66,7 @@ mod token { } /// Initialize a token holding account for a given token definition. + /// The holding target must be uninitialized and authorized. #[instruction] pub fn initialize_account( definition_account: AccountWithMetadata, @@ -91,6 +95,7 @@ mod token { } /// Mint new tokens to the holder's account. + /// Fresh public holders must be explicitly authorized in the same transaction. #[instruction] pub fn mint( definition_account: AccountWithMetadata, @@ -105,6 +110,7 @@ mod token { } /// Print a new NFT from the master copy. + /// The printed copy target must be uninitialized and authorized. #[instruction] pub fn print_nft( master_account: AccountWithMetadata, diff --git a/token/src/initialize.rs b/token/src/initialize.rs index 744fdb6..d8350d4 100644 --- a/token/src/initialize.rs +++ b/token/src/initialize.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::{TokenDefinition, TokenHolding}; @@ -13,6 +13,10 @@ pub fn initialize_account( Account::default(), "Only Uninitialized accounts can be initialized" ); + assert!( + account_to_initialize.is_authorized, + "Account to initialize must be authorized" + ); // TODO: #212 We should check that this is an account owned by the token program. // This check can't be done here since the ID of the program is known only after compiling it @@ -29,6 +33,6 @@ pub fn initialize_account( vec![ AccountPostState::new(definition_post), - AccountPostState::new_claimed(account_to_initialize), + AccountPostState::new_claimed(account_to_initialize, Claim::Authorized), ] } diff --git a/token/src/mint.rs b/token/src/mint.rs index 2f17cc6..ff744d6 100644 --- a/token/src/mint.rs +++ b/token/src/mint.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::{TokenDefinition, TokenHolding}; @@ -66,6 +66,6 @@ pub fn mint( vec![ AccountPostState::new(definition_post), - AccountPostState::new_claimed_if_default(holding_post), + AccountPostState::new_claimed_if_default(holding_post, Claim::Authorized), ] } diff --git a/token/src/new_definition.rs b/token/src/new_definition.rs index b2a9ae9..91967a0 100644 --- a/token/src/new_definition.rs +++ b/token/src/new_definition.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::{ NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, TokenMetadata, @@ -23,6 +23,14 @@ pub fn new_fungible_definition( Account::default(), "Holding target account must have default values" ); + assert!( + definition_target_account.is_authorized, + "Definition target account must be authorized" + ); + assert!( + holding_target_account.is_authorized, + "Holding target account must be authorized" + ); let token_definition = TokenDefinition::Fungible { name, @@ -41,8 +49,8 @@ pub fn new_fungible_definition( holding_target_account_post.data = Data::from(&token_holding); vec![ - AccountPostState::new_claimed(definition_target_account_post), - AccountPostState::new_claimed(holding_target_account_post), + AccountPostState::new_claimed(definition_target_account_post, Claim::Authorized), + AccountPostState::new_claimed(holding_target_account_post, Claim::Authorized), ] } @@ -70,6 +78,18 @@ pub fn new_definition_with_metadata( Account::default(), "Metadata target account must have default values" ); + assert!( + definition_target_account.is_authorized, + "Definition target account must be authorized" + ); + assert!( + holding_target_account.is_authorized, + "Holding target account must be authorized" + ); + assert!( + metadata_target_account.is_authorized, + "Metadata target account must be authorized" + ); let (token_definition, token_holding) = match new_definition { NewTokenDefinition::Fungible { name, total_supply } => ( @@ -117,8 +137,8 @@ pub fn new_definition_with_metadata( metadata_target_account_post.data = Data::from(&token_metadata); vec![ - AccountPostState::new_claimed(definition_target_account_post), - AccountPostState::new_claimed(holding_target_account_post), - AccountPostState::new_claimed(metadata_target_account_post), + AccountPostState::new_claimed(definition_target_account_post, Claim::Authorized), + AccountPostState::new_claimed(holding_target_account_post, Claim::Authorized), + AccountPostState::new_claimed(metadata_target_account_post, Claim::Authorized), ] } diff --git a/token/src/print_nft.rs b/token/src/print_nft.rs index d10533c..837fe48 100644 --- a/token/src/print_nft.rs +++ b/token/src/print_nft.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::TokenHolding; @@ -18,6 +18,10 @@ pub fn print_nft( Account::default(), "Printed Account must be uninitialized" ); + assert!( + printed_account.is_authorized, + "Printed Account must be authorized" + ); let mut master_account_data = TokenHolding::try_from(&master_account.account.data).expect("Invalid Token Holding data"); @@ -49,6 +53,6 @@ pub fn print_nft( vec![ AccountPostState::new(master_account_post), - AccountPostState::new_claimed(printed_account_post), + AccountPostState::new_claimed(printed_account_post, Claim::Authorized), ] } diff --git a/token/src/tests.rs b/token/src/tests.rs index 00380cb..532787e 100644 --- a/token/src/tests.rs +++ b/token/src/tests.rs @@ -1,12 +1,16 @@ #![cfg(test)] -use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data, Nonce}; +use nssa_core::{ + account::{Account, AccountId, AccountWithMetadata, Data, Nonce}, + program::Claim, +}; use token_core::{ MetadataStandard, NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, }; use crate::{ burn::burn, + initialize::initialize_account, mint::mint, new_definition::{new_definition_with_metadata, new_fungible_definition}, print_nft::print_nft, @@ -161,6 +165,14 @@ impl AccountForTests { } } + fn holding_account_uninit_auth() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: IdForTests::holding_id_2(), + } + } + fn init_mint() -> AccountWithMetadata { AccountWithMetadata { account: Account { @@ -251,6 +263,22 @@ impl AccountForTests { } } + fn definition_account_uninit_auth() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn metadata_account_uninit_auth() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([19; 32]), + } + } + fn holding_account_init() -> AccountWithMetadata { AccountWithMetadata { account: Account { @@ -569,10 +597,36 @@ fn test_new_definition_non_default_second_account_should_fail() { ); } +#[should_panic(expected = "Definition target account must be authorized")] +#[test] +fn test_new_definition_requires_authorized_definition_target() { + let definition_account = AccountForTests::definition_account_uninit(); + let holding_account = AccountForTests::holding_account_uninit_auth(); + let _post_states = new_fungible_definition( + definition_account, + holding_account, + String::from("test"), + 10, + ); +} + +#[should_panic(expected = "Holding target account must be authorized")] +#[test] +fn test_new_definition_requires_authorized_holding_target() { + let definition_account = AccountForTests::definition_account_uninit_auth(); + let holding_account = AccountForTests::holding_account_uninit(); + let _post_states = new_fungible_definition( + definition_account, + holding_account, + String::from("test"), + 10, + ); +} + #[test] fn test_new_definition_with_valid_inputs_succeeds() { - let definition_account = AccountForTests::definition_account_uninit(); - let holding_account = AccountForTests::holding_account_uninit(); + let definition_account = AccountForTests::definition_account_uninit_auth(); + let holding_account = AccountForTests::holding_account_uninit_auth(); let post_states = new_fungible_definition( definition_account, @@ -586,11 +640,13 @@ fn test_new_definition_with_valid_inputs_succeeds() { *definition_account.account(), AccountForTests::definition_account_unclaimed().account ); + assert_eq!(definition_account.required_claim(), Some(Claim::Authorized)); assert_eq!( *holding_account.account(), AccountForTests::holding_account_unclaimed().account ); + assert_eq!(holding_account.required_claim(), Some(Claim::Authorized)); } #[should_panic(expected = "Sender and recipient definition id mismatch")] @@ -633,6 +689,8 @@ fn test_transfer_with_valid_inputs_succeeds() { *recipient_post.account(), AccountForTests::holding_account2_init_post_transfer().account ); + assert_eq!(sender_post.required_claim(), None); + assert_eq!(recipient_post.required_claim(), None); } #[should_panic(expected = "Invalid balance for NFT Master transfer")] @@ -669,9 +727,9 @@ fn test_transfer_with_master_nft_success() { } #[test] -fn test_token_initialize_account_succeeds() { +fn test_transfer_with_default_recipient_claims_recipient() { let sender = AccountForTests::holding_account_init(); - let recipient = AccountForTests::holding_account2_init(); + let recipient = AccountForTests::holding_account_uninit(); let post_states = transfer(sender, recipient, BalanceForTests::transfer_amount()); let [sender_post, recipient_post] = post_states.try_into().unwrap(); @@ -681,8 +739,53 @@ fn test_token_initialize_account_succeeds() { ); assert_eq!( *recipient_post.account(), - AccountForTests::holding_account2_init_post_transfer().account + Account { + program_owner: [0u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::transfer_amount(), + }), + nonce: Nonce(0), + } ); + assert_eq!(sender_post.required_claim(), None); + assert_eq!(recipient_post.required_claim(), Some(Claim::Authorized)); +} + +#[test] +fn test_token_initialize_account_succeeds() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::holding_account_uninit_auth(); + let post_states = initialize_account(definition_account, holding_account); + let [definition_post, holding_post] = post_states.try_into().unwrap(); + + assert_eq!( + *definition_post.account(), + AccountForTests::definition_account_auth().account + ); + assert_eq!( + *holding_post.account(), + Account { + program_owner: [0u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: 0, + }), + nonce: Nonce(0), + } + ); + assert_eq!(definition_post.required_claim(), None); + assert_eq!(holding_post.required_claim(), Some(Claim::Authorized)); +} + +#[test] +#[should_panic(expected = "Account to initialize must be authorized")] +fn test_token_initialize_account_requires_authorization() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::holding_account_uninit(); + let _post_states = initialize_account(definition_account, holding_account); } #[test] @@ -824,6 +927,8 @@ fn test_mint_success() { *holding_post.account(), AccountForTests::holding_account_same_definition_mint().account ); + assert_eq!(def_post.required_claim(), None); + assert_eq!(holding_post.required_claim(), None); } #[test] @@ -846,7 +951,8 @@ fn test_mint_uninit_holding_success() { *holding_post.account(), AccountForTests::init_mint().account ); - assert!(holding_post.requires_claim()); + assert_eq!(def_post.required_claim(), None); + assert_eq!(holding_post.required_claim(), Some(Claim::Authorized)); } #[test] @@ -885,6 +991,111 @@ fn test_mint_cannot_mint_unmintable_tokens() { ); } +#[test] +fn test_new_definition_with_metadata_success() { + 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, + }; + 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(); + + assert_eq!(definition_post.required_claim(), Some(Claim::Authorized)); + assert_eq!(holding_post.required_claim(), Some(Claim::Authorized)); + assert_eq!(metadata_post.required_claim(), Some(Claim::Authorized)); +} + +#[should_panic(expected = "Definition target account must be authorized")] +#[test] +fn test_call_new_definition_metadata_requires_authorized_definition() { + let definition_account = AccountForTests::definition_account_uninit(); + 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, + }; + 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, + ); +} + +#[should_panic(expected = "Holding target account must be authorized")] +#[test] +fn test_call_new_definition_metadata_requires_authorized_holding() { + let definition_account = AccountForTests::definition_account_uninit_auth(); + let holding_account = AccountForTests::holding_account_uninit(); + let metadata_account = AccountForTests::metadata_account_uninit_auth(); + let new_definition = NewTokenDefinition::Fungible { + name: String::from("test"), + total_supply: 15u128, + }; + 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, + ); +} + +#[should_panic(expected = "Metadata target account must be authorized")] +#[test] +fn test_call_new_definition_metadata_requires_authorized_metadata() { + let definition_account = AccountForTests::definition_account_uninit_auth(); + let holding_account = AccountForTests::holding_account_uninit_auth(); + let metadata_account = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: AccountId::new([20; 32]), + }; + let new_definition = NewTokenDefinition::Fungible { + name: String::from("test"), + total_supply: 15u128, + }; + 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, + ); +} + #[should_panic(expected = "Definition target account must have default values")] #[test] fn test_call_new_definition_metadata_with_init_definition() { @@ -997,11 +1208,19 @@ fn test_print_nft_print_account_initialized() { let _post_states = print_nft(master_account, printed_account); } +#[should_panic(expected = "Printed Account must be authorized")] +#[test] +fn test_print_nft_print_account_must_be_authorized() { + let master_account = AccountForTests::holding_account_master_nft(); + let printed_account = AccountForTests::holding_account_uninit(); + let _post_states = print_nft(master_account, printed_account); +} + #[should_panic(expected = "Invalid Token Holding data")] #[test] fn test_print_nft_master_nft_invalid_token_holding() { let master_account = AccountForTests::definition_account_auth(); - let printed_account = AccountForTests::holding_account_uninit(); + let printed_account = AccountForTests::holding_account_uninit_auth(); let _post_states = print_nft(master_account, printed_account); } @@ -1009,7 +1228,7 @@ fn test_print_nft_master_nft_invalid_token_holding() { #[test] fn test_print_nft_master_nft_not_nft_master_account() { let master_account = AccountForTests::holding_account_init(); - let printed_account = AccountForTests::holding_account_uninit(); + let printed_account = AccountForTests::holding_account_uninit_auth(); let _post_states = print_nft(master_account, printed_account); } @@ -1017,14 +1236,14 @@ fn test_print_nft_master_nft_not_nft_master_account() { #[test] fn test_print_nft_master_nft_insufficient_balance() { let master_account = AccountForTests::holding_account_master_nft_insufficient_balance(); - let printed_account = AccountForTests::holding_account_uninit(); + let printed_account = AccountForTests::holding_account_uninit_auth(); let _post_states = print_nft(master_account, printed_account); } #[test] fn test_print_nft_success() { let master_account = AccountForTests::holding_account_master_nft(); - let printed_account = AccountForTests::holding_account_uninit(); + let printed_account = AccountForTests::holding_account_uninit_auth(); let post_states = print_nft(master_account, printed_account); let [post_master_nft, post_printed] = post_states.try_into().unwrap(); @@ -1037,4 +1256,6 @@ fn test_print_nft_success() { *post_printed.account(), AccountForTests::holding_account_printed_nft().account ); + assert_eq!(post_master_nft.required_claim(), None); + assert_eq!(post_printed.required_claim(), Some(Claim::Authorized)); } diff --git a/token/src/transfer.rs b/token/src/transfer.rs index a1087bb..2d1fe70 100644 --- a/token/src/transfer.rs +++ b/token/src/transfer.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::TokenHolding; @@ -105,6 +105,6 @@ pub fn transfer( vec![ AccountPostState::new(sender_post), - AccountPostState::new_claimed_if_default(recipient_post), + AccountPostState::new_claimed_if_default(recipient_post, Claim::Authorized), ] } diff --git a/tools/idl-gen/Cargo.toml b/tools/idl-gen/Cargo.toml index dd05892..71dab24 100644 --- a/tools/idl-gen/Cargo.toml +++ b/tools/idl-gen/Cargo.toml @@ -8,7 +8,7 @@ name = "idl-gen" path = "src/main.rs" [dependencies] -spel-framework-core = { git = "https://github.com/logos-co/spel.git", rev = "57201f64b4542bb3f592bc9a0aa654c47aa908a6", features = [ +spel-framework-core = { git = "https://github.com/logos-co/spel.git", tag = "v0.2.0-rc.2", features = [ "idl-gen", ] } serde_json = "1.0"