Skip to content

Commit

Permalink
feat: ownable2step realm (#3594)
Browse files Browse the repository at this point in the history
Addresses: #3520

Add a ownable variant realm to have a transfer of ownership with two
functions where the new owner needs to accept ownership to be sure no
mistake can be made.

---------

Co-authored-by: Leon Hudak <[email protected]>
  • Loading branch information
0xtekgrinder and leohhhn authored Jan 31, 2025
1 parent 57da324 commit d3774ce
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 0 deletions.
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())
}

0 comments on commit d3774ce

Please sign in to comment.