Skip to content

feat(x/erc20): add allowance state #90

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

Open
wants to merge 3 commits into
base: feat/remove-authz-from-precompiles
Choose a base branch
from
Open
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
845 changes: 772 additions & 73 deletions api/cosmos/evm/erc20/v1/erc20.pulsar.go

Large diffs are not rendered by default.

213 changes: 183 additions & 30 deletions api/cosmos/evm/erc20/v1/genesis.pulsar.go

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions evmd/allowance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package evmd

import (
"cosmossdk.io/math"

erc20types "github.com/cosmos/evm/x/erc20/types"
)

const (
ExampleSpender = "0x1e0DE5DB1a39F99cBc67B00fA3415181b3509e42"
ExampleOwner = "0x0AFc8e15F0A74E98d0AEC6C67389D2231384D4B2"
)

// ExampleAllowances creates a slice of allowance, that contains an allowance for the native denom of the example chain
// implementation.
var ExampleAllowances = []erc20types.Allowance{
{
Erc20Address: WEVMOSContractMainnet,
Owner: ExampleOwner,
Spender: ExampleSpender,
Value: math.NewInt(100),
},
}
21 changes: 21 additions & 0 deletions proto/cosmos/evm/erc20/v1/erc20.proto
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ message TokenPair {
Owner contract_owner = 4;
}

//Allowance is a token allowance only for erc20 precompile
message Allowance {
option (gogoproto.equal) = false;

// erc20_address is the hex address of ERC20 contract
string erc20_address = 1;

// owner is the hex address of the owner account
string owner = 2;

// spender is the hex address that is allowed to spend the allowance
string spender = 3;

// value specifies the maximum amount of tokens that can be spent
// by this token allowance and will be updated as tokens are spent.
string value = 4 [
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.nullable) = false
];
}

// protolint:disable MESSAGES_HAVE_COMMENT

// Deprecated: RegisterCoinProposal is a gov Content type to register a token
Expand Down
3 changes: 3 additions & 0 deletions proto/cosmos/evm/erc20/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ message GenesisState {
// token_pairs is a slice of the registered token pairs at genesis
repeated TokenPair token_pairs = 2
[ (gogoproto.nullable) = false, (amino.dont_omitempty) = true ];
// allowances is a slice of the registered allowances at genesis
repeated Allowance allowances = 3
[ (gogoproto.nullable) = false, (amino.dont_omitempty) = true ];
}

// Params defines the erc20 module params
Expand Down
4 changes: 4 additions & 0 deletions testutil/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ const (
WEVMOSContractMainnet = "0xD4949664cD82660AaE99bEdc034a0deA8A0bd517"
// WEVMOSContractTestnet is the WEVMOS contract address for testnet
WEVMOSContractTestnet = "0xcc491f589b45d4a3c679016195b3fb87d7848210"
// ExampleEvmAddress1 is the example EVM address
ExampleEvmAddressAlice = "0x1e0DE5DB1a39F99cBc67B00fA3415181b3509e42"
// ExampleEvmAddress2 is the example EVM address
ExampleEvmAddressBob = "0x0AFc8e15F0A74E98d0AEC6C67389D2231384D4B2"
)

var (
Expand Down
21 changes: 21 additions & 0 deletions x/erc20/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package erc20

import (
"fmt"
"math/big"

"github.com/cosmos/evm/x/erc20/keeper"
"github.com/cosmos/evm/x/erc20/types"
"github.com/ethereum/go-ethereum/common"

sdk "github.com/cosmos/cosmos-sdk/types"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
Expand All @@ -31,12 +33,31 @@ func InitGenesis(
for _, pair := range data.TokenPairs {
k.SetToken(ctx, pair)
}

var erc20, owner, spender common.Address
var value *big.Int
for _, allowance := range data.Allowances {
erc20 = common.HexToAddress(allowance.Erc20Address)
owner = common.HexToAddress(allowance.Owner)
spender = common.HexToAddress(allowance.Spender)
value = allowance.Value.BigInt()
err := k.SetAllowance(ctx, erc20, owner, spender, value)
if err != nil {
if types.ErrERC20TokenPairDisabled.Is(err) {
// NOTES: When SetAllowance is called from the ERC20 precompile, this case is treated as an error,
// but during GenesisState initialization, it’s a valid case, so it is allowed to pass.
continue
}
panic(fmt.Errorf("error setting allowance %s", err))
}
}
}

// ExportGenesis export module status
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState {
return &types.GenesisState{
Params: k.GetParams(ctx),
TokenPairs: k.GetTokenPairs(ctx),
Allowances: k.GetAllowances(ctx),
}
}
34 changes: 33 additions & 1 deletion x/erc20/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"
"time"

"cosmossdk.io/math"
"github.com/stretchr/testify/suite"

"github.com/cometbft/cometbft/crypto/tmhash"
Expand All @@ -29,7 +30,7 @@ type GenesisTestSuite struct {
genesis types.GenesisState
}

const osmoERC20ContractAddr = "0x5dCA2483280D9727c80b5518faC4556617fb19ZZ"
const osmoERC20ContractAddr = "0x5D87876250185593977a6F94aF98877a5E7eD60E"

var osmoDenomTrace = transfertypes.DenomTrace{
BaseDenom: "uosmo",
Expand Down Expand Up @@ -99,6 +100,29 @@ func (suite *GenesisTestSuite) TestERC20InitGenesis() {
ContractOwner: types.OWNER_MODULE,
},
},
[]types.Allowance{},
),
},
{
name: "custom genesis",
genesisState: types.NewGenesisState(
types.DefaultParams(),
[]types.TokenPair{
{
Erc20Address: osmoERC20ContractAddr,
Denom: osmoDenomTrace.IBCDenom(),
Enabled: true,
ContractOwner: types.OWNER_MODULE,
},
},
[]types.Allowance{
{
Erc20Address: osmoERC20ContractAddr,
Owner: utiltx.GenerateAddress().String(),
Spender: utiltx.GenerateAddress().String(),
Value: math.NewInt(100),
},
},
),
},
}
Expand All @@ -120,6 +144,13 @@ func (suite *GenesisTestSuite) TestERC20InitGenesis() {
} else {
suite.Require().Len(tc.genesisState.TokenPairs, 0, tc.name)
}

