Cosmic Bull

deposits

gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/deposits on onyx-1 testnet

realmapplicationescrowlive on onyx-1

Hold a refundable security deposit against agreed conditions.

Settles the deposit on conditions set up front, including splitting one pot between two parties who disagree about it.

In detail curated

REFUNDABLE SECURITY DEPOSITS WITH CONDITION-GATED FORFEITURE -- the one money shape the portfolio never had: ONE POT SPLITTING PARTIALLY between two rival claimants by a conditional rule, and the first realm where the DEPOSITOR'S OWN money is at risk of partial loss. A holder opens an agreement naming a depositor, an exact amount, a term and a FORFEIT CEILING (the most the holder may ever claim); the depositor reads all of it, the named arbiter included, and consents by FUNDING; after the term the holder may file a claim up to the ceiling; the depositor may accept it, dispute it to the arbiter, or let it stand; whatever is not awarded is REFUNDED. It is none of the shipped shapes: crowdfund is all-or-nothing, auction is winner-takes, loans is principal-out for a larger total back, reimbursements is approver-gated to a single claimant -- none of them SPLITS. CUSTODY -- Held == locked + contested + feeledger.Liabilities(), three buckets rather than two, with locked and contested DISJOINT: a dispute moves the amount out of one and into the other in the same transaction, so 'what is the arbiter deciding over' is a readable number rather than an inference. Settlement is PULL, never push. The fee applies to the AWARD ONLY -- a fully-refunded deposit costs the depositor nothing, which is what a security deposit has to do to be worth posting. FIVE routes reach settled and THREE are permissionless (SettleUndisputed, ReleaseUnclaimed, LapseDispute), so no terminal state depends on a party choosing to act. The consent chain award <= claimAmount <= forfeitCeiling <= amount holds on every route and nothing raises either bound after funding. Audit PROCEED, 0 RED / 0 material YELLOW, 3 GREEN / 3 REFUTED; G2's party-disjointness guard on SetFeeRecipient was REFUSED on evidence rather than added (a global over an unbounded growing set can only be snapshot-checked, and a guard any later transaction walks through reads as an invariant without being one). 56-case mutation sweep FULLY COVERED; 25-test suite green. ITS BYTE-MATCH FAILED: see source.divergence.

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/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/deposits.Contested()' \
  --remote https://rpc.onyx.testnets.gno.land:443

Contested 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 19 crossing functions, with the argument names filled in.

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/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/deposits"

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

onyx-1 testnet onyx-1

RenderedSourceCall builderState

chain-attested
Import pathgno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/deposits
Namespaceg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Deployed at height491,920
Deploy transaction24e191b395274422a2c9a6b40c1d48dca4fd32607da4f17770569b57486f6905 look it up on the RPC
Deployerg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Gas used51,078,826
Storagenot attested per package on this chain — see the limitations below
Files on chaindeposits.gno gnomod.toml
Deployed bytesdeposits.gno — 38,013 bytes
sha256f098e3cd2d1e75a01dfa5f134c8ebe675e09e7e92edd79d7841767ac3861d986
Realm addressg1rtphf7raf5zjcvpupze8xwkdq7pcra8whhve4s

Do not take the hash above on trust. $download returns the bytes onyx-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:

curl -sS 'https://onyx.testnets.gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/deposits$download&file=deposits.gno' | shasum -a 256

Expected: f098e3cd2d1e75a01dfa5f134c8ebe675e09e7e92edd79d7841767ac3861d986 — 38,013 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.

API

chain-derived 54 exported functions.

Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:

AcceptAdminAcceptClaimAgreementsOfAmountArbiterDeadlineOfAwardCancelUnfundedClaimClaimAllClaimAmountClaimDeadlineClaimReasonClaimableOfContestedCreatedHeightDisputeDisputeDeadlineOfDisputeReasonFeeBpsFeeBpsOfFeesAccruedFileClaimForfeitCeilingFundFundDeadlineFundedHeightHasAgreementHeightHeldIsClaimableIsDisputableIsSettledLapseDisputeLiabilitiesLockedMaxClaimableMemoNumAgreementsOpenAgreementPendingValveRefundReleaseReleaseUnclaimedRenderResolveDisputeSetFeeBpsSetFeeRecipientSettleUndisputedSettledHeightStatusSweepDenomTermEndTransferAdminWithdrawFees

