fix(msadm): harden multisig_admin_override_refund arithmetic - #418
Merged
godamongstmen897 merged 49 commits intoSep 1, 2026
Conversation
…tests (Goldii-locks#293) Expand CancelEscrowInitiatedEvent with full operational context so downstream indexers can reconstruct the complete cancellation state from the event payload alone, without querying contract storage. Production changes (lib.rs): - Expand CancelEscrowInitiatedEvent with six new fields: caller_is_client bool — true if initiator is the client, false if freelancer client Address — registered client address freelancer Address — registered freelancer address token Address — escrow token contract address milestone_count u32 — number of milestones at cancellation time total_amount i128 — aggregate milestone total (pre-release) - Update cancel_escrow event emission to populate all new fields, deriving caller_is_client from (caller == meta.client) No other code changed. Tests added (test.rs) — 14 new tests (396 -> 410): Event count: - test_cancel_escrow_event_emitted_exactly_once contract_id field: - test_cancel_escrow_event_contract_id_correct caller field: - test_cancel_escrow_event_caller_is_client_address - test_cancel_escrow_event_caller_is_freelancer_address caller_is_client role field: - test_cancel_escrow_event_caller_is_client_true_for_client - test_cancel_escrow_event_caller_is_client_false_for_freelancer client / freelancer / token fields: - test_cancel_escrow_event_client_field_correct - test_cancel_escrow_event_freelancer_field_correct - test_cancel_escrow_event_token_field_correct milestone_count field: - test_cancel_escrow_event_milestone_count_single - test_cancel_escrow_event_milestone_count_multiple total_amount field: - test_cancel_escrow_event_total_amount_correct_single_milestone - test_cancel_escrow_event_total_amount_correct_multi_milestone Full indexer round-trip: - test_cancel_escrow_event_full_indexer_parse All 410 tests pass.
Drop the redundant MilestoneReleased(index) temporary-flag write from multisig_admin_override_release. The flag's only reader (is_milestone_released_flag) is dead code, and the persisted Released status on the milestone is the authoritative completion signal. This reduces the distinct storage keys written by the call from three (Milestone, MilestoneReleased, MultisigLocked) to two (Milestone, MultisigLocked), matching multisig_admin_override_refund. Adds test_multisig_admin_override_release_reduced_storage_footprint to assert the temporary flag is no longer written while token transfer, lock clearing, milestone state, and terminal re-entry rejection still hold. Updates the affected snapshot fixtures accordingly. Closes Goldii-locks#392
Guard the refund arithmetic so no input can cause a wrap or an unhandled panic (issue Goldii-locks#395). Negative amount / released_amount and released_amount > amount now return Error::InvalidAmount before any arithmetic runs, complementing the existing checked_sub and remaining<=0 guards. The same guards are applied to the sibling multisig_admin_override_release for consistency. Adds a comprehensive suite to multisig_admin_override_refund_tests asserting i128::MAX / i128::MIN operands return Error::InvalidAmount (rather than panicking) and that valid amounts refund exactly amount - released_amount, identical to prior behavior. Closes Goldii-locks#395
|
@GreatShinro Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
…_approve (closes Goldii-locks#354) Reorder multisig_approve so the signer-membership check runs before any job/token ledger reads (job meta load, cross-contract balance call), matching the guard ordering used elsewhere in the hardening series. Add dedicated tests asserting unauthorized and empty-balance rejections leave the proposal's approval bitmap unmutated.
…footprint and add checked ops in cancel_refund Issue Goldii-locks#383 - admin_override_cancel_release storage footprint - Remove the redundant store_milestone_released() temporary-storage write from the milestone loop inside admin_override_cancel_release. - The temporary MilestoneReleased(index) flag is a hot-read optimisation for the approve_milestone code path; it is not needed in the admin cancel-override path because the persistent Milestone entry already carries status=Released. - This reduces distinct ledger keys written per call by N (one per updated milestone), lowering both compute cost and storage rent burden. Issue Goldii-locks#386 - admin_override_cancel_refund checked arithmetic - Add explicit non-negativity guards on milestone.amount and milestone.released_amount before the checked_sub call. - A malformed on-chain entry with a negative field (e.g. i128::MIN) previously could pass through the remaining > 0 filter with a nonsensical value; now it is caught early and returns Error::InvalidAmount instead of causing a wrap or silent corruption. - All arithmetic (checked_sub, checked_add) already existed; the guard block tightens the contract so every exit path is provably safe. Tests (admin_override_cancel_tests.rs) - Happy-path release: verifies tokens transferred, milestones Released, CancelLock cleared, and MilestoneReleased temporary flag absent (Goldii-locks#383). - Skip-terminal release: milestones already Released are skipped and no temporary flag is written for the remaining ones (Goldii-locks#383). - Happy-path refund: verifies tokens transferred, milestones Refunded, CancelLock cleared, and YieldAccrued reset (Goldii-locks#386). - Skip-terminal refund: terminal milestones excluded from total (Goldii-locks#386). - All-terminal refund: returns Error::InvalidAmount, not panic (Goldii-locks#386). - Unauthorized / InvalidStatus guards verified for both functions. - Minimum-amount (1 stroop) and multi-milestone sum tests (Goldii-locks#386).
…6-admin-override-storage-checked-ops fix(Goldii-locks#383,Goldii-locks#386): reduce cancel_release ledger footprint and add checked ops in cancel_refund
- annotate test modules with #[cfg(test)] so the wasm build succeeds without dev-dependencies - make setup_funded_escrow pub(crate) and fix test imports so sibling test modules can use it (Address::generate requires testutils trait) - remove redundant admin.require_auth() before require_admin in admin_override_cancel_refund; require_admin already performs the signature check, and env-host 22.1.3 rejects the double auth with Error(Auth, ExistingValue) - fund a terminal-state cancel test through the zero-balance boundary guard in cancel_escrow so its invalid-amount assertion stays intact - add missing admin_override_cancel_tests snapshot files (untracked, would otherwise fail CI on a fresh checkout)
…lance - add Error::EmptyBalance (=32) and assert_nonzero_balance helper - reject emergency_pause_claim_refund while the contract token balance is zero, so an emergency settlement never attempts an empty transfer - fund the initialized-escrow fixture mint via its own token id so the existing claim_refund split-math tests keep passing - add a test asserting EmptyBalance is returned for a paused but unfunded escrow
- annotate test modules with #[cfg(test)] so the wasm build succeeds without dev-dependencies - make setup_funded_escrow pub(crate) and fix test imports so sibling test modules can use it (Address::generate requires testutils trait) - remove redundant admin.require_auth() before require_admin in admin_override_cancel_refund; require_admin already performs the signature check, and env-host 22.1.3 rejects the double auth with Error(Auth, ExistingValue) - fund a terminal-state cancel test through the zero-balance boundary guard in cancel_escrow so its invalid-amount assertion stays intact - add missing admin_override_cancel_tests snapshot files (untracked, would otherwise fail CI on a fresh checkout)
…e on-ledger footprint Rename the emergency-pause instance storage keys to shorter symbols: EmergencyPaused -> Ep (2 chars vs 16) EmergencyPauseLock -> EpLk (4 chars vs 19) Applied consistently across the milestone-escrow contract and the reports copy, and updated the affected test snapshots so the on-ledger symbol assertions match the shorter keys. This is an isolated, self-contained change (Closes Goldii-locks#323).
…ocks#309) Add structured event types and emission for all platform fee allocation functions: set_platform_fee_allocation, lock_platform_fee_allocation, pf_alloc_admin_override, and calculate_platform_fee_split. New event types: - PlatformFeeAllocationSetEvent (topic: pf_set) - PlatformFeeAllocationLockedEvent (topic: pf_lock) - PlatformFeeAllocationOverrideEvent (topic: pf_ovr) - PlatformFeeSplitCalculatedEvent (topic: pf_split) These events enable downstream indexers to track platform fee configuration changes, lock state transitions, admin overrides, and fee split calculations without polling contract storage. Comprehensive test coverage for all 4 event types including: - Happy path event emission verification - Event field validation - Failed operation event suppression - Rounding/conservation verification Co-Authored-By: Codebuff <noreply@codebuff.com>
…x_withholding_deductions (closes Goldii-locks#335)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
fix: repair milestone-escrow CI build and tests
…y-pause feat(Goldii-locks#323): optimize emergency_pause storage keys footprint
perf(cancel): compact escrow lock storage key
Fix: reduce ledger storage footprint of admin_override_cancel_refund
…hecked-math test: add i128::MIN boundary test for escrow_interest_yield
…erride-release-storage-footprint fix(msadm): reduce multisig_admin_override_release storage footprint
…duce-yield-rate-footprint refactor(storage): consolidate admin_set_yield_rate ledger footprint
…grade-authorization-preconditions refactor(upgrade): enforce auth and state preconditions before WASM upgrade (Goldii-locks#352)
…rden-multisig-approve-guards refactor(multisig): harden auth and precondition guards in multisig_approve (Goldii-locks#354)
feat(Goldii-locks#321): guard emergency_pause_claim_refund on zero balance
…ow-structured-events feat(cancel_escrow): enrich structured event and add indexer parsing …
…79-a0 fix: Harden caller authorization and precondition guards in admin_override_release
…ic-multisig-split-refund Goldii-locks#402 - test(multisig_split_refund): add extreme-value boundary tests for checked arithmetic
…ents_during_platform_fee_allocation feat: emit structured events during platform_fee_allocation (Goldii-locks#309)
…335-admin-tax-withholding-guards feat: harden caller authorization and precondition guards in admin_tax_withholding_deductions (closes Goldii-locks#335)
The arithmetic hardening is sound and merges cleanly. One of the new tests failed: refund_valid_amount_equals_amount_minus_released_amount assertion `left == right` failed: left: 0, right: 1 The event is published -- multisig_admin_override_refund emits msadmref on the success path, and the state and balance assertions above it all passed, so the call had run to completion. The tally was just read too late. env.events().all() reflects the most recent contract invocation, and the test made three more (is_multisig_locked, get_job, token.balance) between the override call and the count. The sibling helper in multisig_split_refund_tests.rs uses the identical idiom and passes because it reads the events first. Moved the event assertion directly after the override call; the state and balance assertions follow unchanged. No production code touched. 518 tests passing / WASM release build OK
godamongstmen897
added a commit
to esthertitilayo-dev/escrow-contract
that referenced
this pull request
Sep 1, 2026
The conflict in test.rs was two branches each appending a module declaration at the same spot; both are needed, so both are declared. execute_transfer_swaps_admin_and_emits_event then failed on the event tally (left: 0, right: 1). The event is emitted -- the admin key swap and the pending-transfer removal above it both asserted correctly -- but the count was read after client.get_pending_admin_transfer(), and env.events().all() reflects the most recent contract invocation. Moved the two event assertions directly after execute_admin_transfer; the state assertions follow, unchanged. Same fix as on Goldii-locks#418, which hit this in multisig_admin_override_refund_tests. No production code touched. 543 tests passing / WASM release build OK
godamongstmen897
added a commit
to qa-eden/escrow-contract
that referenced
this pull request
Sep 1, 2026
Dropping the EmergencyPauseLock dance from emergency_pause_admin_override is correct and is the point of Goldii-locks#399: the lock exists to close a reentrancy window around external calls, and this path makes none -- it reads the flag, compares, writes it back, and emits an event. EpLk is still taken by the paths that do call out. Three names had to be corrected against the enum as it actually exists: - DataKey::EmergencyPaused -> DataKey::Ep in lib.rs - DataKey::EmergencyPauseLock -> DataKey::EpLk in the new test suite One test then failed on the event tally. It calls the override twice and expects the count to go 1 then 2, but env.events().all() reports the most recent contract invocation rather than a running total, so the second call reports 1. Adjusted to assert the second call emits exactly one event, with the existing payload check confirming it is the new one. This is the same env behaviour that Goldii-locks#418 and Goldii-locks#438 ran into. 571 tests passing / WASM release build OK
godamongstmen897
added a commit
to esthertitilayo-dev/escrow-contract
that referenced
this pull request
Sep 1, 2026
The branch's own copy of admin_pause_escrow does not parse. A line from
the previous version was left dangling after the new writes:
.set(&DataKey::EmergencyPauseLock, &true);
env.storage().instance().set(&DataKey::Paused, &true);
.set(&DataKey::EpLk, &true); <- orphaned continuation
That produced 90 errors, all downstream of "expected expression, found
`.`". DataKey::EmergencyPauseLock is also not a variant -- the enum
calls it EpLk -- in lib.rs and twice in the new test suite.
Rebuilt the function around the two guards this PR is actually for:
assert_emergency_pause_not_locked before any write, and an early return
when the escrow is already paused so a redundant call mutates nothing.
Because that early return now handles the repeat case, the inner
`if !already_paused` that main used to gate the event is redundant, and
the publish is unconditional inside the lock.
Two of the new tests then failed on event tallies. They read
pause_event_count after is_paused / is_lock_held, and those helpers go
through env.as_contract -- env.events().all() reports the most recent
invocation, not a running total. Reordered so the tally is read first.
The idempotency case now asserts the second call emits nothing at all,
which is what "no-op" means here. Same env behaviour as Goldii-locks#418, Goldii-locks#428
and Goldii-locks#438.
576 tests passing / WASM release build OK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hardens the arithmetic in
multisig_admin_override_refundso no input can cause a wrap or an unhandled panic, closing #395.Why
The remaining-balance arithmetic reads signed
i128amount/released_amountvalues from storage. While the code already usedchecked_sub, pathological operands (e.g.i128::MINamounts or negativereleased_amount) were not guarded explicitly. This change makes the arithmetic exhaustively checked and returns the typedError::InvalidAmountfor any bad input.Changes (
contracts/milestone-escrow/src/lib.rs)multisig_admin_override_refundnow rejects, before any arithmetic runs:amountorreleased_amount(coversi128::MINoperands) →Error::InvalidAmountreleased_amount > amount(an over-full milestone) →Error::InvalidAmountThese complement the existing
checked_sub(...).ok_or(Error::InvalidAmount)?and theremaining <= 0guard, soi128::MAX/i128::MINoperands returnError::InvalidAmountrather than panicking or wrapping. The same guards are applied to the siblingmultisig_admin_override_releasefor consistency.Tests (
contracts/milestone-escrow/src/multisig_admin_override_refund_tests.rs)Added a comprehensive suite:
refund_negative_amount_returns_invalid_amount_without_panic—amount = i128::MIN→Error::InvalidAmount, no panic, lock intact, no event.refund_negative_released_amount_returns_invalid_amount_without_panic—released_amount = i128::MIN(withamount = i128::MAX) →Error::InvalidAmount.refund_released_exceeds_amount_returns_invalid_amount_without_panic—released_amount == amountandreleased_amount > amount(i128::MAXoperands) →Error::InvalidAmount.refund_valid_amount_equals_amount_minus_released_amount— valid amounts refund exactlyamount - released_amount, identical to prior behavior.The extreme operands are injected directly into persistent storage via
env.as_contract, since the normal flow cannot produce them. All existing tests continue to pass unchanged — the new guards only trigger for inputs the validated flow never produces.Closes #395