From 57b396940f1661bdac9af2703114f501ae6e9ea9 Mon Sep 17 00:00:00 2001 From: codchen Date: Thu, 28 Dec 2023 15:43:12 +0800 Subject: [PATCH] Add wei support in bank keeper (#386) ## Describe your changes and provide context Add two new bank keeper functions that allow sub-usei (i.e. wei) sending, where 1 usei = 10^12 wei: ``` SendCoinsAndWei(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, customEscrow sdk.AccAddress, denom string, amt sdk.Int, wei sdk.Int) error GetWeiBalance(ctx sdk.Context, addr sdk.AccAddress) sdk.Int ``` Any usei that is split as a result of wei sending will be stored in an escrow account, which can either be one that's specified by the caller or a default global escrow module account. ## Testing performed to validate your change unit test & local integration with sei-chain --- x/bank/keeper/keeper_test.go | 50 +++++++++++++++++++++++ x/bank/keeper/send.go | 77 ++++++++++++++++++++++++++++++++++++ x/bank/keeper/view.go | 15 +++++++ x/bank/types/key.go | 3 ++ 4 files changed, 145 insertions(+) diff --git a/x/bank/keeper/keeper_test.go b/x/bank/keeper/keeper_test.go index e2eec160a..25600f3f7 100644 --- a/x/bank/keeper/keeper_test.go +++ b/x/bank/keeper/keeper_test.go @@ -76,6 +76,7 @@ func (suite *IntegrationTestSuite) initKeepersWithmAccPerms(blockedAddrs map[str appCodec := simapp.MakeTestEncodingConfig().Marshaler maccPerms[holder] = nil + maccPerms[types.WeiEscrowName] = nil maccPerms[authtypes.Burner] = []string{authtypes.Burner} maccPerms[authtypes.Minter] = []string{authtypes.Minter} maccPerms[multiPerm] = []string{authtypes.Burner, authtypes.Minter, authtypes.Staking} @@ -108,6 +109,55 @@ func (suite *IntegrationTestSuite) SetupTest() { suite.queryClient = queryClient } +func (suite *IntegrationTestSuite) TestSendCoinsAndWei() { + ctx := suite.ctx + require := suite.Require() + authKeeper, keeper := suite.initKeepersWithmAccPerms(make(map[string]bool)) + amt := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))) + require.NoError(keeper.MintCoins(ctx, authtypes.Minter, amt)) + addr1 := sdk.AccAddress([]byte("addr1_______________")) + addr2 := sdk.AccAddress([]byte("addr2_______________")) + addr3 := sdk.AccAddress([]byte("addr3_______________")) + require.NoError(keeper.SendCoinsFromModuleToAccount(ctx, authtypes.Minter, addr1, amt)) + // should no-op if sending zero + require.NoError(keeper.SendCoinsAndWei(ctx, addr1, addr2, nil, sdk.DefaultBondDenom, sdk.ZeroInt(), sdk.ZeroInt())) + require.Equal(sdk.ZeroInt(), keeper.GetWeiBalance(ctx, addr1)) + require.Equal(sdk.ZeroInt(), keeper.GetWeiBalance(ctx, addr2)) + require.Equal(sdk.NewInt(100), keeper.GetBalance(ctx, addr1, sdk.DefaultBondDenom).Amount) + require.Equal(sdk.ZeroInt(), keeper.GetBalance(ctx, addr2, sdk.DefaultBondDenom).Amount) + require.Equal(sdk.ZeroInt(), keeper.GetBalance(ctx, authKeeper.GetModuleAddress(types.WeiEscrowName), sdk.DefaultBondDenom).Amount) + // should just do usei send if wei is zero + require.NoError(keeper.SendCoinsAndWei(ctx, addr1, addr3, nil, sdk.DefaultBondDenom, sdk.NewInt(50), sdk.ZeroInt())) + require.Equal(sdk.ZeroInt(), keeper.GetWeiBalance(ctx, addr1)) + require.Equal(sdk.ZeroInt(), keeper.GetWeiBalance(ctx, addr3)) + require.Equal(sdk.NewInt(50), keeper.GetBalance(ctx, addr1, sdk.DefaultBondDenom).Amount) + require.Equal(sdk.NewInt(50), keeper.GetBalance(ctx, addr3, sdk.DefaultBondDenom).Amount) + require.Equal(sdk.ZeroInt(), keeper.GetBalance(ctx, authKeeper.GetModuleAddress(types.WeiEscrowName), sdk.DefaultBondDenom).Amount) + // should return error if wei amount overflows + require.Error(keeper.SendCoinsAndWei(ctx, addr1, addr2, nil, sdk.DefaultBondDenom, sdk.ZeroInt(), sdk.NewInt(1_000_000_000_000))) + // sender gets escrowed one usei, recipient does not get redeemed + require.NoError(keeper.SendCoinsAndWei(ctx, addr1, addr2, nil, sdk.DefaultBondDenom, sdk.NewInt(1), sdk.NewInt(1))) + require.Equal(sdk.NewInt(999_999_999_999), keeper.GetWeiBalance(ctx, addr1)) + require.Equal(sdk.OneInt(), keeper.GetWeiBalance(ctx, addr2)) + require.Equal(sdk.NewInt(48), keeper.GetBalance(ctx, addr1, sdk.DefaultBondDenom).Amount) + require.Equal(sdk.OneInt(), keeper.GetBalance(ctx, addr2, sdk.DefaultBondDenom).Amount) + require.Equal(sdk.OneInt(), keeper.GetBalance(ctx, authKeeper.GetModuleAddress(types.WeiEscrowName), sdk.DefaultBondDenom).Amount) + // sender does not get escrowed due to sufficient wei balance, recipient does not get redeemed + require.NoError(keeper.SendCoinsAndWei(ctx, addr1, addr3, nil, sdk.DefaultBondDenom, sdk.NewInt(1), sdk.NewInt(999_999_999_999))) + require.Equal(sdk.ZeroInt(), keeper.GetWeiBalance(ctx, addr1)) + require.Equal(sdk.NewInt(999_999_999_999), keeper.GetWeiBalance(ctx, addr3)) + require.Equal(sdk.NewInt(47), keeper.GetBalance(ctx, addr1, sdk.DefaultBondDenom).Amount) + require.Equal(sdk.NewInt(51), keeper.GetBalance(ctx, addr3, sdk.DefaultBondDenom).Amount) + require.Equal(sdk.OneInt(), keeper.GetBalance(ctx, authKeeper.GetModuleAddress(types.WeiEscrowName), sdk.DefaultBondDenom).Amount) + // sender gets escrowed and recipient gets redeemed + require.NoError(keeper.SendCoinsAndWei(ctx, addr1, addr3, nil, sdk.DefaultBondDenom, sdk.NewInt(1), sdk.NewInt(2))) + require.Equal(sdk.NewInt(999_999_999_998), keeper.GetWeiBalance(ctx, addr1)) + require.Equal(sdk.NewInt(1), keeper.GetWeiBalance(ctx, addr3)) + require.Equal(sdk.NewInt(45), keeper.GetBalance(ctx, addr1, sdk.DefaultBondDenom).Amount) + require.Equal(sdk.NewInt(53), keeper.GetBalance(ctx, addr3, sdk.DefaultBondDenom).Amount) + require.Equal(sdk.OneInt(), keeper.GetBalance(ctx, authKeeper.GetModuleAddress(types.WeiEscrowName), sdk.DefaultBondDenom).Amount) +} + func (suite *IntegrationTestSuite) TestSupply() { ctx := suite.ctx diff --git a/x/bank/keeper/send.go b/x/bank/keeper/send.go index a379fb969..93ac398dd 100644 --- a/x/bank/keeper/send.go +++ b/x/bank/keeper/send.go @@ -1,7 +1,10 @@ package keeper import ( + "errors" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -17,6 +20,7 @@ type SendKeeper interface { InputOutputCoins(ctx sdk.Context, inputs []types.Input, outputs []types.Output) error SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsWithoutAccCreation(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error + SendCoinsAndWei(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, customEscrow sdk.AccAddress, denom string, amt sdk.Int, wei sdk.Int) error GetParams(ctx sdk.Context) types.Params SetParams(ctx sdk.Context, params types.Params) @@ -28,6 +32,7 @@ type SendKeeper interface { } var _ SendKeeper = (*BaseSendKeeper)(nil) +var MaxWeiBalance sdk.Int = sdk.NewInt(1_000_000_000_000) // BaseSendKeeper only allows transfers between accounts without the possibility of // creating coins. It implements the SendKeeper interface. @@ -275,6 +280,20 @@ func (k BaseSendKeeper) setBalance(ctx sdk.Context, addr sdk.AccAddress, balance return nil } +func (k BaseSendKeeper) setWeiBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) error { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.WeiBalancesPrefix) + if amt.IsZero() { + store.Delete(addr) + return nil + } + val, err := amt.Marshal() + if err != nil { + return err + } + store.Set(addr, val) + return nil +} + // IsSendEnabledCoins checks the coins provide and returns an ErrSendDisabled if // any of the coins are not configured for sending. Returns nil if sending is enabled // for all provided coin @@ -297,3 +316,61 @@ func (k BaseSendKeeper) IsSendEnabledCoin(ctx sdk.Context, coin sdk.Coin) bool { func (k BaseSendKeeper) BlockedAddr(addr sdk.AccAddress) bool { return k.blockedAddrs[addr.String()] } + +func (k BaseSendKeeper) SendCoinsAndWei(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, customEscrow sdk.AccAddress, denom string, amt sdk.Int, wei sdk.Int) error { + if wei.Equal(sdk.ZeroInt()) { + if amt.Equal(sdk.ZeroInt()) { + return nil + } + return k.SendCoinsWithoutAccCreation(ctx, from, to, sdk.NewCoins(sdk.NewCoin(denom, amt))) + } + if wei.GTE(MaxWeiBalance) { + return errors.New("cannot send more than 10^12 wei") + } + escrow := customEscrow + if escrow == nil { + escrow = k.ak.GetModuleAddress(types.WeiEscrowName) + } + currentWeiBalanceFrom := k.GetWeiBalance(ctx, from) + postWeiBalanceFrom := currentWeiBalanceFrom.Sub(wei) + if postWeiBalanceFrom.GTE(sdk.ZeroInt()) { + if err := k.setWeiBalance(ctx, from, postWeiBalanceFrom); err != nil { + return err + } + } else { + if err := k.setWeiBalance(ctx, from, MaxWeiBalance.Add(postWeiBalanceFrom)); err != nil { + // postWeiBalanceFrom is negative + return err + } + // need to send one sei to escrow because wei balance is insufficient + if err := k.SendCoinsWithoutAccCreation(ctx, from, escrow, sdk.NewCoins(sdk.NewCoin(denom, sdk.OneInt()))); err != nil { + return err + } + } + currentWeiBalanceTo := k.GetWeiBalance(ctx, to) + postWeiBalanceTo := currentWeiBalanceTo.Add(wei) + if postWeiBalanceTo.LT(MaxWeiBalance) { + if err := k.setWeiBalance(ctx, to, postWeiBalanceTo); err != nil { + return err + } + } else { + if err := k.setWeiBalance(ctx, to, postWeiBalanceTo.Sub(MaxWeiBalance)); err != nil { + return err + } + // need to redeem one sei from escrow because wei balance overflowed + one := sdk.NewCoins(sdk.NewCoin(denom, sdk.OneInt())) + if err := k.SendCoinsWithoutAccCreation(ctx, escrow, to, one); err != nil { + if sdkerrors.ErrInsufficientFunds.Is(err) && customEscrow != nil { + // custom escrow does not have enough balance to redeem. Try the global escrow instead + if err := k.SendCoinsWithoutAccCreation(ctx, k.ak.GetModuleAddress(types.WeiEscrowName), to, one); err != nil { + return err + } + } + return err + } + } + if amt.GT(sdk.ZeroInt()) { + return k.SendCoinsWithoutAccCreation(ctx, from, to, sdk.NewCoins(sdk.NewCoin(denom, amt))) + } + return nil +} diff --git a/x/bank/keeper/view.go b/x/bank/keeper/view.go index d126ddd75..7f835ab2b 100644 --- a/x/bank/keeper/view.go +++ b/x/bank/keeper/view.go @@ -26,6 +26,7 @@ type ViewKeeper interface { GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin LockedCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + GetWeiBalance(ctx sdk.Context, addr sdk.AccAddress) sdk.Int IterateAccountBalances(ctx sdk.Context, addr sdk.AccAddress, cb func(coin sdk.Coin) (stop bool)) IterateAllBalances(ctx sdk.Context, cb func(address sdk.AccAddress, coin sdk.Coin) (stop bool)) @@ -232,3 +233,17 @@ func (k BaseViewKeeper) getAccountStore(ctx sdk.Context, addr sdk.AccAddress) pr return prefix.NewStore(store, types.CreateAccountBalancesPrefix(addr)) } + +func (k BaseViewKeeper) GetWeiBalance(ctx sdk.Context, addr sdk.AccAddress) sdk.Int { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.WeiBalancesPrefix) + val := store.Get(addr) + if val == nil { + return sdk.ZeroInt() + } + res := new(sdk.Int) + if err := res.Unmarshal(val); err != nil { + // should never happen + panic(err) + } + return *res +} diff --git a/x/bank/types/key.go b/x/bank/types/key.go index 8b8c25a2d..146b30de1 100644 --- a/x/bank/types/key.go +++ b/x/bank/types/key.go @@ -21,10 +21,13 @@ const ( // QuerierRoute defines the module's query routing key QuerierRoute = ModuleName + + WeiEscrowName = "weiescrow" ) // KVStore keys var ( + WeiBalancesPrefix = []byte{0x04} // BalancesPrefix is the prefix for the account balances store. We use a byte // (instead of `[]byte("balances")` to save some disk space). DeferredCachePrefix = []byte{0x03}