Write a self-contained pure Solidity library for basis-point arithmetic, with no imports and no state, suitable for fee maths in a hook. It should apply a rate in basis points to an amount, split an amount across several bps weights so the parts always sum exactly to the whole with the remainder going to the largest share, and refuse a weight set that does not total 10000. Rounding direction must be explicit at every call site rather than implied.

Cover the failure paths: zero amounts, a single weight of 10000, weights that sum wrong, and amounts large enough that a naive multiply would overflow.

Work

  1. Posted6 minto the first attempt
  2. Implement and test2 files changed

    Done. Two new files, nothing else touched.

    src/BasisPoints.sol — a library with no imports and no state, all internal pure:

    • applyRate(amount, bps, Rounding) — rounding is a required parameter, with no default and no overload that supplies one, so the direction shows up in the diff at every call site.
    • split(amount, weights) — one part per weight, summing to exactly amount, remainder to the largest weight (ties to the earliest, documented).
    • RateAboveOneHundredPercent(bps) / WeightsMustTotalOneHundredPercent(total).

    Two decisions worth flagging:

    Overflow. amount * bps / 10000 reverts above type(uint256).max / bps. Since the result is never bigger than the amount, that's an artefact of operation order, so applyRate divides first and carries the remainder: q*bps + (r*bps)/10000 where amount = q*10000 + r. Exact for the full uint256 range, no assembly, no 512-bit mulDiv — the fixed denominator makes the simpler decomposition sufficient. Every step is provably in range, so I left the arithmetic checked rather than unchecked: the checks never fire and cost a few gas against a swap, but they'd catch that proof being wrong after a future edit. There's one forge-lint suppression for divide-before-multiply, with a comment on why nothing is lost.

    split takes no Rounding. Its parts must total the amount exactly, which pins the sum; the only remaining freedom is who gets the dust, and that's the documented largest-share rule. Adding a direction parameter there would offer a choice that doesn't exist — rounding parts up would break the sum invariant. Rejecting a single weight above 10000 up front (rather than only checking the total) is what bounds the running sum: [type(uint256).max, 10001] wraps to exactly 10000, and there's a test for it.

    Verification — forge build clean, forge test 31 passed / 0 failed (23 new, 8 pre-existing HookTest untouched), forge fmt --check clean, forge lint clean. Re-ran the four fuzz tests at 20,000 runs each, all passing. Failure paths covered: zero amount at any rate, zero rate (rounding up must not invent a wei), single weight of 10000, zero weight, weights summing to 9000 and 11000, empty set, the wrapping weight set, rate above 100%, and type(uint256).max through both applyRate and split — with a naiveApplyRate wrapper in the test that asserts the overflow is real under the same compiler settings rather than claiming it in a comment.

    ran onclaude · claude-opus-5 · 18 turns · 6m 13s · 28 in · 29.7K out · 760.1K cached
    submission2bbd41e560499927f3c533a7963ba388c5ff66a4585995f2feb950bb763aa4a8
    device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9
    started from6b238a0048e85ade820dc964f3e6e731dcc9abd6
    bundle96f8decda2fa9d53f198f7239588ec27c042e02e68f6ef346a7565a8b2142bfb · 7.3 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 2 files
    src/BasisPoints.soltest/BasisPoints.t.sol
    may write
    src/BasisPoints.soltest/BasisPoints.t.sol