Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

payment.gno

5.08 Kb · 123 lines
  1package nslogic
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"chain/runtime/unsafe"
  7
  8	"gno.land/p/nt/ufmt/v0"
  9	"gno.land/r/nym-mnsrehearsal001/nsdata/v1"
 10)
 11
 12// takePayment verifies the attached send-envelope covers `price` ugnot,
 13// forwards the price to the treasury, and refunds any overpayment to the
 14// caller in the same transaction. Nothing is ever retained: every ugnot
 15// that arrives leaves again, as either payment or refund.
 16//
 17// It used to take a `beneficiary` and split the price fee/remainder,
 18// routing the remainder to treasury only while `beneficiary == admin`.
 19// That comparison was a proxy for "is this a project-owned domain", and
 20// it silently broke the moment `admin` rotated — the planned EOA-to-
 21// multisig step — because a domain's owner is frozen into its record at
 22// registration time. After a rotation, every registration's remainder
 23// would have been paid to the retired key instead of the treasury, with
 24// no error and no log line.
 25//
 26// The heuristic is gone rather than patched. Domains are admin-only to
 27// create and permanently non-transferable, so every domain is
 28// project-owned by construction and 100% of every payment belongs to the
 29// treasury. `platformFeeBps` stays in nsdata's config for a future logic
 30// realm that reintroduces third-party domain ownership; reinstating the
 31// split is then a logic-realm swap, not a migration.
 32func takePayment(cur realm, price int64) {
 33	takePaymentSplit(cur, price, "", 0)
 34}
 35
 36// takePaymentSplit is takePayment with a referrer's cut carved out of the
 37// same price. The buyer pays exactly what they were quoted; the cut is
 38// not added on top, it is a share of what the treasury would otherwise
 39// have received.
 40//
 41// All three legs settle inside the buyer's own transaction — treasury,
 42// referrer, refund — so this realm's balance is the same after the call
 43// as before it. That is not a stylistic preference: holding a referrer's
 44// cut until they claim it would make this an account you can have a
 45// balance in, and that is a different kind of thing to operate, subject
 46// to a different set of rules, run by people with licences. The cut is
 47// paid now or it is not paid.
 48func takePaymentSplit(cur realm, price int64, cutTo address, cut int64) {
 49	if !cur.Previous().IsUserCall() {
 50		panic("nslogic: only direct wallet calls may attach payment")
 51	}
 52	caller := cur.Previous().Address()
 53	sent := unsafe.OriginSend()
 54	assertOnlyUgnot(sent)
 55
 56	paid := sent.AmountOf(ugnot)
 57	if paid < price {
 58		panic(ufmt.Sprintf("nslogic: insufficient payment: need %d ugnot, got %d", price, paid))
 59	}
 60	excess := paid - price
 61
 62	// Belt and braces. referralCut already caps the percentage, but this
 63	// is the line where the arithmetic becomes money leaving an address,
 64	// and a bug upstream must not be able to pay out more than came in.
 65	if cut < 0 || cut > price || cutTo == "" {
 66		cut = 0
 67	}
 68
 69	b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
 70	pkgAddr := cur.Address()
 71	if cut > 0 {
 72		b.SendCoins(pkgAddr, cutTo, chain.Coins{chain.NewCoin(ugnot, cut)})
 73	}
 74	if price-cut > 0 {
 75		b.SendCoins(pkgAddr, nsdata.GetTreasury(), chain.Coins{chain.NewCoin(ugnot, price-cut)})
 76	}
 77	if excess > 0 {
 78		b.SendCoins(pkgAddr, caller, chain.Coins{chain.NewCoin(ugnot, excess)})
 79	}
 80}
 81
 82// assertOnlyUgnot rejects any other denomination outright. Only the
 83// ugnot amount was ever accounted for, and neither realm has a
 84// withdrawal path, so a foreign coin sent alongside a payment would have
 85// been stranded in this realm's address permanently. Refusing the whole
 86// transaction is simpler than sweeping it back and keeps the
 87// "this realm never retains funds" invariant absolute.
 88func assertOnlyUgnot(sent chain.Coins) {
 89	for _, c := range sent {
 90		if c.Denom != ugnot {
 91			panic("nslogic: only ugnot is accepted, got: " + c.Denom)
 92		}
 93	}
 94}
 95
 96// assertNoSend guards every entry point that has no price. Without it,
 97// coins attached to a free call (a renewal of a project domain, a
 98// profile edit, an admin setter) would land in this realm's address with
 99// no way back out.
100//
101// IT APPLIES TO DIRECT WALLET CALLS ONLY, and that qualification is not
102// a loosening — it is the guard finally matching its own reason.
103//
104// OriginSend() reports what the TRANSACTION carried, not what this call
105// received. Measured on sapphire-1 with a pair of probe realms: a user
106// attaches 500000ugnot to realm A, A calls realm B, and B's OriginSend()
107// reads 500000ugnot even though every one of those coins is sitting in
108// A's address. B never saw them and cannot strand them.
109//
110// So for a realm-to-realm call this was rejecting a payment this realm
111// never received. It is the reason a marketplace could not transfer a
112// name inside the same transaction that pays for it — the transfer is a
113// free call, the transaction is not, and the two are unrelated. A
114// two-transaction sale is the alternative, and that means somebody's
115// money or somebody's name sitting somewhere in between. Not here.
116func assertNoSend(cur realm) {
117	if !cur.Previous().IsUserCall() {
118		return // the coins went to whoever called us, not to us
119	}
120	if !unsafe.OriginSend().IsZero() {
121		panic("nslogic: this function takes no payment; do not attach coins")
122	}
123}