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

Merged
merged 6 commits into from
Apr 21, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 address of the owner account
string owner = 2;

// spender is the 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
13 changes: 13 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,23 @@ 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()
k.SetAllowance(ctx, erc20, owner, spender, value)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cloudgray
Could you add error handling here? If SetAllowance returns an error, we should panic to prevent inconsistent state.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied in 80036a6

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cloudgray
It looks like disabled token pairs never get their allowances stored during InitGenesis, because SetAllowance calls checkTokenPair and returns an error if the token is disabled. However, InitGenesis is supposed to restore all state from the genesis file — including allowances for disabled pairs.

Right now, we simply continue when types.ErrERC20TokenPairDisabled is encountered, so those allowances don’t actually get written to the KVStore. If the module logic requires that disabled pairs still have their allowances recorded at genesis (so they might be re-enabled later or at least remain consistent with the imported state), we’ll need to revise this flow. One potential fix is to skip the checkTokenPair check during genesis, or handle the “disabled” error as a non-fatal condition that still writes the allowance. Otherwise, we lose a piece of state that was originally present in the genesis file.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied in fcae5bb and 80036a6

}
}

// 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
133 changes: 127 additions & 6 deletions x/erc20/keeper/allowance.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,160 @@ 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) {
allowanceKey := types.AllowanceKey(erc20, owner, spender)
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixAllowance)

// TODO: implement this function
return big.NewInt(0), nil
var allowance types.Allowance
bz := store.Get(allowanceKey)
if bz == nil {
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 {
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.checkAddressesNonZero(erc20, owner, spender); err != nil {
return err
}

if err := k.checkTokenPair(ctx, erc20); err != nil {
return err
}

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)
}

// TODO: implement this function
return nil
}

func (k Keeper) DeleteAllowance(
// 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) checkAddressesNonZero(
erc20 common.Address,
owner common.Address,
spender common.Address,
) error {
if erc20 == (common.Address{}) {
return errorsmod.Wrapf(errortypes.ErrInvalidAddress, "invalid erc20 address: '%s'", erc20)
}

if owner == (common.Address{}) {
return errorsmod.Wrapf(errortypes.ErrInvalidAddress, "invalid owner address: '%s'", owner)
}

if spender == (common.Address{}) {
return errorsmod.Wrapf(errortypes.ErrInvalidAddress, "invalid spender address: '%s'", spender)
}

return nil
}

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
}
}
Loading
Loading