Overview

Package deposits is a refundable security deposit with condition-gated, partially-forfeitable settlement.

The shape

A HOLDER (landlord, service provider, equipment lessor) opens an agreement naming a DEPOSITOR, an exact amount, a term, and a FORFEIT CEILING — the most the holder may ever claim out of the deposit. The depositor reads all of it, including the named arbiter, and consents by FUNDING. At the end of the term the holder may file a claim up to the ceiling; the depositor may accept it, dispute it to the arbiter, or let it stand. Whatever is not awarded to the holder is refunded to the depositor.

That makes this the first realm in the portfolio where ONE POT SPLITS PARTIALLY BETWEEN TWO RIVAL CLAIMANTS by a conditional rule, and the first where the depositor's own money is at risk of partial loss. `crowdfund` is all-or-nothing, `auction` is winner-takes, `loans` is principal-out / larger-back, `reimbursements` is approver-gated to a single claimant. See DISCOVERY.md for the §7 gate and the five candidates it killed.

Lifecycle

OpenAgreement -> open (nothing held; depositor has not consented) Fund -> funded (depositor attaches the exact amount) FileClaim -> claimed (holder, after term end, within the claim window) Dispute -> disputed (depositor, within the dispute window) ResolveDispute -> settled (arbiter awards 0..claim to the holder)

Five routes reach `settled`, and THREE OF THEM ARE PERMISSIONLESS, so no terminal state depends on a party acting:

AcceptClaim depositor agrees award = claim SettleUndisputed ANYONE, dispute window up award = claim ReleaseUnclaimed ANYONE, claim window up award = 0 (holder never claimed) LapseDispute ANYONE, arbiter silent award = 0 (claim unsubstantiated) Release holder waives early award = 0

Custody

Coins are held by the realm from Fund until settlement, then become PULL-CLAIMABLE through `p/feeledger`. Nothing is ever pushed: a settlement credits balances and the parties `Claim` them, which removes the grief and reentrancy surface a push settlement would have.

The conservation invariant carries one bucket more than the sibling realms:

Held == locked + contested + feeledger.Liabilities()

`locked` and `contested` are DISJOINT — a disputed deposit moves out of one and into the other in the same transaction. The split exists because a contested deposit is the one balance whose destination is not yet decided by this realm's own rules; keeping it separate makes "what is the arbiter deciding over" a readable number rather than an inference.

The fee

The protocol fee applies ONLY to the portion AWARDED to the holder. A refund to the depositor is returned in full at fee 0, matching `auction`'s rule that a refund is not a fee event. A fully-refunded deposit therefore costs the depositor nothing.

What the depositor is trusting, stated plainly

Scope

Single-denom (ugnot), single depositor per agreement, flat ceiling. Graduated forfeiture over damage severity (`p/tiers`) is a deliberate v2 candidate, not an omission — see DISCOVERY.md §5.

Imports

Constants and variables

Agreement status values. Settled and Cancelled are terminal.

const (
	DepositOpen      = "open"      // created; the depositor has not funded
	DepositFunded    = "funded"    // funded; the term is running or the claim window is open
	DepositClaimed   = "claimed"   // the holder filed a claim; the depositor may accept or dispute
	DepositDisputed  = "disputed"  // the depositor disputed; the arbiter decides
	DepositSettled   = "settled"   // split credited; both parties pull
	DepositCancelled = "cancelled" // never funded; nothing moved
)

Input bounds.

