Skip to content

Commit 616985f

Browse files
dongsamhallazzang
andauthored
fix: Pool Coin Decimal Truncation During Deposit (#446)
* fix: wip poc for reproduce and fix poolcoin truncation * fix: simplify calculation logic and add more tests WIP * fix: use equality check in MintingPoolCoinsInvariant * test: fix expected value due to the change in deposit logic, expected value in test also changed * test: add test for MintingPoolCoinsInvariant * fix: update deposit truncation logic and simulation ordering * docs: update changelog and readme * fix: revert MulTruncate to Mul on Deposit Co-authored-by: Hanjun Kim <[email protected]>
1 parent e118e21 commit 616985f

File tree

7 files changed

+261
-73
lines changed

7 files changed

+261
-73
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ Ref: https://keepachangelog.com/en/1.0.0/
3535

3636
# Changelog
3737

38+
## v1.4.x - 2021.10.xx
39+
40+
* [\#455](https://github.com/tendermint/liquidity/pull/455) (sdk) Bump SDK version to [v0.44.2](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.44.2)
41+
* [\#446](https://github.com/tendermint/liquidity/pull/446) Fix: Pool Coin Decimal Truncation During Deposit
42+
3843
## [Unreleased]
3944

4045
## [v1.4.0](https://github.com/tendermint/liquidity/releases/tag/v1.4.0) - 2021.09.07

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ For details, see the [Liquidity Module Light Paper](doc/LiquidityModuleLightPape
3535
Requirement | Notes
3636
----------- | -----------------
3737
Go version | Go1.15 or higher
38-
Cosmos SDK | v0.44.0 or higher
38+
Cosmos SDK | v0.44.2 or higher
3939

4040
### Get Liquidity Module source code
4141

app/app.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,8 @@ func NewLiquidityApp(
381381
slashing.NewAppModule(appCodec, app.SlashingKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper),
382382
params.NewAppModule(app.ParamsKeeper),
383383
evidence.NewAppModule(app.EvidenceKeeper),
384-
authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry),
385384
liquidity.NewAppModule(appCodec, app.LiquidityKeeper, app.AccountKeeper, app.BankKeeper, app.DistrKeeper),
385+
authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry),
386386
)
387387

388388
// register the store decoders for simulation tests

x/liquidity/keeper/invariants.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,15 @@ func MintingPoolCoinsInvariant(poolCoinTotalSupply, mintPoolCoin, depositCoinA,
9494
expectedMintPoolCoinAmtBasedA := depositCoinARatio.MulInt(poolCoinTotalSupply).TruncateInt()
9595
expectedMintPoolCoinAmtBasedB := depositCoinBRatio.MulInt(poolCoinTotalSupply).TruncateInt()
9696

97-
// NewPoolCoinAmount / LastPoolCoinSupply <= AfterRefundedDepositCoinA / LastReserveCoinA
98-
// NewPoolCoinAmount / LastPoolCoinSupply <= AfterRefundedDepositCoinA / LastReserveCoinB
99-
if depositCoinARatio.LT(poolCoinRatio) || depositCoinBRatio.LT(poolCoinRatio) {
100-
panic("invariant check fails due to incorrect ratio of pool coins")
97+
// NewPoolCoinAmount / LastPoolCoinSupply == AfterRefundedDepositCoinA / LastReserveCoinA
98+
// NewPoolCoinAmount / LastPoolCoinSupply == AfterRefundedDepositCoinA / LastReserveCoinB
99+
if depositCoinA.GTE(coinAmountThreshold) && depositCoinB.GTE(coinAmountThreshold) &&
100+
lastReserveCoinA.GTE(coinAmountThreshold) && lastReserveCoinB.GTE(coinAmountThreshold) &&
101+
mintPoolCoin.GTE(coinAmountThreshold) && poolCoinTotalSupply.GTE(coinAmountThreshold) {
102+
if errorRate(depositCoinARatio, poolCoinRatio).GT(errorRateThreshold) ||
103+
errorRate(depositCoinBRatio, poolCoinRatio).GT(errorRateThreshold) {
104+
panic("invariant check fails due to incorrect ratio of pool coins")
105+
}
101106
}
102107

103108
if mintPoolCoin.GTE(coinAmountThreshold) &&

x/liquidity/keeper/invariants_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,62 @@ func TestWithdrawRatioInvariant(t *testing.T) {
2121
})
2222
}
2323

24+
func TestMintingPoolCoinsInvariant(t *testing.T) {
25+
for _, tc := range []struct {
26+
poolCoinSupply int64
27+
mintingPoolCoin int64
28+
reserveA int64
29+
depositA int64
30+
refundedA int64
31+
reserveB int64
32+
depositB int64
33+
refundedB int64
34+
expectPanic bool
35+
}{
36+
{
37+
10000, 1000,
38+
100000, 10000, 0,
39+
100000, 10000, 0,
40+
false,
41+
},
42+
{
43+
10000, 1000,
44+
100000, 10000, 5000,
45+
100000, 10000, 300,
46+
true,
47+
},
48+
{
49+
3000000, 100,
50+
100000000, 4000, 667,
51+
200000000, 8000, 1334,
52+
false,
53+
},
54+
{
55+
3000000, 100,
56+
100000000, 4000, 0,
57+
200000000, 8000, 1334,
58+
true,
59+
},
60+
} {
61+
f := require.NotPanics
62+
if tc.expectPanic {
63+
f = require.Panics
64+
}
65+
f(t, func() {
66+
keeper.MintingPoolCoinsInvariant(
67+
sdk.NewInt(tc.poolCoinSupply),
68+
sdk.NewInt(tc.mintingPoolCoin),
69+
sdk.NewInt(tc.depositA),
70+
sdk.NewInt(tc.depositB),
71+
sdk.NewInt(tc.reserveA),
72+
sdk.NewInt(tc.reserveB),
73+
sdk.NewInt(tc.refundedA),
74+
sdk.NewInt(tc.refundedB),
75+
)
76+
})
77+
}
78+
}
79+
2480
func TestLiquidityPoolsEscrowAmountInvariant(t *testing.T) {
2581
simapp, ctx := app.CreateTestInput()
2682

x/liquidity/keeper/liquidity_pool.go

Lines changed: 37 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,6 @@ func (k Keeper) ExecuteDeposit(ctx sdk.Context, msg types.DepositMsgState, batch
188188

189189
params := k.GetParams(ctx)
190190

191-
var inputs []banktypes.Input
192-
var outputs []banktypes.Output
193-
194191
reserveCoins := k.GetReserveCoins(ctx, pool)
195192

196193
// reinitialize pool if the pool is depleted
@@ -240,77 +237,53 @@ func (k Keeper) ExecuteDeposit(ctx sdk.Context, msg types.DepositMsgState, batch
240237
return nil
241238
}
242239

243-
// only two coins are acceptable
244-
if reserveCoins.Len() != msg.Msg.DepositCoins.Len() {
245-
return types.ErrNumOfReserveCoin
246-
}
247-
248240
reserveCoins.Sort()
249241

250-
// Decimal Error, divide the Int coin amount by the Decimal Rate and erase the decimal point to deposit a lower value
251-
lastReserveCoinA := reserveCoins[0].Amount
252-
lastReserveCoinB := reserveCoins[1].Amount
253-
lastReserveRatio := lastReserveCoinA.ToDec().QuoTruncate(lastReserveCoinB.ToDec())
242+
lastReserveCoinA := reserveCoins[0]
243+
lastReserveCoinB := reserveCoins[1]
254244

255245
depositCoinA := depositCoins[0]
256246
depositCoinB := depositCoins[1]
257-
depositCoinAmountA := depositCoinA.Amount
258-
depositCoinAmountB := depositCoinB.Amount
259-
depositableCoinAmountA := depositCoinB.Amount.ToDec().MulTruncate(lastReserveRatio).TruncateInt()
260-
261-
refundedCoins := sdk.NewCoins()
262-
refundedCoinA := sdk.ZeroInt()
263-
refundedCoinB := sdk.ZeroInt()
264247

265-
var acceptedCoins sdk.Coins
266-
// handle when depositing coin A amount is less than, greater than or equal to depositable amount
267-
if depositCoinA.Amount.LT(depositableCoinAmountA) {
268-
depositCoinAmountB = depositCoinA.Amount.ToDec().QuoTruncate(lastReserveRatio).TruncateInt()
269-
acceptedCoins = sdk.NewCoins(depositCoinA, sdk.NewCoin(depositCoinB.Denom, depositCoinAmountB))
270-
271-
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, acceptedCoins))
272-
outputs = append(outputs, banktypes.NewOutput(reserveAcc, acceptedCoins))
273-
274-
refundedCoinB = depositCoinB.Amount.Sub(depositCoinAmountB)
275-
276-
if refundedCoinB.IsPositive() {
277-
refundedCoins = sdk.NewCoins(sdk.NewCoin(depositCoinB.Denom, refundedCoinB))
278-
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, refundedCoins))
279-
outputs = append(outputs, banktypes.NewOutput(depositor, refundedCoins))
280-
}
281-
} else if depositCoinA.Amount.GT(depositableCoinAmountA) {
282-
depositCoinAmountA = depositCoinB.Amount.ToDec().MulTruncate(lastReserveRatio).TruncateInt()
283-
acceptedCoins = sdk.NewCoins(depositCoinB, sdk.NewCoin(depositCoinA.Denom, depositCoinAmountA))
284-
285-
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, acceptedCoins))
286-
outputs = append(outputs, banktypes.NewOutput(reserveAcc, acceptedCoins))
248+
poolCoinTotalSupply := k.GetPoolCoinTotalSupply(ctx, pool)
249+
poolCoinMintAmt := sdk.MinDec(
250+
poolCoinTotalSupply.ToDec().MulTruncate(depositCoinA.Amount.ToDec()).QuoTruncate(lastReserveCoinA.Amount.ToDec()),
251+
poolCoinTotalSupply.ToDec().MulTruncate(depositCoinB.Amount.ToDec()).QuoTruncate(lastReserveCoinB.Amount.ToDec()),
252+
)
253+
mintRate := poolCoinMintAmt.TruncateDec().QuoTruncate(poolCoinTotalSupply.ToDec())
254+
acceptedCoins := sdk.NewCoins(
255+
sdk.NewCoin(depositCoins[0].Denom, lastReserveCoinA.Amount.ToDec().Mul(mintRate).TruncateInt()),
256+
sdk.NewCoin(depositCoins[1].Denom, lastReserveCoinB.Amount.ToDec().Mul(mintRate).TruncateInt()),
257+
)
258+
refundedCoins := depositCoins.Sub(acceptedCoins)
259+
refundedCoinA := sdk.NewCoin(depositCoinA.Denom, refundedCoins.AmountOf(depositCoinA.Denom))
260+
refundedCoinB := sdk.NewCoin(depositCoinB.Denom, refundedCoins.AmountOf(depositCoinB.Denom))
287261

288-
refundedCoinA = depositCoinA.Amount.Sub(depositCoinAmountA)
262+
mintPoolCoin := sdk.NewCoin(pool.PoolCoinDenom, poolCoinMintAmt.TruncateInt())
263+
mintPoolCoins := sdk.NewCoins(mintPoolCoin)
289264

290-
if refundedCoinA.IsPositive() {
291-
refundedCoins = sdk.NewCoins(sdk.NewCoin(depositCoinA.Denom, refundedCoinA))
292-
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, refundedCoins))
293-
outputs = append(outputs, banktypes.NewOutput(depositor, refundedCoins))
294-
}
295-
} else {
296-
acceptedCoins = sdk.NewCoins(depositCoinA, depositCoinB)
297-
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, acceptedCoins))
298-
outputs = append(outputs, banktypes.NewOutput(reserveAcc, acceptedCoins))
265+
if mintPoolCoins.IsZero() || acceptedCoins.IsZero() {
266+
return fmt.Errorf("pool coin truncated, no accepted coin, refund")
299267
}
300268

301-
// calculate pool token mint amount
302-
poolCoinTotalSupply := k.GetPoolCoinTotalSupply(ctx, pool)
303-
poolCoinAmt := sdk.MinInt(
304-
poolCoinTotalSupply.ToDec().MulTruncate(depositCoinAmountA.ToDec()).QuoTruncate(reserveCoins[0].Amount.ToDec()).TruncateInt(),
305-
poolCoinTotalSupply.ToDec().MulTruncate(depositCoinAmountB.ToDec()).QuoTruncate(reserveCoins[1].Amount.ToDec()).TruncateInt())
306-
mintPoolCoin := sdk.NewCoin(pool.PoolCoinDenom, poolCoinAmt)
307-
mintPoolCoins := sdk.NewCoins(mintPoolCoin)
308-
309-
// mint pool token to the depositor
310269
if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, mintPoolCoins); err != nil {
311270
return err
312271
}
313272

