Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: referral #459

Merged
merged 11 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions referral/doc.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Package referral implements a referral system on Gno. It allows
// any authorized caller to register, update, or remove referral
// information. 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**: Provides custom error types (ReferralError) with
// specific error codes and messages.
// 2. **utils.gno**: Contains utility functions for permission checks,
// especially isValidCaller, which ensures only specific, pre-authorized
// callers (e.g., governance or router addresses) can invoke the core
// functions.
// 3. **types.gno**: Defines core constants for event types, attributes,
// and the ReferralKeeper interface, which outlines the fundamental
// methods of the referral system (Register, Update, Remove, etc.).
// 4. **keeper.gno**: Implements the actual business logic behind the
// ReferralKeeper interface. It uses an AVL Tree (avl.Tree) to store
// referral data (address -> referrer). The keeper methods emit events
// when a new referral is registered, updated, or removed.
// 5. **referral.gno**: Exposes a public API (the Referral struct)
// that delegates to the keeper, providing external contracts or
// applications a straightforward way to interact with the system.
//
// ## Workflow
//
// Typical usage of this contract follows these steps:
//
// 1. A caller with valid permissions invokes Register, Update, or Remove
// through the Referral struct.
// 2. The Referral struct forwards the request to the internal keeper
// methods.
// 3. The keeper checks caller permission (via isValidCaller), validates
// addresses, and stores or removes data in the AVL Tree.
// 4. An event is emitted for off-chain or cross-module notifications.
//
// ## Integration with Other Contracts
//
// Other contracts can leverage the referral system in two major ways:
//
// 1. **Direct Calls**: If you wish to directly call this contract,
// instantiate the Referral object (via NewReferral) and invoke its
// methods, assuming you meet the authorized-caller criteria.
//
// 2. **Embedded or Extended**: If you have a complex module that includes
// referral features, import this package and embed a Referral instance
// in your own keeper. This way, you can handle additional validations
// or custom logic before delegating to the existing referral functions.
//
// ## Error Handling
//
// The package defines several error types through ReferralError:
// - `ErrInvalidAddress`: Returned when an address format is invalid
// - `ErrUnauthorized`: Returned when the caller lacks permission
// - `ErrNotFound`: Returned when attempting to get a non-existent referral
// - `ErrZeroAddress`: Returned when attempting operations with zero address
//
// ## Example: Integration with a Staking Contract
//
// Suppose you have a staking contract that wants to reward referrers
// when a new user stakes tokens:
//
// ```go
//
// import (
// "std"
// "gno.land/r/gnoswap/v1/referral"
// "gno.land/p/demo/mystaking" // example staking contract
// )
//
// func rewardReferrerOnStake(user std.Address, amount int) {
// // 1) Access the referral system
// r := referral.NewReferral()
//
// // 2) Get the user's referrer
// refAddr, err := r.GetReferral(user)
// if err != nil {
// // handle error or skip if not found
// return
// }
//
// // 3) Reward the referrer
// mystaking.AddReward(refAddr, calculateReward(amount))
// }
//
// ```
//
// In this simple example, the staking contract checks if the user has
// a referrer by calling `GetReferral`. If a referrer is found, it then
// calculates a reward based on the staked amount.
//
// ## Limitations and Constraints
//
// - A user can have only one referrer at a time
// - Once a referral is removed, it cannot be automatically restored
// - Only authorized contracts can modify referral relationships
// - Address validation is strict and requires proper Bech32 format
//
// # Notes
//
// - The contract strictly enforces caller restrictions via isValidCaller.
// Make sure to configure it to permit only the addresses or roles that
// should be able to register or update referrals.
// - Zero addresses are treated as a trigger for removing a referral record.
// - The system emits events (register_referral, update_referral, remove_referral)
// which can be consumed by other on-chain or off-chain services.
package referral
63 changes: 63 additions & 0 deletions referral/errors.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package referral

import (
"gno.land/p/demo/ufmt"
)

const (
ErrNone = iota
ErrInvalidAddress
ErrZeroAddress
ErrSelfReferral
ErrUnauthorized
ErrInvalidCaller
ErrCyclicReference
ErrTooManyRequests
ErrNotFound
)

var errorMessages = map[int]string{
ErrInvalidAddress: "invalid address format",
ErrZeroAddress: "zero address is not allowed",
ErrSelfReferral: "self referral is not allowed",
ErrUnauthorized: "unauthorized caller",
ErrInvalidCaller: "invalid caller",
ErrCyclicReference: "cyclic reference is not allowed",
ErrTooManyRequests: "too many requests: operations allowed once per 24 hours for each address",
ErrNotFound: "referral not found",
}

type ReferralError struct {
Code int
Message string
Err error
}

func (e *ReferralError) Error() string {
// TODO: format error message to follow previous error message format
if e.Err != nil {
return ufmt.Sprintf("code: %d, message: %s, error: %v", e.Code, e.Message, e.Err)
}
return ufmt.Sprintf("code: %d, message: %s", e.Code, e.Message)
}

func (e *ReferralError) Unwrap() error {
return e.Err
}

func NewError(code int, args ...interface{}) *ReferralError {
msg := errorMessages[code]
var err error

if len(args) > 0 {
if lastArg, ok := args[len(args)-1].(error); ok {
err = lastArg
}
}

return &ReferralError{
Code: code,
Message: msg,
Err: err,
}
}
41 changes: 41 additions & 0 deletions referral/global_keeper.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package referral

import "std"

// gReferralKeeper is the global instance of the referral keeper
var gReferralKeeper ReferralKeeper

func init() {
gReferralKeeper = NewKeeper()
}

