Cosmic Bull

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:443

GetFeeInfo 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 pathgno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/fee_split/v2
Namespaceg1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Deployed at height351,422
Deploy transactione8ab38860e74e35abf7f0ef2b02bba5e3b84744b61c53af113fe15dfc9dc0e4f look it up on the RPC
Deployerg1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Gas used79,913,652
Storagenot attested per package on this chain — see the limitations below
Files on chainfee_split.gno fee_split_test.gno gnomod.toml
Deployed bytesfee_split.gno — 21,078 bytes
sha256fc089ff2654cbeeda65ad7429f4f5b91c883576988d59b6b32d31c2859e16603
Realm addressg156nyjz7ma9tnr847tfv4zd9q3hrd62pwd95m3a 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 256

Expected: 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

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 fieldTypeDoc
Owneraddress
PendingOwneraddresstwo-step handover, must AcceptOwnership
Recipients[]address
Shares[]int64basis points, must sum to 10000
Balancesmap[address]int64
TotalDepositedint64
TotalClaimedint64
Frozenbool
Archivedbool

Functions

AcceptFeeAdmin

func AcceptFeeAdmin(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as AcceptFeeAdmin(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 as AcceptOwnership(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 as Archive(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 as Claim(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 as ClaimFees(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 as CreateSplit(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 as Deposit(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 as Freeze(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 as NominateFeeAdmin(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 as SetFee(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 as TransferOwnership(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 as UpdateShares(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
Importschain, chain/banker, chain/runtime/unsafe, sort, strconv, strings
First-party dependenciesnone

Known limitations

Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.

curated

Source

Source filemainnet/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 bytesyes — byte-identical