const (
	// MinDeposit is the dust floor; MaxDeposit keeps the split, the ceiling and
	// the fee far from int64 overflow. The depositor must ATTACH the deposit, so
	// the real ceiling is their balance.
	MinDeposit = int64(10_000)
	MaxDeposit = int64(1_000_000_000_000) // 1,000,000 GNOT

	// MinClaim is the dust floor for a forfeit ceiling and for any claim filed
	// against it. A ceiling below it would make FileClaim unsatisfiable.
	MinClaim = int64(1_000)

	// Term and window bounds, in blocks. The window floors keep a counterparty
	// from being given a deadline they cannot physically meet; the ceilings keep
	// height arithmetic far from overflow.
	MinTerm   = int64(10)
	MaxTerm   = int64(7_500_000)
	MinWindow = int64(10)
	MaxWindow = int64(7_500_000)

	// FundWindow is how long an unfunded agreement stays fundable. After it,
	// Fund refuses and CancelUnfunded is permissionless, so an abandoned offer
	// cannot occupy a holder's slot forever.
	FundWindow = int64(1_000_000)

	// MaxMemoLen and MaxReasonLen bound the stored free text.
	MaxMemoLen   = 200
	MaxReasonLen = 200

	// MaxAgreementsPerHolder bounds one holder's stored agreements. A terminal
	// agreement does NOT free the slot: a settled deposit is a permanent record
	// both parties and an auditor may need to read.
	MaxAgreementsPerHolder = 50

	// RenderLimit bounds every rendered list — an unbounded Render was YELLOW in
	// several prior audits in this portfolio.
	RenderLimit = 20
)

Denom is the only asset this realm accepts.

const Denom = "ugnot"

MaxFeeBps is the hard protocol-fee cap: 1000 bps = 10%. The fee applies only to the portion AWARDED to the holder, never to a refund.

const MaxFeeBps = int64(1000)

Functions

AcceptAdmin