allowances := nw.App.Erc20Keeper.GetAllowances(nw.GetContext())
if len(allowances) > 0 {
suite.Require().Equal(tc.genesisState.Allowances, allowances, tc.name)
} else {
suite.Require().Len(tc.genesisState.Allowances, 0, tc.name)
}
}
}

Expand Down Expand Up @@ -148,6 +179,7 @@ func (suite *GenesisTestSuite) TestErc20ExportGenesis() {
ContractOwner: types.OWNER_MODULE,
},
},
[]types.Allowance{},
),
},
}
Expand Down
131 changes: 124 additions & 7 deletions x/erc20/keeper/allowance.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,156 @@ import (
"math/big"

sdk "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"

errorsmod "cosmossdk.io/errors"
"cosmossdk.io/store/prefix"
storetypes "cosmossdk.io/store/types"

"github.com/ethereum/go-ethereum/common"

"github.com/cosmos/evm/x/erc20/types"
)

// GetAllowance returns the allowance of the given owner and spender
// on the given erc20 precompile address.
func (k Keeper) GetAllowance(
ctx sdk.Context,
erc20 common.Address,
owner common.Address,
spender common.Address,
) (*big.Int, error) {
if err := k.checkTokenPair(ctx, erc20); err != nil {
return common.Big0, err
}

allowanceKey := types.AllowanceKey(erc20, owner, spender)
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixAllowance)

var allowance types.Allowance
bz := store.Get(allowanceKey)
if bz == nil {
return common.Big0, nil
}

// TODO: implement this function
return big.NewInt(0), nil
k.cdc.MustUnmarshal(bz, &allowance)

return allowance.Value.BigInt(), nil
}

// SetAllowance sets the allowance of the given owner and spender
// on the given erc20 precompile address.
func (k Keeper) SetAllowance(
ctx sdk.Context,
erc20 common.Address,
owner common.Address,
spender common.Address,
value *big.Int,
) error {

// TODO: implement this function
return nil
return k.setAllowance(ctx, erc20, owner, spender, value)
}

// DeleteAllowance deletes the allowance of the given owner and spender
// on the given erc20 precompile address.
func (k Keeper) DeleteAllowance(
ctx sdk.Context,
erc20 common.Address,
owner common.Address,
spender common.Address,
) error {
return k.setAllowance(ctx, erc20, owner, spender, common.Big0)
}

func (k Keeper) setAllowance(
ctx sdk.Context,
erc20 common.Address,
owner common.Address,
spender common.Address,
value *big.Int,
) error {
if err := k.checkTokenPair(ctx, erc20); err != nil {
return err
}
if (owner == common.Address{}) {
return errorsmod.Wrapf(errortypes.ErrInvalidAddress, "erc20 address is empty")
}
if (spender == common.Address{}) {
return errorsmod.Wrapf(errortypes.ErrInvalidAddress, "spender address is empty")
}

allowanceKey := types.AllowanceKey(erc20, owner, spender)
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixAllowance)
switch {
case value == nil || value.Sign() == 0:
store.Delete(allowanceKey)
case value.Sign() < 0:
return errorsmod.Wrapf(types.ErrInvalidAllowance, "value '%s' is less than zero", value)
default:
allowance := types.NewAllowance(erc20, owner, spender, value)
bz := k.cdc.MustMarshal(&allowance)
store.Set(allowanceKey, bz)
}

return nil
}

// GetAllowances returns all allowances stored on the given erc20 precompile address.
func (k Keeper) GetAllowances(
ctx sdk.Context,
) []types.Allowance {
allowances := []types.Allowance{}

k.IterateAllowances(ctx, func(allowance types.Allowance) (stop bool) {
allowances = append(allowances, allowance)
return false
})

return allowances
}

// IterateAllowances iterates through all allowances stored on the given erc20 precompile address.
func (k Keeper) IterateAllowances(
ctx sdk.Context,
cb func(allowance types.Allowance) (stop bool),
) {
store := ctx.KVStore(k.storeKey)
iterator := storetypes.KVStorePrefixIterator(store, types.KeyPrefixAllowance)
defer iterator.Close()

for ; iterator.Valid(); iterator.Next() {
var allowance types.Allowance
k.cdc.MustUnmarshal(iterator.Value(), &allowance)

if cb(allowance) {
break
}
}
}

func (k Keeper) checkTokenPair(ctx sdk.Context, erc20 common.Address) error {
tokenPairID := k.GetERC20Map(ctx, erc20)
tokenPair, found := k.GetTokenPair(ctx, tokenPairID)
if !found {
return errorsmod.Wrapf(
types.ErrTokenPairNotFound, "token pair for address '%s' not registered", erc20,
)
}

if !tokenPair.Enabled {
return errorsmod.Wrapf(
types.ErrERC20TokenPairDisabled, "token pair for address '%s' is disabled", erc20,
)
}

// TODO: implement this function
return nil
}
}

func (k Keeper) deleteAllowances(ctx sdk.Context, erc20 common.Address) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixAllowance)
iterator := storetypes.KVStorePrefixIterator(store, erc20.Bytes())
defer iterator.Close()

for ; iterator.Valid(); iterator.Next() {
store.Delete(iterator.Key())
}
}
Loading
Loading