Skip to content

Latest commit

 

History

16 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

Gno.land · Smart Contracts · Infrastructure

fee_split

Trustless, on-chain revenue splitting for Gno.land.

Protocol fee

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).

Deployment preconditions

  • 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.

Problem

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.

How it works

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%.

  1. Create a split with recipients and their shares (in basis points: 5000 = 50%)
  2. Deposit funds into the split — they're instantly allocated proportionally
  3. Recipients claim their balance at any time — no approval needed
  4. Owner updates shares if the team changes — unclaimed balances are preserved
  5. 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.

Why this approach

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.

Usage

Create a split

// 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 funds

Deposit(cross, "split_1", 1000000)
// alice gets 600000, bob gets 250000, carol gets 150000

Claim your share

Claim(cross, "split_1", "g1alice...")
// returns: your claimable balance

Update shares (owner only)

UpdateShares(cross, "split_1", "g1owner...", "g1alice...,g1bob...,g1dave...", "5000,3000,2000")

Freeze shares permanently

Freeze(cross, "split_1", "g1owner...")
// shares can never be changed again

Query a split

GetSplitInfo("split_1")
GetClaimable("split_1", "g1alice...")

API

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.

Safety

  • 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

Query on Gno.land

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.

Stack

  • Gno — Go-like smart contract language
  • Gno.land — Layer 1 blockchain

Part of the Gno Infrastructure Stack

Realm Layer
fee_split Revenue & value flow
permission_registry Access control
service_registry Discovery
upgrade_registry Upgrade tracking
timelock_guardian Security

Releases

Packages

Contributors

Languages