func AcceptAdmin(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as AcceptAdmin(cross(cur), ...).

AcceptClaim

func AcceptClaim(cur realm, id int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as AcceptClaim(cross(cur), ...).

AcceptClaim settles at the claimed amount. Depositor only, while a claim stands. Accepting early is allowed — the dispute window is a deadline for disputing, not a waiting period.

AgreementsOf

func AgreementsOf(holder address) int64

Amount

func Amount(id int64) int64

ArbiterDeadlineOf

func ArbiterDeadlineOf(id int64) int64

Award

func Award(id int64) int64

CancelUnfunded

func CancelUnfunded(cur realm, id int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as CancelUnfunded(cross(cur), ...).

CancelUnfunded abandons an agreement the depositor never funded. Either party may cancel at any time — refusing to fund is the depositor's right and withdrawing the offer is the holder's — and ANYONE may cancel once the fund deadline has passed, so a dead offer cannot hold a slot forever. No coins have ever been held, so nothing moves.

Claim

func Claim(cur realm, amount int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as Claim(cross(cur), ...).

Claim sends amount ugnot of the caller's claimable balance — refunds and awards from settled agreements — to the caller.

ClaimAll

func ClaimAll(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as ClaimAll(cross(cur), ...).

ClaimAll sends the caller's entire claimable balance to the caller.

ClaimAmount

func ClaimAmount(id int64) int64

ClaimDeadline

func ClaimDeadline(id int64) int64

ClaimReason

func ClaimReason(id int64) string

ClaimableOf

func ClaimableOf(a address) int64

Contested

func Contested() int64

CreatedHeight

func CreatedHeight(id int64) int64

Dispute

func Dispute(cur realm, id int64, reason string)

Crossing function. Callable from a transaction via MsgCall, and from another realm as Dispute(cross(cur), ...).

Dispute sends a standing claim to the arbiter. Depositor only, before the dispute deadline, and only if an arbiter was named — with no arbiter there is nobody to decide, and the depositor saw that before funding. The deposit moves from `locked` into `contested`; no coins move.

DisputeDeadlineOf

func DisputeDeadlineOf(id int64) int64

DisputeDeadlineOf is the last height at which the depositor may Dispute a standing claim; ArbiterDeadlineOf is the last height at which the arbiter's ruling pre-empts LapseDispute. Both are 0 until the relevant stage is live.

DisputeReason

func DisputeReason(id int64) string

FeeBps

func FeeBps() int64

FeeBpsOf

func FeeBpsOf(id int64) int64

FeesAccrued

func FeesAccrued() int64

FileClaim

func FileClaim(cur realm, id, amount int64, reason string)

Crossing function. Callable from a transaction via MsgCall, and from another realm as FileClaim(cross(cur), ...).

FileClaim claims `amount` of the deposit against the ceiling. Holder only, only once, only after the term has ended and before the claim window closes. `amount` must be in [MinClaim, forfeitCeiling]. The depositor may then accept it, dispute it to the arbiter, or let it stand.

ForfeitCeiling

func ForfeitCeiling(id int64) int64

Fund

func Fund(cur realm, id int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as Fund(cross(cur), ...).

Fund attaches the exact deposit and starts the term. The named depositor only; the attached amount must equal the agreement's amount exactly, and the fund deadline must not have passed. Funding IS the depositor's consent to the ceiling, the windows and the arbiter, all of which were readable beforehand.

FundDeadline

func FundDeadline(id int64) int64

FundedHeight

func FundedHeight(id int64) int64

HasAgreement

func HasAgreement(id int64) bool

Height

func Height() int64

Held

func Held() int64

IsClaimable

func IsClaimable(id int64) bool

IsClaimable reports whether FileClaim would be accepted at this height.

IsDisputable

func IsDisputable(id int64) bool

IsDisputable reports whether Dispute would be accepted at this height.

IsSettled

func IsSettled(id int64) bool

LapseDispute

func LapseDispute(cur realm, id int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as LapseDispute(cross(cur), ...).

LapseDispute refunds the whole deposit because the arbiter did not rule before their deadline. Anyone may call it. The award is 0, not the claim: an unsubstantiated claim fails. The holder chose the arbiter and bears the cost of that choice failing — which is also why a disappeared arbiter can never strand a deposit.

Liabilities

func Liabilities() int64

Locked

func Locked() int64

MaxClaimable

func MaxClaimable(id int64) int64

MaxClaimable is the most the holder could still take out of this agreement. It is the ceiling while the deposit is funded, the filed claim once one stands, and 0 once the agreement is terminal.

Memo

func Memo(id int64) string

NumAgreements

func NumAgreements() int64

OpenAgreement

func OpenAgreement(cur realm, depositor address, amount, forfeitCeiling, termBlocks, claimWindow, disputeWindow int64, arbiter address, memo string) int64

Crossing function. Callable from a transaction via MsgCall, and from another realm as OpenAgreement(cross(cur), ...).

OpenAgreement offers a deposit agreement. The caller becomes the holder and names the `depositor`, the exact `amount` they must attach, the `termBlocks` the deposit is held for, the `claimWindow` the holder has to file a claim after the term ends, the `disputeWindow` the depositor has to dispute one (and the arbiter then has to rule), and the `forfeitCeiling` — the most the holder may ever claim, in [MinClaim, amount]. `arbiter` may be the zero address, in which case a claim cannot be disputed; it may not be either party. Nothing is held until the depositor Funds, which is how they consent to all of it. The current protocol fee is snapshotted in. Returns the id.

PendingValve

func PendingValve(id int64) string

PendingValve names the permissionless call this agreement is ready for, or "" when none is. It is the one view a keeper needs.

Refund

func Refund(id int64) int64

Refund is the amount the depositor gets back, known only once settled.

Release

func Release(cur realm, id int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as Release(cross(cur), ...).

Release returns the whole deposit to the depositor early. Holder only, while the agreement is funded and unclaimed. It is the holder waiving the claim they have not yet filed — the one route to settlement that needs no clock.

ReleaseUnclaimed

func ReleaseUnclaimed(cur realm, id int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as ReleaseUnclaimed(cross(cur), ...).

ReleaseUnclaimed refunds the whole deposit because the holder never filed a claim before the window closed. Anyone may call it.

Render

func Render(path string) string

ResolveDispute

func ResolveDispute(cur realm, id, awardToHolder int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as ResolveDispute(cross(cur), ...).

ResolveDispute rules on a disputed claim. Arbiter only. `awardToHolder` is in [0, the claimed amount] — the arbiter may reduce a claim or deny it outright but never award more than was claimed, which is itself capped by the ceiling the depositor consented to. No deadline is enforced here: a late ruling is better than a stranded deposit, and `LapseDispute` is the race that bounds it.

SetFeeBps

func SetFeeBps(cur realm, bps int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SetFeeBps(cross(cur), ...).

SetFeeRecipient

func SetFeeRecipient(cur realm, recipient address)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SetFeeRecipient(cross(cur), ...).

SetFeeRecipient names the address that may withdraw the accrued fee pot.

It deliberately does NOT enforce disjointness from agreement parties, in contrast to OpenAgreement, which refuses an arbiter equal to either party. The two rules are not comparable, and the asymmetry is the point:

Nothing is at stake in the overlap, which is why the honest answer is a documented omission rather than a decorative check. The fee is derived only from the holder's award inside ledger.MustDeposit, never from the refund, so a recipient who is also a holder collects the same coins by two routes instead of more coins. Solvency is unaffected in either direction: feeledger accounts user credit and the fee pot as separate buckets, both are inside ledger.Liabilities(), and SweepDenom's reserve is locked + contested + ledger.Liabilities() — so an overlapping recipient cannot make the swept surplus overstate what the realm is free to release.

SettleUndisputed

func SettleUndisputed(cur realm, id int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SettleUndisputed(cross(cur), ...).

SettleUndisputed settles at the claimed amount because the depositor neither accepted nor disputed before the dispute window closed. Anyone may call it.

SettledHeight

func SettledHeight(id int64) int64

Status

func Status(id int64) string

SweepDenom

func SweepDenom(cur realm, denom string)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SweepDenom(cross(cur), ...).

SweepDenom recovers out-of-band coins to the fee recipient. For the ledger denom the reserve is locked + contested + Liabilities() — held deposits, contested deposits, claimable refunds, awards and the fee pot are all structurally unreachable.

TermEnd

func TermEnd(id int64) int64

TransferAdmin

func TransferAdmin(cur realm, successor address)

Crossing function. Callable from a transaction via MsgCall, and from another realm as TransferAdmin(cross(cur), ...).

WithdrawFees

func WithdrawFees(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as WithdrawFees(cross(cur), ...).


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/runtime, chain/runtime/unsafe, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger, gno.land/p/nt/avl/v0, gno.land/p/nt/markdown/sanitize/v0, strconv
First-party dependenciesgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger

Known limitations

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

curated

Source

Source fileonyx/r/deposits/deposits.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytesno — UNINTENDED divergence: the deploy did not place the committed bytes on chain
WhyBYTE-MATCH FAILED, and the divergence is 10 bytes in two '//' comment lines. Same length (38,013 B both sides): offsets 3677-3683 and 3685-3687, lines 71-72 of the package doc comment's LapseDispute trust bullet, where the inline gno_addpkg emitter re-wrapped the word 'who' across a line break -- a word and a space trading places, which is why the length is preserved. The committed file is NOT edited to match the chain: that would be the comparison deleting itself. The divergence is proven INERT by running the chain's own bytes in a standalone gnowork workspace against the committed test file: 25 --- PASS / 0 --- FAIL, gno lint silent, gofmt -l clean, and the full 75-line test+gas signature IDENTICAL to the committed bytes' (a RenderLimit 20->19 negative control moves it, so the comparison is sensitive). CAUSE: gno_addpkg takes inline files[].body strings and no path, so the emitted string is the real subject; the two-emission gas-reference control agreed to the byte because MsgAddPackage gas is dominated by stored byte count and this edit moved zero bytes -- a gas-reference control is structurally blind to any constant-length edit. Realm bytes are immutable after addpkg, so this is permanent.