From fc34e6e6cba39a31fd27655bd5cb4bf20633b4b3 Mon Sep 17 00:00:00 2001 From: zsystm Date: Mon, 24 Jul 2023 14:38:16 +0900 Subject: [PATCH] sync latest spec with latest code and fix some core logics fix core logics before: * there can be bug because chunk's status is changed to unpairing but, current paired chunk's status is still Paired and chunk have paired insurance id even if it is unpairing chunk. after: if paired insurance of paired chunk have invalid insurance, then unpairing it and add it to out insurances to hande just like other unpairing chunks. add missing invariant checks * newly added RedelegationInfosInvariant was not included chore * refactor variables name in invariants.go * use lsm's own event key types, not other module's. * add module name to each event --- x/liquidstaking/keeper/invariants.go | 60 ++++++------ x/liquidstaking/keeper/liquidstaking.go | 66 +++++++------ x/liquidstaking/spec/05_begin_block.md | 4 +- x/liquidstaking/spec/06_end_block.md | 122 +++++++++++++----------- x/liquidstaking/spec/07_invariants.md | 10 +- x/liquidstaking/spec/08_events.md | 26 +++-- x/liquidstaking/types/events.go | 5 + 7 files changed, 161 insertions(+), 132 deletions(-) diff --git a/x/liquidstaking/keeper/invariants.go b/x/liquidstaking/keeper/invariants.go index 48d9e263..54aebd43 100644 --- a/x/liquidstaking/keeper/invariants.go +++ b/x/liquidstaking/keeper/invariants.go @@ -19,6 +19,8 @@ func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { UnpairingForUnstakingChunkInfosInvariant(k)) ir.RegisterRoute(types.ModuleName, "withdraw-insurance-requests", WithdrawInsuranceRequestsInvariant(k)) + ir.RegisterRoute(types.ModuleName, "redelegation-infos", + RedelegationInfosInvariant(k)) } func AllInvariants(k Keeper) sdk.Invariant { @@ -65,9 +67,9 @@ func ChunksInvariant(k Keeper) sdk.Invariant { k.IterateAllChunks(ctx, func(chunk types.Chunk) bool { switch chunk.Status { case types.CHUNK_STATUS_PAIRING: - // must have empty paired insurance + // must have empty paired pairedIns if chunk.HasPairedInsurance() { - msg += fmt.Sprintf("pairing chunk(id: %d) have non-empty paired insurance\n", chunk.Id) + msg += fmt.Sprintf("pairing chunk(id: %d) have non-empty paired pairedIns\n", chunk.Id) brokenCount++ return false } @@ -86,19 +88,19 @@ func ChunksInvariant(k Keeper) sdk.Invariant { brokenCount++ return false } - insurance, found := k.GetInsurance(ctx, chunk.PairedInsuranceId) + pairedIns, found := k.GetInsurance(ctx, chunk.PairedInsuranceId) if !found { msg += fmt.Sprintf("not found paired insurance for paired chunk(id: %d)\n", chunk.Id) brokenCount++ return false } - if insurance.Status != types.INSURANCE_STATUS_PAIRED { - msg += fmt.Sprintf("paired chunk(id: %d) have paired insurance with invalid status: %s\n", chunk.Id, insurance.Status) + if pairedIns.Status != types.INSURANCE_STATUS_PAIRED { + msg += fmt.Sprintf("paired chunk(id: %d) have paired insurance with invalid status: %s\n", chunk.Id, pairedIns.Status) brokenCount++ return false } // must have valid Delegation object - delegation, found := k.stakingKeeper.GetDelegation(ctx, chunk.DerivedAddress(), insurance.GetValidator()) + delegation, found := k.stakingKeeper.GetDelegation(ctx, chunk.DerivedAddress(), pairedIns.GetValidator()) if !found { msg += fmt.Sprintf("not found delegation for paired chunk(id: %d)\n", chunk.Id) brokenCount++ @@ -106,7 +108,7 @@ func ChunksInvariant(k Keeper) sdk.Invariant { } delShares := delegation.GetShares() if delShares.LT(types.ChunkSize.ToDec()) { - msg += fmt.Sprintf("delegation tokens of paired chunk(id: %d) is less than chunk size tokens: %s\n", chunk.Id, delShares.String()) + msg += fmt.Sprintf("paired chunk's delegation sharesis less than chunk size tokens: %s(chunkId: %d)\n", delShares.String(), chunk.Id) brokenCount++ return false } @@ -117,7 +119,7 @@ func ChunksInvariant(k Keeper) sdk.Invariant { brokenCount++ return false } - insurance, found := k.GetInsurance(ctx, chunk.UnpairingInsuranceId) + unpairingIns, found := k.GetInsurance(ctx, chunk.UnpairingInsuranceId) if !found { msg += fmt.Sprintf("not found unpairing insurance for unpairing chunk(id: %d)\n", chunk.Id) brokenCount++ @@ -130,7 +132,7 @@ func ChunksInvariant(k Keeper) sdk.Invariant { return false } // must have unbonding delegation - ubd, found := k.stakingKeeper.GetUnbondingDelegation(ctx, chunk.DerivedAddress(), insurance.GetValidator()) + ubd, found := k.stakingKeeper.GetUnbondingDelegation(ctx, chunk.DerivedAddress(), unpairingIns.GetValidator()) if !found { msg += fmt.Sprintf("not found unbonding delegation for unpairing chunk(id: %d)\n", chunk.Id) brokenCount++ @@ -162,69 +164,69 @@ func InsurancesInvariant(k Keeper) sdk.Invariant { return func(ctx sdk.Context) (string, bool) { msg := "" brokenCount := 0 - k.IterateAllInsurances(ctx, func(insurance types.Insurance) bool { - switch insurance.Status { + k.IterateAllInsurances(ctx, func(ins types.Insurance) bool { + switch ins.Status { case types.INSURANCE_STATUS_PAIRING: // must have empty chunk - if insurance.HasChunk() { - msg += fmt.Sprintf("pairing insurance(id: %d) have non-empty paired chunk\n", insurance.Id) + if ins.HasChunk() { + msg += fmt.Sprintf("pairing insurance(id: %d) have non-empty paired chunk\n", ins.Id) brokenCount++ return false } case types.INSURANCE_STATUS_PAIRED: // must have paired chunk - if !insurance.HasChunk() { - msg += fmt.Sprintf("paired insurance(id: %d) have empty paired chunk\n", insurance.Id) + if !ins.HasChunk() { + msg += fmt.Sprintf("paired insurance(id: %d) have empty paired chunk\n", ins.Id) brokenCount++ return false } - chunk, found := k.GetChunk(ctx, insurance.ChunkId) + chunk, found := k.GetChunk(ctx, ins.ChunkId) if !found { - msg += fmt.Sprintf("not found paired chunk for paired insurance(id: %d)\n", insurance.Id) + msg += fmt.Sprintf("not found paired chunk for paired insurance(id: %d)\n", ins.Id) brokenCount++ return false } if chunk.Status != types.CHUNK_STATUS_PAIRED { - msg += fmt.Sprintf("paired insurance(id: %d) have invalid paired chunk status: %s\n", insurance.Id, chunk.Status) + msg += fmt.Sprintf("paired insurance(id: %d) have invalid paired chunk status: %s\n", ins.Id, chunk.Status) brokenCount++ return false } case types.INSURANCE_STATUS_UNPAIRING: // must have chunk to protect - if !insurance.HasChunk() { - msg += fmt.Sprintf("unpairing insurance(id: %d) have empty chunk to protect\n", insurance.Id) + if !ins.HasChunk() { + msg += fmt.Sprintf("unpairing insurance(id: %d) have empty chunk to protect\n", ins.Id) brokenCount++ return false } - _, found := k.GetChunk(ctx, insurance.ChunkId) + _, found := k.GetChunk(ctx, ins.ChunkId) if !found { - msg += fmt.Sprintf("not found chunk to protect for unpairing insurance(id: %d)\n", insurance.Id) + msg += fmt.Sprintf("not found chunk to protect for unpairing insurance(id: %d)\n", ins.Id) brokenCount++ return false } case types.INSURANCE_STATUS_UNPAIRED: // must have empty chunk - if insurance.HasChunk() { - msg += fmt.Sprintf("unpaired insurance(id: %d) have non-empty paired chunk\n", insurance.Id) + if ins.HasChunk() { + msg += fmt.Sprintf("unpaired insurance(id: %d) have non-empty paired chunk\n", ins.Id) brokenCount++ return false } case types.INSURANCE_STATUS_UNPAIRING_FOR_WITHDRAWAL: // must have chunk to protect - if !insurance.HasChunk() { - msg += fmt.Sprintf("unpairing for withdrawal insurance(id: %d) have empty chunk to protect\n", insurance.Id) + if !ins.HasChunk() { + msg += fmt.Sprintf("unpairing for withdrawal insurance(id: %d) have empty chunk to protect\n", ins.Id) brokenCount++ return false } - _, found := k.GetChunk(ctx, insurance.ChunkId) + _, found := k.GetChunk(ctx, ins.ChunkId) if !found { - msg += fmt.Sprintf("not found chunk to protect for unpairing for withdrawal insurance(id: %d)\n", insurance.Id) + msg += fmt.Sprintf("not found chunk to protect for unpairing for withdrawal insurance(id: %d)\n", ins.Id) brokenCount++ return false } default: - msg += fmt.Sprintf("insurance(id: %d) have invalid status: %s\n", insurance.Id, insurance.Status) + msg += fmt.Sprintf("insurance(id: %d) have invalid status: %s\n", ins.Id, ins.Status) brokenCount++ return false } diff --git a/x/liquidstaking/keeper/liquidstaking.go b/x/liquidstaking/keeper/liquidstaking.go index fb824582..e3549f06 100644 --- a/x/liquidstaking/keeper/liquidstaking.go +++ b/x/liquidstaking/keeper/liquidstaking.go @@ -60,10 +60,7 @@ func (k Keeper) CoverRedelegationPenalty(ctx sdk.Context) { // 2. Deduct dynamic fee from remaining and burn it. // 3. Send rest of rewards to reward module account. func (k Keeper) CollectRewardAndFee( - ctx sdk.Context, - dynamicFeeRate sdk.Dec, - chunk types.Chunk, - ins types.Insurance, + ctx sdk.Context, dynamicFeeRate sdk.Dec, chunk types.Chunk, ins types.Insurance, ) { // At upper callstack(=DistributeReward), we withdrawed delegation reward of chunk. // So balance of chunk is delegation reward. @@ -200,8 +197,9 @@ func (k Keeper) HandleQueuedLiquidUnstakes(ctx sdk.Context) []types.Chunk { ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeBeginLiquidUnstake, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyChunkIds, strings.Join(chunkIds, ", ")), - sdk.NewAttribute(stakingtypes.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)), + sdk.NewAttribute(types.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)), ), }) } @@ -222,6 +220,7 @@ func (k Keeper) HandleUnprocessedQueuedLiquidUnstakes(ctx sdk.Context) { ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeDeleteQueuedLiquidUnstake, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyDelegator, info.DelegatorAddress), ), }) @@ -263,6 +262,7 @@ func (k Keeper) HandleQueuedWithdrawInsuranceRequests(ctx sdk.Context) []types.I ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeBeginWithdrawInsurance, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyInsuranceIds, strings.Join(withdrawnInsIds, ", ")), ), }) @@ -305,9 +305,10 @@ func (k Keeper) GetAllRePairableChunksAndOutInsurances(ctx sdk.Context) ( case types.CHUNK_STATUS_PAIRED: pairedIns, validator, _ := k.mustValidatePairedChunk(ctx, chunk) if err := k.ValidateValidator(ctx, validator); err != nil { - outInsurances = append(outInsurances, pairedIns) - chunk.SetStatus(types.CHUNK_STATUS_UNPAIRING) - k.SetChunk(ctx, chunk) + k.startUnpairing(ctx, pairedIns, chunk) + chunk = k.mustGetChunk(ctx, chunk.Id) + unpairingIns := k.mustGetInsurance(ctx, chunk.UnpairingInsuranceId) + outInsurances = append(outInsurances, unpairingIns) } else { validPairedInsuranceMap[pairedIns.Id] = struct{}{} } @@ -413,7 +414,6 @@ func (k Keeper) RePairRankedInsurances( if newRankInIns.GetValidator().Equals(outIns.GetValidator()) { // get chunk by outIns.ChunkId chunk := k.mustGetChunk(ctx, outIns.ChunkId) - // TODO: outIns is removed at next epoch? and also it covers penalty if slashing happened after? k.rePairChunkAndInsurance(ctx, chunk, newRankInIns, outIns) hasSameValidator = true // mark outIns as handled, so we will not handle it again @@ -426,6 +426,14 @@ func (k Keeper) RePairRankedInsurances( } } + // Which ranked-out insurances are not handled yet? + remainedOutInsurances := make([]types.Insurance, 0) + for _, outIns := range rankOutInsurances { + if _, ok := handledOutInsurances[outIns.Id]; !ok { + remainedOutInsurances = append(remainedOutInsurances, outIns) + } + } + // pairing chunks are immediately pairable, so just delegate it. var pairingChunks []types.Chunk pairingChunks = k.GetAllPairingChunks(ctx) @@ -455,27 +463,19 @@ func (k Keeper) RePairRankedInsurances( } ctx.EventManager().EmitEvent( sdk.NewEvent( - stakingtypes.EventTypeDelegate, + types.EventTypeDelegate, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyChunkId, fmt.Sprintf("%d", chunk.Id)), sdk.NewAttribute(types.AttributeKeyInsuranceId, fmt.Sprintf("%d", newIns.Id)), - sdk.NewAttribute(stakingtypes.AttributeKeyDelegator, chunk.DerivedAddress().String()), - sdk.NewAttribute(stakingtypes.AttributeKeyValidator, validator.GetOperator().String()), + sdk.NewAttribute(types.AttributeKeyDelegator, chunk.DerivedAddress().String()), + sdk.NewAttribute(types.AttributeKeyValidator, validator.GetOperator().String()), sdk.NewAttribute(sdk.AttributeKeyAmount, types.ChunkSize.String()), - sdk.NewAttribute(stakingtypes.AttributeKeyNewShares, newShares.String()), + sdk.NewAttribute(types.AttributeKeyNewShares, newShares.String()), sdk.NewAttribute(types.AttributeKeyReason, types.AttributeValueReasonPairingChunkPaired), ), ) } - // Which ranked-out insurances are not handled yet? - remainedOutInsurances := make([]types.Insurance, 0) - for _, outIns := range rankOutInsurances { - if _, ok := handledOutInsurances[outIns.Id]; !ok { - remainedOutInsurances = append(remainedOutInsurances, outIns) - } - } - // reset handledOutInsurances to track which out insurances are handled handledOutInsurances = make(map[uint64]struct{}) // rest of rankOutInsurances are replaced with newInsurancesWithDifferentValidators by re-delegation @@ -520,10 +520,11 @@ func (k Keeper) RePairRankedInsurances( ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeBeginRedelegate, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyChunkId, fmt.Sprintf("%d", chunk.Id)), - sdk.NewAttribute(stakingtypes.AttributeKeySrcValidator, outIns.GetValidator().String()), - sdk.NewAttribute(stakingtypes.AttributeKeyDstValidator, newIns.GetValidator().String()), - sdk.NewAttribute(stakingtypes.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)), + sdk.NewAttribute(types.AttributeKeySrcValidator, outIns.GetValidator().String()), + sdk.NewAttribute(types.AttributeKeyDstValidator, newIns.GetValidator().String()), + sdk.NewAttribute(types.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)), ), ) k.rePairChunkAndInsurance(ctx, chunk, newIns, outIns) @@ -557,9 +558,10 @@ func (k Keeper) RePairRankedInsurances( ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeBeginUndelegate, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyChunkId, fmt.Sprintf("%d", chunk.Id)), - sdk.NewAttribute(stakingtypes.AttributeKeyValidator, outIns.GetValidator().String()), - sdk.NewAttribute(stakingtypes.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)), + sdk.NewAttribute(types.AttributeKeyValidator, outIns.GetValidator().String()), + sdk.NewAttribute(types.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)), sdk.NewAttribute(types.AttributeKeyReason, types.AttributeValueReasonNoCandidateIns), ), ) @@ -1402,9 +1404,10 @@ func (k Keeper) mustUndelegate( ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeBeginUndelegate, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyChunkId, fmt.Sprintf("%d", chunk.Id)), - sdk.NewAttribute(stakingtypes.AttributeKeyValidator, validator.GetOperator().String()), - sdk.NewAttribute(stakingtypes.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)), + sdk.NewAttribute(types.AttributeKeyValidator, validator.GetOperator().String()), + sdk.NewAttribute(types.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)), sdk.NewAttribute(types.AttributeKeyReason, reason), ), ) @@ -1426,10 +1429,10 @@ func (k Keeper) mustDelegatePenaltyAmt( sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyChunkId, fmt.Sprintf("%d", chunk.Id)), sdk.NewAttribute(types.AttributeKeyInsuranceId, fmt.Sprintf("%d", insId)), - sdk.NewAttribute(stakingtypes.AttributeKeyDelegator, chunk.DerivedAddress().String()), - sdk.NewAttribute(stakingtypes.AttributeKeyValidator, validator.GetOperator().String()), + sdk.NewAttribute(types.AttributeKeyDelegator, chunk.DerivedAddress().String()), + sdk.NewAttribute(types.AttributeKeyValidator, validator.GetOperator().String()), sdk.NewAttribute(sdk.AttributeKeyAmount, amt.String()), - sdk.NewAttribute(stakingtypes.AttributeKeyNewShares, newShares.String()), + sdk.NewAttribute(types.AttributeKeyNewShares, newShares.String()), sdk.NewAttribute(types.AttributeKeyReason, reason), ), ) @@ -1500,6 +1503,7 @@ func (k Keeper) rePairChunkAndInsurance(ctx sdk.Context, chunk types.Chunk, newI ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeRePairedWithNewInsurance, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyChunkId, fmt.Sprintf("%d", chunk.Id)), sdk.NewAttribute(types.AttributeKeyNewInsuranceId, fmt.Sprintf("%d", newIns.Id)), sdk.NewAttribute(types.AttributeKeyOutInsuranceId, fmt.Sprintf("%d", outIns.Id)), diff --git a/x/liquidstaking/spec/05_begin_block.md b/x/liquidstaking/spec/05_begin_block.md index b65e00a4..0db5ac69 100644 --- a/x/liquidstaking/spec/05_begin_block.md +++ b/x/liquidstaking/spec/05_begin_block.md @@ -2,7 +2,7 @@ # BeginBlock -The end block logic is executed at the end of each epoch. +The begin block logic is executed at the end of each epoch. ## Cover Redelegation Penalty @@ -11,5 +11,5 @@ The end block logic is executed at the end of each epoch. - calc diff between entry.SharesDst and dstDel.Shares - if calc is positive (meaning there is a penalty during the redelegation period) - calc penalty amt which is the token value of shares lost due to slashing - - send penalty amt of tokens to chunk (if unpairing insurance balance is below penalty amt, send all insurance's balance to chunk) + - send penalty amt of tokens to chunk - chunk delegate additional shares corresponding penalty amt \ No newline at end of file diff --git a/x/liquidstaking/spec/06_end_block.md b/x/liquidstaking/spec/06_end_block.md index 9ab1db3e..f9136ef3 100644 --- a/x/liquidstaking/spec/06_end_block.md +++ b/x/liquidstaking/spec/06_end_block.md @@ -15,7 +15,6 @@ The end block logic is executed at the end of each epoch. - burn fee calculated by `fee rate x (balance of chunk - insurance commission)` (Please check the `CalcDynamicFeeRate` in `dynamic_fee_rate.go` for detail.) - send rest of chunk balance to reward pool - ## Cover slashing and handle mature unbondings ### For all unpairing for unstake chunks @@ -25,7 +24,6 @@ The end block logic is executed at the end of each epoch. - if penalty > 0 - if unpairing insurance can cover - unpairing insurance send penalty to chunk - - chunk delegate additional tokens - if unpairing insurance cannot cover - unpairing insurance send penalty to reward pool - refund lstokens corresponding penalty from ls token escrow acc @@ -47,82 +45,92 @@ The end block logic is executed at the end of each epoch. - complete insurance duty because unpairing insurance already covered penalty - if chunk got damaged (unpairing insurance could not cover fully) - send all chunk balances to reward pool because chunk is not valid anymore. + - delete chunk + - if unpairing insurance's fee pool is empty, then delete unpairing insurance - else(= chunk is fine) - - state transition (`Unpairing → Pairing`) + - chunk becomes `Pairing` ### For all paired chunks - calc penalty + - penalty: `(chunk size tokens) - (token values of chunk del shares)` - if penalty > 0 - - check if there are any penalty during re-pairing period (previous epoch ~ current epoch) - - if penalty > 0 - - unpairing insurance send penalty to chunk - - chunk delegate additional tokens + - if chunk is re-paired at previous epoch + - if there was double sign slashing because of evidence created before previous epoch + - unpairing insurance send penalty to chunk + - chunk delegate additional tokens + - deduct covered amt from penalty - if penalty > balance of paired insurance (cannot fully cover it) - - un-delegate chunk - - state transition of paired insurance (`Paired → Unpairing`) - - state transition of chunk (`Paired → Unpairing`) + - un-pair and un-delegate chunk (`Paired → Unpairing`) + - paired insurance becomes `Unpairing` - if penalty ≤ balance of paired insurance (can cover it) - send penalty to chunk - chunk delegate additional shares corresponding penalty - if paired insurance balance < 5.75% after cover penalty and if undelegate not started - - undelegate chunk - - state transition of insurance (`Paired → Unpairing`) - - state transition of chunk (`Paired → Unpairing`) + - un-pair and undelegate chunk (`Paired → Unpairing`) + - paired insurance becomes `Unpairing` - if validator is not valid - - state transition of insurance (`Paired → Unpairing`) - - state transition of chunk (`Paired → Unpairing`) + - un-pair chunk and insurance (both chunk and insurance `Paired → Unpairing`) - if there was an unpairing insurance came from previous epoch and it is already finished its duty - - empty unpairing insurance id from chunk - - if insurance is still valid (balance and validator are all fine), then - - state transition of insurance (`Unpairing → Pairing`) - - if insurance is not valid anymore - - state transition of insurance (`Unpairing → Unpaired`) + - empty unpairing insurance from chunk + - if the insurance is still valid (balance and validator are all fine), then it becomes `Pairing` + - if not, then it becomes `Unpaired` ## Remove Deletable Redelegation Infos - For all re-delegation infos - - if is is matured, then remove it. + - if it is matured, then delete it. ## Handle Queued Liquid Unstakes - For all UnpairingForUnstakingChunkInfos (= info) - got chunk from info.chunkId - - un-delegate chunk - - state transition of insurance (`Paired → Unpairing`) - - state transition of chunk (`Paired → UnpairingForUnstaking`) + - if the chunk is not `Paired`, then do nothing and return. + - un-pair and un-delegate chunk + - paired insurance becomes `Unpairing` + - chunk becomes `UnpairingForUnstaking` + +## Handle Unprocessed Queued Liquid Unstakes + +- For all UnpairingForUnstakingChunkInfos (= info) + - got chunk from info.chunkId + - if the chunk is not `UnpairingForUnstaking`, then delete info and refund info.EscrowedLsTokens to info.DelegatorAddress ## Handle Queued Withdraw Insurance Requests -- For all withdraw insurance requests (= req) +- For all WithdrawInsuranceRequests (= req) - got insurance from req.InsuranceId - - state transition of insurance (`Paired → UnpairingForWithdrawal`) - - state transition of chunk (`Paired → Unpairing`) - - if the status of chunk is `UnpairingForUnstaking`, just keep it as it is + - insurance must be `Paired` or `Unpairing` + - got chunk from insurance.ChunkId + - if the chunk is `Paired`, unpair it + - chunk becomes `Unpairing` + - empty paired insurance id from chunk + - chunk.UnpairingInsuranceId = insurance.Id + - insurance becomes `UnpairingForWithdrawal` - delete request ## Rank Insurances -- get all **re-pairable chunks,** **out insurances, and pairedInsuranceMap** +- get all **re-pairable chunks**, **out insurances**, and **pairedInsuranceMap** - condition of re-pairable chunk (re-pairable means can be paired with new insurance) - must be one of `Pairing`, `Paired`, or `Unpairing (without unbonding obj)` - out insurances are - paired with `Unpairing` chunk which have no unbonding obj - The most common case for this is withdrawing an insurance. - - paired with `Paired` chunk but have invalid validator. (sanity check) - + - paired with `Paired` chunk but have invalid validator. - create candidate insurances - - candidate insurance must be in `Pairing or Paired` -- sort candidate insurances in ascending order, with the cheapest insurance listed first. + - candidate insurance must be in `Pairing` or `Paired` statuses + - candidate insurance must have valid validator +- sort candidate insurances in ascending order, with the cheapest insurance listed first - create rank in insurances and rank out insurances + - if re-pairable chunks are more than candidate insurances, then all candidates can be rank in. + - rank in insurances: `candidates` + - rank out insurances: `out insurances` - rank in insurances: `candidates[:len(rePairableChunks)]` - - rank out insurances: - - for those in `candidates[len(rePairableChunks):]` - - must be `Paired`. others like `Pairing` does not have matched chunk, so it is not rank out, actually. -- append out insurances from get all **re-pairable chunks,out insurances, and pairedInsuranceMap** to **rank out insurances** + - rank out insurances: paired insurances in `candidates[len(rePairableChunks):]` +- append out insurances to rank out insurances - create **newly ranked in insurances** - - **condition** - - for those in **rank in insurances** which not exists in **pairedInsuranceMap** + - for insurances in **rank in insurances** which not exists in **pairedInsuranceMap** - return **newly ranked in insurances** and **rank out insurances** ## RePair Ranked Insurances @@ -133,23 +141,25 @@ The end block logic is executed at the end of each epoch. - for insurance in **newly ranked in insurances** - if there is a rank out insurance which have same validator - replace insurance id of chunk with new one because it directs same validator, we don’t have to re-delegate it - - state transition of rank out insurance (`Paired -> Unpairing`) + - Rank out insurance becomes `Unpairing` insurance of chunk (`Paired → Unpairing`) - if rank out insurance is withdrawing insurance, just keep it as it is - - state transition of rank in insurance (`Pairing -> Paired`) - - state transition of chunk (`Paired | Unpairing → Paired`) and update paired and unpairing insurance ids - - delete matched insurance from **rank out insurances** + - rank in insurance becomes `Paired` insurance of chunk (`Pairing → Paired`) + - state transition of chunk (`Paired | Unpairing → Paired`) + - mark the out insurance as handled - if there is no rank out insurance which have same validator - add it to **new insurances with different validators** -- for **remaining newly ranked in insurances** - - get all **pairing chunks** which is immediately pariable - - pair **pairing chunks** with **remaining insurances** - - delegate chunk -- if there are no remaining **newly ranked in insurances** - - for **out insurance** in **rank out insurances** - - un-delegate chunk -- if there are remaining **newly ranked in insurances** - - for **out insurance** in **rank out insurances** - - begin re-delegation - - src validator: from **out insurance** - - dst validator: from **new insurance** - - shares: original shares of delegation \ No newline at end of file +- make **remained out insurances** (= rank out insurances but not yet handled) +- for insurance in **new insurances with different validators** + - get all **pairing chunks** (=immediately pariable) and pair them with insurance +- for insurance in **remained out insurances** + - if there are no new insurances anymore, then break the loop + - if validator of out insurance (=srcVal) is in Unbonding status, then continue + - if we rere-delegate chunk's delegation from unbonding validator, + then we cannot guarantee the re-delegation ends at the epoch exactly. so we skip. + - begin re-delegation and create tracking obj if srcVal is not Unbonded. + - if srcVal is Unbonded, then re-delegation obj in staking module will not be created. + so we don't need to track it because there will be no re-delegation slashing situation. + - mark the insurance as handled +- make **rest out insurances** by removing handled insurance from **remained out insurances** +- for insurance in **rest out insurances** + - un-delegate chunk diff --git a/x/liquidstaking/spec/07_invariants.md b/x/liquidstaking/spec/07_invariants.md index 6acb4705..7a51a7e5 100644 --- a/x/liquidstaking/spec/07_invariants.md +++ b/x/liquidstaking/spec/07_invariants.md @@ -20,14 +20,13 @@ all of check logics are treated as **OR** conditions, not **AND** condition - there is no paired insurance - cannot find paired insurance obj - cannot find delegation obj - - value of delegation shares ≤ ChunkSize tokens + - delegation shares < ChunkSize tokens - for any Unpairing and UnpairingForUnstaking chunk - there is no unpairing insurance - cannot find unpairing insurance obj - **if it is epoch then** - cannot find unbonding delegation obj - unbonding entries ≠ 1 - - unbonding entries[0].InitialBalance < ChunkSize tokens - for any chunk status == Unspecified **Insurances invariant check broken when** @@ -51,10 +50,13 @@ all of check logics are treated as **OR** conditions, not **AND** condition - for any info - cannot find related chunk obj - - related chunk’s (status ≠ Paried) and (status ≠ UnpairingForUnstaking) **WithdrawInsuranceRequests Invariant check broken when** - for any req - cannot find related insurance obj - - related insurance’s status ≠ Paired \ No newline at end of file + +**RedelgationInfos Invariant check broken when** + +- for any info + - cannot find related chunk obj \ No newline at end of file diff --git a/x/liquidstaking/spec/08_events.md b/x/liquidstaking/spec/08_events.md index 8a747092..012db327 100644 --- a/x/liquidstaking/spec/08_events.md +++ b/x/liquidstaking/spec/08_events.md @@ -5,16 +5,16 @@ The `liquidstaking` module emits the following events: ## BeginBlocker -| Type | Attribute Key | Attribute Value | -|------------------------------|-----------------------|---------------------------------| -| delegate | module | liquidstaking | -| delegate | chunk_id | {chunk.Id} | -| delegate | insurance_id | {insurance.Id} | -| delegate | delegator | {chunk.DerivedAddress} | -| delegate | validator | {validatorAddress} | -| delegate | amount | {amount} | -| delegate | new_shares | {newShares} | -| delegate | reason | {reason} | +| Type | Attribute Key | Attribute Value | +|------------------------------|-----------------------|------------------------| +| delegate | module | liquidstaking | +| delegate | chunk_id | {chunk.Id} | +| delegate | insurance_id | {insurance.Id} | +| delegate | delegator | {chunk.DerivedAddress} | +| delegate | validator | {validatorAddress} | +| delegate | amount | {amount} | +| delegate | new_shares | {newShares} | +| delegate | reason | {reason} | ## EndBlocker | Type | Attribute Key | Attribute Value | @@ -27,16 +27,22 @@ The `liquidstaking` module emits the following events: | delegate | amount | {amount} | | delegate | new_shares | {newShares} | | delegate | reason | {reason} | +| begin_liquid_unstake | module | liquidstaking | | begin_liquid_unstake | chunk_ids | {commaSeparatedChunkIds} | | begin_liquid_unstake | completion_time | {completionTime} | +| delete_queued_liquid_unstake | module | liquidstaking | | delete_queued_liquid_unstake | delegator | {delegatorAddress} | +| begin_withdraw_insurance | module | liquidstaking | | begin_withdraw_insurance | insurance_ids | {commaSeparatedInsuranceIds} | +| begin_undelegate | module | liquidstaking | | begin_undelegate | chunk_id | {chunk.Id} | | begin_undelegate | validator | {validatorAddress} | | begin_undelegate | completion_time | {completionTime} | | begin_undelegate | reason | {reason} | +| re_paired_with_new_insurance | module | liquidstaking | | re_paired_with_new_insurance | chunk_id | {chunk.Id} | | re_paired_with_new_insurance | new_insurance_id | {newInsurance.Id} | +| begin_redelegate | module | liquidstaking | | begin_redelegate | chunk_id | {chunk.Id} | | begin_redelegate | source_validator | {outInsurance.ValidatorAddress} | | begin_redelegate | destination_validator | {newInsurance.ValidatorAddress} | diff --git a/x/liquidstaking/types/events.go b/x/liquidstaking/types/events.go index febdb5f1..8569d9ef 100644 --- a/x/liquidstaking/types/events.go +++ b/x/liquidstaking/types/events.go @@ -15,6 +15,7 @@ const ( EventTypeBeginUndelegate = "begin_undelegate" EventTypeRePairedWithNewInsurance = "re_paired_with_new_insurance" EventTypeBeginRedelegate = "begin_redelegate" + EventTypeDelegate = "delegate" AttributeKeyChunkId = "chunk_id" AttributeKeyChunkIds = "chunk_ids" @@ -33,6 +34,10 @@ const ( AttributeKeyDiscountedMintRate = "discounted_mint_rate" AttributeKeyWithdrawInsuranceRequestQueued = "withdraw_insurance_request_queued" AttributeKeyReason = "reason" + AttributeKeyCompletionTime = "completion_time" + AttributeKeyValidator = "validator" + AttributeKeySrcValidator = "src_validator" + AttributeKeyDstValidator = "dst_validator" AttributeValueCategory = ModuleName AttributeValueReasonNotEnoughPairedInsCoverage = "not_enough_paired_insurance_coverage"