fee_split — security fix series (2026-09-21)
Base: mechanical pearl port (port.py, now a neutered historical artifact) of SillyZir/fee_split @ 819239ad5ddc46cf685f5ff363cabb7290848c1f. The user authorized fixing the audit YELLOWs; the .gno files in this directory are port + fix series and are the workspace source of truth.
Deployment status — read this first. The v1 realm deployed at gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/fee_split (tx 061c89b1, h596803) is IMMUTABLE and retains the pre-fix shape (audited safe on all reachable paths — the fixes remove fragility, not an exploit). This fixed source can only ship as a NEW realm path (e.g. .../fee_split_v2); doing so requires updating the module path in gnomod.toml AND the hardcoded package path in the test fund() helper, and needs separate deploy authorization.
Round-1 audit findings → fixes
| # | Finding | Fix | Regression test |
|---|---|---|---|
| Y1 | caller() helper derived identity via stack-walking unsafe.PreviousRealm() in a non-crossing helper; Deposit's payment guard and the Claim/ClaimFees send-source used the same walker family — correct on all current paths, enforced only by call-site discipline | All 10 guarded entrypoints declare cur realm; identity is cur.Previous().Address() inline; payment guard is cur.Previous().IsUserCall(); sends originate from cur.Address(); helper deleted; unsafe retained only for OriginSend (×2) and init's OriginCaller | whole suite (verification audit traced semantic equivalence on all paths: direct EOA / realm-routed / MsgRun) |
| Y2 | Six test-coverage gaps around the exact guards the port had to preserve | Four new tests: stray-send on the five unpinned functions; archived-split refusal of both value paths; two-coin envelope; equal-share dust tie (first-listed-wins). MsgRun-shape refusal is not constructible in the test driver (SetRealm accepts only user and /r/ realms) — documented at the code-realm test; the init feeAdmin capture is untestable in the test VM and was verified LIVE (GetFeeInfo admin == deployer) | TestStraySendRejectedOnAllSplitFunctions, TestArchivedSplitRefusesDepositAndClaim, TestDeposit_TwoCoinEnvelopeRejected, TestDust_EqualShareTieGoesToFirst |
| Y3 | Deployment preconditions (operational, not code): unrestricted-ugnot network; fee-admin key custody | Unrestricted ugnot verified live before the v1 deploy (realm-address sends succeed on pearl-1); admin == deployer verified live post-deploy. OPEN ITEM: the deployed v1's fee admin is the hot agent key — nominating a user-controlled backup admin via the two-step handover needs an address from the user | — (operational) |
Verification-round finding → fix
| # | Finding | Fix |
|---|---|---|
| Y-NEW-1 | Stale port.py presented itself as the current pipeline and, if rerun as documented, would silently regenerate the pre-fix v1 shape over the fixed files | Both apps' port.py scripts (here and r/timelock_guardian/) now sys.exit with a loud historical-artifact notice before touching anything |
Suite: 59 tests, all passing on the chain/pearl toolchain (c4c72fd); lint clean; full workspace green. Verification audit (fresh context, diffed against the LIVE deployed v1 bytes): all fixes VERIFIED, no behavioral drift outside the authorized edit surface.
Provenance v2 (2026-09-21, post-push)
Remediation pushed upstream: SillyZir/fee_split @ e5aadfebc890c21ade4076e1d75c9c0e0dd77809 (main), authored solely by SillyZir, no AI attribution (verified via fresh clone + GitHub API). GitHub is again the source of truth; the transformation to this directory is T1 ONLY: module-path + PackageAddress namespace rewrite (gno.land/r/fee_split -> gno.land/r/g1ut6.../fee_split), verified byte-exact against the fresh clone.