Skip to content

Commit

Permalink
Merge pull request #119 from Stride-Labs/add-osmo-x/bank-for-x/tokenf…
Browse files Browse the repository at this point in the history
…actory-support

import osmosis' x/bank module for x/tokenfactory support
  • Loading branch information
Manav-Aggarwal authored Jul 30, 2024
2 parents dc19c05 + 04ca9ba commit 8ee2f9e
Show file tree
Hide file tree
Showing 19 changed files with 1,822 additions and 138 deletions.
5 changes: 0 additions & 5 deletions x/bank/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,6 @@ During `InputOutputCoins`, the send restriction is applied after the input coins

A send restriction function should make use of a custom value in the context to allow bypassing that specific restriction.

Send Restrictions are not placed on `ModuleToAccount` or `ModuleToModule` transfers. This is done due to modules needing to move funds to user accounts and other module accounts. This is a design decision to allow for more flexibility in the state machine. The state machine should be able to move funds between module accounts and user accounts without restrictions.

Secondly this limitation would limit the usage of the state machine even for itself. users would not be able to receive rewards, not be able to move funds between module accounts. In the case that a user sends funds from a user account to the community pool and then a governance proposal is used to get those tokens into the users account this would fall under the discretion of the app chain developer to what they would like to do here. We can not make strong assumptions here.
Thirdly, this issue could lead into a chain halt if a token is disabled and the token is moved in the begin/endblock. This is the last reason we see the current change and more damaging then beneficial for users.

For example, in your module's keeper package, you'd define the send restriction function:

