fee_split/v2
gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/fee_split/v2 on gno.land mainnet
realmremediated-applicationrevenue-and-value-flowlive on mainnet
Split incoming revenue between recipients by fixed shares.
Each recipient claims its own share whenever it likes, so paying a group never depends on one party forwarding the money.
In detail curated
Weighted revenue splitting among payees with pull-based withdrawal, deployed as the security-remediated successor path to the immutable v1 realm. Deltas over v1: two-step ownership handover (stage + consent) so a nomination cannot consume the nominee's per-owner quota without consent; a GetPendingOwner/Render read surface so a nominee can discover an offer without paying gas; an IsUserCall-guarded stray-send rejection on every non-Deposit entrypoint; an IsCurrent pin in rejectStraySend. Fee is taken off the top at a compile-time cap of 100 basis points and never touches a credited balance; integer-division dust goes to the highest-share recipient. Portfolio-remediation phase, not a redeploy: v1 is untouched and stays frozen.
Use it
A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.
Read it — no key, no account, no GNOT
gnokey query vm/qeval \
--data 'gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/fee_split/v2.GetFeeInfo()' \
--remote https://rpc.gno.land:443GetFeeInfo is read-only, so this is a query rather than a transaction: it costs nothing, signs nothing and needs no funded account. Every exported function is listed under the API below; swap the name and the arguments to read any of the others.
Change its state
Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 12 crossing functions, with the argument names filled in.
The gno.land mainnet builder spends real GNOT. Start with the read above, and size -gas-fee from a simulation rather than from any example.
Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.
Import it from another realm
import "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/fee_split/v2"The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.
Deployments
gno.land mainnet gnoland-1
RenderedSourceCall builderState
chain-attested| Import path | gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/fee_split/v2 |
|---|---|
| Namespace | g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39 |
| Deployed at height | 351,422 |
| Deploy transaction | e8ab38860e74e35abf7f0ef2b02bba5e3b84744b61c53af113fe15dfc9dc0e4f look it up on the RPC |
| Deployer | g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39 |
| Gas used | 79,913,652 |
| Storage | not attested per package on this chain — see the limitations below |
| Files on chain | fee_split.gno fee_split_test.gno gnomod.toml |
| Deployed bytes | fee_split.gno — 21,078 bytes |
| sha256 | fc089ff2654cbeeda65ad7429f4f5b91c883576988d59b6b32d31c2859e16603 |
| Realm address | g156nyjz7ma9tnr847tfv4zd9q3hrd62pwd95m3a derived, never confirmed against the realm |
Do not take the hash above on trust. $download returns the bytes gnoland-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:
curl -sS 'https://gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/fee_split/v2$download&file=fee_split.gno' | shasum -a 256Expected: fc089ff2654cbeeda65ad7429f4f5b91c883576988d59b6b32d31c2859e16603 — 21,078 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.
API
chain-derived 17 exported functions, 1 type.
Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:
AcceptFeeAdminAcceptOwnershipArchiveClaimClaimFeesCreateSplitDepositFreezeGetClaimableGetFeeInfoGetPendingOwnerGetSplitInfoNominateFeeAdminRenderSetFeeTransferOwnershipUpdateShares
Overview
This package carries no package doc comment on chain, so there is nothing for vm/qdoc to return and gnoweb's $help Overview is empty. Deployed bytes are immutable, so this cannot be repaired in place — see [an unpublished record] §2.4.
Imports
chainchain/bankerchain/runtime/unsafesortstrconvstrings
Constants and variables
const (
MaxRecipients = 20
MaxSplitsPerOwner = 10
// Quotas are PER-OWNER only (round-4 audit): a global cap is a shared
// resource 50 sybil accounts could fill forever — and seeding grief
// splits with balances to keyless recipients made the fill
// unrecoverable even by the sybils. Per-owner quotas mean an attacker
// consumes only their own budget; state growth is gas-priced.
// Render is bounded separately (MaxRenderSplits).
MaxRenderSplits = 100
// MaxFeeBps is an IMMUTABLE ceiling on the protocol fee (1%). The
// admin can set any fee from 0 up to this cap, never above it — the
// cap, not the current setting, is what users must trust.
MaxFeeBps = int64(100)
// Pre-parse input bounds (round-4 audit): caps were enforced only
// AFTER full parsing, so a 1MB recipient list burned ~11B gas before
// refusal. 20 bech32 addresses + separators fit well within these.
MaxRecipientListLen = 1024
MaxShareListLen = 128
// Denomination handled by this realm. Deposits must be exactly one
// coin of this denom; claims pay out in it.
Denom = "ugnot"
// Largest single deposit for which share math (amount * share,
// share <= 10000) cannot overflow int64.
MaxDepositAmount = int64(9223372036854775807) / 10000
)
Types
type Split
type Split struct {
Owner address
PendingOwner address // two-step handover, must AcceptOwnership
Recipients []address
Shares []int64 // basis points, must sum to 10000
Balances map[address]int64
TotalDeposited int64
TotalClaimed int64
Frozen bool
Archived bool
}
Split holds a fee-splitting configuration with percentage-based shares denominated in basis points (1 bp = 0.01%, 10000 bp = 100%).
| Exported field | Type | Doc |
|---|---|---|
Owner | address | |
PendingOwner | address | two-step handover, must AcceptOwnership |
Recipients | []address | |
Shares | []int64 | basis points, must sum to 10000 |
Balances | map[address]int64 | |
TotalDeposited | int64 | |
TotalClaimed | int64 | |
Frozen | bool | |
Archived | bool |
Functions
AcceptFeeAdmin
func AcceptFeeAdmin(cur realm)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAcceptFeeAdmin(cross(cur), ...).
AcceptFeeAdmin completes the handover; only the nominee can accept.
AcceptOwnership
func AcceptOwnership(cur realm, splitID string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAcceptOwnership(cross(cur), ...).
AcceptOwnership completes the handover; only the nominee can accept. The nominee's quota is checked HERE, at consent time — the split slot moves only with the acceptor's own signature (v2 audit Y1: a recipient's quota is never consumed without consent). A nomination on a split that is later archived is inert (mustGetActive).
Archive
func Archive(cur realm, splitID string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asArchive(cross(cur), ...).
Archive marks a fully-claimed split as archived. Only the owner can archive, and only if EVERY balance — including balances held by ex-recipients removed in a share update — is zero, since archiving blocks all further claims. Cannot be undone.
Claim
func Claim(cur realm, splitID string) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asClaim(cross(cur), ...).
Claim withdraws the caller's accumulated balance and SENDS the coins to the caller's address. Balance is zeroed before the transfer. Claims remain possible on frozen splits, and by ex-recipients whose accrued balance predates a share update.
ClaimFees
func ClaimFees(cur realm) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asClaimFees(cross(cur), ...).
ClaimFees sends all accrued protocol fees to the fee admin.
CreateSplit
func CreateSplit(cur realm, recipientList, shareList string) string
Crossing function. Callable from a transaction via
MsgCall, and from another realm asCreateSplit(cross(cur), ...).
CreateSplit registers a new split. The caller becomes the owner. Recipients and shares are comma-separated; shares are in basis points summing to 10000.
Deposit
func Deposit(cur realm, splitID string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asDeposit(cross(cur), ...).
Deposit distributes the coins sent with the call across recipients proportionally.
DEPLOYMENT PRECONDITION (round-4 audit): on a network with restricted/token-locked ugnot transfers, the bank gate is SENDER-whitelist-based — a whitelisted user's Deposit succeeds but Claim sends FROM this realm's (non-whitelisted) address and reverts. Funds would flow in and not out until the restriction lifts. Deploy only to networks with unrestricted ugnot, or have governance whitelist this realm's address first.
LIMITATION (round-3 audit, documented): only direct user calls can deposit. A DAO/realm treasury has NO deposit path — a realm-routed call is refused, and a bare banker send to this realm's address is an unrecoverable donation. Realm treasuries must route deposits through a user account. The deposit is the ACTUAL attached send — exactly one coin of Denom — so balances are always backed by funds this realm holds. Direct user calls only: a deposit routed through an intermediary realm would deliver its coins to that realm, not here, and must be rejected. Rounding dust goes to the highest-share recipient (deterministic, not order-dependent).
Freeze
func Freeze(cur realm, splitID string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asFreeze(cross(cur), ...).
Freeze permanently locks shares and stops further deposits. One-way, cannot be undone. Claims remain possible.
GetClaimable
func GetClaimable(splitID string, addr address) int64
GetClaimable returns claimable balance for an address.
GetFeeInfo
func GetFeeInfo() string
GetFeeInfo returns the current fee configuration and accrued total.
GetPendingOwner
func GetPendingOwner(splitID string) string
GetPendingOwner returns the staged nominee for a split, or "" when no handover is pending. The consent half of a two-step transfer only means something if the nominee can inspect the offer without paying gas to probe it (v2 audit Y1-obs); this is that read path.
GetSplitInfo
func GetSplitInfo(splitID string) string
GetSplitInfo returns a human-readable summary.
NominateFeeAdmin
func NominateFeeAdmin(cur realm, nominee address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asNominateFeeAdmin(cross(cur), ...).
NominateFeeAdmin begins a two-step admin handover; the nominee must AcceptFeeAdmin. Pass "" to clear a pending nomination. Two-step because the admin address is a funds destination: a typo'd one-step transfer would strand all future fees.
Render
func Render(path string) string
Render returns a markdown overview. Never panics.
SetFee
func SetFee(cur realm, bps int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSetFee(cross(cur), ...).
SetFee sets the protocol fee in basis points, admin only, hard-capped at MaxFeeBps. Applies to FUTURE deposits only.
TransferOwnership
func TransferOwnership(cur realm, splitID string, newOwner address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asTransferOwnership(cross(cur), ...).
TransferOwnership stages a two-step ownership handover; nothing moves until the nominee calls AcceptOwnership. Two-step for the same reason as the fee admin, plus one more (v2 audit Y1): a one-step transfer consumed the RECIPIENT's per-owner quota without consent, so poisoned splits could burn a victim's slots unarchivably. Staging consumes nothing of the nominee's. Pass "" to clear a pending nomination.
UpdateShares
func UpdateShares(cur realm, splitID, recipientList, shareList string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asUpdateShares(cross(cur), ...).
UpdateShares replaces recipients and shares. Owner only. Not if frozen. Removed recipients keep any accrued balance and can still Claim it.
Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.
Dependencies
chain-attested| Imports | chain, chain/banker, chain/runtime/unsafe, sort, strconv, strings |
|---|---|
| First-party dependencies | none |
Known limitations
Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.
curated- No state-changing call was made against this realm on gnoland-1. The live evidence is byte custody plus a successful vm/qrender; every state transition rests on the committed test suite (re-run under the mainnet pin on deploy day) and the fresh mainnet-candidate audit plus its independent adversarial challenge pass, not on mainnet behaviour ([an unpublished record], Live verification).
- storage_bytes and storage_deposit are null because gnoland-1 emits no /tm.StorageDepositEvent ON ADDPKG -- a scope that is load-bearing, because it DOES emit one on MsgCall (measured 2026-10-17, AL62); see the chain's storage_deposit_events field. The deposit actually charged was 2,961,000 ugnot, measured by differencing auth/accounts across the deploy heights and recorded in [an unpublished record], Cost; it is not chain-attested per package by this manifest.
- The deployed file set includes the _test.gno file. The pearl-1 deployment of the same realm carried only the source and gnomod.toml, so the mainnet package is three files where pearl's is two -- the test file is stored on chain and paid for in the deposit. It declares no crossing function and so adds no callable surface, but it is part of the immutable package.
- Y1 residual, accepted with documentation: a well-formed but keyless recipient address strands that recipient's balance and permanently bricks that split's Archive (UpdateShares never clears stale Balances keys; Archive walks all historical keys). Contained to one split and its voluntary depositors; not attacker-reachable against a third party. Realm recipients WITH claim-capable code are not stranded -- Claim accepts realm callers. Recipients should be EOAs or realms designed to claim.
- Y2 residual, dormant at deploy: feeBps ships at 0 and only the fee-admin can raise it (immutable 100bps cap). Fee-admin key loss with feeBps > 0 would strand accrued fees AND remove the fee off-switch -- if the fee is ever raised, claim on a short cadence and complete a nomination to a second controlled key first ([an unpublished record], Audit disposition).
- Zero first-party dependencies: the closure verdict compares stdlibs only, so the reported digest means 'no chain dependencies', not 'a closure was verified'. The primary file is byte-identical to the pearl-1 twin because no namespace appears in it.
- No migration path exists from v1 and none was built; the pearl-era fee_split (v1) path on this chain remains undeployed and the pearl v1 realm is untouched.
- Owner can retarget shares between a depositor's read and execution: UpdateShares is effective immediately and Deposit carries no expected-shares pin (Freeze cannot serve -- a frozen split rejects deposits), so a same-block owner update can route 100% of a verified deposit to the owner. Voluntary-trust bounded; depositors should treat the advertised table as mutable mid-flight. Successor option: an optional shares-digest parameter on Deposit. Audit 2026-11-04, disclosed per the operator's disclose-and-queue-successor rule; details and severity in [an unpublished record].
Source
| Source file | mainnet/r/fee_split/v2/fee_split.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository) |
|---|---|
| Matches the deployed bytes | yes — byte-identical |