Transaction
CEB00A109C2D4A…FC830118C1D9
Block 317,473 · index 0 · indexed
Summary
- Hash
- CEB00A109C2D4ADC234E7868BA1B674C37CF40B643055C590BF8FC830118C1D9
- Block
- 317,473
- Size
- 21764 bytes
- Gas used
- 29,635,699 / 200,000,000
- Fee
- 1000000 ugnot
- Status
- success
Messages
- Attached funds
- 90000000 ugnot
Arguments · 11
- #1grc721
- #2gnomod.toml
- #3module = "gno.land/p/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/grc721/v0" gno = "0.9"
- #4tellers.gno
- #5package grc721 import ( "chain" ) // CallerTeller resolves the acting account at each write as rlm.Previous() — // the realm that crossed into the realm holding the teller. // // SECURITY: this accessor hangs off *PrivateLedger, not *Token, and that is // load-bearing. A frame-relative teller acts as whoever crossed into its // holder, so it is only meaningful inside the token's own realm, where the // wrappers act for a caller who knowingly invoked the collection. Anywhere // else it is a confused deputy: a realm a user merely calls could move that // user's tokens. The *Token pointer is published — exported vars, the // collection facade, grc721reg — while the ledger is not, since NewToken hands // it to the creating realm and nowhere else. So a foreign realm cannot mint // one. // // Construction privacy alone is not enough: a realm may legally build a teller // and then export the VALUE. The write methods therefore also verify that the // invoking realm is the token's own (see guardHome), which leaves a leaked // teller inert everywhere but home. func (led *PrivateLedger) CallerTeller() Teller { if led == nil { panic("Ledger cannot be nil") } return &fnTeller{ accountFn: func(_ int, rlm realm) address { return rlm.Previous().Address() }, homeGuard: true, Token: led.token, } } // Write methods return ErrReadonly. func (tok *Token) ReadonlyTeller() Teller { if tok == nil { panic("Token cannot be nil") } return &fnTeller{ accountFn: nil, Token: tok, } } // Permanently acts as the calling realm (verified via IsCurrent). func (tok *Token) RealmTeller(_ int, rlm realm) Teller { if tok == nil { panic("Token cannot be nil") } if !rlm.IsCurrent() { panic(ErrSpoofedRealm) } caller := rlm.Address() return &fnTeller{ accountFn: func(_ int, _ realm) address { return caller }, Token: tok, } } // Like RealmTeller but acts as a sub-account derived from slug. func (tok *Token) RealmSubTeller(_ int, rlm realm, slug string) Teller { if tok == nil { panic("Token cannot be nil") } if !rlm.IsCurrent() { panic(ErrSpoofedRealm) } account := accountSlugAddr(rlm.Address(), slug) return &fnTeller{ accountFn: func(_ int, _ realm) address { return account }, Token: tok, } } // Admin-grade: issuer-only (holds the PrivateLedger). Panics on invalid addr to close the empty-sentinel hole. func (led *PrivateLedger) ImpersonateTeller(addr address) Teller { if led == nil { panic("Ledger cannot be nil") } if !addr.IsValid() { panic(ErrInvalidAddress) } return &fnTeller{ accountFn: func(_ int, _ realm) address { return addr }, Token: led.token, } } // guardHome confines a frame-relative teller to the token's own realm. // Construction privacy stops a foreign realm from minting one; this stops a // minted one from travelling, which is what happens when a realm legally builds // a teller and then exports the value. // // The check is on the invoking realm's path alone — deliberately NOT on whether // the resolved actor is an end user. Keying on the actor only blocks the case // where the debited party is the signing user, and leaves two doors open: a // realm can be charged by a realm it calls, and TransferFrom resolves the // *spender* from the frame, so a realm reached from an honest hub spends that // hub's approvals against any owner who granted one. Both are the same defect // as the original one level up — frame-relative resolution means whoever you // call can act as you — and neither is reachable once the teller only works at // home. // // The host is compared after stripping any "#subpath" synthesized by realm.Sub, // so the token's own sub-realms are not falsely rejected. // // A foreign realm that needs to move a user's tokens uses the ordinary route: // the owner Approves it, and it spends as itself through RealmTeller, which is // eagerly bound to its own address and approval-gated. // // The leading int keeps this a plain method. func (ft *fnTeller) guardHome(_ int, rlm realm) error { if !ft.homeGuard { return nil } host, _, _ := splitPkgSubPath(rlm.PkgPath()) if host != ft.Token.origRealm { return ErrForeignCallerTeller } return nil } func (ft *fnTeller) Approve(_ int, rlm realm, to address, tid TokenID) error { if ft.accountFn == nil { return ErrReadonly } if !rlm.IsCurrent() { return ErrSpoofedRealm } if err := ft.guardHome(0, rlm); err != nil { return err } caller := ft.accountFn(0, rlm) return ft.Token.ledger.Approve(caller, to, tid) } func (ft *fnTeller) SetApprovalForAll(_ int, rlm realm, operator address, approved bool) error { if ft.accountFn == nil { return ErrReadonly } if !rlm.IsCurrent() { return ErrSpoofedRealm } if err := ft.guardHome(0, rlm); err != nil { return err } caller := ft.accountFn(0, rlm) return ft.Token.ledger.SetApprovalForAll(caller, operator, approved) } func (ft *fnTeller) TransferFrom(_ int, rlm realm, from, to address, tid TokenID) error { if ft.accountFn == nil { return ErrReadonly } if !rlm.IsCurrent() { return ErrSpoofedRealm } if err := ft.guardHome(0, rlm); err != nil { return err } spender := ft.accountFn(0, rlm) return ft.Token.ledger.TransferFrom(spender, from, to, tid) } func accountSlugAddr(addr address, slug string) address { if slug == "" { return addr } key := addr.String() + "/" + slug return chain.PackageAddress(key) }
- #6token.gno
- #7package grc721 import ( "chain" "math/overflow" "strconv" "gno.land/p/nt/seqid/v0" "gno.land/p/nt/ufmt/v0" ) // NewToken creates a core token and its ledger. // rlm must be the caller's own captured cur (IsCurrent); its PkgPath becomes the unforgeable origRealm. // // Every successful call emits a NewToken event carrying the resulting Token.ID(). // Because Token's fields are unexported, NewToken is the only way a Token can come // into existence, so this event makes token creation fully observable: an indexer // that sees the same Token.ID() announced twice knows the realm built two // independent ledgers behind one identifier, and that every later // Mint/Burn/Transfer/Approval carrying that id is ambiguous. Such a realm is // emitting untrustworthy events and should be flagged or ignored wholesale. func NewToken(name, symbol string, id seqid.ID, rlm realm) (*Token, *PrivateLedger) { if !rlm.IsCurrent() { panic(ErrSpoofedRealm) } pkgPath := rlm.PkgPath() if pkgPath == "" { panic(ErrNotRealm) } if !validName(name) { panic(ErrInvalidName) } if !validSymbol(symbol) { panic(ErrInvalidSymbol) } // origRealm is the host path, with any "#subpath" synthesized by realm.Sub // stripped. guardHome strips the invoking path the same way, so a token // created from a sub frame is not locked out of its own host realm, and a // host's sub-realms are not falsely rejected. Token.ID() keeps the raw // pkgPath, so identities and the NewToken event are unaffected. origRealm, _, _ := splitPkgSubPath(pkgPath) ledger := &PrivateLedger{} token := &Token{ id: pkgPath + "." + symbol + "." + id.String(), name: name, symbol: symbol, ledger: ledger, origRealm: origRealm, } ledger.token = token chain.Emit( NewTokenEvent, "token", token.id, "name", name, "symbol", symbol, ) return token, ledger } func validName(name string) bool { if name == "" || len(name) > MaxNameLen { return false } for _, c := range name { if c < 0x20 || c == 0x7f { return false } } return true } func validSymbol(s string) bool { if s == "" || len(s) > MaxSymbolLen { return false } for _, c := range s { if !isAlnum(c) && c != '_' && c != '-' { return false } } return true } func isAlnum(c rune) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') } func (tok *Token) GetName() string { return tok.name } func (tok *Token) GetSymbol() string { return tok.symbol } func (tok *Token) ID() string { return tok.id } func (tok *Token) TotalSupply() int64 { return tok.ledger.totalSupply } func (tok *Token) KnownAccounts() int { return tok.ledger.balances.Size() } // EIP-721: balanceOf throws for queries about the zero address, // since NFTs assigned to it are considered invalid. func (tok *Token) BalanceOf(addr address) (int64, error) { if !addr.IsValid() { return 0, ErrInvalidAddress } return tok.ledger.balanceOf(addr), nil } func (tok *Token) OwnerOf(tid TokenID) (address, error) { return tok.ledger.ownerOf(tid) } // EIP-721: getApproved throws for a token that is not a valid NFT, so a missing // token is ErrInvalidTokenId and a live but unapproved one is ErrTokenIdNotApproved. func (tok *Token) GetApproved(tid TokenID) (address, error) { if !tok.ledger.exists(tid) { return zeroAddress, ErrInvalidTokenId } addr := tok.ledger.tokenApprovals.Get(tid.String()) if addr == nil { return zeroAddress, ErrTokenIdNotApproved } return addr.(address), nil } func (tok *Token) IsApprovedForAll(owner, operator address) bool { return tok.ledger.isApprovedForAll(owner, operator) } func (tok *Token) RenderHome() string { str := "" str += ufmt.Sprintf("# %s ($%s)\n\n", tok.name, tok.symbol) str += ufmt.Sprintf("* **Total supply**: %d\n", tok.ledger.totalSupply) str += ufmt.Sprintf("* **Known accounts**: %d\n", tok.KnownAccounts()) return str } // RegisterExtension attaches an issuer-supplied hook set. Issuer-only (holds the // PrivateLedger), which keeps the surface from being attacker-attachable — but the // attachment is still a privilege: per the Extension doc, an attached hook can veto // transfers (a panic aborts the transaction before the Transfer event is emitted) and // adds gas to every movement. // One extension per kind; a second registration of an attached kind panics. func (led *PrivateLedger) RegisterExtension(ext Extension) { if ext == nil { panic("grc721: nil extension") } kind := ext.ExtensionKind() for _, e := range led.extensions { if e.ExtensionKind() == kind { panic("grc721: extension kind already registered: " + kind) } } led.extensions = append(led.extensions, ext) } func (led *PrivateLedger) ReadToken() *Token { return led.token } func (led *PrivateLedger) Mint(to address, tid TokenID) error { if !to.IsValid() { return ErrInvalidAddress } if tid == "" { return ErrInvalidTokenId } if led.exists(tid) { return ErrTokenIdAlreadyExists } led.balances.Set(to.String(), overflow.Add64p(led.balanceOf(to), 1)) led.owners.Set(tid.String(), to) led.totalSupply = overflow.Add64p(led.totalSupply, 1) for _, ext := range led.extensions { ext.OnMint(to, tid) } // Mint emits Transfer from the empty address (EIP-721); indexers reconstruct ownership from Transfer alone. chain.Emit( TransferEvent, "token", led.token.id, "from", "", "to", to.String(), "tokenId", tid.String(), ) return nil } // Fans OnBurn out to every extension so per-token state cannot resurface if tid is re-minted. func (led *PrivateLedger) Burn(tid TokenID) error { owner, err := led.ownerOf(tid) if err != nil { return err } tidStr := tid.String() led.tokenApprovals.Remove(tidStr) led.setBalance(owner, overflow.Sub64p(led.balanceOf(owner), 1)) led.owners.Remove(tidStr) led.totalSupply = overflow.Sub64p(led.totalSupply, 1) for _, ext := range led.extensions { ext.OnBurn(tid) } // Burn emits Transfer to the empty address (EIP-721). chain.Emit( TransferEvent, "token", led.token.id, "from", owner.String(), "to", "", "tokenId", tidStr, ) return nil } // spender must own or be approved for tid; callers derive it from a trusted per-frame identity. func (led *PrivateLedger) TransferFrom(spender, from, to address, tid TokenID) error { if !led.isApprovedOrOwner(spender, tid) { return ErrCallerIsNotOwnerOrApproved } return led.transfer(from, to, tid) } // caller must own tid or be an approved operator of the owner. // EIP-721: approving the zero address clears (revokes) any single-token approval. func (led *PrivateLedger) Approve(caller, to address, tid TokenID) error { revoke := to == zeroAddress if !revoke && !to.IsValid() { return ErrInvalidAddress } owner, err := led.ownerOf(tid) if err != nil { return err } if owner == to { return ErrApprovalToCurrentOwner } if caller != owner && !led.isApprovedForAll(owner, caller) { return ErrCallerIsNotOwnerOrApproved } if revoke { led.tokenApprovals.Remove(tid.String()) } else { led.tokenApprovals.Set(tid.String(), to) } chain.Emit( ApprovalEvent, "token", led.token.id, "owner", owner.String(), "to", to.String(), // empty for a revoke "tokenId", tid.String(), ) return nil } func (led *PrivateLedger) SetApprovalForAll(owner, operator address, approved bool) error { if !owner.IsValid() || !operator.IsValid() { return ErrInvalidAddress } if owner == operator { return ErrApprovalToCurrentOwner } key := operatorKey(owner, operator) if approved { led.operatorApprovals.Set(key, approved) } else { led.operatorApprovals.Remove(key) } chain.Emit( ApprovalForAllEvent, "token", led.token.id, "owner", owner.String(), "to", operator.String(), "approved", strconv.FormatBool(approved), ) return nil } func (led *PrivateLedger) transfer(from, to address, tid TokenID) error { if !from.IsValid() || !to.IsValid() { return ErrInvalidAddress } if from == to { return ErrCannotTransferToSelf } owner, err := led.ownerOf(tid) if err != nil { return err } if owner != from { return ErrTransferFromIncorrectOwner } tidStr := tid.String() led.tokenApprovals.Remove(tidStr) led.setBalance(from, overflow.Sub64p(led.balanceOf(from), 1)) led.setBalance(to, overflow.Add64p(led.balanceOf(to), 1)) led.owners.Set(tidStr, to) for _, ext := range led.extensions { ext.OnTransfer(from, to, tid) } chain.Emit( TransferEvent, "token", led.token.id, "from", from.String(), "to", to.String(), "tokenId", tidStr, ) return nil } func (led *PrivateLedger) ownerOf(tid TokenID) (address, error) { owner := led.owners.Get(tid.String()) if owner == nil { return zeroAddress, ErrInvalidTokenId } return owner.(address), nil } func (led *PrivateLedger) balanceOf(addr address) int64 { balance := led.balances.Get(addr.String()) if balance == nil { return 0 } return balance.(int64) } func (led *PrivateLedger) setBalance(addr address, balance int64) { if balance == 0 { led.balances.Remove(addr.String()) return } led.balances.Set(addr.String(), balance) } func (led *PrivateLedger) isApprovedForAll(owner, operator address) bool { approved := led.operatorApprovals.Get(operatorKey(owner, operator)) if approved == nil { return false } return approved.(bool) } func (led *PrivateLedger) isApprovedOrOwner(addr address, tid TokenID) bool { owner := led.owners.Get(tid.String()) if owner == nil { return false } ownerAddr := owner.(address) if addr == ownerAddr || led.isApprovedForAll(ownerAddr, addr) { return true } approved := led.tokenApprovals.Get(tid.String()) return approved != nil && approved.(address) == addr } func (led *PrivateLedger) exists(tid TokenID) bool { return led.owners.Has(tid.String()) } func operatorKey(owner, operator address) string { return owner.String() + ":" + operator.String() }
- #8types.gno
- #9package grc721 import ( "errors" "gno.land/p/nt/avl/v0" ) type TokenID string func (t TokenID) String() string { return string(t) } // A Teller is a capability that acts as some account whenever it writes. // IsCanonicalTeller confirms a Teller was minted by this package — an embedding // forgery fails the check — but it does NOT reveal which account the Teller acts // as: a caller-scoped CallerTeller and the admin-grade ImpersonateTeller (which // acts as an arbitrary address) are both canonical. So it is an authenticity // check on the implementation, not an authorization guard on the acting account; // a caller accepting a Teller from outside must still establish, out of band, what // account that Teller is entitled to act as. // safeTransferFrom is omitted: EIP-721's receiver check needs a registry, and aliasing it would imply false safety. type Teller interface { GetName() string GetSymbol() string ID() string TotalSupply() int64 BalanceOf(owner address) (int64, error) OwnerOf(tid TokenID) (address, error) GetApproved(tid TokenID) (address, error) IsApprovedForAll(owner, operator address) bool Approve(_ int, rlm realm, to address, tid TokenID) error SetApprovalForAll(_ int, rlm realm, operator address, approved bool) error TransferFrom(_ int, rlm realm, from, to address, tid TokenID) error } // Extension hooks fire on every mint/transfer/burn. This is a trust grant to the // issuer, not an EIP-721 feature: only the issuer can attach one (RegisterExtension // holds the PrivateLedger), but once attached the hook runs arbitrary issuer code on // every movement, a panic in OnMint/OnTransfer/OnBurn aborts the transaction before // the movement is announced — an implicit veto over transfers — and gas scales // linearly with the extension count. Hooks run after the ledger write so they observe // the post-movement state; only a caller that recovers from the panic keeps that write. // Holders of a collection's tokens are therefore trusting the issuer not to freeze or // tax movement through this surface. Hooks take no rlm params (they cannot capture cur); // attach only via RegisterExtension. type Extension interface { ExtensionKind() string // unique; duplicates rejected at register OnMint(to address, tid TokenID) OnTransfer(from, to address, tid TokenID) OnBurn(tid TokenID) } type ExtensionView interface { ExtensionKind() string TokenID() string // core Token.ID } type Token struct { id string // raw creation pkgpath + "." + symbol + "." + id name string symbol string ledger *PrivateLedger // origRealm is the PkgPath of the realm that created the token, captured // unforgeably in NewToken. A frame-relative teller only works there. origRealm string } type PrivateLedger struct { token *Token totalSupply int64 owners avl.Tree // TokenID -> owner address balances avl.Tree // owner address -> int64 tokenApprovals avl.Tree // TokenID -> approved address operatorApprovals avl.Tree // "owner:operator" -> bool extensions []Extension } var ( ErrInvalidTokenId = errors.New("invalid token id") ErrInvalidAddress = errors.New("invalid address") ErrTokenIdNotApproved = errors.New("token id not approved for anyone") ErrApprovalToCurrentOwner = errors.New("approval to current owner") ErrCallerIsNotOwner = errors.New("caller is not token owner") ErrCannotTransferToSelf = errors.New("cannot send transfer to self") ErrTransferFromIncorrectOwner = errors.New("transfer from incorrect owner") ErrCallerIsNotOwnerOrApproved = errors.New("caller is not token owner or approved") ErrTokenIdAlreadyExists = errors.New("token id already exists") ErrReadonly = errors.New("teller is readonly") ErrSpoofedRealm = errors.New("rlm does not match the current crossing frame") ErrForeignCallerTeller = errors.New("frame-relative teller used outside the token's realm") ErrNotRealm = errors.New("rlm must be a realm (got EOA/origin)") ErrInvalidName = errors.New("invalid token name (empty, too long, or contains control chars)") ErrInvalidSymbol = errors.New("invalid token symbol (empty, too long, or contains chars outside [A-Za-z0-9_-])") ) // Symbol charset matches grc20reg slug (embedded in Token.ID / events). const ( MaxNameLen = 64 MaxSymbolLen = 11 ) // splitPkgSubPath matches chain.SplitPkgSubPath ('#' host/sub separator). // Local / Pearl stdlib does not export that helper yet. func splitPkgSubPath(p string) (host, subpath string, ok bool) { for i := 0; i < len(p); i++ { if p[i] == '#' { return p[:i], p[i+1:], true } } return p, "", false } const ( // NewToken announces every token creation; see NewToken for why it is a // complete provenance signal. NewTokenEvent = "NewToken" // Mint emits from empty addr; burn emits to empty addr (EIP-721). TransferEvent = "Transfer" ApprovalEvent = "Approval" ApprovalForAllEvent = "ApprovalForAll" ) var zeroAddress = address("") type fnTeller struct { accountFn func(_ int, rlm realm) address // homeGuard marks a frame-relative teller, whose writes are confined to // the token's own realm. See fnTeller.guardHome. homeGuard bool *Token } var _ Teller = (*fnTeller)(nil) // IsCanonicalTeller reports whether t was minted by this package, rejecting Tellers // forged by embedding *fnTeller in a wrapper. It does NOT discriminate by capability // grade: CallerTeller, ReadonlyTeller, RealmTeller, RealmSubTeller and the admin-grade // ImpersonateTeller all return true. Do not treat a true result as proof that a Teller // is a caller-scoped capability — establish the acting account separately. func IsCanonicalTeller(t Teller) bool { _, ok := t.(*fnTeller) return ok }
- #10/gno.MemPackageType
- #11 MPUserAll
Result log
msg:0,success:true,log:,events:[]