Skip to content

test(canister): pin the deposit/withdraw amount-validation order - #243

Draft
mbjorkqvist wants to merge 3 commits into
mainfrom
mathias/DEFI-2959-pin-validation-order-tests
Draft

test(canister): pin the deposit/withdraw amount-validation order#243
mbjorkqvist wants to merge 3 commits into
mainfrom
mathias/DEFI-2959-pin-validation-order-tests

Conversation

@mbjorkqvist

@mbjorkqvist mbjorkqvist commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Adds regression coverage for the two guarantees #237 established but left untested. Tests only — no production code changes.

  • An out-of-range amount is rejected ahead of every other request-error branch, so no such amount can reach code that might render it. Previously every amount-range test used a known token, so moving the range check back below the known-token check left the suite green.
  • In restricted mode a non-allowlisted caller is turned away by the allowlist check, at a cost independent of the amount's magnitude, rather than by exhausting a message's instruction budget. This was the vulnerability's most severe framing and had no coverage at all, because every integration test ran in general-availability mode.

The second one needs care to be meaningful: the call traps either way — before the fix on the instruction limit, after it on the allowlist panic — so asserting that it traps proves nothing. The test asserts on why it trapped instead, which is what distinguishes a cheap rejection from one that costs a full message's budget.

Both tests were confirmed to fail against the pre-fix behaviour: reverting the reorder makes the first pair report UnsupportedToken, and reintroducing the pre-authorization render makes the second report CanisterInstructionLimitExceeded after 40B instructions.

The behaviour-preserving refactors these tests do not depend on are split out into #242.

mbjorkqvist and others added 2 commits July 31, 2026 12:48
…oken branch

The ordering guarantee — no request-error branch is reachable with an
out-of-range amount — had no coverage: every amount-range test used a known
token, so moving the check back below `is_known_token` kept the suite green.
Add a deposit and a withdraw case combining an unsupported token with an
oversized amount and asserting `AmountExceedsMaximum` wins. Both fail if the
check is moved back down.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… check

The vulnerability's most severe framing was that it worked against a
non-allowlisted caller in restricted mode, since the request was rendered
before any authorization check ran. Nothing covered that path: every
integration test installs the canister in `Mode::GeneralAvailability`.

The call traps either way — pre-fix on the instruction limit, post-fix on the
allowlist panic — so trap-vs-no-trap proves nothing here. Assert instead on
*why* it trapped, which is what makes the rejection cheap rather than costing
a full message's budget. Verified against a reintroduced pre-auth render: the
same call dies with CanisterInstructionLimitExceeded after 40B instructions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds regression tests to lock in the validation/authorization ordering guarantees introduced in #237, ensuring oversized amount inputs are rejected before any error branch that could format/render them, and that restricted-mode authorization rejects non-allowlisted callers before any expensive work can occur.

Changes:

  • Add an integration test that asserts restricted-mode rejection happens for the authorization reason (allowlist), not instruction exhaustion.
  • Add unit tests that pin amount-range validation to occur before the unsupported-token branch for both deposit and withdraw.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
integration_tests/tests/tests.rs Adds restricted-mode integration coverage asserting trap reason corresponds to allowlist rejection (not instruction limit).
canister/src/tests.rs Adds deposit/withdraw unit tests pinning amount validation to win over unsupported-token request errors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread integration_tests/tests/tests.rs Outdated
Comment on lines +1674 to +1677
/// In restricted mode the vulnerable handler rendered the request — exhausting
/// the instruction budget — before any authorization check, so a
/// non-allowlisted caller could trap the call. Now the O(1) allowlist check
/// rejects it first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Correct, and thanks — fixed in fcfd730.

Mode::RestrictedTo holds a BTreeSet<Principal> and assert_caller_is_allowed does allowed.contains(&caller), so the lookup is O(log n) in the allowlist size. The "O(1)" claim was simply wrong.

Reworded to the property the test actually relies on (tests.rs#L1677):

Now the allowlist check rejects it first, at a cost independent of the amount's magnitude.

That is what makes the difference the test asserts: before the fix the rejection cost scaled with the amount and exhausted the message's instruction budget; now it does not scale with the amount at all, whatever it costs in the allowlist size. The PR description carried the same imprecision ("constant-time allowlist check") and has been corrected the same way.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

canbench 🏋 (dir: canister) fa8ebf6 2026-07-31 13:06:42 UTC

canister/canbench_results.yml is up to date
📦 canbench_results_benchmark.csv available in artifacts

---------------------------------------------------

Summary:
  instructions:
    status:   No significant changes 👍
    counts:   [total 16 | regressed 0 | improved 0 | new 0 | unchanged 16]
    change:   [max +42.54M | p75 +1.06M | median +9.50K | p25 +31 | min -42.73K]
    change %: [max +0.19% | p75 +0.11% | median +0.03% | p25 -0.04% | min -0.39%]

  heap_increase:
    status:   No significant changes 👍
    counts:   [total 16 | regressed 0 | improved 0 | new 0 | unchanged 16]
    change:   [max 0 | p75 0 | median 0 | p25 0 | min 0]
    change %: [max 0.00% | p75 0.00% | median 0.00% | p25 0.00% | min 0.00%]

  stable_memory_increase:
    status:   No significant changes 👍
    counts:   [total 16 | regressed 0 | improved 0 | new 0 | unchanged 16]
    change:   [max 0 | p75 0 | median 0 | p25 0 | min 0]
    change %: [max 0.00% | p75 0.00% | median 0.00% | p25 0.00% | min 0.00%]

---------------------------------------------------

Only significant changes:
| status | name                               | calls |    ins |  ins Δ% | HI |  HI Δ% | SMI |  SMI Δ% |
|--------|------------------------------------|-------|--------|---------|----|--------|-----|---------|
|   -    | bench_write_events::AddTradingPair |     1 | 15.92K |  -3.39% |  0 |  0.00% |   0 |   0.00% |

ins = instructions, HI = heap_increase, SMI = stable_memory_increase, Δ% = percent change

---------------------------------------------------
CSV results saved to canbench_results.csv

`Mode::RestrictedTo` holds a `BTreeSet`, so the allowlist membership check is
O(log n) in the allowlist size, not O(1). The property the test actually relies
on is that the check's cost does not depend on the amount's magnitude — say
that instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 31, 2026 13:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants