Skip to content

Commit

Permalink
sync state transitions, messages and fix some logics
Browse files Browse the repository at this point in the history
DoCancelProvideInsurance
when cancel provide insurance, we should return all of its spendable coins from both derived address and fee pool address.

DoWithdrawInsurance
we can accept request only paired or unpaired insurances, not unpairing insurance.
it because unpairing insurance is already in state transition situation at epoch, so its weird to queue the request for that insurance.
  • Loading branch information
zsystm committed Jul 24, 2023
1 parent 6e03a41 commit a9e3316
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 67 deletions.
20 changes: 11 additions & 9 deletions proto/canto/liquidstaking/v1/liquidstaking.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions proto/canto/liquidstaking/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
20 changes: 16 additions & 4 deletions x/liquidstaking/keeper/liquidstaking.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion x/liquidstaking/keeper/liquidstaking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -718,14 +718,16 @@ 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)
canceledInsurance, err := suite.app.LiquidStakingKeeper.DoCancelProvideInsurance(suite.ctx, msg)
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()
}

Expand Down
62 changes: 39 additions & 23 deletions x/liquidstaking/spec/03_state_transition.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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

Expand All @@ -72,15 +74,15 @@ 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`)

### Unpairing → Pairing

**Triggering Condition**

- EndBlock & Epoch
- EndBlock & Epoch **AND**
- When there are no candidate insurances to pair **AND**
- Chunk is not damaged

Expand All @@ -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`)

Expand All @@ -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**

Expand All @@ -120,19 +123,19 @@ 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

**Triggering Condition**

- EndBlock & Epoch **AND**
- If there are an `WithdrawInsuranceRequest`
- If there are a `WithdrawInsuranceRequest`

**Operations**

Expand All @@ -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

Expand All @@ -165,14 +180,15 @@ State transitions in chunks and insurances occur at EndBlocker when Epoch is rea

**Operations**

- state transition of `Insurance` (`PairedUnpairingForWithdrawal`)
- state transition of `Insurance` (`UnpairingForWithdrawalUnpaired`)

### 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**

Expand Down
36 changes: 21 additions & 15 deletions x/liquidstaking/spec/04_messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,46 @@ 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

### MsgProvideInsurance

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
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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`
- `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`
Loading

0 comments on commit a9e3316

Please sign in to comment.