Gno.land · Smart Contracts · Infrastructure
Trustless, on-chain revenue splitting for Gno.land.
The realm carries an optional protocol fee on deposits, off by default and hard-capped at 1% (100 bps) by an immutable constant — the cap, not the current setting, is what users need to trust. Fees are taken at deposit time only (never retroactively from credited balances), accrue inside the realm, and are claimable by the fee admin (the deployer; transferable only via a two-step nominate/accept handover).
- Unrestricted ugnot only. On a token-locked network the bank gate is sender-whitelist-based: a whitelisted user's deposit succeeds, but claims send from this realm's (non-whitelisted) address and revert — funds would flow in and not out. Deploy only where ugnot transfers are unrestricted, or have governance whitelist the realm address first.
- Realm treasuries cannot deposit. Deposits are direct user calls only; a bare bank send to the realm address is an unrecoverable donation.
Every team that earns on-chain revenue — DAOs collecting protocol fees, freelancer collectives invoicing clients, validator teams pooling rewards — faces the same question: who distributes the money, and why should anyone trust them?
Manual distribution is slow, error-prone, and requires trusting a single treasurer. Off-chain spreadsheets don't enforce anything. Existing multisig solutions are built for approval workflows, not for continuous revenue sharing.
fee_split is a Gno realm that lets anyone create a split — a set of recipients with percentage-based shares that must sum to exactly 100%.
- Create a split with recipients and their shares (in basis points: 5000 = 50%)
- Deposit funds into the split — they're instantly allocated proportionally
- Recipients claim their balance at any time — no approval needed
- Owner updates shares if the team changes — unclaimed balances are preserved
- Freeze the split to make shares permanent and immutable
Shares are denominated in basis points (1 bp = 0.01%) for precision without floating point. Rounding dust from integer division is assigned to the last recipient so no value is silently lost.
| Approach | Problem |
|---|---|
| Manual distribution | Trust required, slow, error-prone |
| Multisig wallet | Built for approvals, not continuous splitting |
| Off-chain agreements | Not enforceable |
| fee_split | Automatic, trustless, verifiable, immutable when frozen |
The key design choice: splits are pull-based (recipients claim) rather than push-based (auto-send). This avoids failed transfers blocking the entire split and gives recipients control over when they withdraw.
// 60% to alice, 25% to bob, 15% to carol
// owner is the address that controls the split
CreateSplit(
cross,
"g1owner...",
"g1alice...,g1bob...,g1carol...",
"6000,2500,1500"
)
// returns: "split_1"
Deposit(cross, "split_1", 1000000)
// alice gets 600000, bob gets 250000, carol gets 150000
Claim(cross, "split_1", "g1alice...")
// returns: your claimable balance
UpdateShares(cross, "split_1", "g1owner...", "g1alice...,g1bob...,g1dave...", "5000,3000,2000")
Freeze(cross, "split_1", "g1owner...")
// shares can never be changed again
GetSplitInfo("split_1")
GetClaimable("split_1", "g1alice...")
| Function | Access | Description |
|---|---|---|
CreateSplit(cross, owner, recipients, shares) |
Anyone | Create a new split. Provided owner string controls it. |
Deposit(cross, splitID, amount) |
Anyone | Distribute funds to recipients. |
Claim(cross, splitID, caller) |
Recipient | Withdraw accumulated balance for caller. |
UpdateShares(cross, splitID, caller, recipients, shares) |
Owner | Change recipients and shares. caller must match owner. |
TransferOwnership(cross, splitID, caller, newOwner) |
Owner | Hand off split control. caller must match owner. |
Freeze(cross, splitID, caller) |
Owner | Permanently lock shares. caller must match owner. |
GetSplitInfo(splitID) |
Anyone | View split configuration and balances. |
GetClaimable(splitID, addr) |
Anyone | Check claimable balance for an address. |
Render(path) |
Anyone | Markdown overview of all splits. |
- Percentage validation: shares must sum to exactly 10000 basis points (100%)
- Duplicate detection: rejects duplicate recipients in the same split
- Explicit caller: owner and caller are passed as plain string parameters — the consuming realm is responsible for passing the correct address (typically derived from
std.PreviousRealm().Address()in the caller's context) - Rounding protection: dust from integer division goes to the last recipient
- Freeze mechanism: one-way lock prevents owner from changing shares after trust is established
- Balance preservation: updating shares never destroys unclaimed balances
gnokey query vm/qeval --data 'gno.land/r/fee_split.GetSplitInfo("split_1")' --remote <rpc>
gnokey query vm/qeval --data 'gno.land/r/fee_split.GetClaimable("split_1", "g1alice...")' --remote <rpc>
Visit /r/fee_split on a Gno.land node to see all active splits with their configuration and balances.
| Realm | Layer |
|---|---|
| fee_split | Revenue & value flow |
| permission_registry | Access control |
| service_registry | Discovery |
| upgrade_registry | Upgrade tracking |
| timelock_guardian | Security |