273+
var inputs []banktypes.Input
274+
var outputs []banktypes.Output
275+
276+
if !refundedCoins.IsZero() {
277+
// refund truncated deposit coins
278+
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, refundedCoins))
279+
outputs = append(outputs, banktypes.NewOutput(depositor, refundedCoins))
280+
}
281+
282+
// send accepted deposit coins
283+
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, acceptedCoins))
284+
outputs = append(outputs, banktypes.NewOutput(reserveAcc, acceptedCoins))
285+
286+
// send minted pool coins
314287
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, mintPoolCoins))
315288
outputs = append(outputs, banktypes.NewOutput(depositor, mintPoolCoins))
316289

@@ -329,9 +302,9 @@ func (k Keeper) ExecuteDeposit(ctx sdk.Context, msg types.DepositMsgState, batch
329302
afterReserveCoinB := afterReserveCoins[1].Amount
330303

331304
MintingPoolCoinsInvariant(poolCoinTotalSupply, mintPoolCoin.Amount, depositCoinA.Amount, depositCoinB.Amount,
332-
lastReserveCoinA, lastReserveCoinB, refundedCoinA, refundedCoinB)
333-
DepositInvariant(lastReserveCoinA, lastReserveCoinB, depositCoinA.Amount, depositCoinB.Amount,
334-
afterReserveCoinA, afterReserveCoinB, refundedCoinA, refundedCoinB)
305+
lastReserveCoinA.Amount, lastReserveCoinB.Amount, refundedCoinA.Amount, refundedCoinB.Amount)
306+
DepositInvariant(lastReserveCoinA.Amount, lastReserveCoinB.Amount, depositCoinA.Amount, depositCoinB.Amount,
307+
afterReserveCoinA, afterReserveCoinB, refundedCoinA.Amount, refundedCoinB.Amount)
335308
}
336309

337310
ctx.EventManager().EmitEvent(
@@ -350,7 +323,7 @@ func (k Keeper) ExecuteDeposit(ctx sdk.Context, msg types.DepositMsgState, batch
350323
)
351324

352325
reserveCoins = k.GetReserveCoins(ctx, pool)
353-
lastReserveRatio = sdk.NewDecFromInt(reserveCoins[0].Amount).Quo(sdk.NewDecFromInt(reserveCoins[1].Amount))
326+
lastReserveRatio := sdk.NewDecFromInt(reserveCoins[0].Amount).Quo(sdk.NewDecFromInt(reserveCoins[1].Amount))
354327

355328
logger := k.Logger(ctx)
356329
logger.Debug(

0 commit comments

Comments
 (0)