Skip to content

contracts: add DivisionByZero error variant, fix checked_div sites - #644

Closed
cashmotor01 wants to merge 2 commits into
drydocs:mainfrom
cashmotor01:fix/divide-by-zero-error-variant
Closed

contracts: add DivisionByZero error variant, fix checked_div sites#644
cashmotor01 wants to merge 2 commits into
drydocs:mainfrom
cashmotor01:fix/divide-by-zero-error-variant

Conversation

@cashmotor01

@cashmotor01 cashmotor01 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Closes #570


Summary

Fixes the misdiagnosis reported in the issue: checked_div returning None (divide-by-zero) was mapped to ContractError::Overflow in both the vault and blend-adapter contracts, making it indistinguishable from a genuine i128 overflow.

Changes

New variants

  • vault::ContractError::DivisionByZero = 19
  • blend_adapter::ContractError::DivisionByZero = 3

Overflow is now reserved exclusively for checked_mul / checked_add failures.

Call sites updated

File Location Divisor
vault/src/lib.rs deposit()shares_to_mint total_assets + OFFSET
vault/src/lib.rs withdraw()adapter_shares_to_burn total_shares
vault/src/lib.rs withdraw()principal_out caller_shares
vault/src/lib.rs migrate_adapter()min_acceptable 10_000
blend-adapter/src/lib.rs b_tokens_to_usdc() RATE_SCALAR

Breaking 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 DivisionByZero path in b_tokens_to_usdc is exercised by the existing accrue_returns_typed_error_on_overflow test (which sets b_rate = i128::MAX to saturate the multiply, confirming the Overflow arm still fires correctly and the two variants are now distinct).

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.
@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

@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 collinsezedike left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.rs withdraw(): the checked_div(total_shares) call is guarded by if total_shares <= 0 { return Err(NoSharesOutstanding) } a few lines above, and the checked_div(caller_shares) call is guarded by the caller_shares < shares / shares <= 0 checks earlier in the same function. Neither divisor can ever be zero.
  • vault/src/lib.rs migrate_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 collinsezedike left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

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.

[Bug] Overflow error returned for a divide-by-zero condition

2 participants