fix(platform)!: 3.0 audit report fixes#3053
Conversation
📝 WalkthroughWalkthroughAdds WithdrawalBelowMinAmountError and bounds checks for withdrawals, replaces Fibonacci utility with a non-zero variant, snapshots input/output addresses for fee deduction, caps P2SH signatures, applies overflow-checked arithmetic in transformers, adds input contribution tracking for asset-lock funding, and inserts extensive security-audit tests (some duplicated). Changes
Sequence Diagram(s)(omitted) Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/address_credit_withdrawal/tests.rs (1)
3924-3951:⚠️ Potential issue | 🟡 MinorDerive the large balance from
max_withdrawalto avoid future test brittleness.
If the limit ever increases beyond 600 Dash, the test may fail for the wrong reason.🔧 Suggested tweak
- // Test withdrawal at the maximum allowed amount (500 Dash) + // Test withdrawal at the maximum allowed amount let platform_version = PlatformVersion::latest(); @@ - // Balance large enough to cover max withdrawal + fees - let large_balance = dash_to_credits!(600.0); + let max_withdrawal = platform_version.system_limits.max_withdrawal_amount; + // Balance large enough to cover max withdrawal + fees + let large_balance = max_withdrawal + dash_to_credits!(100.0); setup_address_with_balance(&mut platform, input_address, 0, large_balance); @@ - // Withdraw the max allowed amount (500 Dash = 50_000_000_000_000 credits) - let max_withdrawal = platform_version.system_limits.max_withdrawal_amount; + // Withdraw the max allowed amount inputs.insert(input_address, (1 as AddressNonce, max_withdrawal));
🤖 Fix all issues with AI agents
In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/address_funding_from_asset_lock/tests.rs`:
- Around line 9334-9347: The test creates two outputs summing to > u64::MAX but
omits a remainder output so basic structure validation fails before overflow is
checked; update the test in tests.rs that constructs outputs and calls
create_raw_transition_with_dummy_witnesses to include an explicit remainder
output (e.g., add a third entry in the outputs BTreeMap with Some(value)
representing the remainder) so that the transition passes structure checks and
the arithmetic overflow path in AddressFundsFeeStrategy::from(...) (and related
validation) is exercised; ensure the remainder output is placed alongside the
existing output_addr_1 and output_addr_2 and adjust the
AddressFundsFeeStrategy/fee steps only if needed to still trigger the overflow
failure.
- Around line 9095-9114: The test currently constructs outputs without a
remainder output so structure validation fails before exercising the
remainder-removal path; update the call to
create_signed_address_funding_from_asset_lock_transition to include an explicit
remainder output (insert a None entry in the outputs BTreeMap at the remainder
index) and change the fee strategy step to target that remainder index (use
AddressFundsFeeStrategyStep::ReduceOutput with the remainder's index) so the
test actually exercises the remainder-removal scenario. Ensure you modify the
local outputs map used in the
create_signed_address_funding_from_asset_lock_transition call and keep the fee
strategy vector consistent with the remainder index.
In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/address_funds_transfer/tests.rs`:
- Around line 8298-8306: The new test module currently declared as "mod
security" (the SECURITY AUDIT TESTS block) duplicates an existing module named
"security" and causes a compile error; rename this new module to a unique
identifier (e.g., "mod security_audit" or "mod security_tests") or merge its
contents into the earlier "security" module, and update any internal references
accordingly so the module name no longer conflicts.
In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_create_from_addresses/tests.rs`:
- Around line 11323-11328: The SECURITY AUDIT TESTS module comment currently
says the tests are expected to FAIL until vulnerabilities are fixed, but this PR
claims to fix them; update the module to reflect current intent by either
removing or amending that note and/or mark any intentionally failing tests with
#[ignore] so CI expectations are explicit; locate the failing tests under the
identity_create_from_addresses test module in tests.rs and add #[ignore] to the
specific test functions (or change the comment to indicate which tests remain
intentionally failing) to clarify behavior.
- Around line 11481-11537: The test currently constructs inputs with u64::MAX
values but never funds those addresses so processing can fail with
AddressDoesNotExist instead of an overflow; update the test that builds the
transition (uses create_platform_address,
create_raw_transition_with_dummy_witnesses, AddressFundsFeeStrategy and invokes
platform.process_raw_state_transitions) to seed the input addresses with
sufficient balances in the platform state (via TestPlatformBuilder or
platform.drive.grove transaction setup) so the state transition reaches the
arithmetic path, then change the assertion to check for the specific
overflow/checked-arithmetic rejection (the transformer overflow behavior
referenced in transformer.rs) rather than a generic failure. Ensure the funding
step happens before calling process_raw_state_transitions and that the assertion
inspects processing_result for the overflow-specific error variant.
- Around line 11576-11600: The test uses tiny_balance = min_output + 1 which is
below the identity funding threshold and causes rejection before fee coverage
logic; change the test to compute the platform's min_identity_funding_amount
(platform_version.dpp.state_transitions.identity.min_identity_funding_amount)
and set tiny_balance to at least that value (e.g. min_identity_funding_amount +
1) but still insufficient to cover processing fees so the transition reaches the
fee handling branch; update the call to
create_signed_identity_create_from_addresses_transition and the
inputs/AddressFundsFeeStrategy to use that adjusted tiny_balance so the test
exercises the fee_fully_covered path instead of failing min‑funding early.
🧹 Nitpick comments (6)
packages/rs-dpp/src/address_funds/witness.rs (1)
408-512: Comprehensive bincode test coverage; consider adding serde boundary test.The tests thoroughly cover the bincode deserialization paths with good boundary testing (exact limit succeeds, limit+1 fails). The intentional asymmetry where encoding succeeds but decoding rejects over-limit signatures is correctly verified.
However, the serde
Deserializepath (lines 211-217) is not explicitly tested for limit enforcement. While the existingtest_p2sh_serdetest covers basic round-trip with a small signature count, adding a boundary test for the serde path would ensure parity with the bincode tests.🧪 Optional: Add serde boundary test
#[cfg(feature = "state-transition-serde-conversion")] #[test] fn test_p2sh_serde_rejects_excessive_signatures() { // Manually construct JSON with MAX_P2SH_SIGNATURES + 1 signatures let signatures: Vec<Vec<u8>> = (0..MAX_P2SH_SIGNATURES + 1) .map(|_| vec![0x30, 0x44, 0x02, 0x20]) .collect(); let json = serde_json::json!({ "type": "p2sh", "signatures": signatures, "redeemScript": [0x52, 0xae] }); let result: Result<AddressWitness, _> = serde_json::from_value(json); assert!( result.is_err(), "P2SH witness with {} signatures should be rejected during serde deserialization", MAX_P2SH_SIGNATURES + 1, ); }packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_top_up_from_addresses/tests.rs (1)
3430-3492: Test may not verify the intended overflow behavior.The test
test_transformer_subtraction_uses_checked_arithmeticcreates addresses usingcreate_platform_addressbut never callssetup_address_with_balanceto register them on the platform. The transition will likely be rejected withAddressDoesNotExistErrorduring state validation rather than an overflow error during structure validation.While the broad assertion
!matches!(...SuccessfulExecution...)will pass, it doesn't verify that the overflow path is actually being tested. Consider either:
- Setting up the addresses with balances matching the inputs to allow the transition to reach overflow validation
- Adding a more specific assertion that checks for
OverflowError♻️ Suggested improvement to test the actual overflow path
If the intent is to verify overflow detection in structure validation, the addresses need to exist:
// Two inputs that sum to > u64::MAX + let mut signer = TestAddressSigner::new(); + let addr1 = signer.add_p2pkh([1u8; 32]); + let addr2 = signer.add_p2pkh([2u8; 32]); + // Store balances at max storable value (i64::MAX fits in sum tree) + setup_address_with_balance(&mut platform, addr1, 0, i64::MAX as u64); + setup_address_with_balance(&mut platform, addr2, 0, i64::MAX as u64); + let mut inputs = BTreeMap::new(); - inputs.insert(create_platform_address(1), (0 as AddressNonce, u64::MAX)); - inputs.insert(create_platform_address(2), (0 as AddressNonce, u64::MAX)); + inputs.insert(addr1, (1 as AddressNonce, u64::MAX)); + inputs.insert(addr2, (1 as AddressNonce, u64::MAX));Then update the assertion to be more specific:
- assert!( - !matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ), + // Expect either OverflowError (structure) or AddressNotEnoughFundsError (state) + // depending on validation order + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError(_)]packages/rs-dpp/src/util/is_non_zero_fibonacci_number.rs (1)
42-51: Consider updating the test documentation comment.The comment describes the previous floating-point implementation ("casts u64 to f64 and checks
sqrt().fract() == 0.0") that was vulnerable to precision issues. Since the implementation now uses integer Newton's method, this documentation could confuse future readers into thinking the current code has this vulnerability.Consider updating to clarify this documents the fixed behavior and why the test should pass.
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/address_funding_from_asset_lock/tests.rs (1)
9276-9292: Use the actual fee result instead ofmax_reasonable_fee.The fixed 0.01‑Dash threshold can go stale if fee schedules change. Use the execution
fee_result.total_base_fee()to assert exact conservation. Apply the same pattern in both conservation tests.♻️ Example (M5 test) using fee_result
- assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }], - "Pure asset lock funding should succeed" - ); + let fee_result = assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { fee_result, .. }] => fee_result.clone(), + "Pure asset lock funding should succeed" + ); @@ - let max_reasonable_fee = dash_to_credits!(0.01); - - assert!( - total_in_addresses >= asset_lock_credits - max_reasonable_fee, - "AUDIT M5: Credits not conserved. Asset lock provided {} credits, \ - but output addresses only received {} credits total. \ - Expected at least {} (asset_lock - max_fee {}). \ - There is no explicit conservation-of-credits assertion in the processing code.", - asset_lock_credits, - total_in_addresses, - asset_lock_credits - max_reasonable_fee, - max_reasonable_fee, - ); - - assert!( - total_in_addresses <= asset_lock_credits, - "AUDIT M5: Output total {} exceeds asset lock credits {}. \ - Credits were created from nothing!", - total_in_addresses, - asset_lock_credits, - ); + let total_fee = fee_result.total_base_fee(); + assert_eq!( + total_in_addresses + total_fee, + asset_lock_credits, + "Credits should conserve: outputs + fees must equal asset lock credits" + );Also applies to: 9611-9624
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_create_from_addresses/tests.rs (2)
8842-8848: Consider asserting the overflow guard here.The test sets a near‑max balance but never executes the transition, so regressions in overflow handling wouldn’t be caught. Suggest running the transition and asserting the expected overflow/consensus error (or mark as TODO/ignored).
11377-11385: Make the fee‑drain condition explicit.This assumes fees exceed 1000 credits so the first input is fully drained; if fees drop below that, the test won’t exercise the index‑shift bug. Consider computing the fee (or asserting the first entry is removed) before checking
second_final.
...ution/validation/state_transition/state_transitions/address_funding_from_asset_lock/tests.rs
Show resolved
Hide resolved
...ution/validation/state_transition/state_transitions/address_funding_from_asset_lock/tests.rs
Outdated
Show resolved
Hide resolved
.../src/execution/validation/state_transition/state_transitions/address_funds_transfer/tests.rs
Outdated
Show resolved
Hide resolved
...cution/validation/state_transition/state_transitions/identity_create_from_addresses/tests.rs
Outdated
Show resolved
Hide resolved
...cution/validation/state_transition/state_transitions/identity_create_from_addresses/tests.rs
Show resolved
Hide resolved
...cution/validation/state_transition/state_transitions/identity_create_from_addresses/tests.rs
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_create_from_addresses/tests.rs`:
- Around line 11371-11454: The test must assert that the first input was
actually drained/removed so the index-shift scenario is exercised: after
committing the transaction (after
platform.drive.grove.commit_transaction(...).expect("should commit")), fetch the
balance/nonce for the address identified by variable first using
fetch_balance_and_nonce and assert that the balance is zero or the address is
absent, ensuring first_input was fully consumed before proceeding to check the
second address; update the test around variables first, first_balance,
first_input, fetch_balance_and_nonce, and the existing assertions so the check
runs immediately before computing second_remaining_before_fee and the subsequent
assertion about the second_final balance.
🧹 Nitpick comments (2)
packages/rs-dpp/src/util/is_non_zero_fibonacci_number.rs (1)
42-51: Test comments describe old floating-point implementation in present tense.The comments at lines 44-46 state that
is_perfect_square"casts u64 to f64 and checkssqrt().fract() == 0.0", but the actual implementation now uses integer Newton's method. This could confuse future maintainers.Consider updating the comments to past tense to clarify these describe the previous implementation that was fixed:
📝 Suggested documentation update
/// AUDIT M6: Floating-point imprecision in is_perfect_square for large values. /// - /// `is_perfect_square` casts u64 to f64 and checks `sqrt().fract() == 0.0`. - /// f64 has only 52 bits of mantissa, so for values above 2^52, precision is lost. - /// This means `is_perfect_square` can return incorrect results for large numbers. + /// The previous `is_perfect_square` implementation cast u64 to f64 and checked + /// `sqrt().fract() == 0.0`. f64 has only 52 bits of mantissa, so for values + /// above 2^52, precision was lost, causing incorrect results for large numbers. + /// This was fixed by replacing the floating-point approach with integer Newton's method. /// - /// Currently not exploitable for `core_fee_per_byte` (u32), but the function - /// accepts u64 and is technically unsound for large inputs. - /// - /// Location: rs-dpp/src/util/is_non_zero_fibonacci_number.rs:1-3 + /// This test verifies the integer implementation doesn't have false positives.packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/address_credit_withdrawal/tests.rs (1)
3923-3988: Derivelarge_balancefrommax_withdrawal_amountto keep the test future-proof.
large_balanceis hard-coded to 600 DASH while the withdrawal amount is sourced fromplatform_version.system_limits.max_withdrawal_amount. If the max changes, this test can fail due to insufficient balance even though the intent is to validate the max withdrawal boundary. Consider tying the balance to the max plus a small fee buffer.Suggested update
- // Balance large enough to cover max withdrawal + fees - let large_balance = dash_to_credits!(600.0); + let max_withdrawal = platform_version.system_limits.max_withdrawal_amount; + // Balance large enough to cover max withdrawal + fees + let large_balance = max_withdrawal.saturating_add(dash_to_credits!(10.0)); setup_address_with_balance(&mut platform, input_address, 0, large_balance); @@ - // Withdraw the max allowed amount (500 Dash = 50_000_000_000_000 credits) - let max_withdrawal = platform_version.system_limits.max_withdrawal_amount; + // Withdraw the max allowed amount inputs.insert(input_address, (1 as AddressNonce, max_withdrawal));
Issue being fixed or feature implemented
Internal security review of address-based state transitions identified several correctness and robustness issues in the new address funds system (transfers, withdrawals,
asset lock funding, identity creation/top-up from addresses, credit transfers to addresses). This PR fixes the confirmed issues and adds defensive checks where the code
was technically correct but lacked defense-in-depth.
What was done?
Bug Fixes
Fee strategy index shifting after BTreeMap mutation — The fee deduction function used positional .nth() lookups on BTreeMaps that were being mutated during iteration.
When an entry was drained to zero and removed, subsequent indices shifted, causing fee steps to target wrong addresses. Fixed by snapshotting BTreeMap keys into Vecs
before the loop and resolving indices via the stable snapshots.
Credits conservation violation in asset lock with address inputs — AddressFundingFromAssetLock remainder calculation only considered the asset lock amount, ignoring
credits contributed from address inputs. This caused incorrect remainder outputs. Fixed by threading input_contributions_total through the action struct and including it
in the available balance.
Witness validation bypass via zip() — validate_witnesses() paired inputs with witnesses using zip(), which silently stops at the shorter iterator. With 0 witnesses on 2
inputs, validation passed with zero iterations. Fixed by adding a length equality check before the zip loop.
Missing minimum/maximum withdrawal amount validation — Address credit withdrawals had no check that the withdrawal amount met MIN_WITHDRAWAL_AMOUNT or stayed under
max_withdrawal_amount. Added a new WithdrawalBelowMinAmountError consensus error type and validation check.
Floating-point imprecision in is_fibonacci_number — is_perfect_square used (n as f64).sqrt() which loses precision above 2^52, producing false positives. Replaced with
integer Newton's method. Also renamed to is_non_zero_fibonacci_number since core_fee_per_byte = 0 is not a valid fee rate despite 0 being mathematically Fibonacci.
Defensive Hardening
Overflow guard for u64→i64 cast in Drive sum trees — set_balance_to_address cast Credits (u64) to SumValue (i64) without checking for overflow. While upstream validation
prevents balances above i64::MAX, Drive now returns ProtocolError::Overflow defensively.
Maximum P2SH signatures count during deserialization — AddressWitness::P2sh accepted arbitrarily large signature vectors. Added MAX_P2SH_SIGNATURES = 17 (16 keys from
OP_PUSHNUM_16 + 1 CHECKMULTISIG dummy) enforced in all three deserialization paths (bincode Decode, BorrowDecode, serde Deserialize).
Checked arithmetic in transformers
Multiple transformers for AddressCreditWithdrawal, IdentityCreateFromAddresses, and IdentityTopUpFromAddresses were updated to use checked_sub, checked_add, and try_fold
instead of unchecked arithmetic for input/output sum calculations and remainder computation.
How Has This Been Tested?
Comprehensive test coverage across all 6 address-based state transitions:
address_funding_from_asset_lock, identity_create_from_addresses, and identity_top_up_from_addresses
Breaking Changes
Checklist:
For repository code-owners and collaborators only
Summary by CodeRabbit
Bug Fixes
New Features
Tests