diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3b0b22..6b6ff6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,15 @@ jobs: ${{ runner.os }}-cargo- - name: Clippy - run: RISC0_SKIP_BUILD=1 cargo clippy --workspace --all-targets -- -D warnings + run: | + if rg -n -U --multiline '#\[(allow|expect)\([\s\S]*?reason\s*=\s*"(TODO|FIXME|fix later|temporary|hack)' . -g '*.rs' -g '!*/target/*'; then + echo "Found non-actionable lint suppression reason" + exit 1 + fi + make clippy + + - name: Guest Clippy + run: make clippy-guest unit-tests: name: Unit Tests diff --git a/CLAUDE.md b/CLAUDE.md index 992296f..3b7d7c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,4 +103,4 @@ Each program follows a layered pattern: **Chained calls**: The AMM's swap and liquidity operations compose with the token program via `ChainedCall` — the AMM instructs the token program to execute transfers as part of the same atomic operation. -**Testing**: Tests call program functions directly (no zkVM overhead). Set `RISC0_DEV_MODE=1` to skip ZK proof generation when running integration tests that go through the zkVM. The Rust toolchain pinned version is **1.91.1**. +**Testing**: Tests call program functions directly (no zkVM overhead). Set `RISC0_DEV_MODE=1` to skip ZK proof generation when running integration tests that go through the zkVM. The Rust toolchain pinned version is **1.94.0**. diff --git a/Cargo.toml b/Cargo.toml index b7a0a1e..f439e9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,45 @@ 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"] } + +[workspace.lints.rust] +rust_2018_idioms = { level = "deny", priority = -1 } +# deny (not forbid) so a targeted per-item #[allow] remains possible if ever needed +unsafe_code = "deny" + +[workspace.lints.clippy] +# Deny only the groups where a new lint should always be a hard error. +# style/pedantic lints default to warn so toolchain upgrades don't break the +# build unexpectedly — they can be evaluated and addressed at our own pace. +correctness = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +style = { level = "warn", priority = -1 } + +# Generated-code / placeholder blockers. +dbg_macro = "deny" +todo = "deny" +unimplemented = "deny" +unwrap_used = "deny" + +# Lint suppression hygiene. +allow_attributes = "warn" +allow_attributes_without_reason = "deny" + +# Determinism, panic-safety, and arithmetic correctness. +arithmetic_side_effects = "deny" +indexing_slicing = "deny" + +# Cast discipline. +as_conversions = "deny" +cast_possible_truncation = "deny" +cast_possible_wrap = "deny" +cast_sign_loss = "deny" + +# API and enum evolution. +large_enum_variant = "deny" +wildcard_enum_match_arm = "deny" + +# Too noisy for this codebase unless enforced selectively. +module_name_repetitions = "allow" +similar_names = "allow" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5dbb110 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: clippy clippy-guest clippy-all test fmt + +clippy: + RISC0_SKIP_BUILD=1 cargo clippy --workspace --all-targets -- -D warnings + +clippy-guest: + for manifest in token/methods/guest/Cargo.toml amm/methods/guest/Cargo.toml ata/methods/guest/Cargo.toml; do \ + cargo clippy --manifest-path "$$manifest" --all-targets -- -D warnings || exit 1; \ + done + +clippy-all: clippy clippy-guest + +test: + RISC0_DEV_MODE=1 cargo test --workspace + +fmt: + cargo +nightly fmt --all diff --git a/amm/Cargo.toml b/amm/Cargo.toml index 9ca582f..28dce03 100644 --- a/amm/Cargo.toml +++ b/amm/Cargo.toml @@ -3,6 +3,9 @@ name = "amm_program" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] } amm_core = { path = "core" } diff --git a/amm/core/Cargo.toml b/amm/core/Cargo.toml index c832b9b..f45bcbd 100644 --- a/amm/core/Cargo.toml +++ b/amm/core/Cargo.toml @@ -3,6 +3,9 @@ name = "amm_core" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] } spel-framework-macros = { git = "https://github.com/logos-co/spel.git", tag = "v0.3.0", package = "spel-framework-macros" } diff --git a/amm/core/src/lib.rs b/amm/core/src/lib.rs index 60366df..b041429 100644 --- a/amm/core/src/lib.rs +++ b/amm/core/src/lib.rs @@ -204,8 +204,9 @@ pub fn compute_pool_pda_seed( }; let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&token_1.to_bytes()); - bytes[32..].copy_from_slice(&token_2.to_bytes()); + let (token_1_bytes, token_2_bytes) = bytes.split_at_mut(32); + token_1_bytes.copy_from_slice(&token_1.to_bytes()); + token_2_bytes.copy_from_slice(&token_2.to_bytes()); PdaSeed::new( Impl::hash_bytes(&bytes) @@ -230,8 +231,9 @@ pub fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId use risc0_zkvm::sha::{Impl, Sha256}; let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&pool_id.to_bytes()); - bytes[32..].copy_from_slice(&definition_token_id.to_bytes()); + let (pool_bytes, definition_bytes) = bytes.split_at_mut(32); + pool_bytes.copy_from_slice(&pool_id.to_bytes()); + definition_bytes.copy_from_slice(&definition_token_id.to_bytes()); PdaSeed::new( Impl::hash_bytes(&bytes) @@ -249,8 +251,9 @@ pub fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed { use risc0_zkvm::sha::{Impl, Sha256}; let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&pool_id.to_bytes()); - bytes[32..].copy_from_slice(&LIQUIDITY_TOKEN_PDA_SEED); + let (pool_bytes, seed_bytes) = bytes.split_at_mut(32); + pool_bytes.copy_from_slice(&pool_id.to_bytes()); + seed_bytes.copy_from_slice(&LIQUIDITY_TOKEN_PDA_SEED); PdaSeed::new( Impl::hash_bytes(&bytes) @@ -268,8 +271,9 @@ pub fn compute_lp_lock_holding_pda_seed(pool_id: AccountId) -> PdaSeed { use risc0_zkvm::sha::{Impl, Sha256}; let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&pool_id.to_bytes()); - bytes[32..].copy_from_slice(&LP_LOCK_HOLDING_PDA_SEED); + let (pool_bytes, seed_bytes) = bytes.split_at_mut(32); + pool_bytes.copy_from_slice(&pool_id.to_bytes()); + seed_bytes.copy_from_slice(&LP_LOCK_HOLDING_PDA_SEED); PdaSeed::new( Impl::hash_bytes(&bytes) diff --git a/amm/methods/Cargo.toml b/amm/methods/Cargo.toml index 5bc00df..1139347 100644 --- a/amm/methods/Cargo.toml +++ b/amm/methods/Cargo.toml @@ -3,6 +3,9 @@ name = "amm-methods" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [build-dependencies] risc0-build = "=3.0.5" diff --git a/amm/methods/guest/Cargo.toml b/amm/methods/guest/Cargo.toml index a840422..a18d38e 100644 --- a/amm/methods/guest/Cargo.toml +++ b/amm/methods/guest/Cargo.toml @@ -5,6 +5,48 @@ edition = "2021" [workspace] +[lints.rust] +rust_2018_idioms = { level = "deny", priority = -1 } +# deny (not forbid) so a targeted per-item #[allow] remains possible if ever needed +unsafe_code = "deny" + +[lints.clippy] +# Deny only the groups where a new lint should always be a hard error. +# style/pedantic lints default to warn so toolchain upgrades don't break the +# build unexpectedly — they can be evaluated and addressed at our own pace. +correctness = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +style = { level = "warn", priority = -1 } + +# Generated-code / placeholder blockers. +dbg_macro = "deny" +todo = "deny" +unimplemented = "deny" +unwrap_used = "deny" + +# Lint suppression hygiene. +allow_attributes = "warn" +allow_attributes_without_reason = "deny" + +# Determinism, panic-safety, and arithmetic correctness. +arithmetic_side_effects = "deny" +indexing_slicing = "deny" + +# Cast discipline. +as_conversions = "deny" +cast_possible_truncation = "deny" +cast_possible_wrap = "deny" +cast_sign_loss = "deny" + +# API and enum evolution. +large_enum_variant = "deny" +wildcard_enum_match_arm = "deny" + +# Too noisy for this codebase unless enforced selectively. +module_name_repetitions = "allow" +similar_names = "allow" + [[bin]] name = "amm" path = "src/bin/amm.rs" diff --git a/amm/methods/guest/src/bin/amm.rs b/amm/methods/guest/src/bin/amm.rs index 99356f1..ff08986 100644 --- a/amm/methods/guest/src/bin/amm.rs +++ b/amm/methods/guest/src/bin/amm.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(not(test), no_main)] use std::num::NonZeroU128; @@ -8,15 +8,23 @@ use nssa_core::{ account::{AccountId, AccountWithMetadata}, }; +#[cfg(not(test))] risc0_zkvm::guest::entry!(main); #[lez_program(instruction = "amm_core::Instruction")] mod amm { - #[allow(unused_imports)] + #[expect( + unused_imports, + reason = "SPEL instruction macro requires importing parent-scope handler types" + )] 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. + #[expect( + clippy::too_many_arguments, + reason = "instruction interface requires explicit pool, vault, mint, lock, and user accounts" + )] #[instruction] pub fn new_definition( ctx: ProgramContext, @@ -52,6 +60,10 @@ mod amm { } /// Adds liquidity to the Pool. + #[expect( + clippy::too_many_arguments, + reason = "instruction interface requires explicit pool, vault, and user accounts" + )] #[instruction] pub fn add_liquidity( pool: AccountWithMetadata, @@ -83,6 +95,10 @@ mod amm { } /// Removes liquidity from the Pool. + #[expect( + clippy::too_many_arguments, + reason = "instruction interface requires explicit pool, vault, and user accounts" + )] #[instruction] pub fn remove_liquidity( pool: AccountWithMetadata, @@ -115,6 +131,10 @@ mod amm { } /// Swap some quantity of tokens while maintaining the pool constant product. + #[expect( + clippy::too_many_arguments, + reason = "instruction interface requires explicit pool, vault, user accounts, and bounds" + )] #[instruction] pub fn swap_exact_input( pool: AccountWithMetadata, @@ -142,6 +162,10 @@ mod amm { } /// Swap tokens specifying the exact desired output amount. + #[expect( + clippy::too_many_arguments, + reason = "instruction interface requires explicit pool, vault, user accounts, and bounds" + )] #[instruction] pub fn swap_exact_output( pool: AccountWithMetadata, diff --git a/amm/src/add.rs b/amm/src/add.rs index 7c18726..c91e092 100644 --- a/amm/src/add.rs +++ b/amm/src/add.rs @@ -9,7 +9,10 @@ use nssa_core::{ program::{AccountPostState, ChainedCall}, }; -#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[expect( + clippy::too_many_arguments, + reason = "instruction surface passes explicit pool, vault, and user accounts" +)] pub fn add_liquidity( pool: AccountWithMetadata, vault_a: AccountWithMetadata, @@ -77,12 +80,14 @@ pub fn add_liquidity( .reserve_a .checked_mul(max_amount_to_add_token_b) .expect("reserve_a * max_amount_b overflows u128") - / pool_def_data.reserve_b; + .checked_div(pool_def_data.reserve_b) + .expect("reserve_b must be nonzero after validation"); let ideal_b: u128 = pool_def_data .reserve_b .checked_mul(max_amount_to_add_token_a) .expect("reserve_b * max_amount_a overflows u128") - / pool_def_data.reserve_a; + .checked_div(pool_def_data.reserve_a) + .expect("reserve_a must be nonzero after validation"); let actual_amount_a = if ideal_a > max_amount_to_add_token_a { max_amount_to_add_token_a @@ -114,12 +119,14 @@ pub fn add_liquidity( .liquidity_pool_supply .checked_mul(actual_amount_a) .expect("liquidity_pool_supply * actual_amount_a overflows u128") - / pool_def_data.reserve_a, + .checked_div(pool_def_data.reserve_a) + .expect("reserve_a must be nonzero after validation"), pool_def_data .liquidity_pool_supply .checked_mul(actual_amount_b) .expect("liquidity_pool_supply * actual_amount_b overflows u128") - / pool_def_data.reserve_b, + .checked_div(pool_def_data.reserve_b) + .expect("reserve_b must be nonzero after validation"), ); assert!(delta_lp != 0, "Payable LP must be nonzero"); diff --git a/amm/src/new_definition.rs b/amm/src/new_definition.rs index b7a45cc..8c47a6a 100644 --- a/amm/src/new_definition.rs +++ b/amm/src/new_definition.rs @@ -12,7 +12,10 @@ use nssa_core::{ }; use token_core::TokenDefinition; -#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[expect( + clippy::too_many_arguments, + reason = "instruction surface passes explicit pool, vault, mint, lock, and user accounts" +)] pub fn new_definition( pool: AccountWithMetadata, vault_a: AccountWithMetadata, @@ -94,7 +97,9 @@ pub fn new_definition( initial_lp > MINIMUM_LIQUIDITY, "Initial liquidity must exceed minimum liquidity lock" ); - let user_lp = initial_lp - MINIMUM_LIQUIDITY; + let user_lp = initial_lp + .checked_sub(MINIMUM_LIQUIDITY) + .expect("initial liquidity must exceed minimum liquidity after validation"); // Update pool account let mut pool_post = pool.account.clone(); diff --git a/amm/src/remove.rs b/amm/src/remove.rs index 078ef04..3a57379 100644 --- a/amm/src/remove.rs +++ b/amm/src/remove.rs @@ -9,7 +9,10 @@ use nssa_core::{ program::{AccountPostState, ChainedCall}, }; -#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[expect( + clippy::too_many_arguments, + reason = "instruction surface passes explicit pool, vault, and user accounts" +)] pub fn remove_liquidity( pool: AccountWithMetadata, vault_a: AccountWithMetadata, @@ -105,7 +108,10 @@ pub fn remove_liquidity( remove_liquidity_amount <= user_lp_balance, "Remove amount exceeds user LP balance" ); - let unlocked_liquidity = pool_def_data.liquidity_pool_supply - MINIMUM_LIQUIDITY; + let unlocked_liquidity = pool_def_data + .liquidity_pool_supply + .checked_sub(MINIMUM_LIQUIDITY) + .expect("liquidity supply must be at least the locked minimum after validation"); // The remove instruction never sees the LP lock account directly, so we must still refuse any // request that would burn through the permanent floor even if ownership is already corrupted. assert!( @@ -117,12 +123,14 @@ pub fn remove_liquidity( .reserve_a .checked_mul(remove_liquidity_amount) .expect("reserve_a * remove_liquidity_amount overflows u128") - / pool_def_data.liquidity_pool_supply; + .checked_div(pool_def_data.liquidity_pool_supply) + .expect("liquidity supply must be nonzero after validation"); let withdraw_amount_b = pool_def_data .reserve_b .checked_mul(remove_liquidity_amount) .expect("reserve_b * remove_liquidity_amount overflows u128") - / pool_def_data.liquidity_pool_supply; + .checked_div(pool_def_data.liquidity_pool_supply) + .expect("liquidity supply must be nonzero after validation"); // 3. Validate and slippage check assert!( diff --git a/amm/src/swap.rs b/amm/src/swap.rs index e8b38a0..f490825 100644 --- a/amm/src/swap.rs +++ b/amm/src/swap.rs @@ -46,7 +46,10 @@ fn validate_swap_setup( } /// Creates post-state and returns reserves after swap. -#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[expect( + clippy::too_many_arguments, + reason = "post-state assembly keeps pool, vault, user account, and delta state explicit" +)] #[expect( clippy::needless_pass_by_value, reason = "consistent with codebase style" @@ -91,7 +94,10 @@ fn create_swap_post_states( ] } -#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[expect( + clippy::too_many_arguments, + reason = "instruction surface passes explicit pool, vault, and user accounts" +)] #[must_use] pub fn swap_exact_input( pool: AccountWithMetadata, @@ -166,7 +172,10 @@ pub fn swap_exact_input( (post_states, chained_calls) } -#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[expect( + clippy::too_many_arguments, + reason = "swap calculation keeps account context and pricing parameters explicit" +)] fn swap_logic( user_deposit: AccountWithMetadata, vault_deposit: AccountWithMetadata, @@ -179,10 +188,14 @@ fn swap_logic( reserve_withdraw_vault_amount: u128, pool_id: AccountId, ) -> (Vec, u128, u128) { + let fee_multiplier = FEE_BPS_DENOMINATOR + .checked_sub(fee_bps) + .expect("fee_bps exceeds fee denominator"); let effective_amount_in = swap_amount_in - .checked_mul(FEE_BPS_DENOMINATOR - fee_bps) + .checked_mul(fee_multiplier) .expect("swap_amount_in * (FEE_BPS_DENOMINATOR - fee_bps) overflows u128") - / FEE_BPS_DENOMINATOR; + .checked_div(FEE_BPS_DENOMINATOR) + .expect("fee denominator must be nonzero"); assert!( effective_amount_in != 0, "Effective swap amount should be nonzero" @@ -194,9 +207,12 @@ fn swap_logic( let withdraw_amount = reserve_withdraw_vault_amount .checked_mul(effective_amount_in) .expect("reserve * effective_amount_in overflows u128") - / reserve_deposit_vault_amount - .checked_add(effective_amount_in) - .expect("reserve + effective_amount_in overflows u128"); + .checked_div( + reserve_deposit_vault_amount + .checked_add(effective_amount_in) + .expect("reserve + effective_amount_in overflows u128"), + ) + .expect("reserve plus effective input must be nonzero"); // Slippage check assert!( @@ -240,7 +256,10 @@ fn swap_logic( (chained_calls, swap_amount_in, withdraw_amount) } -#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[expect( + clippy::too_many_arguments, + reason = "instruction surface passes explicit pool, vault, and user accounts" +)] #[must_use] pub fn swap_exact_output( pool: AccountWithMetadata, @@ -315,7 +334,10 @@ pub fn swap_exact_output( (post_states, chained_calls) } -#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[expect( + clippy::too_many_arguments, + reason = "swap calculation keeps account context and pricing parameters explicit" +)] fn exact_output_swap_logic( user_deposit: AccountWithMetadata, vault_deposit: AccountWithMetadata, diff --git a/amm/src/tests.rs b/amm/src/tests.rs index c845815..486b895 100644 --- a/amm/src/tests.rs +++ b/amm/src/tests.rs @@ -1,4 +1,9 @@ #![cfg(test)] +#![expect( + clippy::arithmetic_side_effects, + clippy::integer_division, + reason = "test fixtures use fixed values to lock AMM math boundaries" +)] use std::num::NonZero; @@ -2225,7 +2230,8 @@ fn test_call_swap_below_minimum_liquidity() { #[test] fn test_call_swap_rejects_unsupported_fee_tier() { let mut pool = AccountWithMetadataForTests::pool_definition_init(); - let mut pool_def = PoolDefinition::try_from(&pool.account.data).unwrap(); + let mut pool_def = + PoolDefinition::try_from(&pool.account.data).unwrap(); pool_def.fees = 2; pool.account.data = Data::from(&pool_def); @@ -2759,7 +2765,8 @@ fn test_new_definition_lp_asymmetric_amounts() { // check the minted LP amount let pool_post = post_states[0].clone(); - let pool_def = PoolDefinition::try_from(&pool_post.account().data).unwrap(); + let pool_def = + PoolDefinition::try_from(&pool_post.account().data).unwrap(); assert_eq!( pool_def.liquidity_pool_supply, BalanceForTests::lp_supply_init() @@ -2795,7 +2802,8 @@ fn test_new_definition_lp_symmetric_amounts() { ); let pool_post = post_states[0].clone(); - let pool_def = PoolDefinition::try_from(&pool_post.account().data).unwrap(); + let pool_def = + PoolDefinition::try_from(&pool_post.account().data).unwrap(); assert_eq!(pool_def.liquidity_pool_supply, expected_lp); let chained_call_lp_lock = chained_calls[0].clone(); @@ -2896,7 +2904,8 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() { assert_eq!(chained_calls[0], expected_lock_call); assert_eq!(chained_calls[1], expected_user_call); - let pool_post = PoolDefinition::try_from(&post_states[0].account().data).unwrap(); + let pool_post = PoolDefinition::try_from(&post_states[0].account().data) + .unwrap(); assert_eq!(pool_post.liquidity_pool_supply, initial_lp); let pool_for_remove = AccountWithMetadata { @@ -2917,8 +2926,8 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() { 1, ); - let pool_after_remove = - PoolDefinition::try_from(&remove_post_states[0].account().data).unwrap(); + let pool_after_remove = PoolDefinition::try_from(&remove_post_states[0].account().data) + .unwrap(); assert_eq!(pool_after_remove.liquidity_pool_supply, MINIMUM_LIQUIDITY); assert!(pool_after_remove.reserve_a > 0); assert!(pool_after_remove.reserve_b > 0); @@ -2945,7 +2954,8 @@ fn test_sync_reserves_with_donation() { ); assert!(chained_calls.is_empty()); - let pool_post = PoolDefinition::try_from(&post_states[0].account().data).unwrap(); + let pool_post = PoolDefinition::try_from(&post_states[0].account().data) + .unwrap(); assert_eq!( pool_post.reserve_a, BalanceForTests::vault_a_reserve_init() + donation_a @@ -2987,7 +2997,8 @@ fn test_sync_reserves_rejects_pool_below_minimum_liquidity() { #[test] fn test_sync_reserves_rejects_unsupported_fee_tier() { let mut pool = AccountWithMetadataForTests::pool_definition_init(); - let mut pool_def = PoolDefinition::try_from(&pool.account.data).unwrap(); + let mut pool_def = + PoolDefinition::try_from(&pool.account.data).unwrap(); pool_def.fees = 2; pool.account.data = Data::from(&pool_def); @@ -3021,7 +3032,8 @@ fn test_donation_then_add_liquidity_sync_mitigates_mispricing() { 100, 50, ); - let unsynced_pool_post = PoolDefinition::try_from(&post_unsynced[0].account().data).unwrap(); + let unsynced_pool_post = PoolDefinition::try_from(&post_unsynced[0].account().data) + .unwrap(); let unsynced_delta_lp = unsynced_pool_post.liquidity_pool_supply - BalanceForTests::lp_supply_init(); @@ -3051,7 +3063,8 @@ fn test_donation_then_add_liquidity_sync_mitigates_mispricing() { 100, 50, ); - let synced_pool_post = PoolDefinition::try_from(&post_synced[0].account().data).unwrap(); + let synced_pool_post = PoolDefinition::try_from(&post_synced[0].account().data) + .unwrap(); let synced_delta_lp = synced_pool_post.liquidity_pool_supply - PoolDefinition::try_from(&sync_post[0].account().data) .unwrap() @@ -3228,7 +3241,8 @@ fn remove_liquidity_overflow_protection() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), user_lp, - NonZero::new(2).unwrap(), // remove_amount=2 → reserve_a * 2 overflows + NonZero::new(2).unwrap(), /* remove_amount=2 → reserve_a * 2 + * overflows */ 1, 1, ); @@ -3321,14 +3335,17 @@ fn test_new_definition_supports_all_fee_tiers() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), - NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), - NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), + NonZero::new(BalanceForTests::vault_a_reserve_init()) + .unwrap(), + NonZero::new(BalanceForTests::vault_b_reserve_init()) + .unwrap(), fees, AMM_PROGRAM_ID, ); let pool_post = post_states[0].clone(); - let pool_def = PoolDefinition::try_from(&pool_post.account().data).unwrap(); + let pool_def = + PoolDefinition::try_from(&pool_post.account().data).unwrap(); assert_eq!(pool_def.fees, fees); } } @@ -3365,7 +3382,7 @@ fn test_add_liquidity_rejects_user_holding_a_wrong_program() { AccountWithMetadataForTests::user_holding_a_wrong_program(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_init(), - NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + NonZero::new(BalanceForTests::add_min_amount_lp()).expect("test value must be nonzero"), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), ); @@ -3382,7 +3399,7 @@ fn test_add_liquidity_rejects_user_holding_b_wrong_program() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b_wrong_program(), AccountWithMetadataForTests::user_holding_lp_init(), - NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + NonZero::new(BalanceForTests::add_min_amount_lp()).expect("test value must be nonzero"), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), ); @@ -3401,7 +3418,7 @@ fn test_remove_liquidity_rejects_user_holding_a_wrong_program() { AccountWithMetadataForTests::user_holding_lp_with_balance( BalanceForTests::remove_amount_lp(), ), - NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), + NonZero::new(BalanceForTests::remove_amount_lp()).expect("test value must be nonzero"), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b_low(), ); @@ -3420,7 +3437,7 @@ fn test_remove_liquidity_rejects_user_holding_b_wrong_program() { AccountWithMetadataForTests::user_holding_lp_with_balance( BalanceForTests::remove_amount_lp(), ), - NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), + NonZero::new(BalanceForTests::remove_amount_lp()).expect("test value must be nonzero"), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b_low(), ); @@ -3438,7 +3455,7 @@ fn test_remove_liquidity_rejects_amount_exceeding_user_lp_balance() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_with_balance(lp_balance), - NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), + NonZero::new(BalanceForTests::remove_amount_lp()).expect("test value must be nonzero"), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b_low(), ); diff --git a/ata/Cargo.toml b/ata/Cargo.toml index ca47107..0601e65 100644 --- a/ata/Cargo.toml +++ b/ata/Cargo.toml @@ -3,6 +3,9 @@ name = "ata_program" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] } ata_core = { path = "core" } diff --git a/ata/core/Cargo.toml b/ata/core/Cargo.toml index 0304639..946bead 100644 --- a/ata/core/Cargo.toml +++ b/ata/core/Cargo.toml @@ -3,6 +3,9 @@ name = "ata_core" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] } borsh = { version = "1.5", features = ["derive"] } diff --git a/ata/core/src/lib.rs b/ata/core/src/lib.rs index f4a8ac0..e4be506 100644 --- a/ata/core/src/lib.rs +++ b/ata/core/src/lib.rs @@ -60,12 +60,16 @@ pub fn compute_ata_seed( ) -> PdaSeed { use risc0_zkvm::sha::{Impl, Sha256}; let mut bytes = [0u8; 96]; - for (index, word) in token_program_id.iter().enumerate() { - let offset = index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + let (program_id_bytes, rest) = bytes.split_at_mut(32); + let (owner_bytes, definition_bytes) = rest.split_at_mut(32); + for (chunk, word) in program_id_bytes + .chunks_exact_mut(4) + .zip(token_program_id.iter()) + { + chunk.copy_from_slice(&word.to_le_bytes()); } - bytes[32..64].copy_from_slice(&owner_id.to_bytes()); - bytes[64..96].copy_from_slice(&definition_id.to_bytes()); + owner_bytes.copy_from_slice(&owner_id.to_bytes()); + definition_bytes.copy_from_slice(&definition_id.to_bytes()); PdaSeed::new( Impl::hash_bytes(&bytes) .as_bytes() diff --git a/ata/methods/Cargo.toml b/ata/methods/Cargo.toml index 09d4849..6bb49b0 100644 --- a/ata/methods/Cargo.toml +++ b/ata/methods/Cargo.toml @@ -3,6 +3,9 @@ name = "ata-methods" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [build-dependencies] risc0-build = "=3.0.5" diff --git a/ata/methods/guest/Cargo.toml b/ata/methods/guest/Cargo.toml index d82715b..3ba0eb7 100644 --- a/ata/methods/guest/Cargo.toml +++ b/ata/methods/guest/Cargo.toml @@ -5,6 +5,48 @@ edition = "2021" [workspace] +[lints.rust] +rust_2018_idioms = { level = "deny", priority = -1 } +# deny (not forbid) so a targeted per-item #[allow] remains possible if ever needed +unsafe_code = "deny" + +[lints.clippy] +# Deny only the groups where a new lint should always be a hard error. +# style/pedantic lints default to warn so toolchain upgrades don't break the +# build unexpectedly — they can be evaluated and addressed at our own pace. +correctness = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +style = { level = "warn", priority = -1 } + +# Generated-code / placeholder blockers. +dbg_macro = "deny" +todo = "deny" +unimplemented = "deny" +unwrap_used = "deny" + +# Lint suppression hygiene. +allow_attributes = "warn" +allow_attributes_without_reason = "deny" + +# Determinism, panic-safety, and arithmetic correctness. +arithmetic_side_effects = "deny" +indexing_slicing = "deny" + +# Cast discipline. +as_conversions = "deny" +cast_possible_truncation = "deny" +cast_possible_wrap = "deny" +cast_sign_loss = "deny" + +# API and enum evolution. +large_enum_variant = "deny" +wildcard_enum_match_arm = "deny" + +# Too noisy for this codebase unless enforced selectively. +module_name_repetitions = "allow" +similar_names = "allow" + [[bin]] name = "ata" path = "src/bin/ata.rs" diff --git a/ata/methods/guest/src/bin/ata.rs b/ata/methods/guest/src/bin/ata.rs index 7d892b2..43e2ef0 100644 --- a/ata/methods/guest/src/bin/ata.rs +++ b/ata/methods/guest/src/bin/ata.rs @@ -1,14 +1,18 @@ -#![no_main] +#![cfg_attr(not(test), no_main)] use spel_framework::prelude::*; use spel_framework::context::ProgramContext; use nssa_core::{account::AccountWithMetadata, program::ProgramId}; +#[cfg(not(test))] risc0_zkvm::guest::entry!(main); #[lez_program(instruction = "ata_core::Instruction")] mod ata { - #[allow(unused_imports)] + #[expect( + unused_imports, + reason = "SPEL instruction macro requires importing parent-scope handler types" + )] use super::*; /// Create the Associated Token Account for (token program, owner, definition). diff --git a/ata/src/tests.rs b/ata/src/tests.rs index 595cfdd..ce7229c 100644 --- a/ata/src/tests.rs +++ b/ata/src/tests.rs @@ -357,7 +357,8 @@ fn transfer_panics_when_recipient_is_foreign_owned() { #[should_panic(expected = "Recipient must hold a valid token")] fn transfer_panics_when_recipient_data_is_malformed() { let mut malformed_recipient = initialized_recipient_account(); - malformed_recipient.account.data = Data::try_from(vec![0xFFu8, 0xFE, 0xFD]).unwrap(); + malformed_recipient.account.data = + Data::try_from(vec![0xFFu8, 0xFE, 0xFD]).unwrap(); crate::transfer::transfer_from_associated_token_account( owner_account(), diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..c2d6c32 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,11 @@ +msrv = "1.94.0" + +avoid-breaking-exported-api = false +check-private-items = true +warn-on-all-wildcard-imports = true + +allow-dbg-in-tests = false +allow-expect-in-tests = true +allow-indexing-slicing-in-tests = true +allow-print-in-tests = true +allow-unwrap-in-tests = true diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 3254cdc..5f85b31 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -3,6 +3,9 @@ name = "integration_tests" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] nssa = { workspace = true } nssa_core = { workspace = true, features = ["host"] } diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index ada0f46..53459b7 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -1,3 +1,8 @@ +#![expect( + clippy::arithmetic_side_effects, + reason = "integration fixtures use fixed balances to assert AMM state transitions" +)] + use amm_core::{ PoolDefinition, FEE_TIER_BPS_1, FEE_TIER_BPS_100, FEE_TIER_BPS_30, FEE_TIER_BPS_5, MINIMUM_LIQUIDITY, @@ -956,6 +961,7 @@ fn state_for_amm_tests_with_precreated_user_lp_for_new_def() -> V03State { state } +#[cfg(test)] fn try_execute_new_definition( state: &mut V03State, fees: u128, @@ -1009,10 +1015,12 @@ fn try_execute_new_definition( state.transition_from_public_transaction(&tx, 0, 0) } +#[cfg(test)] fn execute_new_definition(state: &mut V03State, fees: u128) { try_execute_new_definition(state, fees, true).unwrap(); } +#[cfg(test)] fn execute_swap_a_to_b(state: &mut V03State, swap_amount_in: u128, min_amount_out: u128) { let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in, @@ -1038,9 +1046,12 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); } +#[cfg(test)] fn execute_swap_b_to_a(state: &mut V03State, swap_amount_in: u128, min_amount_out: u128) { let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in, @@ -1066,9 +1077,12 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); } +#[cfg(test)] fn execute_add_liquidity( state: &mut V03State, min_amount_liquidity: u128, @@ -1105,9 +1119,12 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); } +#[cfg(test)] fn execute_remove_liquidity( state: &mut V03State, remove_liquidity_amount: u128, @@ -1140,7 +1157,9 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); } fn fungible_balance(account: &Account) -> u128 { @@ -1204,7 +1223,9 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), @@ -1358,7 +1379,8 @@ fn amm_new_definition_precreated_zero_balance_user_lp() { 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(); + try_execute_new_definition(&mut state, Balances::fee_tier(), false) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), @@ -1493,7 +1515,9 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), @@ -1553,7 +1577,9 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), @@ -1605,7 +1631,9 @@ 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, 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 d88bd3b..c58bae1 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -180,7 +180,9 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::owner_ata()), @@ -215,7 +217,9 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); // Already initialized — should remain unchanged assert_eq!( @@ -344,7 +348,9 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::owner_ata()), @@ -472,7 +478,9 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::owner_ata()), @@ -532,15 +540,18 @@ fn ata_create_from_private_owner() { let instruction = ata_core::Instruction::Create { token_program_id: Ids::token_program(), }; - let instruction_data = Program::serialize_instruction(instruction).unwrap(); + let instruction_data = + Program::serialize_instruction(instruction).unwrap(); // Ephemeral key for encrypting the private owner's post-state let esk: Scalar = [3u8; 32]; let shared_secret = SharedSecretKey::new(&esk, &owner_vpk); let epk = EphemeralPublicKey::from_scalar(esk); - let ata_program = Program::new(ata_methods::ATA_ELF.to_vec()).unwrap(); - let token_program = Program::new(token_methods::TOKEN_ELF.to_vec()).unwrap(); + let ata_program = + Program::new(ata_methods::ATA_ELF.to_vec()).unwrap(); + let token_program = + Program::new(token_methods::TOKEN_ELF.to_vec()).unwrap(); let program_with_deps = ProgramWithDependencies::new( ata_program, HashMap::from([(Ids::token_program(), token_program)]), diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index e0b552b..1c6ef61 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -154,7 +154,9 @@ fn token_new_fungible_definition() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::token_definition()), @@ -202,7 +204,9 @@ fn token_initialize_account_succeeds_for_canonical_definition() { public_transaction::WitnessSet::for_message(&message, &[&Keys::recipient_key()]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::token_definition()), @@ -275,7 +279,9 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::holder()), @@ -357,7 +363,9 @@ fn token_transfer_fresh_authorized_public_recipient() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::holder()), @@ -405,7 +413,9 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::token_definition()), @@ -454,7 +464,9 @@ 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, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::token_definition()), @@ -575,7 +587,9 @@ fn token_mint_fresh_authorized_public_recipient() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + state + .transition_from_public_transaction(&tx, 0, 0) + .unwrap(); assert_eq!( state.get_account_by_id(Ids::token_definition()), @@ -648,6 +662,7 @@ fn token_program() -> Program { /// Performs a shielded transfer (public → private) of `amount` tokens from /// `Ids::holder()` to a new private account keyed by `PrivateKeys::recipient_*`. /// Returns the resulting private recipient account. +#[cfg(test)] fn shielded_token_transfer(amount: u128, state: &mut V03State) -> Account { let sender_id = Ids::holder(); let sender_account = state.get_account_by_id(sender_id); diff --git a/rust-toolchain.yml b/rust-toolchain.toml similarity index 100% rename from rust-toolchain.yml rename to rust-toolchain.toml diff --git a/token/Cargo.toml b/token/Cargo.toml index 0620df3..ac644ff 100644 --- a/token/Cargo.toml +++ b/token/Cargo.toml @@ -3,6 +3,9 @@ name = "token_program" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] } token_core = { path = "core" } diff --git a/token/core/Cargo.toml b/token/core/Cargo.toml index 34aaf17..d5d5f3d 100644 --- a/token/core/Cargo.toml +++ b/token/core/Cargo.toml @@ -3,6 +3,9 @@ name = "token_core" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] } spel-framework-macros = { git = "https://github.com/logos-co/spel.git", tag = "v0.3.0", package = "spel-framework-macros" } diff --git a/token/methods/Cargo.toml b/token/methods/Cargo.toml index a4897fb..e7007e2 100644 --- a/token/methods/Cargo.toml +++ b/token/methods/Cargo.toml @@ -3,6 +3,9 @@ name = "token-methods" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [build-dependencies] risc0-build = "=3.0.5" diff --git a/token/methods/guest/Cargo.toml b/token/methods/guest/Cargo.toml index f09f6ae..7cd8567 100644 --- a/token/methods/guest/Cargo.toml +++ b/token/methods/guest/Cargo.toml @@ -5,6 +5,48 @@ edition = "2021" [workspace] +[lints.rust] +rust_2018_idioms = { level = "deny", priority = -1 } +# deny (not forbid) so a targeted per-item #[allow] remains possible if ever needed +unsafe_code = "deny" + +[lints.clippy] +# Deny only the groups where a new lint should always be a hard error. +# style/pedantic lints default to warn so toolchain upgrades don't break the +# build unexpectedly — they can be evaluated and addressed at our own pace. +correctness = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +style = { level = "warn", priority = -1 } + +# Generated-code / placeholder blockers. +dbg_macro = "deny" +todo = "deny" +unimplemented = "deny" +unwrap_used = "deny" + +# Lint suppression hygiene. +allow_attributes = "warn" +allow_attributes_without_reason = "deny" + +# Determinism, panic-safety, and arithmetic correctness. +arithmetic_side_effects = "deny" +indexing_slicing = "deny" + +# Cast discipline. +as_conversions = "deny" +cast_possible_truncation = "deny" +cast_possible_wrap = "deny" +cast_sign_loss = "deny" + +# API and enum evolution. +large_enum_variant = "deny" +wildcard_enum_match_arm = "deny" + +# Too noisy for this codebase unless enforced selectively. +module_name_repetitions = "allow" +similar_names = "allow" + [[bin]] name = "token" path = "src/bin/token.rs" diff --git a/token/methods/guest/src/bin/token.rs b/token/methods/guest/src/bin/token.rs index 924b1b7..e3955fc 100644 --- a/token/methods/guest/src/bin/token.rs +++ b/token/methods/guest/src/bin/token.rs @@ -1,14 +1,18 @@ -#![no_main] +#![cfg_attr(not(test), no_main)] use spel_framework::prelude::*; use spel_framework::context::ProgramContext; use nssa_core::account::AccountWithMetadata; +#[cfg(not(test))] risc0_zkvm::guest::entry!(main); #[lez_program(instruction = "token_core::Instruction")] mod token { - #[allow(unused_imports)] + #[expect( + unused_imports, + reason = "SPEL instruction macro requires importing parent-scope handler types" + )] use super::*; /// Transfer tokens from sender to recipient. @@ -48,6 +52,10 @@ mod token { /// Create a new fungible or non-fungible token definition with metadata. /// Definition, holding, and metadata targets must be uninitialized and authorized. + #[expect( + clippy::boxed_local, + reason = "boxed metadata keeps the instruction argument size bounded on the stack" + )] #[instruction] pub fn new_definition_with_metadata( definition_target_account: AccountWithMetadata, diff --git a/token/src/print_nft.rs b/token/src/print_nft.rs index 837fe48..c45e6ad 100644 --- a/token/src/print_nft.rs +++ b/token/src/print_nft.rs @@ -40,7 +40,9 @@ pub fn print_nft( *print_balance > 1, "Insufficient balance to print another NFT copy" ); - *print_balance -= 1; + *print_balance = print_balance + .checked_sub(1) + .expect("print balance must be greater than one after validation"); let mut master_account_post = master_account.account; master_account_post.data = Data::from(&master_account_data); diff --git a/token/src/tests.rs b/token/src/tests.rs index 3c5494c..3b09f7e 100644 --- a/token/src/tests.rs +++ b/token/src/tests.rs @@ -1,4 +1,8 @@ #![cfg(test)] +#![expect( + clippy::arithmetic_side_effects, + reason = "test fixtures use fixed values to lock boundary behavior" +)] use nssa_core::{ account::{Account, AccountId, AccountWithMetadata, Data, Nonce}, @@ -655,7 +659,8 @@ fn test_new_definition_with_valid_inputs_succeeds() { BalanceForTests::init_supply(), ); - let [definition_account, holding_account] = post_states.try_into().unwrap(); + let [definition_account, holding_account] = + post_states.try_into().unwrap(); assert_eq!( *definition_account.account(), AccountForTests::definition_account_unclaimed().account @@ -778,7 +783,8 @@ 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, TOKEN_PROGRAM_ID); - let [definition_post, holding_post] = post_states.try_into().unwrap(); + let [definition_post, holding_post] = + post_states.try_into().unwrap(); assert_eq!( *definition_post.account(), @@ -1063,7 +1069,8 @@ fn test_new_definition_with_metadata_success() { new_definition, metadata, ); - let [definition_post, holding_post, metadata_post] = post_states.try_into().unwrap(); + 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)); @@ -1296,7 +1303,8 @@ fn test_print_nft_success() { 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(); + let [post_master_nft, post_printed] = + post_states.try_into().unwrap(); assert_eq!( *post_master_nft.account(), diff --git a/tools/idl-gen/Cargo.toml b/tools/idl-gen/Cargo.toml index dc7f289..ddcdc13 100644 --- a/tools/idl-gen/Cargo.toml +++ b/tools/idl-gen/Cargo.toml @@ -3,6 +3,9 @@ name = "idl-gen" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [[bin]] name = "idl-gen" path = "src/main.rs" diff --git a/tools/idl-gen/src/main.rs b/tools/idl-gen/src/main.rs index 0d508c4..bfedda7 100644 --- a/tools/idl-gen/src/main.rs +++ b/tools/idl-gen/src/main.rs @@ -16,7 +16,13 @@ fn main() { let dep_dirs = find_path_dep_dirs(&path); match spel_framework_core::idl_gen::generate_idl_from_file_with_deps(&path, &dep_dirs) { - Ok(idl) => println!("{}", serde_json::to_string_pretty(&idl).unwrap()), + Ok(idl) => match serde_json::to_string_pretty(&idl) { + Ok(json) => println!("{json}"), + Err(e) => { + eprintln!("Error serializing IDL to JSON: {e}"); + process::exit(1); + } + }, Err(e) => { eprintln!("Error: {e}"); process::exit(1);