-
Notifications
You must be signed in to change notification settings - Fork 130
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(distribution)!: custom fee allocation (#2231)
- Loading branch information
Showing
12 changed files
with
2,199 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
syntax = "proto3"; | ||
package axelar.distribution.v1beta1; | ||
|
||
import "cosmos/base/v1beta1/coin.proto"; | ||
import "gogoproto/gogo.proto"; | ||
|
||
option go_package = "github.com/axelarnetwork/axelar-core/x/distribution/types"; | ||
|
||
message FeeBurnedEvent { | ||
repeated cosmos.base.v1beta1.Coin coins = 2 | ||
[(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package keeper | ||
|
||
import ( | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
distribution "github.com/cosmos/cosmos-sdk/x/distribution/keeper" | ||
distributionTypes "github.com/cosmos/cosmos-sdk/x/distribution/types" | ||
abci "github.com/tendermint/tendermint/abci/types" | ||
|
||
"github.com/axelarnetwork/axelar-core/utils/events" | ||
"github.com/axelarnetwork/axelar-core/x/distribution/types" | ||
"github.com/axelarnetwork/utils/funcs" | ||
"github.com/axelarnetwork/utils/slices" | ||
) | ||
|
||
// Keeper wraps the distribution keeper to customize fee allocation mechanism | ||
type Keeper struct { | ||
distribution.Keeper | ||
|
||
authKeeper types.AccountKeeper | ||
bankKeeper types.BankKeeper | ||
stakingKeeper types.StakingKeeper | ||
feeCollectorName string | ||
} | ||
|
||
func NewKeeper( | ||
k distribution.Keeper, ak types.AccountKeeper, bk types.BankKeeper, | ||
sk types.StakingKeeper, feeCollectorName string, | ||
) Keeper { | ||
return Keeper{ | ||
Keeper: k, | ||
authKeeper: ak, | ||
bankKeeper: bk, | ||
stakingKeeper: sk, | ||
feeCollectorName: feeCollectorName, | ||
} | ||
} | ||
|
||
// AllocateTokens modifies the fee distribution by: | ||
// - Allocating the community tax portion to the community pool | ||
// - Burning all remaining tokens instead of distributing to validators | ||
func (k Keeper) AllocateTokens(ctx sdk.Context, _, _ int64, _ sdk.ConsAddress, _ []abci.VoteInfo) { | ||
// fetch and clear the collected fees for distribution, since this is | ||
// called in BeginBlock, collected fees will be from the previous block | ||
// (and distributed to the previous proposer) | ||
feeCollector := k.authKeeper.GetModuleAccount(ctx, k.feeCollectorName) | ||
feesCollectedInt := k.bankKeeper.GetAllBalances(ctx, feeCollector.GetAddress()) | ||
feesCollected := sdk.NewDecCoinsFromCoins(feesCollectedInt...) | ||
|
||
// transfer collected fees to the distribution module account | ||
err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, k.feeCollectorName, distributionTypes.ModuleName, feesCollectedInt) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
feePool := k.GetFeePool(ctx) | ||
|
||
communityTaxRate := k.GetCommunityTax(ctx) | ||
communityPoolAmount := feesCollected.MulDecTruncate(communityTaxRate) | ||
remaining := feesCollected.Sub(communityPoolAmount) | ||
|
||
// truncate the remaining coins, return remainder to community pool | ||
feeToBurn, truncationRemainder := remaining.TruncateDecimal() | ||
communityPoolAmount = communityPoolAmount.Add(truncationRemainder...) | ||
|
||
// allocate community funding | ||
feePool.CommunityPool = feePool.CommunityPool.Add(communityPoolAmount...) | ||
k.SetFeePool(ctx, feePool) | ||
|
||
// burn the rest | ||
funcs.MustNoErr(k.bankKeeper.BurnCoins(ctx, distributionTypes.ModuleName, feeToBurn)) | ||
events.Emit(ctx, &types.FeeBurnedEvent{ | ||
Coins: feeToBurn, | ||
}) | ||
|
||
// track cumulative burned fee | ||
feeBurned := slices.Map(feeToBurn, types.WithBurnedPrefix) | ||
funcs.MustNoErr(k.bankKeeper.MintCoins(ctx, distributionTypes.ModuleName, feeBurned)) | ||
funcs.MustNoErr(k.bankKeeper.SendCoinsFromModuleToAccount(ctx, distributionTypes.ModuleName, types.ZeroAddress, feeBurned)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
package keeper_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" | ||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" | ||
distribution "github.com/cosmos/cosmos-sdk/x/distribution/keeper" | ||
distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" | ||
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" | ||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" | ||
"github.com/gogo/protobuf/proto" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/tendermint/tendermint/libs/log" | ||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types" | ||
|
||
"github.com/axelarnetwork/axelar-core/app/params" | ||
"github.com/axelarnetwork/axelar-core/testutils/fake" | ||
"github.com/axelarnetwork/axelar-core/testutils/rand" | ||
axelarnettypes "github.com/axelarnetwork/axelar-core/x/axelarnet/exported" | ||
"github.com/axelarnetwork/axelar-core/x/distribution/keeper" | ||
"github.com/axelarnetwork/axelar-core/x/distribution/types" | ||
"github.com/axelarnetwork/axelar-core/x/distribution/types/mock" | ||
"github.com/axelarnetwork/utils/funcs" | ||
"github.com/axelarnetwork/utils/slices" | ||
. "github.com/axelarnetwork/utils/test" | ||
) | ||
|
||
func TestAllocateTokens(t *testing.T) { | ||
var ( | ||
k keeper.Keeper | ||
accBalances map[string]sdk.Coins | ||
bk *mock.BankKeeperMock | ||
fee sdk.Coins | ||
) | ||
|
||
ctx := sdk.NewContext(fake.NewMultiStore(), tmproto.Header{}, false, log.TestingLogger()) | ||
|
||
fee = sdk.NewCoins(sdk.NewCoin(axelarnettypes.NativeAsset, sdk.NewInt(rand.PosI64()))) | ||
accBalances = map[string]sdk.Coins{ | ||
authtypes.NewModuleAddress(authtypes.FeeCollectorName).String(): fee, | ||
} | ||
|
||
Given("an axelar distribution keeper", func() { | ||
encCfg := params.MakeEncodingConfig() | ||
subspace := paramstypes.NewSubspace(encCfg.Codec, encCfg.Amino, sdk.NewKVStoreKey(distributiontypes.StoreKey), sdk.NewKVStoreKey("tKey"), distributiontypes.ModuleName) | ||
ak := &mock.AccountKeeperMock{ | ||
GetModuleAccountFunc: func(ctx sdk.Context, name string) authtypes.ModuleAccountI { | ||
return authtypes.NewEmptyModuleAccount(name) | ||
}, | ||
GetModuleAddressFunc: func(name string) sdk.AccAddress { | ||
return authtypes.NewModuleAddress(name) | ||
}, | ||
} | ||
bk = &mock.BankKeeperMock{ | ||
GetAllBalancesFunc: func(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins { | ||
return accBalances[addr.String()] | ||
}, | ||
SendCoinsFromModuleToModuleFunc: func(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error { | ||
senderModule = authtypes.NewModuleAddress(senderModule).String() | ||
recipientModule = authtypes.NewModuleAddress(recipientModule).String() | ||
|
||
accBalances[senderModule] = accBalances[senderModule].Sub(amt) | ||
accBalances[recipientModule] = accBalances[recipientModule].Add(amt...) | ||
|
||
return nil | ||
}, | ||
SendCoinsFromModuleToAccountFunc: func(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error { | ||
senderModule = authtypes.NewModuleAddress(senderModule).String() | ||
|
||
accBalances[senderModule] = accBalances[senderModule].Sub(amt) | ||
accBalances[recipientAddr.String()] = accBalances[recipientAddr.String()].Add(amt...) | ||
|
||
return nil | ||
}, | ||
BurnCoinsFunc: func(ctx sdk.Context, name string, amt sdk.Coins) error { | ||
acc := authtypes.NewModuleAddress(name).String() | ||
accBalances[acc] = accBalances[acc].Sub(amt) | ||
|
||
return nil | ||
}, | ||
MintCoinsFunc: func(ctx sdk.Context, name string, amt sdk.Coins) error { | ||
acc := authtypes.NewModuleAddress(name).String() | ||
accBalances[acc] = accBalances[acc].Add(amt...) | ||
|
||
return nil | ||
}, | ||
} | ||
sk := &mock.StakingKeeperMock{ | ||
ValidatorByConsAddrFunc: func(ctx sdk.Context, addr sdk.ConsAddress) stakingtypes.ValidatorI { | ||
seed := []byte("key") | ||
consKey := ed25519.GenPrivKeyFromSecret(seed).PubKey() | ||
pk := secp256k1.GenPrivKeyFromSecret(seed) | ||
valAddr := sdk.ValAddress(pk.PubKey().Address().Bytes()) | ||
return funcs.Must(stakingtypes.NewValidator(valAddr, consKey, stakingtypes.Description{})) | ||
}, | ||
} | ||
|
||
distriK := distribution.NewKeeper(encCfg.Codec, sdk.NewKVStoreKey(distributiontypes.StoreKey), subspace, ak, bk, sk, authtypes.FeeCollectorName, map[string]bool{}) | ||
k = keeper.NewKeeper(distriK, ak, bk, sk, authtypes.FeeCollectorName) | ||
k.SetFeePool(ctx, distributiontypes.FeePool{CommunityPool: sdk.DecCoins{}}) | ||
k.SetParams(ctx, distributiontypes.DefaultParams()) | ||
}). | ||
When("allocate token", func() { | ||
k.AllocateTokens(ctx, 0, 1, sdk.ConsAddress{}, nil) | ||
}). | ||
Then("allocate to community pool and burn the rest", func(t *testing.T) { | ||
assert.Len(t, bk.BurnCoinsCalls(), 1) | ||
|
||
feeBurnedType := proto.MessageName(&types.FeeBurnedEvent{}) | ||
assert.Len(t, slices.Filter(ctx.EventManager().Events(), func(e sdk.Event) bool { | ||
return e.Type == feeBurnedType | ||
}), 1) | ||
|
||
burned, tax := expectedBurnAndTax(ctx, k, fee) | ||
expectedBurnedFee := sdk.NewCoins(slices.Map(burned, types.WithBurnedPrefix)...) | ||
|
||
assert.Equal(t, expectedBurnedFee, accBalances[types.ZeroAddress.String()]) | ||
assert.Equal(t, k.GetFeePool(ctx).CommunityPool, tax) | ||
}). | ||
Run(t) | ||
} | ||
|
||
func expectedBurnAndTax(ctx sdk.Context, k keeper.Keeper, fee sdk.Coins) (sdk.Coins, sdk.DecCoins) { | ||
feesDec := sdk.NewDecCoinsFromCoins(fee...) | ||
tax := feesDec.MulDecTruncate(k.GetCommunityTax(ctx)) | ||
burnAmt, remainder := feesDec.Sub(tax).TruncateDecimal() | ||
|
||
return burnAmt, tax.Add(remainder...) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package distribution | ||
|
||
import ( | ||
"github.com/cosmos/cosmos-sdk/types/module" | ||
distr "github.com/cosmos/cosmos-sdk/x/distribution" | ||
|
||
"github.com/axelarnetwork/axelar-core/x/distribution/keeper" | ||
) | ||
|
||
var _ module.AppModule = AppModule{} | ||
|
||
type AppModule struct { | ||
distr.AppModule | ||
|
||
keeper keeper.Keeper | ||
} | ||
|
||
// NewAppModule creates a new AppModule object | ||
func NewAppModule(distrAppModule distr.AppModule, keeper keeper.Keeper) AppModule { | ||
return AppModule{ | ||
AppModule: distrAppModule, | ||
keeper: keeper, | ||
} | ||
} |
Oops, something went wrong.