contracts: add DivisionByZero error variant, fix checked_div sites - #644
contracts: add DivisionByZero error variant, fix checked_div sites#644cashmotor01 wants to merge 2 commits into
Conversation
checked_div returning None indicates a zero divisor, which in these contracts means a broken adapter reporting degenerate total_assets. Previously every checked_div site mapped None to ContractError::Overflow, making divide-by-zero indistinguishable from a genuine i128 overflow. Add DivisionByZero = 19 to vault::ContractError and DivisionByZero = 3 to blend-adapter::ContractError. Update all checked_div call sites: vault/src/lib.rs - deposit(): shares_to_mint denominator (total_assets + OFFSET) - withdraw(): adapter_shares_to_burn denominator (total_shares) - withdraw(): principal_out denominator (caller_shares) - migrate_adapter(): min_acceptable denominator (10_000) blend-adapter/src/lib.rs - b_tokens_to_usdc(): RATE_SCALAR divisor (shared by accrue + withdraw) Overflow is now reserved exclusively for checked_mul / checked_add failures. New variants are appended; no existing discriminants change, so this is a non-breaking ABI addition.
|
@cashmotor01 is attempting to deploy a commit to the Collins' projects Team on Vercel. A member of the Team first needs to authorize it. |
collinsezedike
left a comment
There was a problem hiding this comment.
CI never actually ran on this PR, same as I've seen on a couple others recently (e.g. #599) — not something on your end as far as I can tell.
The intent (a distinct DivisionByZero error instead of overloading Overflow for a zero divisor) is right, but 4 of the 5 new DivisionByZero branches are unreachable dead code:
blend-adapter/src/lib.rs:44: RATE_SCALAR is the fixed positive constant 1_000_000_000_000, so this checked_div can never see a zero divisor. It can only ever fail via the preceding checked_mul overflow. The PR's own justification (that accrue_returns_typed_error_on_overflow with b_rate = i128::MAX exercises this path) doesn't hold up either: that test's b_rate makes the checked_mul overflow first, so it only proves the Overflow arm fires and never reaches checked_div at all.vault/src/lib.rswithdraw(): the checked_div(total_shares) call is guarded byif total_shares <= 0 { return Err(NoSharesOutstanding) }a few lines above, and the checked_div(caller_shares) call is guarded by thecaller_shares < shares/shares <= 0checks earlier in the same function. Neither divisor can ever be zero.vault/src/lib.rsmigrate_adapter(): checked_div(10_000i128) divides by a hardcoded literal, never zero under any input.
The one call site where this actually matters is deposit()'s checked_div(total_assets + OFFSET) — total_assets comes from the adapter and could plausibly be reported as a degenerate/negative value that zeroes out against OFFSET, so that one's a real fix.
The new doc comments compound this: both describe the trigger as "the adapter reporting degenerate totals" / "a degenerate b_rate," which is only accurate for deposit(). Someone debugging a live DivisionByZero from withdraw() or migrate_adapter() would go looking for an adapter/rate problem that has nothing to do with the actual divisor (total_shares, caller_shares, or the literal 10_000).
Also, apps/docs/architecture/vault-contract.md's error table isn't updated with the new DivisionByZero (19) row.
Worth narrowing this to just the deposit() fix, dropping the other three (which don't fix anything since #570 was never reachable through them), and fixing the doc comment + table for whatever ends up staying.
collinsezedike
left a comment
There was a problem hiding this comment.
This branch is stale against current main (GitHub already flags it CONFLICTING) and both new error codes collide with variants that already exist there.
| /// A `checked_div` call was given a zero divisor. Distinct from | ||
| /// `Overflow`: indicates a degenerate `b_rate` (zero), not a saturating | ||
| /// multiplication. | ||
| DivisionByZero = 3, |
There was a problem hiding this comment.
main already has NotInitialized = 3 in this enum (added after this branch was cut). Adding DivisionByZero = 3 here duplicates that discriminant instead of colliding with it in git, since your diff doesn't even show NotInitialized in this hunk. Please rebase onto current main and pick the next free code (4).
| /// `Overflow`: a zero divisor means the adapter is reporting degenerate | ||
| /// totals (zero or negative `total_assets`), not that an intermediate | ||
| /// multiplication saturated `i128`. | ||
| DivisionByZero = 19, |
There was a problem hiding this comment.
Same issue here: current main already goes up to NoPendingAdmin = 16 (MinAmountOutNotMet = 15 and NoPendingAdmin = 16 were both added after this branch was cut, neither appears in your diff). DivisionByZero = 19 skips over both of them and leaves 17-18 as an unexplained gap. After rebasing, this should be 17.
Closes #570
Summary
Fixes the misdiagnosis reported in the issue:
checked_divreturningNone(divide-by-zero) was mapped toContractError::Overflowin both the vault and blend-adapter contracts, making it indistinguishable from a genuinei128overflow.Changes
New variants
vault::ContractError::DivisionByZero = 19blend_adapter::ContractError::DivisionByZero = 3Overflowis now reserved exclusively forchecked_mul/checked_addfailures.Call sites updated
vault/src/lib.rsdeposit()—shares_to_minttotal_assets + OFFSETvault/src/lib.rswithdraw()—adapter_shares_to_burntotal_sharesvault/src/lib.rswithdraw()—principal_outcaller_sharesvault/src/lib.rsmigrate_adapter()—min_acceptable10_000blend-adapter/src/lib.rsb_tokens_to_usdc()RATE_SCALARBreaking change
None. New variants are appended; no existing discriminants are renumbered. Off-chain code matching on numeric error codes is unaffected.
Testing
Existing unit tests pass unchanged. The
DivisionByZeropath inb_tokens_to_usdcis exercised by the existingaccrue_returns_typed_error_on_overflowtest (which setsb_rate = i128::MAXto saturate the multiply, confirming theOverflowarm still fires correctly and the two variants are now distinct).