diff --git a/proto/canto/liquidstaking/v1/liquidstaking.proto b/proto/canto/liquidstaking/v1/liquidstaking.proto index 1cab85b1..e8aeff94 100644 --- a/proto/canto/liquidstaking/v1/liquidstaking.proto +++ b/proto/canto/liquidstaking/v1/liquidstaking.proto @@ -88,15 +88,16 @@ enum ChunkStatus { // designated by the insurance provider. CHUNK_STATUS_PAIRED = 2; // A paired chunk enters this status when paired insurance is started to be - // withdrawn or it's balance <= 5.75%(double_sign_fraction + down_time_fraction) of chunk size tokens - // or the validator becomes tombstoned. + // withdrawn or it's balance <= 5.75%(double_sign_fraction + + // down_time_fraction) of chunk size tokens or the validator becomes + // tombstoned. CHUNK_STATUS_UNPAIRING = 3; // When a delegator (also known as a liquid staker) sends a MsgLiquidUnstake, // it is queued as a UnpairingForUnstakingChunkInfo. At the end of the epoch, // the actual undelegation is triggered and the chunk enters this state. - // Once the unbonding period is over in next epoch, the tokens corresponding chunk size are - // returned to the delegator's account and the associated chunk object is - // removed. + // Once the unbonding period is over in next epoch, the tokens corresponding + // chunk size are returned to the delegator's account and the associated chunk + // object is removed. CHUNK_STATUS_UNPAIRING_FOR_UNSTAKING = 4; } @@ -131,10 +132,11 @@ enum InsuranceStatus { // unexpected loss that may occur due to validator slashing. This ensures that // the chunk remains same size and maximize its staking rewards. INSURANCE_STATUS_PAIRED = 2; - // A paired insurance enters this status when it no longer has enough balance (=5.75% of chunk size tokens) - // to cover slashing penalties, when the validator is tombstoned, or - // when the paired chunk is started to be undelegated by MsgLiquidUnstake. - // At the next epoch, unpairing will be unpaired or pairing if it still valid. + // A paired insurance enters this status when it no longer has enough balance + // (=5.75% of chunk size tokens) to cover slashing penalties, when the + // validator is tombstoned, or when the paired chunk is started to be + // undelegated by MsgLiquidUnstake. At the next epoch, unpairing will be + // unpaired or pairing if it still valid. INSURANCE_STATUS_UNPAIRING = 3; // A paired insurance enters this status when there was a // queued WithdrawalInsuranceRequest created by MsgWithdrawInsurance at the diff --git a/proto/canto/liquidstaking/v1/tx.proto b/proto/canto/liquidstaking/v1/tx.proto index 5dd2820d..028a323f 100644 --- a/proto/canto/liquidstaking/v1/tx.proto +++ b/proto/canto/liquidstaking/v1/tx.proto @@ -53,7 +53,7 @@ message MsgLiquidStake { // delegator_address is the address of the user who requests the liquid // staking. string delegator_address = 1; - // amount is the amount of native token to be liquid staked. + // (How many chunks to liquid stake?) x ChunkSize string amount = 2 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Coin", (gogoproto.nullable) = false @@ -65,8 +65,7 @@ message MsgLiquidStakeResponse {} message MsgLiquidUnstake { // delegator_address is the address of the user who want to liquid unstaking. string delegator_address = 1; - // amount is the number calculated by (number of chunks want to unstake) * - // chunk.size. The delegator must have corresponding ls tokens to unstake. + // (How many chunks to be unstaked?) x ChunkSize string amount = 2 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Coin", (gogoproto.nullable) = false diff --git a/x/liquidstaking/keeper/liquidstaking.go b/x/liquidstaking/keeper/liquidstaking.go index 69fba2e8..fb824582 100644 --- a/x/liquidstaking/keeper/liquidstaking.go +++ b/x/liquidstaking/keeper/liquidstaking.go @@ -812,9 +812,21 @@ func (k Keeper) DoCancelProvideInsurance(ctx sdk.Context, msg *types.MsgCancelPr } // Unescrow provider's balance - escrowed := k.bankKeeper.GetBalance(ctx, ins.DerivedAddress(), k.stakingKeeper.BondDenom(ctx)) - if err = k.bankKeeper.SendCoins(ctx, ins.DerivedAddress(), providerAddr, sdk.NewCoins(escrowed)); err != nil { - return + escrowed := k.bankKeeper.SpendableCoins(ctx, ins.DerivedAddress()) + fees := k.bankKeeper.SpendableCoins(ctx, ins.FeePoolAddress()) + + var inputs []banktypes.Input + var outputs []banktypes.Output + if escrowed.IsValid() && escrowed.IsAllPositive() { + inputs = append(inputs, banktypes.NewInput(ins.DerivedAddress(), escrowed)) + outputs = append(outputs, banktypes.NewOutput(providerAddr, escrowed)) + } + if fees.IsValid() && fees.IsAllPositive() { + inputs = append(inputs, banktypes.NewInput(ins.FeePoolAddress(), fees)) + outputs = append(outputs, banktypes.NewOutput(providerAddr, fees)) + } + if err := k.bankKeeper.InputOutputCoins(ctx, inputs, outputs); err != nil { + return ins, err } k.DeleteInsurance(ctx, insId) return @@ -832,7 +844,7 @@ func (k Keeper) DoWithdrawInsurance(ctx sdk.Context, msg *types.MsgWithdrawInsur // If insurnace is paired or unpairing, then queue request // If insurnace is unpaired then immediately withdraw ins switch ins.Status { - case types.INSURANCE_STATUS_PAIRED, types.INSURANCE_STATUS_UNPAIRING: + case types.INSURANCE_STATUS_PAIRED: req = types.NewWithdrawInsuranceRequest(msg.Id) k.SetWithdrawInsuranceRequest(ctx, req) case types.INSURANCE_STATUS_UNPAIRED: diff --git a/x/liquidstaking/keeper/liquidstaking_test.go b/x/liquidstaking/keeper/liquidstaking_test.go index b357aa71..f90b7254 100644 --- a/x/liquidstaking/keeper/liquidstaking_test.go +++ b/x/liquidstaking/keeper/liquidstaking_test.go @@ -718,6 +718,8 @@ func (suite *KeeperTestSuite) TestCancelProvideInsuranceSuccess() { provider := providers[0] insurance := insurances[0] + remainingCommissions := sdk.NewInt(100) + suite.fundAccount(suite.ctx, insurance.FeePoolAddress(), remainingCommissions) escrowed := suite.app.BankKeeper.GetBalance(suite.ctx, insurance.DerivedAddress(), suite.denom) beforeProviderBalance := suite.app.BankKeeper.GetBalance(suite.ctx, provider, suite.denom) msg := types.NewMsgCancelProvideInsurance(provider.String(), insurance.Id) @@ -725,7 +727,7 @@ func (suite *KeeperTestSuite) TestCancelProvideInsuranceSuccess() { suite.NoError(err) suite.True(insurance.Equal(canceledInsurance)) afterProviderBalance := suite.app.BankKeeper.GetBalance(suite.ctx, provider, suite.denom) - suite.True(afterProviderBalance.Amount.Equal(beforeProviderBalance.Amount.Add(escrowed.Amount)), "provider should get back escrowed amount") + suite.True(afterProviderBalance.Amount.Equal(beforeProviderBalance.Amount.Add(escrowed.Amount).Add(remainingCommissions)), "provider should get back escrowed amount and remaining commissions") suite.mustPassInvariants() } diff --git a/x/liquidstaking/spec/03_state_transition.md b/x/liquidstaking/spec/03_state_transition.md index 7c2b0531..bf5394b3 100644 --- a/x/liquidstaking/spec/03_state_transition.md +++ b/x/liquidstaking/spec/03_state_transition.md @@ -17,7 +17,8 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea **Triggering Condition** -- Upon receipt of a valid `MsgLiquidStake` message if an empty chunk slot and a pairing insurance is available. Otherwise `MsgLiquidStake` fails. +- Upon receipt of a valid `MsgLiquidStake` if an empty chunk slot and a pairing insurance is available. +Otherwise `MsgLiquidStake` fails. **Operations** @@ -27,7 +28,7 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea - send chunk size native tokens to `Chunk` - `Chunk` delegate tokens to validator of paired insurance - mint ls tokens and send minted ls tokens to msg.Delegator (=liquid staker) - - state transition of `Insurance` (`Pairiing → Paired`) + - state transition of `Insurance` (`Pairing → Paired`) - state transition of `Chunk` (`nil → Paired`) ### Paired → UnpairingForUnstaking @@ -39,11 +40,12 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea **Operations** -- with `UnpairingForUnstakingChunkInfo` which created when delegator request liquid unstake +- with `UnpairingForUnstakingChunkInfo` which is created upon receipt of a valid `MsgLiquidUnstake`. - get a related `Chunk` - - undelegate a `Chunk` - - state transition of `Insurance` (`Paired → Unpairing`) - - state transition of `Chunk` (`Paired → UnpairingForUnstaking`) + - if chunk is still Paired, then undelegate a `Chunk` + - state transition of `Insurance` (`Paired → Unpairing`) + - state transition of `Chunk` (`Paired → UnpairingForUnstaking`) + - if not, don't do anything ### Paired → Unpairing @@ -52,14 +54,14 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea - EndBlock & Epoch **AND** - **(** - When paired `Insurance` start to be withdrawn **OR** - - When paired Insurance have inSufficient (balance of insurance < 5.75% of chunkSize tokens) **OR** - - When validator of paired insurance is invalid(e.g. tombstoned) + - When paired Insurance's balance < 5.75% of chunkSize tokens **OR** + - When a validator becomes invalid(e.g. tombstoned) - **)** **Operations** - state transition of paired `Insurance` (`Paired → Unpairing|UnpairingForWtihdrawal`) -- state transition of `Chunk` (`Paired → UnpairingForUnstaking`) +- state transition of `Chunk` (`Paired → Unpairing`) ### UnpairingForUnstaking → nil @@ -72,7 +74,7 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea - finish unbonding - burn escrowed ls tokens - send chunk size tokens back to liquid unstaker -- state transition of `Insurance` (`Unpairing → Unpaired`) +- state transition of `Insurance` (`Unpairing → Pairing|Unpaired`) - delete `UnpairingForUnstakingChunkInfo` - delete `Chunk` (`UnpairingForUnstaking → nil`) @@ -80,7 +82,7 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea **Triggering Condition** -- EndBlock & Epoch +- EndBlock & Epoch **AND** - When there are no candidate insurances to pair **AND** - Chunk is not damaged @@ -93,12 +95,12 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea **Triggering Condition** -- EndBlock & Epoch -- The chunk got damaged, which meant that the insurance couldn't fully cover the penalty. +- EndBlock & Epoch **AND** +- The chunk got damaged, which meant that the insurance couldn't fully cover the penalty so it goes to reward pool. **Operations** -- send all balances of `Chunk` to reward module +- send all balances of `Chunk` to reward pool - state transition of `Insurance` (`Unpairing | UnpairingForWithdrawal → Unpaired`) - delete `Chunk` (`Unpairing → nil`) @@ -108,7 +110,8 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea **Triggering Condition** -- Upon receipt of a valid `MsgProvideInsurance` message if an empty chunk slot and a pairing insurance is available. Otherwise `MsgProvideInsurance` fails. +- Upon receipt of a valid `MsgProvideInsurance` if an empty chunk slot and a pairing insurance is available. +Otherwise `MsgProvideInsurance` fails. **Operations** @@ -120,11 +123,11 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea **Triggering Condition** - EndBlock & Epoch **OR** -- If there are an empty slot and got MsgLiquidStake +- If there are an empty slot and got `MsgLiquidStake` **Operations** -- state transition of `Insurance` (`Pairiing → Paired`) +- state transition of `Insurance` (`Pairing → Paired`) - state transition of `Chunk` (`nil → Paired`) ### Paired → UnpairingForWithdrawal @@ -132,7 +135,7 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea **Triggering Condition** - EndBlock & Epoch **AND** -- If there are an `WithdrawInsuranceRequest` +- If there are a `WithdrawInsuranceRequest` **Operations** @@ -148,14 +151,26 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea - EndBlock & Epoch **AND** - **(** - paired `Chunk` is started to undelegate **OR** - - When paired Insurance have inSufficient (balance of insurance < 5.75% of chunkSize tokens) **OR** - - When validator of paired insurance is invalid(e.g. tombstoned) + - When paired Insurance's balance < 5.75% of chunkSize tokens **OR** + - When a validator becomes invalid(e.g. tombstoned) - **)** **Operations** - state transition of `Insurance` (`Paired → Unpairing`) -- state transition of paired **`Chunk`** (`Paired → Unpairing`) +- state transition of paired `Chunk` (`Paired → Unpairing`) + +### Unpairing → Pairing + +**Triggering Condition** + +- EndBlock & Epoch **AND** +- Insurance is still valid + - A validator is valid and balance >= 5.75% of chunk size tokens + +**Operations** + +- state transition of `Insurance` (`Unpairing → Pairing`) ### UnpairingForWithdrawal → Unpaired @@ -165,14 +180,15 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea **Operations** -- state transition of `Insurance` (`Paired → UnpairingForWithdrawal`) +- state transition of `Insurance` (`UnpairingForWithdrawal → Unpaired`) ### UnpairingForWithdrawal | Unpairing → nil **Triggering Condition** - EndBlock & Epoch **AND** -- Unpairing chunk got damaged(meaning insurance already send all of its balance to chunk, but was not enough) and there are no balances of insurance fee pool +- Unpairing chunk got damaged(meaning insurance already send all of its balance to chunk, but was not enough) and +there are no balances of insurance **Operations** diff --git a/x/liquidstaking/spec/04_messages.md b/x/liquidstaking/spec/04_messages.md index c5f7fef2..a5333d7e 100644 --- a/x/liquidstaking/spec/04_messages.md +++ b/x/liquidstaking/spec/04_messages.md @@ -11,34 +11,37 @@ Liquid stake with an amount of native tokens. A liquid staker is expected to rec ```go type MsgLiquidStake struct { DelegatorAddress string - Amount types.Coin + Amount types.Coin // (How many chunks to liquid stake?) x ChunkSize } ``` **msg is failed if:** -- `msg.Amount` is not bond denom +- `msg.Amount` is not a bond denom - `msg.Amount` is not multiple of ChunkSize tokens -- If there are no empty slot +- If there are no empty slot or pairing insurance +- If chunks to liquid stake is bigger than empty slot or pairing insurance - The balance of msg sender(=Delegator) does not have enough amount of coins for `msg.Amount` ### MsgLiquidUnstake -Liquid unstake with an amount of native tokens which is expected to sent to unstaker when unstaking is done. +Liquid unstake with an amount of native tokens which is expected to be sent to unstaker when unstaking is done. The liquid unstake request will be queued until the upcoming Epoch and will initiate the unstaking process. ```go type MsgLiquidUnstake struct { DelegatorAddress string - Amount sdk.Coin // (How many chunks to be unstaked?) x chunk.size + Amount sdk.Coin // (How many chunks to be unstaked?) x ChunkSize } ``` **msg is failed if:** -- `msg.Amount` is not bond denom +- `msg.Amount` is not a bond denom - `msg.Amount` is not multiple of ChunkSize tokens -- The balance of msg sender(=Delegator) does not have enough amount of ls tokens for corresponding value of `msg.Amount` +- If there are no paired chunks +- If chunks to liquid unstake is bigger than paired chunks +- The balance of msg sender(=Delegator) does not have enough amount of ls tokens corresponding value of `msg.Amount` ## Insurance @@ -46,7 +49,8 @@ type MsgLiquidUnstake struct { Provide insurance to cover slashing penalties for chunks and to receive commission. * 9% of chunk size tokens is recommended for the `msg.Amount`. -* 7% is minimum collateral for the chunk size tokens. If the collateral is less than 7%, the insurance will be unpaired and the provider will not receive commission. +* 7% is minimum collateral for the chunk size tokens. If the collateral is less than 5.75% of chunk size tokens, +then the insurance will be unpaired and the provider will not receive commission. * The fee rate + Validator(msg.ValidatorAddress)'s fee rate must be less than 50%. ```go @@ -60,10 +64,10 @@ type MsgProvideInsurance struct { **msg is failed if:** -- `msg.Amount` is not bond denom +- `msg.Amount` is not a bond denom - `msg.Amount` must be bigger than minimum collateral (7% of chunk size tokens) - `msg.ValidatorAddress` is not valid validator -- `msg.FeeRate` + Validator(msg.ValidatorAddress).Commission.Rate >= 0.5 (50%) +- `msg.FeeRate` + `Validator(msg.ValidatorAddress).Commission.Rate` >= 0.5 (50%) ### MsgCancelProvideInsurance @@ -83,7 +87,7 @@ type MsgCancelInsuranceProvide struct { ### MsgWithdrawInsurance -Create a pending insurance request for withdrawal or immediately withdraw all its commissions and collaterals when it is unpaired insurance. +Create a pending request for withdrawal or immediately withdraw all its commissions and collaterals when it is unpaired insurance. If it is not unpaired, then withdrawal will be triggered during the upcoming Epoch. ```go @@ -95,8 +99,8 @@ type MsgWithdrawInsurance struct { **msg is failed if:** -- There are no paired, unpairing or unpaired insurance with given `msg.Id` - Provider of Insurance with given id is different with `msg.ProviderAddress` +- There are no paired or unpaired insurance with given `msg.Id` ### MsgWithdrawInsuranceCommission @@ -112,6 +116,7 @@ type MsgWithdrawInsuranceCommission struct { **msg is failed if:** +- There are no insurance with given `msg.Id` - Provider of Insurance with given id is different with `msg.ProviderAddress` ### MsgDepositInsurance @@ -142,11 +147,12 @@ How much to get rewards is calculated by `msg.Amount` and discounted mint rate. type MsgClaimDiscountedReward struct { RequesterAddress string Amount sdk.Coin - minimumDiscountRate sdk.Dec + MinimumDiscountRate sdk.Dec } ``` **msg is failed if:** -- `msg.Amount` is not liquid bond denom -- current discount rate is lower than `msg.MinimumDiscountRate` \ No newline at end of file +- `msg.Amount` is not a liquid bond denom +- current discount rate is lower than `msg.MinimumDiscountRate` +- if `msg.RequesterAddress` doesn't have enough amount of ls tokens corresponding value of `msg.Amount` \ No newline at end of file diff --git a/x/liquidstaking/types/liquidstaking.pb.go b/x/liquidstaking/types/liquidstaking.pb.go index 44efd865..632b9670 100644 --- a/x/liquidstaking/types/liquidstaking.pb.go +++ b/x/liquidstaking/types/liquidstaking.pb.go @@ -44,15 +44,16 @@ const ( // designated by the insurance provider. CHUNK_STATUS_PAIRED ChunkStatus = 2 // A paired chunk enters this status when paired insurance is started to be - // withdrawn or it's balance <= 5.75%(double_sign_fraction + down_time_fraction) of chunk size tokens - // or the validator becomes tombstoned. + // withdrawn or it's balance <= 5.75%(double_sign_fraction + + // down_time_fraction) of chunk size tokens or the validator becomes + // tombstoned. CHUNK_STATUS_UNPAIRING ChunkStatus = 3 // When a delegator (also known as a liquid staker) sends a MsgLiquidUnstake, // it is queued as a UnpairingForUnstakingChunkInfo. At the end of the epoch, // the actual undelegation is triggered and the chunk enters this state. - // Once the unbonding period is over in next epoch, the tokens corresponding chunk size are - // returned to the delegator's account and the associated chunk object is - // removed. + // Once the unbonding period is over in next epoch, the tokens corresponding + // chunk size are returned to the delegator's account and the associated chunk + // object is removed. CHUNK_STATUS_UNPAIRING_FOR_UNSTAKING ChunkStatus = 4 ) @@ -98,10 +99,11 @@ const ( // unexpected loss that may occur due to validator slashing. This ensures that // the chunk remains same size and maximize its staking rewards. INSURANCE_STATUS_PAIRED InsuranceStatus = 2 - // A paired insurance enters this status when it no longer has enough balance (=5.75% of chunk size tokens) - // to cover slashing penalties, when the validator is tombstoned, or - // when the paired chunk is started to be undelegated by MsgLiquidUnstake. - // At the next epoch, unpairing will be unpaired or pairing if it still valid. + // A paired insurance enters this status when it no longer has enough balance + // (=5.75% of chunk size tokens) to cover slashing penalties, when the + // validator is tombstoned, or when the paired chunk is started to be + // undelegated by MsgLiquidUnstake. At the next epoch, unpairing will be + // unpaired or pairing if it still valid. INSURANCE_STATUS_UNPAIRING InsuranceStatus = 3 // A paired insurance enters this status when there was a // queued WithdrawalInsuranceRequest created by MsgWithdrawInsurance at the diff --git a/x/liquidstaking/types/tx.pb.go b/x/liquidstaking/types/tx.pb.go index 92766eae..c82f3488 100644 --- a/x/liquidstaking/types/tx.pb.go +++ b/x/liquidstaking/types/tx.pb.go @@ -34,7 +34,7 @@ type MsgLiquidStake struct { // delegator_address is the address of the user who requests the liquid // staking. DelegatorAddress string `protobuf:"bytes,1,opt,name=delegator_address,json=delegatorAddress,proto3" json:"delegator_address,omitempty"` - // amount is the amount of native token to be liquid staked. + // (How many chunks to liquid stake?) x ChunkSize Amount github_com_cosmos_cosmos_sdk_types.Coin `protobuf:"bytes,2,opt,name=amount,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Coin" json:"amount"` } @@ -117,8 +117,7 @@ var xxx_messageInfo_MsgLiquidStakeResponse proto.InternalMessageInfo type MsgLiquidUnstake struct { // delegator_address is the address of the user who want to liquid unstaking. DelegatorAddress string `protobuf:"bytes,1,opt,name=delegator_address,json=delegatorAddress,proto3" json:"delegator_address,omitempty"` - // amount is the number calculated by (number of chunks want to unstake) * - // chunk.size. The delegator must have corresponding ls tokens to unstake. + // (How many chunks to be unstaked?) x ChunkSize Amount github_com_cosmos_cosmos_sdk_types.Coin `protobuf:"bytes,2,opt,name=amount,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Coin" json:"amount"` }