```golang
Expand Down
164 changes: 164 additions & 0 deletions x/bank/hooks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package bank_test

import (
"context"
"fmt"
"testing"

tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
"github.com/stretchr/testify/require"

"cosmossdk.io/math"

sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cosmos/cosmos-sdk/x/bank/keeper"
banktestutil "github.com/cosmos/cosmos-sdk/x/bank/testutil"
"github.com/cosmos/cosmos-sdk/x/bank/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)

var _ types.BankHooks = &MockBankHooksReceiver{}

// BankHooks event hooks for bank (noalias)
type MockBankHooksReceiver struct{}

// Mock BlockBeforeSend bank hook that doesn't allow the sending of exactly 100 coins of any denom.
func (h *MockBankHooksReceiver) BlockBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) error {
for _, coin := range amount {
if coin.Amount.Equal(math.NewInt(100)) {
return fmt.Errorf("not allowed; expected %v, got: %v", 100, coin.Amount)
}
}
return nil
}

// variable for counting `TrackBeforeSend`
var (
countTrackBeforeSend = 0
expNextCount = 1
)

// Mock TrackBeforeSend bank hook that simply tracks the sending of exactly 50 coins of any denom.
func (h *MockBankHooksReceiver) TrackBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) {
for _, coin := range amount {
if coin.Amount.Equal(math.NewInt(50)) {
countTrackBeforeSend++
}
}
}

func TestHooks(t *testing.T) {
acc := &authtypes.BaseAccount{
Address: addr1.String(),
}

genAccs := []authtypes.GenesisAccount{acc}
app := createTestSuite(t, genAccs)
baseApp := app.App.BaseApp
ctx := baseApp.NewContextLegacy(false, tmproto.Header{})

require.NoError(t, banktestutil.FundAccount(ctx, app.BankKeeper, addr1, sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 10000))))
require.NoError(t, banktestutil.FundAccount(ctx, app.BankKeeper, addr2, sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 10000))))
banktestutil.FundModuleAccount(ctx, app.BankKeeper, stakingtypes.BondedPoolName, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(1000))))

// create a valid send amount which is 1 coin, and an invalidSendAmount which is 100 coins
validSendAmount := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(1)))
triggerTrackSendAmount := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(50)))
invalidBlockSendAmount := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100)))

// setup our mock bank hooks receiver that prevents the send of 100 coins
bankHooksReceiver := MockBankHooksReceiver{}
baseBankKeeper, ok := app.BankKeeper.(keeper.BaseKeeper)
require.True(t, ok)
baseBankKeeper.SetHooks(
types.NewMultiBankHooks(&bankHooksReceiver),
)
app.BankKeeper = baseBankKeeper

// try sending a validSendAmount and it should work
err := app.BankKeeper.SendCoins(ctx, addr1, addr2, validSendAmount)
require.NoError(t, err)

// try sending an trigger track send amount and it should work
err = app.BankKeeper.SendCoins(ctx, addr1, addr2, triggerTrackSendAmount)
require.NoError(t, err)

require.Equal(t, countTrackBeforeSend, expNextCount)
expNextCount++

// try sending an invalidSendAmount and it should not work
err = app.BankKeeper.SendCoins(ctx, addr1, addr2, invalidBlockSendAmount)
require.Error(t, err)

// try doing SendManyCoins and make sure if even a single subsend is invalid, the entire function fails
err = app.BankKeeper.SendManyCoins(ctx, addr1, []sdk.AccAddress{addr1, addr2}, []sdk.Coins{invalidBlockSendAmount, validSendAmount})
require.Error(t, err)

err = app.BankKeeper.SendManyCoins(ctx, addr1, []sdk.AccAddress{addr1, addr2}, []sdk.Coins{triggerTrackSendAmount, validSendAmount})
require.NoError(t, err)
require.Equal(t, countTrackBeforeSend, expNextCount)
expNextCount++

// make sure that account to module doesn't bypass hook
err = app.BankKeeper.SendCoinsFromAccountToModule(ctx, addr1, stakingtypes.BondedPoolName, validSendAmount)
require.NoError(t, err)
err = app.BankKeeper.SendCoinsFromAccountToModule(ctx, addr1, stakingtypes.BondedPoolName, invalidBlockSendAmount)
require.Error(t, err)
err = app.BankKeeper.SendCoinsFromAccountToModule(ctx, addr1, stakingtypes.BondedPoolName, triggerTrackSendAmount)
require.NoError(t, err)
require.Equal(t, countTrackBeforeSend, expNextCount)
expNextCount++

// make sure that module to account doesn't bypass hook
err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, stakingtypes.BondedPoolName, addr1, validSendAmount)
require.NoError(t, err)
err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, stakingtypes.BondedPoolName, addr1, invalidBlockSendAmount)
require.Error(t, err)
err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, stakingtypes.BondedPoolName, addr1, triggerTrackSendAmount)
require.NoError(t, err)
require.Equal(t, countTrackBeforeSend, expNextCount)
expNextCount++

// make sure that module to module doesn't bypass hook
err = app.BankKeeper.SendCoinsFromModuleToModule(ctx, stakingtypes.BondedPoolName, stakingtypes.NotBondedPoolName, validSendAmount)
require.NoError(t, err)
err = app.BankKeeper.SendCoinsFromModuleToModule(ctx, stakingtypes.BondedPoolName, stakingtypes.NotBondedPoolName, invalidBlockSendAmount)
// there should be no error since module to module does not call block before send hooks
require.NoError(t, err)
err = app.BankKeeper.SendCoinsFromModuleToModule(ctx, stakingtypes.BondedPoolName, stakingtypes.NotBondedPoolName, triggerTrackSendAmount)
require.NoError(t, err)
require.Equal(t, countTrackBeforeSend, expNextCount)
expNextCount++

// make sure that module to many accounts doesn't bypass hook
err = app.BankKeeper.SendCoinsFromModuleToManyAccounts(ctx, stakingtypes.BondedPoolName, []sdk.AccAddress{addr1, addr2}, []sdk.Coins{validSendAmount, validSendAmount})
require.NoError(t, err)
err = app.BankKeeper.SendCoinsFromModuleToManyAccounts(ctx, stakingtypes.BondedPoolName, []sdk.AccAddress{addr1, addr2}, []sdk.Coins{validSendAmount, invalidBlockSendAmount})
require.Error(t, err)
err = app.BankKeeper.SendCoinsFromModuleToManyAccounts(ctx, stakingtypes.BondedPoolName, []sdk.AccAddress{addr1, addr2}, []sdk.Coins{validSendAmount, triggerTrackSendAmount})
require.NoError(t, err)
require.Equal(t, countTrackBeforeSend, expNextCount)
expNextCount++

// make sure that DelegateCoins doesn't bypass the hook
err = app.BankKeeper.DelegateCoins(ctx, addr1, app.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), validSendAmount)
require.NoError(t, err)
err = app.BankKeeper.DelegateCoins(ctx, addr1, app.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), invalidBlockSendAmount)
require.Error(t, err)
err = app.BankKeeper.DelegateCoins(ctx, addr1, app.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), triggerTrackSendAmount)
require.NoError(t, err)
require.Equal(t, countTrackBeforeSend, expNextCount)
expNextCount++

// make sure that UndelegateCoins doesn't bypass the hook
err = app.BankKeeper.UndelegateCoins(ctx, app.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), addr1, validSendAmount)
require.NoError(t, err)
err = app.BankKeeper.UndelegateCoins(ctx, app.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), addr1, invalidBlockSendAmount)
require.Error(t, err)

err = app.BankKeeper.UndelegateCoins(ctx, app.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), addr1, triggerTrackSendAmount)
require.NoError(t, err)
require.Equal(t, countTrackBeforeSend, expNextCount)
expNextCount++
}
31 changes: 29 additions & 2 deletions x/bank/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (k BaseKeeper) SpendableBalanceByDenom(ctx context.Context, req *types.Quer
// TotalSupply implements the Query/TotalSupply gRPC method
func (k BaseKeeper) TotalSupply(ctx context.Context, req *types.QueryTotalSupplyRequest) (*types.QueryTotalSupplyResponse, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
totalSupply, pageRes, err := k.GetPaginatedTotalSupply(sdkCtx, req.Pagination)
totalSupply, pageRes, err := k.GetPaginatedTotalSupplyWithOffsets(sdkCtx, req.Pagination)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
Expand All @@ -158,11 +158,38 @@ func (k BaseKeeper) SupplyOf(c context.Context, req *types.QuerySupplyOfRequest)
}

ctx := sdk.UnwrapSDKContext(c)
supply := k.GetSupply(ctx, req.Denom)
supply := k.GetSupplyWithOffset(ctx, req.Denom)

return &types.QuerySupplyOfResponse{Amount: sdk.NewCoin(req.Denom, supply.Amount)}, nil
}

// TotalSupply implements the Query/TotalSupplyWithoutOffset gRPC method
func (k BaseKeeper) TotalSupplyWithoutOffset(ctx context.Context, req *types.QueryTotalSupplyWithoutOffsetRequest) (*types.QueryTotalSupplyWithoutOffsetResponse, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
totalSupply, pageRes, err := k.GetPaginatedTotalSupply(sdkCtx, req.Pagination)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}

return &types.QueryTotalSupplyWithoutOffsetResponse{Supply: totalSupply, Pagination: pageRes}, nil
}

// SupplyOf implements the Query/SupplyOf gRPC method
func (k BaseKeeper) SupplyOfWithoutOffset(c context.Context, req *types.QuerySupplyOfWithoutOffsetRequest) (*types.QuerySupplyOfWithoutOffsetResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}

if req.Denom == "" {
return nil, status.Error(codes.InvalidArgument, "invalid denom")
}

ctx := sdk.UnwrapSDKContext(c)
supply := k.GetSupply(ctx, req.Denom)

return &types.QuerySupplyOfWithoutOffsetResponse{Amount: sdk.NewCoin(req.Denom, supply.Amount)}, nil
}

// Params implements the gRPC service handler for querying x/bank parameters.
func (k BaseKeeper) Params(ctx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) {
if req == nil {
Expand Down
43 changes: 42 additions & 1 deletion x/bank/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"time"

"cosmossdk.io/math"

"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
Expand Down Expand Up @@ -279,6 +281,7 @@ func (suite *KeeperTestSuite) TestSpendableBalanceByDenom() {

func (suite *KeeperTestSuite) TestQueryTotalSupply() {
ctx, queryClient := suite.ctx, suite.queryClient

res, err := queryClient.TotalSupply(gocontext.Background(), &types.QueryTotalSupplyRequest{})
suite.Require().NoError(err)
suite.Require().NotNil(res)
Expand All @@ -294,7 +297,22 @@ func (suite *KeeperTestSuite) TestQueryTotalSupply() {

expectedTotalSupply := genesisSupply.Add(testCoins...)
suite.Require().Equal(1, len(res.Supply))
suite.Require().Equal(res.Supply, expectedTotalSupply)
suite.Require().Equal(expectedTotalSupply, res.Supply)

// test total supply query with supply offset
suite.bankKeeper.AddSupplyOffset(ctx, "test", math.NewInt(-100000000))
res, err = queryClient.TotalSupply(gocontext.Background(), &types.QueryTotalSupplyRequest{})
suite.Require().NoError(err)
suite.Require().NotNil(res)

suite.Require().Equal(expectedTotalSupply.Sub(sdk.NewCoins(sdk.NewInt64Coin("test", 100000000))...), res.Supply)

// make sure query without offsets hasn't changed
res2, err := queryClient.TotalSupplyWithoutOffset(gocontext.Background(), &types.QueryTotalSupplyWithoutOffsetRequest{})
suite.Require().NoError(err)
suite.Require().NotNil(res2)

suite.Require().Equal(expectedTotalSupply, res2.Supply)
}

func (suite *KeeperTestSuite) TestQueryTotalSupplyOf() {
Expand All @@ -320,6 +338,29 @@ func (suite *KeeperTestSuite) TestQueryTotalSupplyOf() {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(sdk.NewInt64Coin("bogus", 0), res.Amount)

// test total supply of query with supply offset
suite.bankKeeper.AddSupplyOffset(ctx, "test1", math.NewInt(-1000000))
res, err = queryClient.SupplyOf(gocontext.Background(), &types.QuerySupplyOfRequest{Denom: test1Supply.Denom})
suite.Require().NoError(err)
suite.Require().NotNil(res)

suite.Require().Equal(test1Supply.Sub(sdk.NewInt64Coin("test1", 1000000)), res.Amount)

// make sure query without offsets hasn't changed
res2, err := queryClient.SupplyOfWithoutOffset(gocontext.Background(), &types.QuerySupplyOfWithoutOffsetRequest{Denom: test1Supply.Denom})
suite.Require().NoError(err)
suite.Require().NotNil(res2)

suite.Require().Equal(test1Supply, res2.Amount)

// try to make SupplyWithOffset negative, should return as 0
suite.bankKeeper.AddSupplyOffset(ctx, "test1", math.NewInt(-100000000000))
res, err = queryClient.SupplyOf(gocontext.Background(), &types.QuerySupplyOfRequest{Denom: test1Supply.Denom})
suite.Require().NoError(err)
suite.Require().NotNil(res)

suite.Require().Equal(sdk.NewInt64Coin("test1", 0), res.Amount)
}

func (suite *KeeperTestSuite) TestQueryParams() {
Expand Down
26 changes: 26 additions & 0 deletions x/bank/keeper/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package keeper

import (
"context"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank/types"
)

// Implements StakingHooks interface
var _ types.BankHooks = BaseSendKeeper{}

// TrackBeforeSend executes the TrackBeforeSend hook if registered.
func (k BaseSendKeeper) TrackBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) {
if k.hooks != nil {
k.hooks.TrackBeforeSend(ctx, from, to, amount)
}
}

// BlockBeforeSend executes the BlockBeforeSend hook if registered.
func (k BaseSendKeeper) BlockBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) error {
if k.hooks != nil {
return k.hooks.BlockBeforeSend(ctx, from, to, amount)
}
return nil
}
11 changes: 11 additions & 0 deletions x/bank/keeper/internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package keeper

import "github.com/cosmos/cosmos-sdk/x/bank/types"

// UnsafeSetHooks updates the x/bank keeper's hooks, overriding any potential
// pre-existing hooks.
//
// WARNING: this function should only be used in tests.
func UnsafeSetHooks(k *BaseKeeper, h types.BankHooks) {
k.hooks = h
}
Loading

0 comments on commit 8ee2f9e

Please sign in to comment.