github.com/lino-network/lino@v0.6.11/x/vote/manager/manager.go (about)

     1  package manager
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  
     7  	codec "github.com/cosmos/cosmos-sdk/codec"
     8  	sdk "github.com/cosmos/cosmos-sdk/types"
     9  
    10  	"github.com/lino-network/lino/param"
    11  	linotypes "github.com/lino-network/lino/types"
    12  	"github.com/lino-network/lino/utils"
    13  	acc "github.com/lino-network/lino/x/account"
    14  	accmn "github.com/lino-network/lino/x/account/manager"
    15  	"github.com/lino-network/lino/x/global"
    16  	"github.com/lino-network/lino/x/vote/model"
    17  	"github.com/lino-network/lino/x/vote/types"
    18  )
    19  
    20  const (
    21  	exportVersion = 2
    22  	importVersion = 2
    23  )
    24  
    25  // VoteManager - vote manager
    26  type VoteManager struct {
    27  	storage model.VoteStorage
    28  
    29  	// deps
    30  	paramHolder param.ParamKeeper
    31  	am          acc.AccountKeeper
    32  	gm          global.GlobalKeeper
    33  
    34  	// mutable hooks
    35  	hooks StakingHooks
    36  }
    37  
    38  // NewVoteManager - new vote manager
    39  func NewVoteManager(key sdk.StoreKey, holder param.ParamKeeper, am acc.AccountKeeper, gm global.GlobalKeeper) VoteManager {
    40  	return VoteManager{
    41  		am:          am,
    42  		storage:     model.NewVoteStorage(key),
    43  		paramHolder: holder,
    44  		gm:          gm,
    45  	}
    46  }
    47  
    48  func (vm VoteManager) InitGenesis(ctx sdk.Context) {
    49  	linoStakeStat := &model.LinoStakeStat{
    50  		TotalConsumptionFriction: linotypes.NewCoinFromInt64(0),
    51  		TotalLinoStake:           linotypes.NewCoinFromInt64(0),
    52  		UnclaimedFriction:        linotypes.NewCoinFromInt64(0),
    53  		UnclaimedLinoStake:       linotypes.NewCoinFromInt64(0),
    54  	}
    55  	vm.storage.SetLinoStakeStat(ctx, 0, linoStakeStat)
    56  }
    57  
    58  // Set the validator hooks
    59  func (vm *VoteManager) SetHooks(sh StakingHooks) *VoteManager {
    60  	if vm.hooks != nil {
    61  		panic("cannot set vote hooks twice")
    62  	}
    63  	vm.hooks = sh
    64  	return vm
    65  }
    66  
    67  // DoesVoterExist - check if voter exist or not
    68  func (vm VoteManager) DoesVoterExist(ctx sdk.Context, username linotypes.AccountKey) bool {
    69  	return vm.storage.DoesVoterExist(ctx, username)
    70  }
    71  
    72  func (vm VoteManager) StakeIn(ctx sdk.Context, username linotypes.AccountKey, amount linotypes.Coin) sdk.Error {
    73  	if vm.paramHolder.GetVoteParam(ctx).MinStakeIn.IsGT(amount) {
    74  		return types.ErrInsufficientDeposit()
    75  	}
    76  
    77  	err := vm.am.MoveToPool(
    78  		ctx, linotypes.VoteStakeInPool, linotypes.NewAccOrAddrFromAcc(username), amount)
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	return vm.addStake(ctx, username, amount)
    84  }
    85  
    86  func (vm VoteManager) StakeInFor(ctx sdk.Context, sender linotypes.AccountKey,
    87  	receiver linotypes.AccountKey, amount linotypes.Coin) sdk.Error {
    88  	if vm.paramHolder.GetVoteParam(ctx).MinStakeIn.IsGT(amount) {
    89  		return types.ErrInsufficientDeposit()
    90  	}
    91  
    92  	// withdraw money from sender's bank and add stake to receiver
    93  	err := vm.am.MoveToPool(
    94  		ctx, linotypes.VoteStakeInPool, linotypes.NewAccOrAddrFromAcc(sender), amount)
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	return vm.addStake(ctx, receiver, amount)
   100  }
   101  
   102  func (vm VoteManager) addStake(ctx sdk.Context, username linotypes.AccountKey, amount linotypes.Coin) sdk.Error {
   103  	voter, err := vm.storage.GetVoter(ctx, username)
   104  	if err != nil {
   105  		if err.Code() != linotypes.CodeVoterNotFound {
   106  			return err
   107  		}
   108  		voter = &model.Voter{
   109  			Username:          username,
   110  			LinoStake:         linotypes.NewCoinFromInt64(0),
   111  			LastPowerChangeAt: ctx.BlockHeader().Time.Unix(),
   112  			Duty:              types.DutyVoter,
   113  			Interest:          linotypes.NewCoinFromInt64(0),
   114  		}
   115  	}
   116  
   117  	interest, err := vm.popInterestSince(ctx, voter.LastPowerChangeAt, voter.LinoStake)
   118  	if err != nil {
   119  		return err
   120  	}
   121  	voter.Interest = voter.Interest.Plus(interest)
   122  	voter.LinoStake = voter.LinoStake.Plus(amount)
   123  	voter.LastPowerChangeAt = ctx.BlockHeader().Time.Unix()
   124  
   125  	vm.storage.SetVoter(ctx, voter)
   126  	// add linoStake to stats
   127  	if err := vm.updateLinoStakeStat(ctx, amount, true); err != nil {
   128  		return err
   129  	}
   130  	return vm.AfterAddingStake(ctx, username)
   131  }
   132  
   133  func (vm VoteManager) StakeOut(ctx sdk.Context, username linotypes.AccountKey, amount linotypes.Coin) sdk.Error {
   134  	// minus stake stats
   135  	if err := vm.minusStake(ctx, username, amount); err != nil {
   136  		return err
   137  	}
   138  
   139  	// move stake to stake return pool.
   140  	err := vm.am.MoveBetweenPools(
   141  		ctx, linotypes.VoteStakeInPool, linotypes.VoteStakeReturnPool, amount)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	// create coin return events to return coins from stake return pool.
   147  	//// add frozen money for records.
   148  	param := vm.paramHolder.GetVoteParam(ctx)
   149  	if ctx.BlockHeight() < linotypes.Upgrade5Update1 ||
   150  		ctx.BlockHeight() >= linotypes.Upgrade5Update2 {
   151  		param.VoterCoinReturnIntervalSec = 86401 // 1 day
   152  		param.VoterCoinReturnTimes = 1
   153  	}
   154  
   155  	if err := vm.am.AddFrozenMoney(
   156  		ctx, username, amount, ctx.BlockHeader().Time.Unix(),
   157  		param.VoterCoinReturnIntervalSec, param.VoterCoinReturnTimes); err != nil {
   158  		return err
   159  	}
   160  
   161  	//// create and register the events.
   162  	events := accmn.CreateCoinReturnEvents(
   163  		username, ctx.BlockTime().Unix(),
   164  		param.VoterCoinReturnIntervalSec, param.VoterCoinReturnTimes,
   165  		amount, linotypes.VoteReturnCoin, linotypes.VoteStakeReturnPool)
   166  	for _, event := range events {
   167  		err := vm.gm.RegisterEventAtTime(ctx, event.At, event)
   168  		if err != nil {
   169  			return err
   170  		}
   171  	}
   172  
   173  	return nil
   174  }
   175  
   176  func (vm VoteManager) minusStake(ctx sdk.Context, username linotypes.AccountKey, amount linotypes.Coin) sdk.Error {
   177  	voter, err := vm.storage.GetVoter(ctx, username)
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	// make sure stake is sufficient excludes frozen amount
   183  	if !voter.LinoStake.Minus(voter.FrozenAmount).IsGTE(amount) {
   184  		return types.ErrInsufficientStake()
   185  	}
   186  
   187  	interest, err := vm.popInterestSince(ctx, voter.LastPowerChangeAt, voter.LinoStake)
   188  	if err != nil {
   189  		return err
   190  	}
   191  	voter.Interest = voter.Interest.Plus(interest)
   192  	voter.LinoStake = voter.LinoStake.Minus(amount)
   193  	voter.LastPowerChangeAt = ctx.BlockHeader().Time.Unix()
   194  	vm.storage.SetVoter(ctx, voter)
   195  
   196  	// minus linoStake from stats
   197  	if err := vm.updateLinoStakeStat(ctx, amount, false); err != nil {
   198  		return err
   199  	}
   200  	return vm.AfterSubtractingStake(ctx, username)
   201  }
   202  
   203  // ClaimInterest - add lino power interst to user balance
   204  func (vm VoteManager) ClaimInterest(ctx sdk.Context, username linotypes.AccountKey) sdk.Error {
   205  	voter, err := vm.storage.GetVoter(ctx, username)
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	interest, err := vm.popInterestSince(ctx, voter.LastPowerChangeAt, voter.LinoStake)
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	if err := vm.am.MoveFromPool(ctx,
   216  		linotypes.VoteFrictionPool, linotypes.NewAccOrAddrFromAcc(username),
   217  		voter.Interest.Plus(interest)); err != nil {
   218  		return err
   219  	}
   220  
   221  	voter.Interest = linotypes.NewCoinFromInt64(0)
   222  	voter.LastPowerChangeAt = ctx.BlockHeader().Time.Unix()
   223  	vm.storage.SetVoter(ctx, voter)
   224  	return nil
   225  }
   226  
   227  // AssignDuty froze some amount of stake and assign a duty to user.
   228  func (vm VoteManager) AssignDuty(ctx sdk.Context, username linotypes.AccountKey, duty types.VoterDuty, frozenAmount linotypes.Coin) sdk.Error {
   229  	if frozenAmount.IsNegative() {
   230  		return types.ErrNegativeFrozenAmount()
   231  	}
   232  	voter, err := vm.storage.GetVoter(ctx, username)
   233  	if err != nil {
   234  		return err
   235  	}
   236  	if voter.Duty != types.DutyVoter {
   237  		return types.ErrNotAVoterOrHasDuty()
   238  	}
   239  
   240  	if voter.FrozenAmount.IsPositive() {
   241  		return types.ErrFrozenAmountIsNotEmpty()
   242  	}
   243  
   244  	if !voter.LinoStake.IsGTE(frozenAmount) {
   245  		return types.ErrInsufficientStake()
   246  	}
   247  
   248  	voter.Duty = duty
   249  	voter.FrozenAmount = frozenAmount
   250  	vm.storage.SetVoter(ctx, voter)
   251  	return nil
   252  }
   253  
   254  // UnassignDuty register unassign duty event with time after waitingPeriodSec seconds.
   255  func (vm VoteManager) UnassignDuty(ctx sdk.Context, username linotypes.AccountKey, waitingPeriodSec int64) sdk.Error {
   256  	voter, err := vm.storage.GetVoter(ctx, username)
   257  	if err != nil {
   258  		return err
   259  	}
   260  	if voter.Duty == types.DutyVoter || voter.Duty == types.DutyPending {
   261  		return types.ErrNoDuty()
   262  	}
   263  	if err := vm.gm.RegisterEventAtTime(
   264  		ctx, ctx.BlockHeader().Time.Unix()+waitingPeriodSec,
   265  		types.UnassignDutyEvent{Username: username}); err != nil {
   266  		return err
   267  	}
   268  	voter.Duty = types.DutyPending
   269  	vm.storage.SetVoter(ctx, voter)
   270  	return nil
   271  }
   272  
   273  // SlashStake - slash as much as it can, regardless of frozen money
   274  func (vm VoteManager) SlashStake(ctx sdk.Context, username linotypes.AccountKey, amount linotypes.Coin, destPool linotypes.PoolName) (slashedAmount linotypes.Coin, err sdk.Error) {
   275  	voter, err := vm.storage.GetVoter(ctx, username)
   276  	if err != nil {
   277  		return linotypes.NewCoinFromInt64(0), err
   278  	}
   279  
   280  	interest, err := vm.popInterestSince(ctx, voter.LastPowerChangeAt, voter.LinoStake)
   281  	if err != nil {
   282  		return linotypes.NewCoinFromInt64(0), err
   283  	}
   284  
   285  	voter.Interest = voter.Interest.Plus(interest)
   286  	if !voter.LinoStake.IsGTE(amount) {
   287  		slashedAmount = voter.LinoStake
   288  		voter.LinoStake = linotypes.NewCoinFromInt64(0)
   289  	} else {
   290  		slashedAmount = amount
   291  		voter.LinoStake = voter.LinoStake.Minus(amount)
   292  	}
   293  	voter.LastPowerChangeAt = ctx.BlockHeader().Time.Unix()
   294  
   295  	vm.storage.SetVoter(ctx, voter)
   296  	// minus linoStake from global stat
   297  	if err := vm.updateLinoStakeStat(ctx, slashedAmount, false); err != nil {
   298  		return linotypes.NewCoinFromInt64(0), err
   299  	}
   300  
   301  	// move slashed coins to destination pool.
   302  	err = vm.am.MoveBetweenPools(ctx, linotypes.VoteStakeInPool, destPool, slashedAmount)
   303  	if err != nil {
   304  		return linotypes.NewCoinFromInt64(0), err
   305  	}
   306  
   307  	if err := vm.AfterSlashing(ctx, username); err != nil {
   308  		return linotypes.NewCoinFromInt64(0), err
   309  	}
   310  	return slashedAmount, nil
   311  }
   312  
   313  // ExecUnassignDutyEvent - execute unassign duty events.
   314  func (vm VoteManager) ExecUnassignDutyEvent(ctx sdk.Context, event types.UnassignDutyEvent) sdk.Error {
   315  	// Check if it is voter or not
   316  	voter, err := vm.storage.GetVoter(ctx, event.Username)
   317  	if err != nil {
   318  		return err
   319  	}
   320  	// set frozen amount to zero and duty to voter
   321  	voter.FrozenAmount = linotypes.NewCoinFromInt64(0)
   322  	voter.Duty = types.DutyVoter
   323  	vm.storage.SetVoter(ctx, voter)
   324  	return nil
   325  }
   326  
   327  // popInterestSince - pop interest from unix time till now (exclusive)
   328  func (vm VoteManager) popInterestSince(ctx sdk.Context, unixTime int64, linoStake linotypes.Coin) (linotypes.Coin, sdk.Error) {
   329  	startDay := vm.gm.GetPastDay(ctx, unixTime)
   330  	endDay := vm.gm.GetPastDay(ctx, ctx.BlockHeader().Time.Unix())
   331  	totalInterest := linotypes.NewCoinFromInt64(0)
   332  	for day := startDay; day < endDay; day++ {
   333  		linoStakeStat, err := vm.storage.GetLinoStakeStat(ctx, day)
   334  		if err != nil {
   335  			return linotypes.NewCoinFromInt64(0), err
   336  		}
   337  		if linoStakeStat.UnclaimedLinoStake.IsZero() ||
   338  			!linoStakeStat.UnclaimedLinoStake.IsGTE(linoStake) {
   339  			continue
   340  		}
   341  		interest :=
   342  			linotypes.DecToCoin(linoStakeStat.UnclaimedFriction.ToDec().Mul(
   343  				linoStake.ToDec().Quo(linoStakeStat.UnclaimedLinoStake.ToDec())))
   344  		totalInterest = totalInterest.Plus(interest)
   345  		linoStakeStat.UnclaimedFriction = linoStakeStat.UnclaimedFriction.Minus(interest)
   346  		linoStakeStat.UnclaimedLinoStake = linoStakeStat.UnclaimedLinoStake.Minus(linoStake)
   347  		vm.storage.SetLinoStakeStat(ctx, day, linoStakeStat)
   348  	}
   349  	return totalInterest, nil
   350  }
   351  
   352  // updateLinoStakeStat - add/sub lino power to total lino power at current day
   353  func (vm VoteManager) updateLinoStakeStat(ctx sdk.Context, linoStake linotypes.Coin, isAdd bool) sdk.Error {
   354  	pastDay := vm.gm.GetPastDay(ctx, ctx.BlockHeader().Time.Unix())
   355  	linoStakeStat, err := vm.storage.GetLinoStakeStat(ctx, pastDay)
   356  	if err != nil {
   357  		return err
   358  	}
   359  	if isAdd {
   360  		linoStakeStat.TotalLinoStake = linoStakeStat.TotalLinoStake.Plus(linoStake)
   361  		linoStakeStat.UnclaimedLinoStake = linoStakeStat.UnclaimedLinoStake.Plus(linoStake)
   362  	} else {
   363  		linoStakeStat.TotalLinoStake = linoStakeStat.TotalLinoStake.Minus(linoStake)
   364  		linoStakeStat.UnclaimedLinoStake = linoStakeStat.UnclaimedLinoStake.Minus(linoStake)
   365  	}
   366  	vm.storage.SetLinoStakeStat(ctx, pastDay, linoStakeStat)
   367  	return nil
   368  }
   369  
   370  func (vm VoteManager) RecordFriction(ctx sdk.Context, friction linotypes.Coin) sdk.Error {
   371  	pastDay := vm.gm.GetPastDay(ctx, ctx.BlockHeader().Time.Unix())
   372  	linoStakeStat, err := vm.storage.GetLinoStakeStat(ctx, pastDay)
   373  	if err != nil {
   374  		return err
   375  	}
   376  	linoStakeStat.TotalConsumptionFriction = linoStakeStat.TotalConsumptionFriction.Plus(friction)
   377  	linoStakeStat.UnclaimedFriction = linoStakeStat.UnclaimedFriction.Plus(friction)
   378  	vm.storage.SetLinoStakeStat(ctx, pastDay, linoStakeStat)
   379  	return nil
   380  }
   381  
   382  // AdvanceLinoStakeStats - save consumption and lino power to LinoStakeStat of a new day.
   383  // It need to be executed daily.
   384  func (vm VoteManager) DailyAdvanceLinoStakeStats(ctx sdk.Context) sdk.Error {
   385  	nDay := vm.gm.GetPastDay(ctx, ctx.BlockTime().Unix())
   386  	if nDay < 1 {
   387  		return nil
   388  	}
   389  	// XXX(yumin): CANNOT use nDay - 1 as the last day, because there might be
   390  	// skipped days if the chain has down for > 24 hours. In those cases,
   391  	// in the last version, claim interests will fail due to lino stake stats of
   392  	// those days are missing. Here, we set all of them of the default value to avoid it.
   393  	prev := nDay - 1
   394  	var lastStats *model.LinoStakeStat
   395  	for prev >= 0 {
   396  		stats, err := vm.storage.GetLinoStakeStat(ctx, prev)
   397  		if err != nil {
   398  			prev--
   399  		} else {
   400  			lastStats = stats
   401  			break
   402  		}
   403  	}
   404  
   405  	// If lino stake exist last day, the consumption will keep for lino stake holder that day
   406  	// Otherwise, moved to the next day. It's fine to have a stake stats that has zero total
   407  	// stake but consumption, as no one can claim interests from that day.
   408  	// XXX(yumin): the above statement means that, if you want to aggregate consumtionps of days,
   409  	// MUST skip those days where TotalLinoStake is Zero.
   410  	if !lastStats.TotalLinoStake.IsZero() {
   411  		lastStats.TotalConsumptionFriction = linotypes.NewCoinFromInt64(0)
   412  		lastStats.UnclaimedFriction = linotypes.NewCoinFromInt64(0)
   413  	}
   414  
   415  	for day := prev + 1; day <= nDay; day++ {
   416  		vm.storage.SetLinoStakeStat(ctx, day, lastStats)
   417  	}
   418  
   419  	return nil
   420  }
   421  
   422  func (vm VoteManager) GetVoter(ctx sdk.Context, username linotypes.AccountKey) (*model.Voter, sdk.Error) {
   423  	return vm.storage.GetVoter(ctx, username)
   424  }
   425  
   426  func (vm VoteManager) GetVoterDuty(ctx sdk.Context, username linotypes.AccountKey) (types.VoterDuty, sdk.Error) {
   427  	voter, err := vm.storage.GetVoter(ctx, username)
   428  	if err != nil {
   429  		return types.DutyVoter, err
   430  	}
   431  	return voter.Duty, nil
   432  }
   433  
   434  func (vm VoteManager) GetLinoStake(ctx sdk.Context, username linotypes.AccountKey) (linotypes.Coin, sdk.Error) {
   435  	voter, err := vm.storage.GetVoter(ctx, username)
   436  	if err != nil {
   437  		return linotypes.NewCoinFromInt64(0), err
   438  	}
   439  	return voter.LinoStake, nil
   440  }
   441  
   442  func (vm VoteManager) GetStakeStatsOfDay(ctx sdk.Context, day int64) (*model.LinoStakeStat, sdk.Error) {
   443  	stats, err := vm.storage.GetLinoStakeStat(ctx, day)
   444  	return stats, err
   445  }
   446  
   447  // Export storage state.
   448  func (vm VoteManager) ExportToFile(ctx sdk.Context, cdc *codec.Codec, filepath string) error {
   449  	state := &model.VoterTablesIR{
   450  		Version: exportVersion,
   451  	}
   452  	storeMap := vm.storage.StoreMap(ctx)
   453  
   454  	// export voters
   455  	storeMap[string(model.VoterSubstore)].Iterate(func(key []byte, val interface{}) bool {
   456  		voter := val.(*model.Voter)
   457  		state.Voters = append(state.Voters, model.VoterIR(*voter))
   458  		return false
   459  	})
   460  
   461  	// export stakes
   462  	storeMap[string(model.LinoStakeStatSubStore)].Iterate(func(key []byte, val interface{}) bool {
   463  		day, err := strconv.ParseInt(string(key), 10, 64)
   464  		if err != nil {
   465  			panic(err)
   466  		}
   467  		stakeStats := val.(*model.LinoStakeStat)
   468  		state.StakeStats = append(state.StakeStats, model.StakeStatDayIR{
   469  			Day:       day,
   470  			StakeStat: model.LinoStakeStatIR(*stakeStats),
   471  		})
   472  		return false
   473  	})
   474  
   475  	return utils.Save(filepath, cdc, state)
   476  
   477  }
   478  
   479  // Import storage state.
   480  func (vm VoteManager) ImportFromFile(ctx sdk.Context, cdc *codec.Codec, filepath string) error {
   481  	rst, err := utils.Load(filepath, cdc, func() interface{} { return &model.VoterTablesIR{} })
   482  	if err != nil {
   483  		return err
   484  	}
   485  	table := rst.(*model.VoterTablesIR)
   486  
   487  	if table.Version != importVersion {
   488  		return fmt.Errorf("unsupported import version: %d", table.Version)
   489  	}
   490  
   491  	for _, voterir := range table.Voters {
   492  		voter := model.Voter(voterir)
   493  		vm.storage.SetVoter(ctx, &voter)
   494  	}
   495  
   496  	// import table.GlobalStakeStats
   497  	for _, v := range table.StakeStats {
   498  		stat := model.LinoStakeStat(v.StakeStat)
   499  		vm.storage.SetLinoStakeStat(ctx, v.Day, &stat)
   500  	}
   501  
   502  	return nil
   503  }