diff --git a/app/app.go b/app/app.go index 46fdd22a8..a5024a0fd 100644 --- a/app/app.go +++ b/app/app.go @@ -777,6 +777,7 @@ func NewExocoreApp( // set the hooks at the end, after all modules are instantiated. (&app.OperatorKeeper).SetHooks( + app.DistrKeeper.OperatorHooks(), app.StakingKeeper.OperatorHooks(), ) diff --git a/x/feedistribution/keeper/allocation.go b/x/feedistribution/keeper/allocation.go index 2c86585bf..78172ceae 100644 --- a/x/feedistribution/keeper/allocation.go +++ b/x/feedistribution/keeper/allocation.go @@ -4,24 +4,57 @@ import ( "sort" "cosmossdk.io/math" + sdkmath "cosmossdk.io/math" avstypes "github.com/ExocoreNetwork/exocore/x/avs/types" "github.com/ExocoreNetwork/exocore/x/feedistribution/types" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) -// Based on the epoch, AllocateTokens performs reward and fee distribution to all validators. -func (k Keeper) AllocateTokens(ctx sdk.Context, totalPreviousPower int64) error { +// AllocateTokens performs reward and fee distribution to all validators. +// 1. afterSlash distributed accumlated fees till now in current epoch and the portion of minted coins +// corresponding to the time passed in current epoch +// 2. afterEpoch distributes all left coins including both fees and minted coins +// +// CONTRACT: before we adopt f1 like mechnisam to deal with precisely distribution, +// we need to set the epochIdentify the same to both dogfood and exomint +func (k Keeper) AllocateTokens(ctx sdk.Context, isSlash bool) error { logger := k.Logger() feeCollector := k.authKeeper.GetModuleAccount(ctx, k.feeCollectorName) feesCollectedInt := k.bankKeeper.GetAllBalances(ctx, feeCollector.GetAddress()) feesCollected := sdk.NewDecCoinsFromCoins(feesCollectedInt...) + // if this is triggered by slash instead of epochEnd, we need to calculated the amount of minted coins + // corresponding to passed time of current epoch + if isSlash { + mintParams := k.mintKeeper.GetParams(ctx) + mintedCoin := sdk.NewCoin( + mintParams.MintDenom, mintParams.EpochReward, + ) + + mintedCoinDec := sdk.NewDecCoinFromCoin(mintedCoin) + // we only distribute fees (excluding minted coins) from current epoch + // if the minted coins of current epoch had been allocated, we calculate the corresponding portion of minted tokens + if feesCollectedInt.AmountOf(mintParams.MintDenom).GTE(mintParams.EpochReward) { + epochInfo, found := k.epochsKeeper.GetEpochInfo(ctx, mintParams.EpochIdentifier) + if !found { + // skip the calculation and distribute no minted coins out, the remaining will be handled at the end of the epoch + feesCollected.Sub(sdk.DecCoins{mintedCoinDec}) + logger.Error("Failed to find epoch info") + } else { + passedDuration := sdkmath.LegacyNewDec(int64(ctx.BlockTime().Sub(epochInfo.StartTime))) + epochDuration := sdkmath.LegacyNewDec(int64(epochInfo.Duration)) + mintedCoinDec.Amount.MulMut(sdkmath.LegacyOneDec().Sub(passedDuration.QuoTruncate(epochDuration))) + feesCollected.Sub(sdk.DecCoins{mintedCoinDec}) + } + } + } - // transfer collected fees to the distribution module account + // transfer collected fees including minted coins to the distribution module account if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, k.feeCollectorName, types.ModuleName, feesCollectedInt); err != nil { return err } + totalPreviousPower := k.StakingKeeper.GetLastTotalPower(ctx).Int64() feePool := k.GetFeePool(ctx) if totalPreviousPower == 0 { feePool.CommunityPool = feePool.CommunityPool.Add(feesCollected...) @@ -114,33 +147,28 @@ func (k Keeper) AllocateTokensToValidator(ctx sdk.Context, val stakingtypes.Vali func (k Keeper) AllocateTokensToStakers(ctx sdk.Context, operatorAddress sdk.AccAddress, rewardToAllStakers sdk.DecCoins, feePool *types.FeePool) { logger := k.Logger() logger.Info("AllocateTokensToStakers", "operatorAddress", operatorAddress.String()) - avsList, err := k.StakingKeeper.GetOptedInAVSForOperator(ctx, operatorAddress.String()) - if err != nil { - logger.Debug("avs address lists not found; skipping") - return - } stakersPowerMap, curTotalStakersPowers := make(map[string]math.LegacyDec), math.LegacyNewDec(0) globalStakerAddressList := make([]string, 0) - for _, avsAddress := range avsList { - avsAssets, err := k.StakingKeeper.GetAVSSupportedAssets(ctx, avsAddress) + isAvs, avsAddress := k.avsKeeper.IsAVSByChainID(ctx, ctx.ChainID()) + if !isAvs { + logger.Error("Skipping distribution for due to fail to generate avsAddr from chainID", "chainID", ctx.ChainID()) + return + } + + assetIDs := k.StakingKeeper.GetAssetIDs(ctx) + for _, assetID := range assetIDs { + stakerList, err := k.StakingKeeper.GetStakersByOperator(ctx, operatorAddress.String(), assetID) if err != nil { - logger.Debug("avs address lists not found; skipping") + logger.Debug("staker lists not found; skipping") continue } - for assetID := range avsAssets { - stakerList, err := k.StakingKeeper.GetStakersByOperator(ctx, operatorAddress.String(), assetID) - if err != nil { - logger.Debug("staker lists not found; skipping") - continue - } - for _, staker := range stakerList.Stakers { - if curStakerPower, err := k.StakingKeeper.CalculateUSDValueForStaker(ctx, staker, avsAddress, operatorAddress.Bytes()); err != nil { - logger.Error("curStakerPower error", "error", err) - } else { - stakersPowerMap[staker] = curStakerPower - globalStakerAddressList = append(globalStakerAddressList, staker) - curTotalStakersPowers = curTotalStakersPowers.Add(curStakerPower) - } + for _, staker := range stakerList.Stakers { + if curStakerPower, err := k.StakingKeeper.CalculateUSDValueForStaker(ctx, staker, avsAddress, operatorAddress.Bytes()); err != nil { + logger.Error("curStakerPower error", "error", err) + } else { + stakersPowerMap[staker] = curStakerPower + globalStakerAddressList = append(globalStakerAddressList, staker) + curTotalStakersPowers = curTotalStakersPowers.Add(curStakerPower) } } } diff --git a/x/feedistribution/keeper/hooks.go b/x/feedistribution/keeper/impl_epoch_hooks.go similarity index 68% rename from x/feedistribution/keeper/hooks.go rename to x/feedistribution/keeper/impl_epoch_hooks.go index be8cc0a4d..803ab03e9 100644 --- a/x/feedistribution/keeper/hooks.go +++ b/x/feedistribution/keeper/impl_epoch_hooks.go @@ -30,15 +30,17 @@ func (wrapper EpochsHooksWrapper) AfterEpochEnd(ctx sdk.Context, epochIdentifier expEpochID := wrapper.keeper.GetParams(ctx).EpochIdentifier if strings.Compare(epochIdentifier, expEpochID) == 0 { // the minted coins generated by minting module will do the token allocation and distribution here - previousTotalPower := wrapper.keeper.StakingKeeper.GetLastTotalPower(ctx) + // previousTotalPower := wrapper.keeper.StakingKeeper.GetLastTotalPower(ctx) logger := wrapper.keeper.Logger() logger.Info( "AfterEpochEnd of distribution", ) - err := wrapper.keeper.AllocateTokens(ctx, previousTotalPower.Int64()) - if err != nil { + // at the end of an epoch we distribute all minted tokens for one epoch and left fees + // here we have the temporary approach that we allocate minited tokens for next epoch then we can correctly + // distribute those tokens based on potential slash event. And that should be ok since the amount of minited + // token for each epoch is the same, so we just missed the very first epoch's minting. + if err := wrapper.keeper.AllocateTokens(ctx, false); err != nil { logger.Error("failed to allocate tokens", "err", err) - return } } } diff --git a/x/feedistribution/keeper/impl_operator_hooks.go b/x/feedistribution/keeper/impl_operator_hooks.go new file mode 100644 index 000000000..987c68147 --- /dev/null +++ b/x/feedistribution/keeper/impl_operator_hooks.go @@ -0,0 +1,54 @@ +package keeper + +import ( + keytypes "github.com/ExocoreNetwork/exocore/types/keys" + operatortypes "github.com/ExocoreNetwork/exocore/x/operator/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type OperatorHooksWrapper struct { + keeper *Keeper +} + +var _ operatortypes.OperatorHooks = OperatorHooksWrapper{} + +func (k *Keeper) OperatorHooks() OperatorHooksWrapper { + return OperatorHooksWrapper{k} +} + +// AfterOperatorKeySet is the implementation of the operator hooks. +// CONTRACT: an operator cannot set their key if they are already in the process of removing it. +func (wrapper OperatorHooksWrapper) AfterOperatorKeySet( + sdk.Context, sdk.AccAddress, string, keytypes.WrappedConsKey, +) { +} + +// AfterOperatorKeyReplaced is the implementation of the operator hooks. +// CONTRACT: key replacement is not allowed if the operator is in the process of removing their +// key. +// CONTRACT: key replacement from newKey to oldKey is not allowed, after a replacement from +// oldKey to newKey. +func (wrapper OperatorHooksWrapper) AfterOperatorKeyReplaced( + _ sdk.Context, _ sdk.AccAddress, _ keytypes.WrappedConsKey, + _ keytypes.WrappedConsKey, _ string, +) { +} + +// AfterOperatorKeyRemovalInitiated is the implementation of the operator hooks. +func (wrapper OperatorHooksWrapper) AfterOperatorKeyRemovalInitiated( + _ sdk.Context, _ sdk.AccAddress, _ string, _ keytypes.WrappedConsKey, +) { +} + +func (wrapper OperatorHooksWrapper) AfterSlash( + ctx sdk.Context, _ sdk.AccAddress, _ []string, +) { + logger := wrapper.keeper.Logger() + logger.Info( + "AfterSlash of distribution", + ) + // When distribution triggered by slash, we only distribute fee collected until now from last distribution + if err := wrapper.keeper.AllocateTokens(ctx, true); err != nil { + logger.Error("failed to allocate tokens", "err", err) + } +} diff --git a/x/feedistribution/keeper/keeper.go b/x/feedistribution/keeper/keeper.go index 0a68b4723..fe487352f 100644 --- a/x/feedistribution/keeper/keeper.go +++ b/x/feedistribution/keeper/keeper.go @@ -24,6 +24,8 @@ type ( authKeeper types.AccountKeeper bankKeeper types.BankKeeper epochsKeeper types.EpochsKeeper + mintKeeper types.MintKeeper + avsKeeper types.AVSKeeper feeCollectorName string @@ -40,6 +42,8 @@ func NewKeeper( accountKeeper types.AccountKeeper, stakingkeeper stakingkeeper.Keeper, epochKeeper types.EpochsKeeper, + mintKeeper types.MintKeeper, + avsKeeper types.AVSKeeper, ) Keeper { // ensure distribution module account is set if addr := accountKeeper.GetModuleAddress(types.ModuleName); addr == nil { @@ -60,6 +64,8 @@ func NewKeeper( epochsKeeper: epochKeeper, feeCollectorName: feeCollectorName, StakingKeeper: stakingkeeper, + mintKeeper: mintKeeper, + avsKeeper: avsKeeper, } return *k diff --git a/x/feedistribution/types/expected_keepers.go b/x/feedistribution/types/expected_keepers.go index 8a0374f7d..e21d10dc3 100644 --- a/x/feedistribution/types/expected_keepers.go +++ b/x/feedistribution/types/expected_keepers.go @@ -5,6 +5,7 @@ import ( epochsTypes "github.com/ExocoreNetwork/exocore/x/epochs/types" + minttypes "github.com/ExocoreNetwork/exocore/x/exomint/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/types" ) @@ -54,3 +55,11 @@ type PoolKeeper interface { GetCommunityPool(ctx context.Context) (sdk.Coins, error) SetToDistribute(ctx context.Context, amount sdk.Coins, addr string) error } + +type MintKeeper interface { + GetParams(ctx sdk.Context) minttypes.Params +} + +type AVSKeeper interface { + IsAVSByChainID(ctx sdk.Context, chainID string) (bool, string) +}