Transaction

96CE187C0776A2…CBD12876A88C

Block 77,263 · index 0 · indexed

Summary

Hash
96CE187C0776A245CA82AEE3DA9D119AB1B682ED9B5D50BD4ECBCBD12876A88C
Block
77,263
Size
21853 bytes
Gas used
28,386,480 / 34,063,740
Fee
34064 ugnot
Status
success

Messages

#1AddPackagegno.land/r/gnoswap/referral/v121 arguments

Arguments · 21

  1. #1referral
  2. #2README.md
  3. #3# Referral Referral system for tracking user relationships. ## Overview Manages referral relationships between users. Non-removal writes have a 24-hour cooldown per user. ## Global Functions ### `TryRegister(cur realm, addr address, referral string) string` Attempts to register, update, or remove a referral relationship and returns the effective referrer string. - An empty `referral` only reads and returns the user's current referrer. It does not require authorization or emit an event. - An authorized non-empty write that fails emits `ReferralRegistrationFailed` and returns the currently stored referrer. - Passing `ContractAddress()` as `referral` removes the relationship and returns an empty string. The zero address is not the removal sentinel and is invalid. ### `GetReferral(addr string) string` Returns the referral address for the given address. Returns empty string if not found. ### `HasReferral(addr string) bool` Returns true if the given address has a referral. ### `IsEmpty() bool` Returns true if no referrals exist in the system. ### `GetLastOpTimestamp(addr string) (int64, error)` Returns the last non-removal registration or update timestamp for the address. Returns `ErrNotFound` if no such operation has been recorded. ### `ContractAddress() string` Returns the address of the referral contract. Pass this value as `referral` to remove an existing relationship. ## Gnoweb `Render("")` explains the registration interval and removal behavior. `Render("address/<address>")` shows the current referrer and last registration or update time in UTC. Missing records and unsupported paths return `404`. ## Usage ### Registering or Updating a Referral ```go package example import ( "gno.land/r/gnoswap/referral/v1" ) // RegisterUserReferral registers or updates a referral relationship. // The returned string is the effective referrer. func RegisterUserReferral(cur realm, userAddr, referrerAddr address) string { return referral.TryRegister(cross(cur), userAddr, referrerAddr.String()) } ``` ### Removing a Referral ```go package example import ( "gno.land/r/gnoswap/referral/v1" ) // RemoveUserReferral removes the referral relationship for a user. func RemoveUserReferral(cur realm, userAddr address) string { return referral.TryRegister(cross(cur), userAddr, referral.ContractAddress()) } ``` ### Querying Referrals ```go package example import ( "gno.land/r/gnoswap/referral/v1" ) // GetUserReferrer returns the referrer address for a user. // Returns empty string if no referral exists. func GetUserReferrer(userAddr string) string { return referral.GetReferral(userAddr) } // CheckUserHasReferral returns true if the user has a registered referral. func CheckUserHasReferral(userAddr string) bool { return referral.HasReferral(userAddr) } ``` ## Rate Limiting - Non-removal registrations and updates are limited to one operation per 24 hours per address. - Passing the referral contract's own address removes a relationship and bypasses the rate-limit check. - Removal does not overwrite the previous non-removal timestamp, so immediate re-registration can still be rejected while that timestamp is within the cooldown. ## Events - `RegisterReferral` is emitted for every successful non-empty write, including creation, update, and removal. - `ReferralRegistrationFailed` is emitted when an authorized non-empty write fails. - Empty referral queries do not emit events. ## Security - One referral per address - Self-referrals are rejected - Non-empty writes require an authorized caller - The referral contract's own address is the removal sentinel; the zero address is invalid
  4. #4assert.gno
  5. #5package referral import ( prabc "gno.land/p/gnoswap/rbac/v1" "gno.land/r/gnoswap/access/v1" _ "gno.land/r/gnoswap/rbac/v1" ) // MUST BE IMMUTABLE, DO NOT MODIFY. // validCallerRoles is a list of roles that are authorized to modify referral data. // This includes governance, governance staker, router, position, staker, and launchpad contracts. var validCallerRoles = []string{ prabc.ROLE_GOVERNANCE.String(), prabc.ROLE_GOV_STAKER.String(), prabc.ROLE_ROUTER.String(), prabc.ROLE_POSITION.String(), prabc.ROLE_STAKER.String(), prabc.ROLE_LAUNCHPAD.String(), } // assertValidCaller checks if the caller address has permission to modify referral data. // Only addresses with specific roles defined in validCallerRoles are authorized. // // Errors: // - ErrUnauthorized: the caller is not authorized to modify referral data func assertValidCaller(caller address) { for _, role := range validCallerRoles { if access.IsAuthorized(role, caller) { return } } panic(makeErrorWithDetails(ErrUnauthorized, caller.String())) }
  6. #6doc.gno
  7. #7// Package referral implements a referral system on Gno. It allows // authorized contracts to register, update, or remove referral // relationships. A referral link is defined as a mapping from one // address (the "user") to another address (the "referrer"). // // ## Overview // // The referral package is composed of the following components: // // 1. **errors.gno**: Defines error types for invalid addresses, // unauthorized callers, self-referrals, rate limits, and missing referrals. // 2. **assert.gno**: Checks whether a caller has an authorized role. // 3. **type.gno**: Defines the ReferralKeeper interface and the contract-address // sentinel used to request removal. // 4. **keeper.gno**: Implements ReferralKeeper with BPTree storage. Non-removal // registrations and updates have a 24-hour cooldown. // 5. **global_keeper.gno**: Exposes the public API and emits referral events. // // ## Public API // // The package exposes the following public functions: // // - GetReferral(addr string) string: Returns the referrer address for // the given user. Returns empty string if not found. // - HasReferral(addr string) bool: Returns true if the user has a // registered referrer. // - IsEmpty() bool: Returns true if no referral relationships exist. // - GetLastOpTimestamp(addr string) (int64, error): Returns the last // non-removal registration or update timestamp for the user. // - TryRegister(cur realm, addr address, referral string) string: // Empty input reads the stored referral without authorization. A non-empty // input registers, updates, or removes a relationship and returns the // effective referrer string. Passing ContractAddress() requests removal. // // ## Workflow // // Typical usage of this contract follows these steps: // // 1. A caller uses TryRegister to resolve the effective referrer. Empty input // only reads; non-empty input must come from an authorized role. // 2. The keeper validates the caller's permissions via assertValidCaller. // 3. Address validation ensures the user address is valid, the referral is // valid for registration, and self-referrals are rejected. The contract's // own address is the removal sentinel. // 4. The 24-hour rate limit is checked for non-removal registrations and // updates. Removal bypasses this check. // 5. Successful non-removal writes store a new timestamp. Successful // non-empty writes emit a RegisterReferral event. // // ## Authorized Callers // // Only contracts with the following roles can modify referral data: // // - ROLE_GOVERNANCE: Governance contracts // - ROLE_GOV_STAKER: Governance staker contracts // - ROLE_ROUTER: Router contracts // - ROLE_POSITION: Position manager contracts // - ROLE_STAKER: Staker contracts // - ROLE_LAUNCHPAD: Launchpad contracts // // ## Rate Limiting // // To prevent abuse, the system enforces a 24-hour cooldown between non-removal // registrations or updates for each address. This means: // // - A new referral can only be registered once per 24 hours per address. // - Updates are also subject to the same rate limit. // - Removal via the contract-address sentinel bypasses the rate-limit check. // - Removal does not overwrite the previous timestamp, so immediate // re-registration can still be rejected during the prior cooldown. // - Attempts that exceed the cooldown return ErrTooManyRequests. // // ## Events // // The package emits the following events: // // - RegisterReferral: Emitted for every successful non-empty write, including // creation, update, and removal. // - ReferralRegistrationFailed: Emitted when an authorized non-empty write // fails. // - Empty referral queries do not emit events. // // ## Error Handling // // The package defines several error types: // - `ErrInvalidAddress`: Returned when an address format is invalid // - `ErrSelfReferral`: Returned when attempting to set self as referrer // - `ErrUnauthorized`: Returned when the caller lacks permission // - `ErrTooManyRequests`: Returned when rate limit is exceeded (24-hour cooldown) // - `ErrNotFound`: Returned when attempting to get a non-existent referral // - `ErrInvalidTime`: Returned when the stored timestamp format is invalid // // ## Example: Integration with Router Contract // // The router contract can register referrals during swap operations: // // ```go // // import ( // "gno.land/r/gnoswap/referral/v1" // ) // // func SwapWithReferral(cur realm, referralCode string, ...) { // // Get the caller address // caller := cur.Previous().Address() // // actualReferrer := referral.TryRegister(cross(cur), caller, referralCode) // // // Continue with swap logic... // } // // ``` // // ## Example: Checking Referral for Rewards // // Other contracts can check referral relationships for reward distribution: // // ```go // // import ( // "gno.land/r/gnoswap/referral/v1" // ) // // func DistributeRewards(user address, amount uint64) { // // Check if user has a referrer // if referral.HasReferral(user.String()) { // referrerAddr := referral.GetReferral(user.String()) // // Calculate and distribute referral bonus // referrerBonus := amount * referralRate / 100 // sendReward(address(referrerAddr), referrerBonus) // } // } // // ``` // // ## Limitations and Constraints // // - A user can have only one referrer at a time. // - Self-referral is not allowed. // - Non-removal registrations and updates are rate-limited to once per 24 // hours per address. // - Only callers with an authorized role can perform non-empty writes. // - The referral contract's own address is the removal sentinel; the zero // address is invalid. // // ## Notes // // - The contract uses RBAC (Role-Based Access Control) for authorization. // - Rate-limit state persists across transactions. // - The referral relationship is stored in a BPTree. package referral
  8. #8errors.gno
  9. #9package referral import ( ufmt "gno.land/p/nt/ufmt/v0" ) const ( ErrInvalidAddress = "invalid address format" ErrSelfReferral = "self referral is not allowed" ErrUnauthorized = "unauthorized caller" ErrTooManyRequests = "too many requests: operations allowed once per 24 hours for each address" ErrNotFound = "referral not found" ErrInvalidTime = "invalid time format" ) func makeErrorWithDetails(message string, detail string) error { return ufmt.Errorf("%s: %s", message, detail) }
  10. #10global_keeper.gno
  11. #11package referral import ( "chain" ) var gReferralKeeper ReferralKeeper // EventRegisterFailed is emitted when an authorized non-empty write fails. // Successful non-empty writes emit the RegisterReferral event. const EventRegisterFailed = "ReferralRegistrationFailed" func init() { if gReferralKeeper == nil { gReferralKeeper = NewKeeper() } } // GetReferral returns the referral address string stored for the given address. // // Parameters: // - addr: address string whose referral relationship is queried. // // Returns: // - referral: stored referrer address string, or an empty string when no valid referral is found. func GetReferral(addr string) string { referral, err := gReferralKeeper.get(address(addr)) if err != nil { return "" } return referral.String() } // HasReferral reports whether the given address has a stored referral. // // Parameters: // - addr: address string whose referral relationship is checked. // // Returns: // - hasReferral: true when a referral record exists; false when it is absent or invalid. func HasReferral(addr string) bool { _, err := gReferralKeeper.get(address(addr)) return err == nil } // IsEmpty reports whether the referral keeper contains no referral records. // // Returns: // - empty: true when the keeper store has zero referral records. func IsEmpty() bool { return gReferralKeeper.isEmpty() } // GetLastOpTimestamp returns the last non-removal registration or update timestamp for an address. // // Parameters: // - addr: address string whose last non-removal operation is queried. // // Returns: // - timestamp: Unix timestamp of the last successful registration or update. // - error: nil when a timestamp exists; otherwise an invalid-address or ErrNotFound error. func GetLastOpTimestamp(addr string) (int64, error) { return gReferralKeeper.getLastOpTimestamp(address(addr)) } // ContractAddress returns the address of the referral contract. // Use this address as the referral parameter in TryRegister to remove an existing referral. // // Returns: // - address: referral contract address string, used as the removal sentinel. func ContractAddress() string { return selfAddress.String() } // TryRegister attempts to register, update, or remove a referral. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // - addr: address whose referral relationship is read or changed. // - referral: empty string for a read, ContractAddress() to remove, or a candidate referrer address string. // // Returns: // - effectiveReferral: stored referrer after the operation or fallback; empty when none is stored. // // Empty input is treated as a read and returns the stored referrer without // authorization, rate-limit checks, or event emission. // Non-empty input requires an authorized caller. func TryRegister(cur realm, addr address, referral string) string { if referral == "" { return GetReferral(addr.String()) } caller := cur.Previous().Address() assertValidCaller(caller) result, err := gReferralKeeper.register(addr, address(referral)) if err != nil { chain.Emit( EventRegisterFailed, "address", addr.String(), "error", err.Error(), ) return GetReferral(addr.String()) } chain.Emit( "RegisterReferral", "prevAddr", caller.String(), "address", addr.String(), "referral", result.String(), ) return result.String() }
  12. #12gnomod.toml
  13. #13module = "gno.land/r/gnoswap/referral/v1" gno = "0.9"
  14. #14keeper.gno
  15. #15package referral import ( "errors" "time" bptree "gno.land/p/nt/bptree/v0" ) const ( // MinTimeBetweenUpdates is minimum duration between operations (24 hours). MinTimeBetweenUpdates int64 = 24 * 60 * 60 ) // keeper implements ReferralKeeper using BPTree storage. // Non-removal registrations and updates use a 24-hour cooldown; removal via // the contract-address sentinel bypasses that check and preserves the timestamp. type keeper struct { store *bptree.BPTree // address(string) -> referral address(string) lastOps *bptree.BPTree // address(string) -> last operation timestamp(int64) } var _ ReferralKeeper = &keeper{} // NewKeeper creates an empty ReferralKeeper backed by independent referral and // last-operation BPTrees. // // Returns: // - keeper: new referral store with no relationships or operation timestamps func NewKeeper() ReferralKeeper { return &keeper{ store: bptree.NewBPTreeN(16), lastOps: bptree.NewBPTreeN(16), } } // register creates or updates a referral relationship between addresses. // Setting refAddr to the contract's own address removes the referral. func (k *keeper) register(addr, refAddr address) (address, error) { if err := k.validateAddresses(addr, refAddr); err != nil { return zeroAddress, err } addrStr := addr.String() refAddrStr := refAddr.String() if isRemovalRequest(refAddr) { if k.has(addr) { _, ok := k.store.Remove(addrStr) if !ok { return zeroAddress, errors.New(ErrNotFound) } } return zeroAddress, nil } if err := k.checkRateLimit(addrStr); err != nil { return zeroAddress, err } k.store.Set(addrStr, refAddrStr) k.lastOps.Set(addrStr, time.Now().Unix()) return refAddr, nil } // validateAddresses validates that addresses are properly formatted and not self-referencing. func (k *keeper) validateAddresses(addr, refAddr address) error { if !addr.IsValid() || (!isRemovalRequest(refAddr) && !refAddr.IsValid()) { return errors.New(ErrInvalidAddress) } if addr == refAddr { return errors.New(ErrSelfReferral) } return nil } // has returns true if a referral exists for the given address. func (k *keeper) has(addr address) bool { exists := k.store.Get(addr.String()) != nil return exists } // get retrieves the referral address for a given address. // Returns ErrNotFound if no referral exists. func (k *keeper) get(addr address) (address, error) { if !addr.IsValid() { return zeroAddress, errors.New(ErrInvalidAddress) } val := k.store.Get(addr.String()) if val == nil { return zeroAddress, errors.New(ErrNotFound) } refAddr, ok := val.(string) if !ok { return zeroAddress, errors.New(ErrInvalidAddress) } return address(refAddr), nil } // isEmpty returns true if no referrals exist in the store. func (k *keeper) isEmpty() bool { return k.store.Size() == 0 } // getLastOpTimestamp retrieves the last operation timestamp for a given address. // Returns ErrNotFound if no operation exists. func (k *keeper) getLastOpTimestamp(addr address) (int64, error) { if !addr.IsValid() { return 0, errors.New(ErrInvalidAddress) } val := k.lastOps.Get(addr.String()) if val == nil { return 0, errors.New(ErrNotFound) } ts, ok := val.(int64) if !ok { return 0, errors.New(ErrInvalidTime) } return ts, nil } // checkRateLimit verifies if enough time has passed since the last operation. // Returns ErrTooManyRequests if rate limit is exceeded. func (k *keeper) checkRateLimit(addr string) error { now := time.Now().Unix() lastOpTimeRaw := k.lastOps.Get(addr) if lastOpTimeRaw == nil { return nil } lastOpTime, ok := lastOpTimeRaw.(int64) if !ok { return errors.New(ErrInvalidTime) } timeSinceLastOp := now - lastOpTime if timeSinceLastOp < MinTimeBetweenUpdates { return errors.New(ErrTooManyRequests) } return nil }
  16. #16render.gno
  17. #17package referral import ( "strings" "time" "gno.land/p/moul/md/v0" "gno.land/p/moul/mdtable/v0" ufmt "gno.land/p/nt/ufmt/v0" ) // Render describes referral registration or shows the referral for an address. func Render(path string) string { if path == "" { table := mdtable.Table{ Headers: []string{"Rule", "Value"}, Rows: [][]string{ {"Minimum interval between registrations or updates", ufmt.Sprintf("%d hours", MinTimeBetweenUpdates/3600)}, }, } return md.H1("Gnoswap Referrals") + "\n" + md.Paragraph("Authorized protocol contracts register a referrer for each address.") + md.H2("Registration") + "\n" + table.String() + "\n" + md.Paragraph( "Removing a referral bypasses the interval but does not reset the last registration time.\n"+ "Address-specific records are available at "+md.InlineCode("address/<address>")+".", ) } parts := strings.Split(path, "/") if len(parts) != 2 || parts[0] != "address" || parts[1] == "" { return "404\n" } addr := parts[1] if !HasReferral(addr) { return "404\n" } lastOpTimestamp, err := GetLastOpTimestamp(addr) if err != nil { return "404\n" } table := mdtable.Table{ Headers: []string{"Field", "Value"}, Rows: [][]string{ {"Address", addr}, {"Referrer", GetReferral(addr)}, {"Last registration or update (UTC)", time.Unix(lastOpTimestamp, 0).UTC().Format("2006-01-02 15:04:05")}, }, } return md.H1("Gnoswap Referral") + "\n" + md.H2("Referral record") + "\n" + table.String() }
  18. #18type.gno
  19. #19package referral const zeroAddress = address("") // selfAddress is cached at package initialization to ensure consistent comparison. var selfAddress address func init(cur realm) { selfAddress = cur.Address() } // contractAddress returns the address of the referral contract. // This is used as a sentinel value for removing referrals. func contractAddress() address { return selfAddress } // isRemovalRequest checks if the given address indicates a removal request. // Removal is indicated by passing the contract's own address as the referral. func isRemovalRequest(refAddr address) bool { return refAddr == selfAddress } // ReferralKeeper defines the interface for managing referral relationships. type ReferralKeeper interface { // register creates or updates a referral relationship between addresses. // Setting refAddr to the contract's own address removes the referral. register(addr, refAddr address) (address, error) // has returns true if a referral exists for the given address. has(addr address) bool // get retrieves the referral address for a given address. get(addr address) (address, error) // isEmpty returns true if no referrals exist in the system. isEmpty() bool // getLastOpTimestamp returns the last operation timestamp for an address. getLastOpTimestamp(addr address) (int64, error) }
  20. #20/gno.MemPackageType
  21. #21 MPUserAll

Result log

msg:0,success:true,log:,events:[]