diff --git a/README.md b/README.md index 324c82e3..fdc737b9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +

+

๐Ÿค Alliance

+ +

+ Litepaper + ยท + Technical Documentation + ยท + Integration Guide +

+ +
+ # x/alliance interchain security The Alliance module is part of the Interchain Security (Cosmos Shared Security that benefits from the IBC standard). Alliance is a friction free Interchain Security solution because there is no necessity to share hardware resources, have the blockchains synchronized nor modify the core of the origin chain that provide Interchain Security. Alliance module introduces the concept of alliance coins that can be seen as foreign coins bridged thru an IBC channel (ICS-004), whitelisted with the help of on-chain governance in the Alliance module and delegated by users or smart contracts to the active set of network validators. @@ -15,6 +28,9 @@ By design, x/alliance use the following CosmosSDK modules to implement interchai - [x/distribution](https://github.com/cosmos/cosmos-sdk/blob/main/x/distribution/README.md), - [x/gov](https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/README.md). +> **Note** +> This is module currently in beta and may not suitable for production use yet. Please submit bugs and feature requests through Github Issues. + # Development environment This project uses [Go v1.18](https://go.dev/dl/) and was bootstrapped with [Ignite CLI v0.25.1](https://docs.ignite.com/). diff --git a/app/app.go b/app/app.go index 3b56c8fa..56b847de 100644 --- a/app/app.go +++ b/app/app.go @@ -35,7 +35,6 @@ import ( authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" authzmodule "github.com/cosmos/cosmos-sdk/x/authz/module" "github.com/cosmos/cosmos-sdk/x/bank" - bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/capability" capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" @@ -80,6 +79,8 @@ import ( "github.com/tendermint/tendermint/libs/log" tmos "github.com/tendermint/tendermint/libs/os" dbm "github.com/tendermint/tm-db" + custombankmodule "github.com/terra-money/alliance/custom/bank" + custombankkeeper "github.com/terra-money/alliance/custom/bank/keeper" "github.com/terra-money/alliance/docs" @@ -188,7 +189,7 @@ type App struct { // keepers AccountKeeper authkeeper.AccountKeeper AuthzKeeper authzkeeper.Keeper - BankKeeper bankkeeper.Keeper + BankKeeper custombankkeeper.Keeper CapabilityKeeper *capabilitykeeper.Keeper StakingKeeper stakingkeeper.Keeper SlashingKeeper slashingkeeper.Keeper @@ -296,7 +297,7 @@ func New( app.MsgServiceRouter(), ) - app.BankKeeper = bankkeeper.NewBaseKeeper( + app.BankKeeper = custombankkeeper.NewBaseKeeper( appCodec, keys[banktypes.StoreKey], app.AccountKeeper, @@ -363,6 +364,8 @@ func New( app.DistrKeeper, ) + app.BankKeeper.RegisterKeepers(app.AllianceKeeper, &stakingKeeper) + // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks app.StakingKeeper = *stakingKeeper.SetHooks( @@ -394,7 +397,7 @@ func New( auth.NewAppModule(appCodec, app.AccountKeeper, nil), authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), vesting.NewAppModule(app.AccountKeeper, app.BankKeeper), - bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper), + custombankmodule.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper), capability.NewAppModule(appCodec, *app.CapabilityKeeper), feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry), crisis.NewAppModule(&app.CrisisKeeper, skipGenesisInvariants), diff --git a/config.yml b/config.yml index 3c58c579..5e0a2f4c 100644 --- a/config.yml +++ b/config.yml @@ -38,15 +38,15 @@ genesis: assets: - denom: uluna reward_weight: "0.5" - take_rate: "0.5" + take_rate: "0.000005" - denom: bluna reward_weight: "0.9" - take_rate: "0.9" + take_rate: "0.0000009" - denom: token reward_weight: "5" - take_rate: "0.9" + take_rate: "0.0000009" params: - reward_claim_interval: 10s + take_rate_claim_interval: 10s reward_delay_time: 60s validators: - name: alice diff --git a/custom/bank/keeper/keeper.go b/custom/bank/keeper/keeper.go new file mode 100644 index 00000000..9ead3966 --- /dev/null +++ b/custom/bank/keeper/keeper.go @@ -0,0 +1,94 @@ +package keeper + +import ( + "context" + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + accountkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/cosmos/cosmos-sdk/x/bank/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + banktypes "github.com/terra-money/alliance/custom/bank/types" + alliancekeeper "github.com/terra-money/alliance/x/alliance/keeper" + alliancetypes "github.com/terra-money/alliance/x/alliance/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type Keeper struct { + bankkeeper.BaseKeeper + + ak alliancekeeper.Keeper + sk banktypes.StakingKeeper + acck accountkeeper.AccountKeeper +} + +var ( + _ bankkeeper.Keeper = Keeper{} +) + +func NewBaseKeeper( + cdc codec.BinaryCodec, + storeKey storetypes.StoreKey, + ak accountkeeper.AccountKeeper, + paramSpace paramtypes.Subspace, + blockedAddrs map[string]bool, +) Keeper { + keeper := Keeper{ + BaseKeeper: bankkeeper.NewBaseKeeper(cdc, storeKey, ak, paramSpace, blockedAddrs), + ak: alliancekeeper.Keeper{}, + sk: stakingkeeper.Keeper{}, + acck: ak, + } + return keeper +} + +func (k *Keeper) RegisterKeepers(ak alliancekeeper.Keeper, sk banktypes.StakingKeeper) { + k.ak = ak + k.sk = sk +} + +// SupplyOf implements the Query/SupplyOf gRPC method +func (k Keeper) SupplyOf(c context.Context, req *types.QuerySupplyOfRequest) (*types.QuerySupplyOfResponse, 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) + + if req.Denom == k.sk.BondDenom(ctx) { + assets := k.ak.GetAllAssets(ctx) + totalRewardWeights := sdk.ZeroDec() + for _, asset := range assets { + totalRewardWeights = totalRewardWeights.Add(asset.RewardWeight) + } + allianceBonded := k.ak.GetAllianceBondedAmount(ctx, k.acck.GetModuleAddress(alliancetypes.ModuleName)) + supply.Amount = supply.Amount.Sub(allianceBonded) + } + + return &types.QuerySupplyOfResponse{Amount: sdk.NewCoin(req.Denom, supply.Amount)}, nil +} + +// TotalSupply implements the Query/TotalSupply gRPC method +func (k Keeper) TotalSupply(ctx context.Context, req *types.QueryTotalSupplyRequest) (*types.QueryTotalSupplyResponse, error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + totalSupply, pageRes, err := k.GetPaginatedTotalSupply(sdkCtx, req.Pagination) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + allianceBonded := k.ak.GetAllianceBondedAmount(sdkCtx, k.acck.GetModuleAddress(alliancetypes.ModuleName)) + bondDenom := k.sk.BondDenom(sdkCtx) + if totalSupply.AmountOf(bondDenom).IsPositive() { + totalSupply = totalSupply.Sub(sdk.NewCoin(bondDenom, allianceBonded)) + } + + return &types.QueryTotalSupplyResponse{Supply: totalSupply, Pagination: pageRes}, nil +} diff --git a/custom/bank/module.go b/custom/bank/module.go new file mode 100644 index 00000000..8f837988 --- /dev/null +++ b/custom/bank/module.go @@ -0,0 +1,45 @@ +package bank + +import ( + "fmt" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/module" + bankmodule "github.com/cosmos/cosmos-sdk/x/bank" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/cosmos/cosmos-sdk/x/bank/types" + custombankkeeper "github.com/terra-money/alliance/custom/bank/keeper" +) + +// AppModule wraps around the bank module and the bank keeper to return the right total supply ignoring bonded tokens +// that the alliance module minted to rebalance the voting power +// It modifies the TotalSupply and SupplyOf GRPC queries +type AppModule struct { + bankmodule.AppModule + keeper custombankkeeper.Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(cdc codec.Codec, keeper custombankkeeper.Keeper, accountKeeper types.AccountKeeper) AppModule { + bankModule := bankmodule.NewAppModule(cdc, keeper, accountKeeper) + return AppModule{ + AppModule: bankModule, + keeper: keeper, + } +} + +// RegisterServices registers module services. +// NOTE: Overriding this method as not doing so will cause a panic +// when trying to force this custom keeper into a bankkeeper.BaseKeeper +func (am AppModule) RegisterServices(cfg module.Configurator) { + types.RegisterMsgServer(cfg.MsgServer(), bankkeeper.NewMsgServerImpl(am.keeper)) + types.RegisterQueryServer(cfg.QueryServer(), am.keeper) + + m := bankkeeper.NewMigrator(am.keeper.BaseKeeper) + if err := cfg.RegisterMigration(types.ModuleName, 1, m.Migrate1to2); err != nil { + panic(fmt.Sprintf("failed to migrate x/bank from version 1 to 2: %v", err)) + } + + if err := cfg.RegisterMigration(types.ModuleName, 2, m.Migrate2to3); err != nil { + panic(fmt.Sprintf("failed to migrate x/bank from version 2 to 3: %v", err)) + } +} diff --git a/custom/bank/types/keeper_interfaces.go b/custom/bank/types/keeper_interfaces.go new file mode 100644 index 00000000..4fc75155 --- /dev/null +++ b/custom/bank/types/keeper_interfaces.go @@ -0,0 +1,7 @@ +package types + +import sdk "github.com/cosmos/cosmos-sdk/types" + +type StakingKeeper interface { + BondDenom(ctx sdk.Context) (res string) +} diff --git a/scripts/local/README.md b/scripts/local/README.md index c99f7dc0..b6f647f9 100644 --- a/scripts/local/README.md +++ b/scripts/local/README.md @@ -8,6 +8,6 @@ Folder containing some scripts to test or/and demo the functionality. Before exe 4. **delegate.sh** delegate to the previously create alliance and query the modified alliance 5. **rewards.sh** claim rewards and query information about the evidences of the process 6. **undelegate.sh** undelegante the tokens from the alliance and query the evidences -7. **gov.sh** with the file gov-delete.json deletes the alliance created in third step. +7. **gov-del.sh** with the file gov-delete.json deletes the alliance created in third step. > This scripts must be executed in the specified order since they have dependencies on each other. diff --git a/scripts/local/gov-del.sh b/scripts/local/gov-del.sh new file mode 100644 index 00000000..b61cad08 --- /dev/null +++ b/scripts/local/gov-del.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +printf "#1) Submit proposal to delete the ulunax Alliance...\n\n" +allianced tx gov submit-legacy-proposal delete-alliance ulunax --from=demowallet1 --home ./data/alliance --keyring-backend=test --broadcast-mode=block --gas 1000000 -y > /dev/null 2>&1 + +PROPOSAL_ID=$(allianced query gov proposals --count-total --output json --home ./data/alliance | jq .pagination.total -r) + +printf "\n#2) Deposit funds to proposal $PROPOSAL_ID...\n\n" +allianced tx gov deposit $PROPOSAL_ID 10000000stake --from=demowallet1 --home ./data/alliance --keyring-backend=test --broadcast-mode=block --gas 1000000 -y > /dev/null 2>&1 + +printf "\n#3) Vote to pass the proposal...\n\n" +allianced tx gov vote $PROPOSAL_ID yes --from=val1 --home ./data/alliance --keyring-backend=test --broadcast-mode=block --gas 1000000 -y > /dev/null 2>&1 + +printf "\n#4) Query proposals...\n\n" +allianced query gov proposal $PROPOSAL_ID --home ./data/alliance + +printf "\n#5) Query alliances...\n\n" +allianced query alliance alliances --home ./data/alliance + +printf "\n#6) Waiting for gov proposal to pass...\n\n" +sleep 8 + +printf "\n#7) Query alliances after proposal passed...\n\n" +allianced query alliance alliances --home ./data/alliance \ No newline at end of file diff --git a/scripts/local/gov-delete.json b/scripts/local/gov-delete.json deleted file mode 100644 index 88d04957..00000000 --- a/scripts/local/gov-delete.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "messages": [ - { - "@type": "/alliance.alliance.MsgDeleteAlliance", - "denom" : "ulunax", - "authority" : "alliance10d07y265gmmuvt4z0w9aw880jnsr700j8hf8cu" - } - ], - "metadata": "Alliance with our own 'stake' denomination" -} \ No newline at end of file diff --git a/scripts/local/gov.json b/scripts/local/gov.json deleted file mode 100644 index 9c3238c1..00000000 --- a/scripts/local/gov.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "content": [ - { - "@type": "/alliance.alliance.MsgCreateAllianceProposal", - "denom" : "ibc/hash", - "rewardWeight" : "1", - "takeRate" : "1" - } - ], - "metadata": "Create Alliance with our own 'ibc/hash' denomination" -} \ No newline at end of file diff --git a/scripts/local/gov.sh b/scripts/local/gov.sh index 61ffb96a..7c374a4a 100644 --- a/scripts/local/gov.sh +++ b/scripts/local/gov.sh @@ -1,7 +1,7 @@ #!/bin/bash printf "#1) Submit proposal to create ulunax Alliance...\n\n" -allianced tx gov submit-legacy-proposal create-alliance ulunax 0.5 0.5 --from=demowallet1 --home ./data/alliance --keyring-backend=test --broadcast-mode=block --gas 1000000 -y > /dev/null 2>&1 +allianced tx gov submit-legacy-proposal create-alliance ulunax 0.5 0.00005 1 0 --from=demowallet1 --home ./data/alliance --keyring-backend=test --broadcast-mode=block --gas 1000000 -y > /dev/null 2>&1 PROPOSAL_ID=$(allianced query gov proposals --count-total --output json --home ./data/alliance | jq .pagination.total -r) diff --git a/scripts/local/undelegate.sh b/scripts/local/undelegate.sh index 0fd51c16..b526bc46 100644 --- a/scripts/local/undelegate.sh +++ b/scripts/local/undelegate.sh @@ -4,7 +4,7 @@ DEMO_WALLET_ADDRESS=$(allianced --home ./data/alliance keys show demowallet1 --k VAL_ADDR=$(allianced query staking validators --output json | jq .validators[0].operator_address --raw-output) COIN_DENOM=ulunax COIN_AMOUNT=$(allianced query alliance delegation $DEMO_WALLET_ADDRESS $VAL_ADDR $COIN_DENOM --home ./data/alliance --output json | jq .delegation.balance.amount --raw-output | sed 's/\.[0-9]*//') -COINS=5000000000$COIN_DENOM +COINS=$COIN_AMOUNT$COIN_DENOM # FIX: failed to execute message; message index: 0: invalid shares amount: invalid printf "#1) Undelegate 5000000000$COIN_DENOM from x/alliance $COIN_DENOM...\n\n" diff --git a/scripts/testnet/README.md b/scripts/testnet/README.md index 7a0f3b81..edb0cd3e 100644 --- a/scripts/testnet/README.md +++ b/scripts/testnet/README.md @@ -1,9 +1,9 @@ # Scripts -Folder containing scripts to crate the alliances on testnet and delegate automatically +This folder contains a sequence of helper scripts for creating an alliance on testnet and automatic delegation. -1. **gov.sh** submit the gov.json governance proposal, votes on favor and query the created alliance -2. **delegate.sh** delegate to the previously create alliance and query the modified alliance -3. **rewards.sh** claim rewards and query information about the evidences of the process +1. **gov.sh** submits a gov.json governance proposal, votes in favor of it and then queries the created alliance. +2. **delegate.sh** delegates to the previously create alliance and queries the modified alliance. +3. **rewards.sh** claims available rewards and retrieves information about the process -> This scripts must be executed in the specified order since they have dependencies on each other. +> Note that these scripts must be executed in the specified order since they have dependencies on each other. diff --git a/x/alliance/abci.go b/x/alliance/abci.go index bf449c30..4c8b5308 100644 --- a/x/alliance/abci.go +++ b/x/alliance/abci.go @@ -1,6 +1,7 @@ package alliance import ( + "fmt" "time" "github.com/terra-money/alliance/x/alliance/keeper" @@ -16,16 +17,16 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate { defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker) k.CompleteRedelegations(ctx) if err := k.CompleteUndelegations(ctx); err != nil { - panic(err) + panic(fmt.Errorf("Failed to complete undelegations from x/alliance module: %s", err)) } assets := k.GetAllAssets(ctx) if _, err := k.DeductAssetsHook(ctx, assets); err != nil { - panic(err) + panic(fmt.Errorf("Failed to deduct take rate from alliance in x/alliance module: %s", err)) } - k.RewardWeightDecayHook(ctx, assets) + k.RewardWeightChangeHook(ctx, assets) if err := k.RebalanceHook(ctx, assets); err != nil { - panic(err) + panic(fmt.Errorf("Failed to rebalance assets in x/alliance module: %s", err)) } return []abci.ValidatorUpdate{} } diff --git a/x/alliance/client/cli/gov.go b/x/alliance/client/cli/gov.go index 082e9ca0..4044b818 100644 --- a/x/alliance/client/cli/gov.go +++ b/x/alliance/client/cli/gov.go @@ -8,12 +8,13 @@ import ( govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/spf13/cobra" "github.com/terra-money/alliance/x/alliance/types" + "time" ) func CreateAlliance() *cobra.Command { cmd := &cobra.Command{ - Use: "create-alliance denom rewards-weight take-rate", - Args: cobra.ExactArgs(3), + Use: "create-alliance denom rewards-weight take-rate reward-change-rate reward-change-interval", + Args: cobra.ExactArgs(5), Short: "Create an alliance with the specified parameters", RunE: func(cmd *cobra.Command, args []string) error { clientCtx, err := client.GetClientTxContext(cmd) @@ -40,6 +41,16 @@ func CreateAlliance() *cobra.Command { return err } + rewardChangeRate, err := sdk.NewDecFromStr(args[3]) + if err != nil { + return err + } + + rewardChangeInterval, err := time.ParseDuration(args[4]) + if err != nil { + return err + } + from := clientCtx.GetFromAddress() depositStr, err := cmd.Flags().GetString(govcli.FlagDeposit) @@ -58,6 +69,8 @@ func CreateAlliance() *cobra.Command { args[0], rewardWeight, takeRate, + rewardChangeRate, + rewardChangeInterval, ) err = content.ValidateBasic() @@ -88,8 +101,8 @@ func CreateAlliance() *cobra.Command { func UpdateAlliance() *cobra.Command { cmd := &cobra.Command{ - Use: "update-alliance denom rewards-weight take-rate", - Args: cobra.ExactArgs(3), + Use: "update-alliance denom rewards-weight take-rate reward-change-rate reward-change-interval", + Args: cobra.ExactArgs(5), Short: "Update an alliance with the specified parameters", RunE: func(cmd *cobra.Command, args []string) error { clientCtx, err := client.GetClientTxContext(cmd) @@ -116,6 +129,16 @@ func UpdateAlliance() *cobra.Command { return err } + rewardChangeRate, err := sdk.NewDecFromStr(args[3]) + if err != nil { + return err + } + + rewardChangeInterval, err := time.ParseDuration(args[4]) + if err != nil { + return err + } + from := clientCtx.GetFromAddress() depositStr, err := cmd.Flags().GetString(govcli.FlagDeposit) @@ -134,6 +157,8 @@ func UpdateAlliance() *cobra.Command { args[0], rewardWeight, takeRate, + rewardChangeRate, + rewardChangeInterval, ) err = content.ValidateBasic() diff --git a/x/alliance/invariants.go b/x/alliance/invariants.go index 8346ba26..0bf188d2 100644 --- a/x/alliance/invariants.go +++ b/x/alliance/invariants.go @@ -107,8 +107,8 @@ func DelegatorSharesInvariant(k keeper.Keeper) sdk.Invariant { for denom, amount := range assets { if !shares.AmountOf(denom).Equal(amount) { msg += fmt.Sprintf("broken alliance delegation share invariance: \n"+ - "validator.TotalDelegatorShares(%s): %s\n"+ - "sum of delegator shares: %s\n", denom, shares, amount) + "validator (%s) TotalDelegatorShares(%s): %s\n"+ + "sum of delegator shares: %s\n", valAddr.String(), denom, shares, amount) broken = true } } diff --git a/x/alliance/keeper/asset.go b/x/alliance/keeper/asset.go index 5562c15b..82ba89fb 100644 --- a/x/alliance/keeper/asset.go +++ b/x/alliance/keeper/asset.go @@ -57,6 +57,7 @@ func (k Keeper) UpdateAllianceAsset(ctx sdk.Context, newAsset types.AllianceAsse asset.RewardWeight = newAsset.RewardWeight asset.RewardChangeRate = newAsset.RewardChangeRate asset.RewardChangeInterval = newAsset.RewardChangeInterval + asset.LastRewardChangeTime = newAsset.LastRewardChangeTime k.SetAsset(ctx, asset) return nil @@ -75,7 +76,7 @@ func (k Keeper) RebalanceHook(ctx sdk.Context, assets []*types.AllianceAsset) er // the difference. func (k Keeper) RebalanceBondTokenWeights(ctx sdk.Context, assets []*types.AllianceAsset) (err error) { moduleAddr := k.accountKeeper.GetModuleAddress(types.ModuleName) - allianceBondAmount := k.getAllianceBondedAmount(ctx, moduleAddr) + allianceBondAmount := k.GetAllianceBondedAmount(ctx, moduleAddr) nativeBondAmount := k.stakingKeeper.TotalBondedTokens(ctx).Sub(allianceBondAmount) bondDenom := k.stakingKeeper.BondDenom(ctx) @@ -121,7 +122,7 @@ func (k Keeper) RebalanceBondTokenWeights(ctx sdk.Context, assets []*types.Allia bondedValidatorShares := asset.TotalValidatorShares.Sub(unbondedValidatorShares.AmountOf(asset.Denom)) if valShares.IsPositive() && bondedValidatorShares.IsPositive() { - expectedBondAmount = expectedBondAmount.Add(valShares.Mul(expectedBondAmountForAsset).Quo(bondedValidatorShares)) + expectedBondAmount = expectedBondAmount.Add(valShares.Quo(bondedValidatorShares).Mul(expectedBondAmountForAsset)) } } if expectedBondAmount.GT(currentBondedAmount) { @@ -237,7 +238,12 @@ func (k Keeper) DeductAssetsWithTakeRate(ctx sdk.Context, lastClaim time.Time, a // take rate must be < 1 so multiple is also < 1 multiplier := sdk.OneDec().Sub(asset.TakeRate).Power(intervalsSinceLastClaim) oldAmount := asset.TotalTokens - asset.TotalTokens = multiplier.MulInt(asset.TotalTokens).TruncateInt() + newAmount := multiplier.MulInt(asset.TotalTokens) + if newAmount.LTE(sdk.OneDec()) { + // If the next update reduces the amount of tokens to less than or equal to 1, stop reducing + continue + } + asset.TotalTokens = newAmount.TruncateInt() deductedAmount := oldAmount.Sub(asset.TotalTokens) coins = coins.Add(sdk.NewCoin(asset.Denom, deductedAmount)) k.SetAsset(ctx, *asset) @@ -288,7 +294,7 @@ func (k Keeper) IterateAllWeightChangeSnapshot(ctx sdk.Context, cb func(denom st } } -func (k Keeper) RewardWeightDecayHook(ctx sdk.Context, assets []*types.AllianceAsset) { +func (k Keeper) RewardWeightChangeHook(ctx sdk.Context, assets []*types.AllianceAsset) { for _, asset := range assets { // If no reward changes are required, skip if asset.RewardChangeInterval == 0 || asset.RewardChangeRate.Equal(sdk.OneDec()) { diff --git a/x/alliance/keeper/asset_test.go b/x/alliance/keeper/asset_test.go index 016b6d46..79d729df 100644 --- a/x/alliance/keeper/asset_test.go +++ b/x/alliance/keeper/asset_test.go @@ -629,13 +629,13 @@ func TestRewardWeightDecay(t *testing.T) { assets := app.AllianceKeeper.GetAllAssets(ctx) // Running the decay hook now should do nothing - app.AllianceKeeper.RewardWeightDecayHook(ctx, assets) + app.AllianceKeeper.RewardWeightChangeHook(ctx, assets) // Move block time to after change interval + one year ctx = ctx.WithBlockTime(asset.RewardStartTime.Add(decayInterval)) // Running the decay hook should update reward weight - app.AllianceKeeper.RewardWeightDecayHook(ctx, assets) + app.AllianceKeeper.RewardWeightChangeHook(ctx, assets) updatedAsset, _ := app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_TOKEN_DENOM) require.Equal(t, types.AllianceAsset{ Denom: ALLIANCE_TOKEN_DENOM, @@ -760,7 +760,7 @@ func TestRewardWeightDecayOverTime(t *testing.T) { ctx = ctx.WithBlockTime(ctx.BlockTime().Add(blockTime)).WithBlockHeight(ctx.BlockHeight() + 1) assets = app.AllianceKeeper.GetAllAssets(ctx) // Running the decay hook should update reward weight - app.AllianceKeeper.RewardWeightDecayHook(ctx, assets) + app.AllianceKeeper.RewardWeightChangeHook(ctx, assets) } // time passed minus reward delay time (rewards and decay only start after the delay) @@ -857,3 +857,50 @@ func TestClaimTakeRate(t *testing.T) { rewards.AmountOf(ALLIANCE_TOKEN_DENOM).Add(community.AmountOf(ALLIANCE_TOKEN_DENOM)), ) } + +func TestClaimTakeRateToZero(t *testing.T) { + app, ctx := createTestContext(t) + startTime := time.Now().UTC() + ctx = ctx.WithBlockTime(startTime) + ctx = ctx.WithBlockHeight(1) + takeRateInterval := time.Minute * 5 + asset := types.NewAllianceAsset(ALLIANCE_TOKEN_DENOM, sdk.NewDec(2), sdk.MustNewDecFromStr("0.8"), startTime) + app.AllianceKeeper.InitGenesis(ctx, &types.GenesisState{ + Params: types.Params{ + RewardDelayTime: time.Minute * 60, + TakeRateClaimInterval: takeRateInterval, + LastTakeRateClaimTime: startTime, + }, + Assets: []types.AllianceAsset{ + asset, + }, + }) + + // Accounts + delegations := app.StakingKeeper.GetAllDelegations(ctx) + valAddr1, err := sdk.ValAddressFromBech32(delegations[0].ValidatorAddress) + require.NoError(t, err) + val1, err := app.AllianceKeeper.GetAllianceValidator(ctx, valAddr1) + require.NoError(t, err) + addrs := test_helpers.AddTestAddrsIncremental(app, ctx, 1, sdk.NewCoins( + sdk.NewCoin(ALLIANCE_TOKEN_DENOM, sdk.NewInt(1000_000_000)), + )) + user1 := addrs[0] + + app.AllianceKeeper.Delegate(ctx, user1, val1, sdk.NewCoin(ALLIANCE_TOKEN_DENOM, sdk.NewInt(1000_000_000))) + assets := app.AllianceKeeper.GetAllAssets(ctx) + err = app.AllianceKeeper.RebalanceBondTokenWeights(ctx, assets) + require.NoError(t, err) + + timePassed := time.Minute * 5 + // Advance block time + for i := 0; i < 1000; i++ { + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(timePassed)) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + _, err = app.AllianceKeeper.DeductAssetsHook(ctx, assets) + require.NoError(t, err) + } + + asset, _ = app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_TOKEN_DENOM) + require.True(t, asset.TotalTokens.GTE(sdk.OneInt())) +} diff --git a/x/alliance/keeper/delegation.go b/x/alliance/keeper/delegation.go index 50fd05dc..417296d5 100644 --- a/x/alliance/keeper/delegation.go +++ b/x/alliance/keeper/delegation.go @@ -55,6 +55,10 @@ func (k Keeper) Delegate(ctx sdk.Context, delAddr sdk.AccAddress, validator type // Redelegate from one validator to another func (k Keeper) Redelegate(ctx sdk.Context, delAddr sdk.AccAddress, srcVal types.AllianceValidator, dstVal types.AllianceValidator, coin sdk.Coin) (*time.Time, error) { + if srcVal.Validator.Equal(dstVal.Validator) { + return nil, status.Errorf(codes.InvalidArgument, "Cannot redelegate to the same validator") + } + asset, found := k.GetAssetByDenom(ctx, coin.Denom) if !found { return nil, status.Errorf(codes.NotFound, "Asset with denom: %s does not exist", coin.Denom) @@ -117,6 +121,7 @@ func (k Keeper) Redelegate(ctx sdk.Context, delAddr sdk.AccAddress, srcVal types k.addRedelegation(ctx, delAddr, srcVal.GetOperator(), dstVal.GetOperator(), coin, completionTime) k.QueueAssetRebalanceEvent(ctx) + return &completionTime, nil } @@ -478,11 +483,10 @@ func (k Keeper) setUnbondingIndexByVal(ctx sdk.Context, valAddr sdk.ValAddress, func (k Keeper) upsertDelegationWithNewTokens(ctx sdk.Context, delAddr sdk.AccAddress, validator types.AllianceValidator, coin sdk.Coin, asset types.AllianceAsset) (types.Delegation, sdk.Dec) { newShares := types.GetDelegationSharesFromTokens(validator, asset, coin.Amount) delegation, found := k.GetDelegation(ctx, delAddr, validator, coin.Denom) - latestClaimHistory := validator.GlobalRewardHistory if !found { - delegation = types.NewDelegation(ctx, delAddr, validator.GetOperator(), coin.Denom, newShares, latestClaimHistory) + delegation = types.NewDelegation(ctx, delAddr, validator.GetOperator(), coin.Denom, newShares, validator.GlobalRewardHistory) } else { - delegation.AddShares(newShares) + delegation.Shares = delegation.Shares.Add(newShares) } k.SetDelegation(ctx, delAddr, validator.GetOperator(), coin.Denom, delegation) return delegation, newShares @@ -491,15 +495,13 @@ func (k Keeper) upsertDelegationWithNewTokens(ctx sdk.Context, delAddr sdk.AccAd // reduceDelegationShares // If shares after reduction = 0, delegation will be deleted func (k Keeper) reduceDelegationShares(ctx sdk.Context, delAddr sdk.AccAddress, validator types.AllianceValidator, coin sdk.Coin, shares sdk.Dec, delegation types.Delegation) { - delegation.ReduceShares(shares) - store := ctx.KVStore(k.storeKey) - key := types.GetDelegationKey(delAddr, validator.GetOperator(), coin.Denom) + delegation.Shares = delegation.Shares.Sub(shares) if delegation.Shares.IsZero() { + store := ctx.KVStore(k.storeKey) + key := types.GetDelegationKey(delAddr, validator.GetOperator(), coin.Denom) store.Delete(key) } else { - b := k.cdc.MustMarshal(&delegation) - ctx.KVStore(k.storeKey).Set(key, b) - store.Set(key, b) + k.SetDelegation(ctx, delAddr, validator.GetOperator(), coin.Denom, delegation) } } @@ -512,7 +514,7 @@ func (k Keeper) updateValidatorShares(ctx sdk.Context, validator types.AllianceV k.SetValidator(ctx, validator) } -// getAllianceBondedAmount returns the total amount of bonded native tokens that are not in the +// GetAllianceBondedAmount returns the total amount of bonded native tokens that are not in the // unbonding pool func (k Keeper) getAllianceBondedAmount(ctx sdk.Context, delegator sdk.AccAddress) sdk.Int { bonded := sdk.ZeroDec() diff --git a/x/alliance/keeper/delegation_test.go b/x/alliance/keeper/delegation_test.go index a747e517..b154964b 100644 --- a/x/alliance/keeper/delegation_test.go +++ b/x/alliance/keeper/delegation_test.go @@ -2,6 +2,7 @@ package keeper_test import ( test_helpers "github.com/terra-money/alliance/app" + "github.com/terra-money/alliance/x/alliance" "github.com/terra-money/alliance/x/alliance/keeper" "github.com/terra-money/alliance/x/alliance/types" "testing" @@ -290,6 +291,9 @@ func TestSuccessfulRedelegation(t *testing.T) { _, err = app.AllianceKeeper.Redelegate(ctx, delAddr1, val1, val2, sdk.NewCoin(ALLIANCE_TOKEN_DENOM, sdk.NewInt(500_000))) require.NoError(t, err) + _, stop := alliance.RunAllInvariants(ctx, app.AllianceKeeper) + require.False(t, stop) + assets := app.AllianceKeeper.GetAllAssets(ctx) err = app.AllianceKeeper.RebalanceBondTokenWeights(ctx, assets) require.NoError(t, err) diff --git a/x/alliance/keeper/params.go b/x/alliance/keeper/params.go index de3ca460..fd3be91e 100644 --- a/x/alliance/keeper/params.go +++ b/x/alliance/keeper/params.go @@ -13,15 +13,15 @@ func (k Keeper) RewardDelayTime(ctx sdk.Context) (res time.Duration) { } func (k Keeper) RewardClaimInterval(ctx sdk.Context) (res time.Duration) { - k.paramstore.Get(ctx, types.RewardClaimInterval, &res) + k.paramstore.Get(ctx, types.TakeRateClaimInterval, &res) return } func (k Keeper) LastRewardClaimTime(ctx sdk.Context) (res time.Time) { - k.paramstore.Get(ctx, types.LastRewardClaimTime, &res) + k.paramstore.Get(ctx, types.LastTakeRateClaimTime, &res) return } func (k Keeper) SetLastRewardClaimTime(ctx sdk.Context, lastTime time.Time) { - k.paramstore.Set(ctx, types.LastRewardClaimTime, &lastTime) + k.paramstore.Set(ctx, types.LastTakeRateClaimTime, &lastTime) } diff --git a/x/alliance/keeper/proposal_test.go b/x/alliance/keeper/proposal_test.go index e0ba723d..36a20778 100644 --- a/x/alliance/keeper/proposal_test.go +++ b/x/alliance/keeper/proposal_test.go @@ -53,7 +53,7 @@ func TestCreateAlliance(t *testing.T) { }) } -func TestCreateAllianceFailWithDuplicate(t *testing.T) { +func TestCreateAllianceFailWithDuplicatedDenom(t *testing.T) { // GIVEN app, ctx := createTestContext(t) startTime := time.Now() diff --git a/x/alliance/keeper/reward_test.go b/x/alliance/keeper/reward_test.go index 6b2b096c..73b7e1aa 100644 --- a/x/alliance/keeper/reward_test.go +++ b/x/alliance/keeper/reward_test.go @@ -180,12 +180,12 @@ func TestClaimRewards(t *testing.T) { asset, _ := app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_TOKEN_DENOM) require.Equal(t, sdk.NewInt(1000_000), - val1.TotalTokensWithAsset(asset), + val1.TotalTokensWithAsset(asset).TruncateInt(), ) asset, _ = app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_2_TOKEN_DENOM) require.Equal(t, sdk.NewInt(1000_000), - val1.TotalTokensWithAsset(asset), + val1.TotalTokensWithAsset(asset).TruncateInt(), ) // Transfer another token to reward pool @@ -648,7 +648,7 @@ func TestRewardClaimingAfterRatesDecay(t *testing.T) { assets = app.AllianceKeeper.GetAllAssets(ctx) // Running the decay hook should update reward weight - app.AllianceKeeper.RewardWeightDecayHook(ctx, assets) + app.AllianceKeeper.RewardWeightChangeHook(ctx, assets) asset, _ := app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_TOKEN_DENOM) require.Equal(t, sdk.MustNewDecFromStr("0.25"), asset.RewardWeight) app.AllianceKeeper.AddAssetsToRewardPool(ctx, addrs[0], val0, sdk.NewCoins(sdk.NewCoin(bondDenom, sdk.NewInt(1000_000)))) diff --git a/x/alliance/keeper/slash_test.go b/x/alliance/keeper/slash_test.go index 2b8798bc..714f96f9 100644 --- a/x/alliance/keeper/slash_test.go +++ b/x/alliance/keeper/slash_test.go @@ -112,10 +112,10 @@ func TestSlashingEvent(t *testing.T) { // Tokens should remain the same before slashing asset1, _ := app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_TOKEN_DENOM) - tokens := val1.TotalTokensWithAsset(asset1) + tokens := val1.TotalTokensWithAsset(asset1).TruncateInt() require.Equal(t, sdk.NewInt(20_000_000), tokens) asset2, _ := app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_2_TOKEN_DENOM) - tokens = val1.TotalTokensWithAsset(asset2) + tokens = val1.TotalTokensWithAsset(asset2).TruncateInt() require.Equal(t, sdk.NewInt(20_000_000), tokens) app.SlashingKeeper.Slash(ctx, valConAddr1, app.SlashingKeeper.SlashFractionDoubleSign(ctx), valPower1, 1) @@ -131,19 +131,19 @@ func TestSlashingEvent(t *testing.T) { // Expect that total tokens with validator 1 are reduced val1, _ = app.AllianceKeeper.GetAllianceValidator(ctx, valAddr1) asset1, _ = app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_TOKEN_DENOM) - tokens = val1.TotalTokensWithAsset(asset1) + tokens = val1.TotalTokensWithAsset(asset1).TruncateInt() require.Greater(t, sdk.NewInt(20_000_000).Int64(), tokens.Int64()) asset2, _ = app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_2_TOKEN_DENOM) - tokens = val1.TotalTokensWithAsset(asset2) + tokens = val1.TotalTokensWithAsset(asset2).TruncateInt() require.Greater(t, sdk.NewInt(20_000_000).Int64(), tokens.Int64()) // Expect that total tokens with validator 2 increased (redistributed from slashing) val2, _ = app.AllianceKeeper.GetAllianceValidator(ctx, valAddr2) asset1, _ = app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_TOKEN_DENOM) - tokens = val2.TotalTokensWithAsset(asset1) + tokens = val2.TotalTokensWithAsset(asset1).TruncateInt() require.Less(t, sdk.NewInt(20_000_000).Int64(), tokens.Int64()) asset2, _ = app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_2_TOKEN_DENOM) - tokens = val2.TotalTokensWithAsset(asset2) + tokens = val2.TotalTokensWithAsset(asset2).TruncateInt() require.Less(t, sdk.NewInt(20_000_000).Int64(), tokens.Int64()) // Expect that consensus power for val1 dropped diff --git a/x/alliance/types/asset.go b/x/alliance/types/asset.go index dba8062b..ac55e351 100644 --- a/x/alliance/types/asset.go +++ b/x/alliance/types/asset.go @@ -19,18 +19,18 @@ func NewAllianceAsset(denom string, rewardWeight sdk.Dec, takeRate sdk.Dec, rewa } } -func ConvertNewTokenToShares(totalTokens sdk.Int, totalShares sdk.Dec, newTokens sdk.Int) (shares sdk.Dec) { - if totalShares.IsZero() || totalTokens.IsZero() { +func ConvertNewTokenToShares(totalTokens sdk.Dec, totalShares sdk.Dec, newTokens sdk.Int) (shares sdk.Dec) { + if totalShares.IsZero() { return sdk.NewDecFromInt(newTokens) } - return totalShares.MulInt(newTokens).QuoInt(totalTokens) + return totalShares.Quo(totalTokens).MulInt(newTokens) } func ConvertNewShareToDecToken(totalTokens sdk.Dec, totalShares sdk.Dec, shares sdk.Dec) (token sdk.Dec) { if totalShares.IsZero() { return totalTokens } - return shares.Mul(totalTokens).Quo(totalShares) + return shares.Quo(totalShares).Mul(totalTokens) } func GetDelegationTokens(del Delegation, val AllianceValidator, asset AllianceAsset) sdk.Coin { @@ -43,11 +43,14 @@ func GetDelegationTokens(del Delegation, val AllianceValidator, asset AllianceAs func GetDelegationSharesFromTokens(val AllianceValidator, asset AllianceAsset, token sdk.Int) sdk.Dec { valTokens := val.TotalTokensWithAsset(asset) totalDelegationShares := val.TotalDelegationSharesWithDenom(asset.Denom) + if totalDelegationShares.TruncateInt().Equal(sdk.ZeroInt()) { + return sdk.NewDecFromInt(token) + } return ConvertNewTokenToShares(valTokens, totalDelegationShares, token) } func GetValidatorShares(asset AllianceAsset, token sdk.Int) sdk.Dec { - return ConvertNewTokenToShares(asset.TotalTokens, asset.TotalValidatorShares, token) + return ConvertNewTokenToShares(sdk.NewDecFromInt(asset.TotalTokens), asset.TotalValidatorShares, token) } func (a AllianceAsset) HasPositiveDecay() bool { diff --git a/x/alliance/types/delegations.go b/x/alliance/types/delegations.go index d0c40dff..3c5968ed 100644 --- a/x/alliance/types/delegations.go +++ b/x/alliance/types/delegations.go @@ -14,17 +14,3 @@ func NewDelegation(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddre LastRewardClaimHeight: uint64(ctx.BlockHeight()), } } - -// ReduceShares -func (d *Delegation) ReduceShares(shares sdk.Dec) { - if d.Shares.LTE(shares) { - d.Shares = sdk.ZeroDec() - } else { - d.Shares = d.Shares.Sub(shares) - } -} - -// AddShares -func (d *Delegation) AddShares(shares sdk.Dec) { - d.Shares = d.Shares.Add(shares) -} diff --git a/x/alliance/types/gov.go b/x/alliance/types/gov.go index 57001fbe..c2d0beaf 100644 --- a/x/alliance/types/gov.go +++ b/x/alliance/types/gov.go @@ -25,13 +25,15 @@ func init() { govtypes.RegisterProposalType(ProposalTypeUpdateAlliance) govtypes.RegisterProposalType(ProposalTypeDeleteAlliance) } -func NewMsgCreateAllianceProposal(title, description, denom string, rewardWeight, takeRate sdk.Dec) govtypes.Content { +func NewMsgCreateAllianceProposal(title, description, denom string, rewardWeight, takeRate sdk.Dec, rewardChangeRate sdk.Dec, rewardChangeInterval time.Duration) govtypes.Content { return &MsgCreateAllianceProposal{ - Title: title, - Description: description, - Denom: denom, - RewardWeight: rewardWeight, - TakeRate: takeRate, + Title: title, + Description: description, + Denom: denom, + RewardWeight: rewardWeight, + TakeRate: takeRate, + RewardChangeRate: rewardChangeRate, + RewardChangeInterval: rewardChangeInterval, } } func (m *MsgCreateAllianceProposal) GetTitle() string { return m.Title } @@ -49,8 +51,8 @@ func (m *MsgCreateAllianceProposal) ValidateBasic() error { return status.Errorf(codes.InvalidArgument, "Alliance rewardWeight must be a positive number") } - if m.TakeRate.IsNil() || m.TakeRate.LTE(sdk.ZeroDec()) { - return status.Errorf(codes.InvalidArgument, "Alliance takeRate must be zero or a positive number") + if m.TakeRate.IsNil() || m.TakeRate.IsNegative() || m.TakeRate.GTE(sdk.OneDec()) { + return status.Errorf(codes.InvalidArgument, "Alliance takeRate must be more or equals to 0 but strictly less than 1") } if m.RewardChangeRate.IsZero() || m.RewardChangeRate.IsNegative() { @@ -85,8 +87,8 @@ func (m *MsgUpdateAllianceProposal) ValidateBasic() error { return status.Errorf(codes.InvalidArgument, "Alliance rewardWeight must be a positive number") } - if m.TakeRate.IsNil() || m.TakeRate.LTE(sdk.ZeroDec()) { - return status.Errorf(codes.InvalidArgument, "Alliance takeRate must be a positive number") + if m.TakeRate.IsNil() || m.TakeRate.IsNegative() || m.TakeRate.GTE(sdk.OneDec()) { + return status.Errorf(codes.InvalidArgument, "Alliance takeRate must be more or equals to 0 but strictly less than 1") } if m.RewardChangeRate.IsZero() || m.RewardChangeRate.IsNegative() { diff --git a/x/alliance/types/params.go b/x/alliance/types/params.go index 47f381c2..5ba4c306 100644 --- a/x/alliance/types/params.go +++ b/x/alliance/types/params.go @@ -10,9 +10,9 @@ import ( ) var ( - RewardDelayTime = []byte("RewardDelayTime") - RewardClaimInterval = []byte("TakeRateClaimInterval") - LastRewardClaimTime = []byte("LastTakeRateClaimTime") + RewardDelayTime = []byte("RewardDelayTime") + TakeRateClaimInterval = []byte("TakeRateClaimInterval") + LastTakeRateClaimTime = []byte("LastTakeRateClaimTime") ) var _ paramtypes.ParamSet = (*Params)(nil) @@ -20,8 +20,8 @@ var _ paramtypes.ParamSet = (*Params)(nil) func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { return paramtypes.ParamSetPairs{ paramtypes.NewParamSetPair(RewardDelayTime, &p.RewardDelayTime, validatePositiveDuration), - paramtypes.NewParamSetPair(RewardClaimInterval, &p.TakeRateClaimInterval, validatePositiveDuration), - paramtypes.NewParamSetPair(LastRewardClaimTime, &p.LastTakeRateClaimTime, validateTime), + paramtypes.NewParamSetPair(TakeRateClaimInterval, &p.TakeRateClaimInterval, validatePositiveDuration), + paramtypes.NewParamSetPair(LastTakeRateClaimTime, &p.LastTakeRateClaimTime, validateTime), } } diff --git a/x/alliance/types/validator.go b/x/alliance/types/validator.go index 202345ba..3d23296a 100644 --- a/x/alliance/types/validator.go +++ b/x/alliance/types/validator.go @@ -19,29 +19,30 @@ func NewAllianceValidatorInfo() AllianceValidatorInfo { } func (v *AllianceValidator) AddShares(delegationShares sdk.DecCoins, validatorShares sdk.DecCoins) { - v.TotalDelegatorShares = delegationShares.Add(v.TotalDelegatorShares...) - v.ValidatorShares = validatorShares.Add(v.ValidatorShares...) + v.TotalDelegatorShares = sdk.DecCoins(v.TotalDelegatorShares).Add(delegationShares...) + v.ValidatorShares = sdk.DecCoins(v.ValidatorShares).Add(validatorShares...) } -// ReduceShares handles small inaccuracies when subtracting shares due to rounding errors +// ReduceShares handles small inaccuracies (~ < 1) when subtracting shares due to rounding errors func (v *AllianceValidator) ReduceShares(delegationShares sdk.DecCoins, validatorShares sdk.DecCoins) { - diffs := SubtractDecCoinsWithRounding(v.TotalDelegatorShares, delegationShares) - v.TotalDelegatorShares = diffs - diffs = SubtractDecCoinsWithRounding(v.ValidatorShares, validatorShares) - v.ValidatorShares = diffs + newDelegatorShares := SubtractDecCoinsWithRounding(v.TotalDelegatorShares, delegationShares) + v.TotalDelegatorShares = newDelegatorShares + newValidatorShares := SubtractDecCoinsWithRounding(v.ValidatorShares, validatorShares) + v.ValidatorShares = newValidatorShares } -func SubtractDecCoinsWithRounding(d1s sdk.DecCoins, d2s sdk.DecCoins) (d3s sdk.DecCoins) { - d3s = sdk.NewDecCoins(d1s...) +func SubtractDecCoinsWithRounding(d1s sdk.DecCoins, d2s sdk.DecCoins) sdk.DecCoins { + d1Copy := sdk.NewDecCoins(d1s...) for _, d2 := range d2s { a1 := d1s.AmountOf(d2.Denom) - if d2.Amount.GT(a1) && d2.Amount.Sub(a1).LT(sdk.OneDec()) { - d3s = d3s.Sub(sdk.NewDecCoins(sdk.NewDecCoinFromDec(d2.Denom, a1))) + a2 := d2.Amount + if a2.GT(a1) && a2.Sub(a1).LT(sdk.OneDec()) { + d1Copy = d1Copy.Sub(sdk.NewDecCoins(sdk.NewDecCoinFromDec(d2.Denom, a1))) } else { - d3s = d3s.Sub(sdk.NewDecCoins(d2)) + d1Copy = d1Copy.Sub(sdk.NewDecCoins(d2)) } } - return d3s + return d1Copy } func (v AllianceValidator) TotalSharesWithDenom(denom string) sdk.Dec { @@ -62,9 +63,10 @@ func (v AllianceValidator) TotalDelegationSharesWithDenom(denom string) sdk.Dec return sdk.DecCoins(v.TotalDelegatorShares).AmountOf(denom) } -func (v AllianceValidator) TotalTokensWithAsset(asset AllianceAsset) sdk.Int { +func (v AllianceValidator) TotalTokensWithAsset(asset AllianceAsset) sdk.Dec { shares := v.ValidatorSharesWithDenom(asset.Denom) - return ConvertNewShareToDecToken(sdk.NewDecFromInt(asset.TotalTokens), asset.TotalValidatorShares, shares).TruncateInt() + dec := ConvertNewShareToDecToken(sdk.NewDecFromInt(asset.TotalTokens), asset.TotalValidatorShares, shares) + return dec } func (v AllianceValidator) TotalDecTokensWithAsset(asset AllianceAsset) sdk.Dec { diff --git a/x/alliance/types/validator_test.go b/x/alliance/types/validator_test.go new file mode 100644 index 00000000..2e7ac03a --- /dev/null +++ b/x/alliance/types/validator_test.go @@ -0,0 +1,70 @@ +package types_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "github.com/terra-money/alliance/x/alliance/types" + "testing" +) + +func TestSubtractDecCoinsWithRounding(t *testing.T) { + // Normal case + a := sdk.NewDecCoins( + sdk.NewDecCoinFromDec("aaa", sdk.MustNewDecFromStr("1000.00")), + sdk.NewDecCoinFromDec("bbb", sdk.MustNewDecFromStr("1000.00")), + sdk.NewDecCoinFromDec("ccc", sdk.MustNewDecFromStr("1000.00")), + ) + b := sdk.NewDecCoins( + sdk.NewDecCoinFromDec("aaa", sdk.MustNewDecFromStr("400.00")), + sdk.NewDecCoinFromDec("bbb", sdk.MustNewDecFromStr("1000.00")), + ) + + c := types.SubtractDecCoinsWithRounding(a, b) + require.Equal(t, sdk.NewDecCoins( + sdk.NewDecCoinFromDec("aaa", sdk.MustNewDecFromStr("600.00")), + sdk.NewDecCoinFromDec("bbb", sdk.MustNewDecFromStr("0")), + sdk.NewDecCoinFromDec("ccc", sdk.MustNewDecFromStr("1000.00")), + ), c) +} + +func TestSubtractDecCoinsWithRoundingWithSmallErrors(t *testing.T) { + a := sdk.NewDecCoins( + sdk.NewDecCoinFromDec("aaa", sdk.MustNewDecFromStr("1000.00")), + sdk.NewDecCoinFromDec("bbb", sdk.MustNewDecFromStr("1000.00")), + sdk.NewDecCoinFromDec("ccc", sdk.MustNewDecFromStr("1000.00")), + ) + b := sdk.NewDecCoins( + sdk.NewDecCoinFromDec("aaa", sdk.MustNewDecFromStr("400.00")), + sdk.NewDecCoinFromDec("bbb", sdk.MustNewDecFromStr("1000.90")), + ) + + c := types.SubtractDecCoinsWithRounding(a, b) + require.Equal(t, sdk.NewDecCoins( + sdk.NewDecCoinFromDec("aaa", sdk.MustNewDecFromStr("600.00")), + sdk.NewDecCoinFromDec("bbb", sdk.MustNewDecFromStr("0")), + sdk.NewDecCoinFromDec("ccc", sdk.MustNewDecFromStr("1000.00")), + ), c) +} + +func TestSubtractDecCoinsWithRoundingWithBigErrors(t *testing.T) { + defer func() { + err := recover() + require.NotNil(t, err) + }() + a := sdk.NewDecCoins( + sdk.NewDecCoinFromDec("aaa", sdk.MustNewDecFromStr("1000.00")), + sdk.NewDecCoinFromDec("bbb", sdk.MustNewDecFromStr("1000.00")), + sdk.NewDecCoinFromDec("ccc", sdk.MustNewDecFromStr("1000.00")), + ) + b := sdk.NewDecCoins( + sdk.NewDecCoinFromDec("aaa", sdk.MustNewDecFromStr("400.00")), + sdk.NewDecCoinFromDec("bbb", sdk.MustNewDecFromStr("1010.10")), + ) + + c := types.SubtractDecCoinsWithRounding(a, b) + require.Equal(t, sdk.NewDecCoins( + sdk.NewDecCoinFromDec("aaa", sdk.MustNewDecFromStr("600.00")), + sdk.NewDecCoinFromDec("bbb", sdk.MustNewDecFromStr("0")), + sdk.NewDecCoinFromDec("ccc", sdk.MustNewDecFromStr("1000.00")), + ), c) +}