fix(contracts): cap fee_bps with a MAX_FEE_BPS sanity ceiling - #71
Open
ZuLu0890 wants to merge 2 commits into
Open
fix(contracts): cap fee_bps with a MAX_FEE_BPS sanity ceiling#71ZuLu0890 wants to merge 2 commits into
ZuLu0890 wants to merge 2 commits into
Conversation
|
@ZuLu0890 is attempting to deploy a commit to the chonilius' projects Team on Vercel. A member of the Team first needs to authorize it. |
Add MAX_FEE_BPS = 1000 (10%) to all three contracts and reject any fee_bps above it at initialize with the existing InvalidFee error, closing the full-fee (100%) configuration that silently zeroed every payout (MergeFi#40). Add boundary tests locking in the inclusive ceiling and update the README. Rebasing onto current main also surfaced pre-existing CI breakages that this commit repairs so the workspace is green again: - restore the missing closing brace on milestones::sort_remainders_desc, which left the workspace unable to compile; - advance the multi-sponsor refund test past the new grace-period window so the permissionless path is actually exercised; - normalize trailing whitespace so cargo fmt --check passes. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
ZuLu0890
force-pushed
the
fix/fee-bps-ceiling
branch
from
August 18, 2026 19:53
181cb87 to
8c8a9e9
Compare
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.
Closes #40
Summary
All three contracts validate
fee_bpsatinitializewith the same guard:The comparison is strictly
>, not>=, sofee_bps == 10_000— exactly100% — is accepted rather than rejected. Because
fee_bpsis immutableafter
initialize(tracked separately in #20), whatever value passes thischeck governs every payout for the contract's entire lifetime. A 100% fee is
mathematically "valid" (no overflow, no panic) but fully predatory: it
silently zeroes every recipient payout while routing the entire escrow /
allocation / pool balance to the treasury.
This PR adds a
MAX_FEE_BPSsanity ceiling (1000 bps = 10%) and rejectsanything above it with the existing
InvalidFeeerror.Severity / impact
This is a silent-loss, not a visible failure. The exploit produces no error,
no revert, and no panic — just a legitimate-looking
release/release_issue/
withdrawcall that pays nothing to the people who did the work andeverything to the treasury:
compute_split, byte-identical in both): atfee_bps = 10_000,fee = total * 10000 / 10000 = total, sodistributable = total - fee = 0. Every recipient'sshareis0, thelargest-remainder dust loop has nothing to distribute
(
dust = distributable - allocated = 0 - 0 = 0), and theif share > 0guard silently skips every transfer.
withdraw):payout = amount - fee = 0, so themaintainer receives nothing.
Whoever controls
initialize(see #33's front-running analysis for who thatcould be) can set this on day one, before any sponsor ever deposits.
Why this is a "very hard"-caliber finding, not a nit
There was no lower sanity ceiling between "some reasonable treasury fee" and
"the mathematical maximum". The previous check only guarded against arithmetic
overflow past 100%, not against a legitimate-looking but fully predatory
100% fee. This mirrors the pattern flagged in
SmartDropLabs/smartdrop-contracts#89
— an admin-settable value with a lower bound but no sane upper bound — which
is the direct inspiration for treating this as a first-class finding.
Fix
1.
MAX_FEE_BPSconstant (all three contracts)Chosen and documented with reasoning on the constant itself: bounty-payout
platforms charge single-digit-percent treasury fees in practice, so 10% is
already an order of magnitude below the mathematical maximum (100%), and any
fee approaching the ceiling is itself a red flag worth surfacing. Capping here
also makes it structurally impossible to ever configure the zero-payout
failure mode.
2. Tightened guard (all three contracts)
Reuses the existing
InvalidFeevariant — no new error variant — and theceiling is inclusive (
fee_bps == MAX_FEE_BPSremains valid). The check isapplied only in
initialize, which is the sole placefee_bpsis set today.3. Ready for #20
The constant is
puband documented, so if/when #20 introduces a boundedfee_bpssetter, that setter can apply the identical ceiling and keep the twoissues' constants in sync.
Changes by file
contracts/escrow/src/lib.rsMAX_FEE_BPS; guardfee_bps > MAX_FEE_BPScontracts/milestones/src/lib.rscontracts/maintenance-pool/src/lib.rscontracts/escrow/src/test.rscontracts/milestones/src/test.rscontracts/maintenance-pool/src/test.rsREADME.mdTests
Added to all three suites:
test_initialize_rejects_fee_bps_above_ceiling— asserts that bothMAX_FEE_BPS + 1(one above the ceiling) and10_000(the old 100%mathematical maximum) are rejected with
Error::InvalidFee. Testing10_000specifically is what locks in the fix: it was the value the old
>guard silently accepted.
test_initialize_accepts_fee_bps_at_ceiling— asserts the ceiling isinclusive:
fee_bps == MAX_FEE_BPSinitializes successfully (escrowadditionally asserts
get_fee_bps() == MAX_FEE_BPS).Verification
All CI checks pass locally:
Backward compatibility
use 2.5%–10%) are unaffected: the ceiling is inclusive.
fee_bps > 10%atinitialize, whichno legitimate treasury fee for a bounty-payout platform should ever need.
Non-goals / out of scope
fee_bpsis immutable afterinitialize— design and implement a secure, bounded update mechanism #20 (fee immutability) — this PR does not add afee_bpssetter. Itonly bounds the value accepted at the one place it's currently set. The
MAX_FEE_BPSconstant is positioned to be reused byfee_bpsis immutable afterinitialize— design and implement a secure, bounded update mechanism #20's eventual setter.InvalidFeecovers the ceiling.Checklist
MAX_FEE_BPSconstant added and documented with reasoning in all three contractsinitializein all three contracts rejectsfee_bps > MAX_FEE_BPSviaInvalidFeetest_initialize_rejects_fee_bps_above_ceilingin all three suitestest_initialize_accepts_fee_bps_at_ceilingin all three suitescargo test --workspacepassescargo fmt --check,cargo clippy -D warnings, and the wasm build pass