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 }