// GetKeeper returns the global instance of the referral keeper
//
// Example:
//
// // In other packages:
// keeper := referral.GetKeeper()
// keeper.register(addr, refAddr)
func GetKeeper() ReferralKeeper {
return gReferralKeeper
}

func GetReferral(addr string) string {
referral, err := gReferralKeeper.get(std.Address(addr))
if err != nil {
panic(err)
}
return referral.String()
}

func HasReferral(addr string) bool {
referral, err := gReferralKeeper.get(std.Address(addr))
if err != nil {
return false
}
return referral != zeroAddress
}

func IsEmpty() bool {
return gReferralKeeper.isEmpty()
}
1 change: 1 addition & 0 deletions referral/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/r/gnoswap/v1/referral
174 changes: 174 additions & 0 deletions referral/keeper.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package referral

import (
"std"
"time"

"gno.land/p/demo/avl"
)

const (
// MinTimeBetweenUpdates represents minimum duration between operations
MinTimeBetweenUpdates int64 = 24 * 60 * 60
)

// keeper implements the `ReferralKeeper` interface using an AVL tree for storage.
type keeper struct {
store *avl.Tree
lastOps map[string]int64
}

// check interface implementation at compile time
var _ ReferralKeeper = &keeper{}

// NewKeeper creates and returns a new instance of ReferralKeeper.
// The keeper is initialized with an empty AVL tree for storing referral relationships.
func NewKeeper() ReferralKeeper {
return &keeper{
store: avl.NewTree(),
lastOps: make(map[string]int64),
}
}

// register implements the `register` method of the `ReferralKeeper` interface.
// It sets a new referral relationship between the given address and referral address.
func (k *keeper) register(addr, refAddr std.Address) error {
dongwon8247 marked this conversation as resolved.
Show resolved Hide resolved
return k.setReferral(addr, refAddr, EventTypeRegister)
}

// update implements the `update` method of the `ReferralKeeper` interface.
// It updates the referral address for a given address.
func (k *keeper) update(addr, newRefAddr std.Address) error {
return k.setReferral(addr, newRefAddr, EventTypeUpdate)
}

// setReferral handles the common logic for registering and updating referrals.
// It validates the addresses and caller, then stores the referral relationship.
//
// Note: The current implementation allows circular references, but since it only manages
// simple reference relationships, cycles are not a significant issue. However, when introducing
// a referral-based reward system in the future or adding business logic where cycles could cause problems,
// it will be necessary to implement validation checks.
//
// TODO: need to discuss what values to emit as event
func (k *keeper) setReferral(addr, refAddr std.Address, eventType string) error {
if err := isValidCaller(std.PrevRealm().Addr()); err != nil {
return err
}

if err := k.checkRateLimit(addr.String()); err != nil {
return err
}

if err := k.validateAddresses(addr, refAddr); err != nil {
return err
}

if refAddr == zeroAddress {
std.Emit(
EventTypeRemove,
"removedAddress", addr.String(),
)
return k.remove(addr)
onlyhyde marked this conversation as resolved.
Show resolved Hide resolved
}

k.store.Set(addr.String(), refAddr.String())
std.Emit(
eventType,
"myAddress", addr.String(),
"refAddress", refAddr.String(),
)

return nil
}

// validateAddresses checks if the given addresses are valid for referral
func (k *keeper) validateAddresses(addr, refAddr std.Address) error {
dongwon8247 marked this conversation as resolved.
Show resolved Hide resolved
if !addr.IsValid() || (refAddr != zeroAddress && !refAddr.IsValid()) {
return NewError(ErrInvalidAddress)
}
if addr == refAddr {
return NewError(ErrSelfReferral)
}
return nil
}

// remove implements the `remove` method of the `ReferralKeeper` interface.
// It validates the caller and address before removing the referral relationship.
//
// TODO: need to discuss what values to emit as event
func (k *keeper) remove(target std.Address) error {
if err := isValidCaller(std.PrevRealm().Addr()); err != nil {
return err
}

if err := k.checkRateLimit(target.String()); err != nil {
dongwon8247 marked this conversation as resolved.
Show resolved Hide resolved
return err
}

if !target.IsValid() {
return NewError(ErrInvalidAddress)
}

k.store.Remove(target.String())

// TODO: update event
std.Emit(
EventTypeRemove,
"removedAddress", target.String(),
)

return nil
}

// has implements the `has` method of the `ReferralKeeper` interface.
// It checks if a referral relationship exists for a given address.
func (k *keeper) has(addr std.Address) bool {
_, exists := k.store.Get(addr.String())
return exists
}

// get implements the `get` method of the `ReferralKeeper` interface.
// It retrieves the referral address for a given address.
func (k *keeper) get(addr std.Address) (std.Address, error) {
if !addr.IsValid() {
return zeroAddress, NewError(ErrInvalidAddress)
}

val, ok := k.store.Get(addr.String())
if !ok {
return zeroAddress, NewError(ErrNotFound)
}

refAddr, ok := val.(string)
if !ok {
return zeroAddress, NewError(ErrInvalidAddress)
}

return std.Address(refAddr), nil
}

func (k *keeper) isEmpty() bool {
empty := true
k.store.Iterate("", "", func(key string, value interface{}) bool {
empty = false
return true // stop iteration on first item
})
return empty
}

// checkRateLimit verifies if enough time has passed since the last operation
func (k *keeper) checkRateLimit(addr string) error {
now := time.Now().Unix()

if lastOpTime, exists := k.lastOps[addr]; exists {
timeSinceLastOp := now - lastOpTime

if timeSinceLastOp < MinTimeBetweenUpdates {
return NewError(ErrTooManyRequests)
}
}

k.lastOps[addr] = now
return nil
}
Loading
Loading