diff --git a/x/oracle/keeper/msg_server_create_price.go b/x/oracle/keeper/msg_server_create_price.go index 7eca28a82..bbfe83922 100644 --- a/x/oracle/keeper/msg_server_create_price.go +++ b/x/oracle/keeper/msg_server_create_price.go @@ -6,13 +6,16 @@ import ( "strconv" "time" + "crypto/sha256" + "github.com/ExocoreNetwork/exocore/x/oracle/types" sdk "github.com/cosmos/cosmos-sdk/types" ) const ( - layout = "2006-01-02 15:04:05" - maxFutureOffset = 5 * time.Second + layout = "2006-01-02 15:04:05" + maxFutureOffset = 5 * time.Second + maxEventPriceSize = 64 ) // CreatePrice proposes price for new round of specific tokenFeeder @@ -63,10 +66,18 @@ func (ms msgServer) CreatePrice(goCtx context.Context, msg *types.MsgCreatePrice decimalStr := strconv.FormatInt(int64(newItem.PriceTR.Decimal), 10) tokenIDStr := strconv.FormatUint(newItem.TokenID, 10) roundIDStr := strconv.FormatUint(newItem.PriceTR.RoundID, 10) + priceFormat := "price" + if len(newItem.PriceTR.Price) > maxEventPriceSize { + // This is mainly used for NST since they might have a string with big size to indicate the changes for stakers + hashPrice := sha256.Sum256([]byte(newItem.PriceTR.Price)) + newItem.PriceTR.Price = string(hashPrice[:]) + priceFormat = "hash" + } ctx.EventManager().EmitEvent(sdk.NewEvent( types.EventTypeCreatePrice, sdk.NewAttribute(types.AttributeKeyRoundID, roundIDStr), - sdk.NewAttribute(types.AttributeKeyFinalPrice, tokenIDStr+"_"+roundIDStr+"_"+newItem.PriceTR.Price+"_"+decimalStr), + // [tokenIDStr]_[roundIDStr]_[price]_[decimal]_price/hash + sdk.NewAttribute(types.AttributeKeyFinalPrice, tokenIDStr+"_"+roundIDStr+"_"+newItem.PriceTR.Price+"_"+decimalStr+"_"+priceFormat), sdk.NewAttribute(types.AttributeKeyPriceUpdated, types.AttributeValuePriceUpdatedSuccess)), ) if !ctx.IsCheckTx() { diff --git a/x/oracle/keeper/native_token.go b/x/oracle/keeper/native_token.go index a2b5a8151..6f248b695 100644 --- a/x/oracle/keeper/native_token.go +++ b/x/oracle/keeper/native_token.go @@ -14,16 +14,6 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" ) -// deposit: update staker's totalDeposit -// withdoraw: update staker's totalDeposit -// delegate: update operator's price, operator's totalAmount, operator's totalShare, staker's share -// undelegate: update operator's price, operator's totalAmount, operator's totalShare, staker's share -// msg(refund or slash on beaconChain): update staker's price, operator's price - -const ( - NSTETHASSETID = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee_0x65" -) - // SetStakerInfos set stakerInfos for the specific assetID func (k Keeper) SetStakerInfos(ctx sdk.Context, assetID string, stakerInfos []*types.StakerInfo) { store := ctx.KVStore(k.storeKey) @@ -33,10 +23,6 @@ func (k Keeper) SetStakerInfos(ctx sdk.Context, assetID string, stakerInfos []*t } } -var maxEffectiveBalance = map[string]int{ - NSTETHASSETID: 32, -} - // GetStakerInfo returns details about staker for native-restaking under asset of assetID func (k Keeper) GetStakerInfo(ctx sdk.Context, assetID, stakerAddr string) types.StakerInfo { store := ctx.KVStore(k.storeKey) @@ -132,7 +118,16 @@ func (k Keeper) GetAllStakerListAssets(ctx sdk.Context) (ret []types.StakerListA return ret } +// UpdateValidatorListForStaker invoked when deposit/withdraw happedn for an NST asset +// deposit wiil increase the staker's balance with a new validatorPubkey added into that staker's validatorList +// withdraw will decrease the staker's balanec with a vadlidatorPubkey removed from that staker's validatorList func (k Keeper) UpdateNSTValidatorListForStaker(ctx sdk.Context, assetID, stakerAddr, validatorPubkey string, amount sdkmath.Int) error { + _, decimalInt, err := k.getDecimal(ctx, assetID) + if err != nil { + return err + } + // transfer amount into integer, for restaking the effective balance should always be whole unit + amountInt64 := amount.Quo(decimalInt).Int64() // emit an event to tell that a staker's validator list has changed ctx.EventManager().EmitEvent(sdk.NewEvent( types.EventTypeCreatePrice, @@ -146,7 +141,7 @@ func (k Keeper) UpdateNSTValidatorListForStaker(ctx sdk.Context, assetID, staker stakerInfo = types.NewStakerInfo(stakerAddr, validatorPubkey) } else { k.cdc.MustUnmarshal(value, stakerInfo) - if amount.IsPositive() { + if amountInt64 > 0 { // deopsit add a new validator into staker's validatorList stakerInfo.ValidatorPubkeyList = append(stakerInfo.ValidatorPubkeyList, validatorPubkey) } @@ -159,7 +154,7 @@ func (k Keeper) UpdateNSTValidatorListForStaker(ctx sdk.Context, assetID, staker newBalance.Index++ } newBalance.Block = uint64(ctx.BlockHeight()) - if amount.IsPositive() { + if amountInt64 > 0 { newBalance.Change = types.Action_ACTION_DEPOSIT } else { // TODO: check if this validator has withdraw all its asset and then we can move it out from the staker's validatorList @@ -174,19 +169,8 @@ func (k Keeper) UpdateNSTValidatorListForStaker(ctx sdk.Context, assetID, staker } } - decimal, decimalInt, err := k.getDecimal(ctx, assetID) - if err != nil { - return err - } - - // the amount should be checked by caller - // in case of nstETH, deposit should be equal to 32e18 as the maxeffectivebalance - efbUnit := sdkmath.NewIntWithDecimal(int64(maxEffectiveBalance[assetID]), decimal) - if amount.GTE(efbUnit) { - newBalance.Balance += int64(maxEffectiveBalance[assetID]) - } else { - newBalance.Balance += amount.Quo(decimalInt).Int64() - } + // TODO: should caller need extra check to make sure the amount is interger of unit + newBalance.Balance += amountInt64 keyStakerList := types.NativeTokenStakerListKey(assetID) valueStakerList := store.Get(keyStakerList) @@ -210,7 +194,7 @@ func (k Keeper) UpdateNSTValidatorListForStaker(ctx sdk.Context, assetID, staker } } if !exists { - if !amount.IsPositive() { + if amountInt64 <= 0 { return errors.New("remove unexist validator") } stakerList.StakerAddrs = append(stakerList.StakerAddrs, stakerAddr) @@ -242,6 +226,8 @@ func (k Keeper) UpdateNSTValidatorListForStaker(ctx sdk.Context, assetID, staker return nil } +// TODO: currently we limit the change for a single staker no more than 16, this suites for beaconchain. +// may need to be upgraded to be compatible with other chains like solana // UpdateNSTByBalanceChange updates balance info for staker under native-restaking asset of assetID when its balance changed by slash/refund on the source chain (beacon chain for eth) func (k Keeper) UpdateNSTByBalanceChange(ctx sdk.Context, assetID string, rawData []byte, roundID uint64) error { _, chainID, _ := assetstypes.ParseID(assetID) @@ -257,9 +243,7 @@ func (k Keeper) UpdateNSTByBalanceChange(ctx sdk.Context, assetID string, rawDat return err } store := ctx.KVStore(k.storeKey) - for _, stakerAddr := range sl.StakerAddrs { - // if stakerAddr is not in stakerChanges, then the change would be set to 0 which is expected - change := stakerChanges[stakerAddr] + for stakerAddr, change := range stakerChanges { key := types.NativeTokenStakerKey(assetID, stakerAddr) value := store.Get(key) if value == nil { @@ -268,9 +252,12 @@ func (k Keeper) UpdateNSTByBalanceChange(ctx sdk.Context, assetID string, rawDat stakerInfo := &types.StakerInfo{} k.cdc.MustUnmarshal(value, stakerInfo) newBalance := types.BalanceInfo{} - if length := len(stakerInfo.BalanceList); length > 0 { - newBalance = *(stakerInfo.BalanceList[length-1]) + length := len(stakerInfo.BalanceList) + // length should always be greater than 0 since the staker must deposit first, then we can update balance change + if length <= 0 { + return errors.New("UpdateBalane should not be executed on an empty balanceList") } + newBalance = *(stakerInfo.BalanceList[length-1]) newBalance.Block = uint64(ctx.BlockHeight()) if newBalance.RoundID == roundID { newBalance.Index++ @@ -279,30 +266,15 @@ func (k Keeper) UpdateNSTByBalanceChange(ctx sdk.Context, assetID string, rawDat newBalance.Index = 0 } newBalance.Change = types.Action_ACTION_SLASH_REFUND - // balance update are based on initial/max effective balance: 32 - maxBalance := maxEffectiveBalance[assetID] * (len(stakerInfo.ValidatorPubkeyList)) - balance := maxBalance + change - // there's one case that this delta might be more than previous Balance - // staker's validatorlist: {v1, v2, v3, v5} - // in one same block: withdraw v2, v3, v5, balance of v2, v3, v5 all be slashed by -16 - // => amount: 32*4->32(by withdraw), the validatorList of feeder will be updated on next block, so it will report the balance change of v5: -16 as in the staker's balance change, result to: 32*4->32-> 32-16*3 = -16 - // we will just ingore this misbehavior introduced by synchronize-issue, and this will be correct in next block/round - if balance > maxBalance || balance <= 0 { - // balance should not be able to be reduced to 0 by balance change - return errors.New("effective balance should never exceeds 32 for one validator and should be positive") + newBalance.Balance += int64(change) + decimal, _, err := k.getDecimal(ctx, assetID) + if err != nil { + return err } - - if delta := int64(balance) - newBalance.Balance; delta != 0 { - decimal, _, err := k.getDecimal(ctx, assetID) - if err != nil { - return err - } - if err := k.delegationKeeper.UpdateNSTBalance(ctx, getStakerID(stakerAddr, chainID), assetID, sdkmath.NewIntWithDecimal(delta, decimal)); err != nil { - return err - } - newBalance.Balance = int64(balance) + if err = k.delegationKeeper.UpdateNSTBalance(ctx, getStakerID(stakerAddr, chainID), assetID, sdkmath.NewIntWithDecimal(int64(change), decimal)); err != nil { + return err } - // newBalance.Balance += int64(change) + stakerInfo.Append(&newBalance) bz := k.cdc.MustMarshal(stakerInfo) store.Set(key, bz) @@ -310,6 +282,17 @@ func (k Keeper) UpdateNSTByBalanceChange(ctx sdk.Context, assetID string, rawDat return nil } +// TODO: set a persistent state to track this number +// GetNSTTotalIndex returns the count of how many time the NST balance of assetID has been changed including sources of deposit/withdraw, balanceChange +func (k Keeper) GetNSTTotalIndex(ctx sdk.Context, assetID string) int64 { + stakerInfos := k.GetStakerInfos(ctx, assetID) + totalIndex := int64(0) + for _, stakerInfo := range stakerInfos { + totalIndex += int64(len(stakerInfo.BalanceList)) + } + return totalIndex +} + func (k Keeper) getDecimal(ctx sdk.Context, assetID string) (int, sdkmath.Int, error) { decimalMap, err := k.assetsKeeper.GetAssetsDecimal(ctx, map[string]interface{}{assetID: nil}) if err != nil { diff --git a/x/oracle/keeper/native_token_test.go b/x/oracle/keeper/native_token_test.go index ec02bb94c..54744322c 100644 --- a/x/oracle/keeper/native_token_test.go +++ b/x/oracle/keeper/native_token_test.go @@ -138,7 +138,7 @@ func (ks *KeeperSuite) TestNSTLifeCycleOneStaker() { // - 4.1 check stakerInfo stakerInfo = ks.App.OracleKeeper.GetStakerInfo(ks.Ctx, assetID, stakerStr) ks.Equal(types.BalanceInfo{ - Balance: 59, + Balance: 49, Block: 1, RoundID: 11, Index: 0, @@ -146,10 +146,10 @@ func (ks *KeeperSuite) TestNSTLifeCycleOneStaker() { }, *stakerInfo.BalanceList[3]) // check stakerAssetInfo is updated correctly in assets module, this should be triggered in assets module by oracle module's UpdateNSTByBalanceChange stakerAssetInfo, _ = ks.App.AssetsKeeper.GetStakerSpecifiedAssetInfo(ks.Ctx, stakerID, assetID) - amount59 := sdkmath.NewIntWithDecimal(59, 18) + amount49 := sdkmath.NewIntWithDecimal(49, 18) ks.Equal(assetstypes.StakerAssetInfo{ - TotalDepositAmount: amount59, - WithdrawableAmount: amount59, + TotalDepositAmount: amount49, + WithdrawableAmount: amount49, PendingUndelegationAmount: sdk.ZeroInt(), }, *stakerAssetInfo) @@ -159,7 +159,7 @@ func (ks *KeeperSuite) TestNSTLifeCycleOneStaker() { // - 5.1 check stakerInfo stakerInfo = ks.App.OracleKeeper.GetStakerInfo(ks.Ctx, assetID, stakerStr) ks.Equal(types.BalanceInfo{ - Balance: 29, + Balance: 19, Block: 1, RoundID: 11, Index: 1,