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: ownable2step realm #3594

Merged
merged 8 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions examples/gno.land/p/oxtekgrinder/ownable2step/errors.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ownable2step

import "errors"

var (
ErrNoPendingOwner = errors.New("ownable2step: no pending owner")
ErrUnauthorized = errors.New("ownable2step: caller is not owner")
ErrPendingUnauthorized = errors.New("ownable2step: caller is not pending owner")
ErrInvalidAddress = errors.New("ownable2step: new owner address is invalid")
)
1 change: 1 addition & 0 deletions examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/oxtekgrinder/ownable2step
98 changes: 98 additions & 0 deletions examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package ownable2step

import (
"std"
)

const OwnershipTransferEvent = "OwnershipTransfer"

// Ownable2Step is a two-step ownership transfer package
// It allows the current owner to set a new owner and the new owner will need to accept the ownership before it is transferred
type Ownable2Step struct {
owner std.Address
pendingOwner std.Address
}

func New() *Ownable2Step {
return &Ownable2Step{
owner: std.PrevRealm().Addr(),
pendingOwner: "",
}
}

func NewWithAddress(addr std.Address) *Ownable2Step {
return &Ownable2Step{
owner: addr,
pendingOwner: "",
}
}

// TransferOwnership initiate the transfer of the ownership to a new address by setting the PendingOwner
func (o *Ownable2Step) TransferOwnership(newOwner std.Address) error {
if !o.CallerIsOwner() {
return ErrUnauthorized
}
if !newOwner.IsValid() {
return ErrInvalidAddress
}

o.pendingOwner = newOwner
return nil
}

// AcceptOwnership accepts the pending ownership transfer
func (o *Ownable2Step) AcceptOwnership() error {
if o.pendingOwner.String() == "" {
return ErrNoPendingOwner
}
if std.PrevRealm().Addr() != o.pendingOwner {
return ErrPendingUnauthorized
}

o.owner = o.pendingOwner
o.pendingOwner = ""

return nil
}

// DropOwnership removes the owner, effectively disabling any owner-related actions
// Top-level usage: disables all only-owner actions/functions,
// Embedded usage: behaves like a burn functionality, removing the owner from the struct
func (o *Ownable2Step) DropOwnership() error {
if !o.CallerIsOwner() {
return ErrUnauthorized
}

prevOwner := o.owner
o.owner = ""

std.Emit(
OwnershipTransferEvent,
"from", prevOwner.String(),
"to", "",
)

return nil
}

// Owner returns the owner address from Ownable
func (o *Ownable2Step) Owner() std.Address {
return o.owner
}

// PendingOwner returns the pending owner address from Ownable2Step
func (o *Ownable2Step) PendingOwner() std.Address {
return o.pendingOwner
}

// CallerIsOwner checks if the caller of the function is the Realm's owner
func (o *Ownable2Step) CallerIsOwner() bool {
return std.PrevRealm().Addr() == o.owner
}

// AssertCallerIsOwner panics if the caller is not the owner
func (o *Ownable2Step) AssertCallerIsOwner() {
if std.PrevRealm().Addr() != o.owner {
panic(ErrUnauthorized)
}
}
156 changes: 156 additions & 0 deletions examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package ownable2step

import (
"std"
"testing"

"gno.land/p/demo/testutils"
"gno.land/p/demo/uassert"
"gno.land/p/demo/urequire"
)

var (
alice = testutils.TestAddress("alice")
bob = testutils.TestAddress("bob")
)

func TestNew(t *testing.T) {
std.TestSetRealm(std.NewUserRealm(alice))
std.TestSetOrigCaller(alice)

o := New()
got := o.Owner()
pendingOwner := o.PendingOwner()

uassert.Equal(t, got, alice)
uassert.Equal(t, pendingOwner.String(), "")
}

func TestNewWithAddress(t *testing.T) {
o := NewWithAddress(alice)

got := o.Owner()
pendingOwner := o.PendingOwner()

uassert.Equal(t, got, alice)
uassert.Equal(t, pendingOwner.String(), "")
}

func TestInitiateTransferOwnership(t *testing.T) {
std.TestSetRealm(std.NewUserRealm(alice))
std.TestSetOrigCaller(alice)

o := New()

err := o.TransferOwnership(bob)
urequire.NoError(t, err)

owner := o.Owner()
pendingOwner := o.PendingOwner()

uassert.Equal(t, owner, alice)
uassert.Equal(t, pendingOwner, bob)
}

func TestTransferOwnership(t *testing.T) {
std.TestSetRealm(std.NewUserRealm(alice))
std.TestSetOrigCaller(alice)

o := New()

err := o.TransferOwnership(bob)
urequire.NoError(t, err)

owner := o.Owner()
pendingOwner := o.PendingOwner()

uassert.Equal(t, owner, alice)
uassert.Equal(t, pendingOwner, bob)

std.TestSetRealm(std.NewUserRealm(bob))
std.TestSetOrigCaller(bob)

err = o.AcceptOwnership()
urequire.NoError(t, err)

owner = o.Owner()
pendingOwner = o.PendingOwner()

uassert.Equal(t, owner, bob)
uassert.Equal(t, pendingOwner.String(), "")
}

func TestCallerIsOwner(t *testing.T) {
std.TestSetRealm(std.NewUserRealm(alice))
std.TestSetOrigCaller(alice)

o := New()
unauthorizedCaller := bob

std.TestSetRealm(std.NewUserRealm(unauthorizedCaller))
std.TestSetOrigCaller(unauthorizedCaller)

uassert.False(t, o.CallerIsOwner())
}

func TestDropOwnership(t *testing.T) {
std.TestSetRealm(std.NewUserRealm(alice))

o := New()

err := o.DropOwnership()
urequire.NoError(t, err, "DropOwnership failed")

owner := o.Owner()
uassert.Empty(t, owner, "owner should be empty")
}

// Errors

func TestErrUnauthorized(t *testing.T) {
std.TestSetRealm(std.NewUserRealm(alice))
std.TestSetOrigCaller(alice)

o := New()

std.TestSetRealm(std.NewUserRealm(bob))
std.TestSetOrigCaller(bob)

uassert.ErrorContains(t, o.TransferOwnership(alice), ErrUnauthorized.Error())
uassert.ErrorContains(t, o.DropOwnership(), ErrUnauthorized.Error())
}

func TestErrInvalidAddress(t *testing.T) {
std.TestSetRealm(std.NewUserRealm(alice))

o := New()

err := o.TransferOwnership("")
uassert.ErrorContains(t, err, ErrInvalidAddress.Error())

err = o.TransferOwnership("10000000001000000000100000000010000000001000000000")
uassert.ErrorContains(t, err, ErrInvalidAddress.Error())
}

func TestErrNoPendingOwner(t *testing.T) {
std.TestSetRealm(std.NewUserRealm(alice))

o := New()

err := o.AcceptOwnership()
uassert.ErrorContains(t, err, ErrNoPendingOwner.Error())
}

func TestErrPendingUnauthorized(t *testing.T) {
std.TestSetRealm(std.NewUserRealm(alice))

o := New()

err := o.TransferOwnership(bob)
urequire.NoError(t, err)

std.TestSetRealm(std.NewUserRealm(alice))

err = o.AcceptOwnership()
uassert.ErrorContains(t, err, ErrPendingUnauthorized.Error())
}
Loading