Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Pool Coin Decimal Truncation During Deposit #446

Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ Ref: https://keepachangelog.com/en/1.0.0/

# Changelog

## v1.4.x - 2021.10.xx

* [\#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)
* [\#446](https://github.com/tendermint/liquidity/pull/446) Fix: Pool Coin Decimal Truncation During Deposit

## [Unreleased]

## [v1.4.0](https://github.com/tendermint/liquidity/releases/tag/v1.4.0) - 2021.09.07
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ For details, see the [Liquidity Module Light Paper](doc/LiquidityModuleLightPape
Requirement | Notes
----------- | -----------------
Go version | Go1.15 or higher
Cosmos SDK | v0.44.0 or higher
Cosmos SDK | v0.44.2 or higher

### Get Liquidity Module source code

Expand Down
2 changes: 1 addition & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,8 @@ func NewLiquidityApp(
slashing.NewAppModule(appCodec, app.SlashingKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper),
params.NewAppModule(app.ParamsKeeper),
evidence.NewAppModule(app.EvidenceKeeper),
authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry),
liquidity.NewAppModule(appCodec, app.LiquidityKeeper, app.AccountKeeper, app.BankKeeper, app.DistrKeeper),
authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry),
)

// register the store decoders for simulation tests
Expand Down
13 changes: 9 additions & 4 deletions x/liquidity/keeper/invariants.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,15 @@ func MintingPoolCoinsInvariant(poolCoinTotalSupply, mintPoolCoin, depositCoinA,
expectedMintPoolCoinAmtBasedA := depositCoinARatio.MulInt(poolCoinTotalSupply).TruncateInt()
expectedMintPoolCoinAmtBasedB := depositCoinBRatio.MulInt(poolCoinTotalSupply).TruncateInt()

// NewPoolCoinAmount / LastPoolCoinSupply <= AfterRefundedDepositCoinA / LastReserveCoinA
// NewPoolCoinAmount / LastPoolCoinSupply <= AfterRefundedDepositCoinA / LastReserveCoinB
if depositCoinARatio.LT(poolCoinRatio) || depositCoinBRatio.LT(poolCoinRatio) {
panic("invariant check fails due to incorrect ratio of pool coins")
// NewPoolCoinAmount / LastPoolCoinSupply == AfterRefundedDepositCoinA / LastReserveCoinA
// NewPoolCoinAmount / LastPoolCoinSupply == AfterRefundedDepositCoinA / LastReserveCoinB
if depositCoinA.GTE(coinAmountThreshold) && depositCoinB.GTE(coinAmountThreshold) &&
lastReserveCoinA.GTE(coinAmountThreshold) && lastReserveCoinB.GTE(coinAmountThreshold) &&
mintPoolCoin.GTE(coinAmountThreshold) && poolCoinTotalSupply.GTE(coinAmountThreshold) {
if errorRate(depositCoinARatio, poolCoinRatio).GT(errorRateThreshold) ||
errorRate(depositCoinBRatio, poolCoinRatio).GT(errorRateThreshold) {
panic("invariant check fails due to incorrect ratio of pool coins")
}
}

if mintPoolCoin.GTE(coinAmountThreshold) &&
Expand Down
56 changes: 56 additions & 0 deletions x/liquidity/keeper/invariants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,62 @@ func TestWithdrawRatioInvariant(t *testing.T) {
})
}

func TestMintingPoolCoinsInvariant(t *testing.T) {
for _, tc := range []struct {
poolCoinSupply int64
mintingPoolCoin int64
reserveA int64
depositA int64
refundedA int64
reserveB int64
depositB int64
refundedB int64
expectPanic bool
}{
{
10000, 1000,
100000, 10000, 0,
100000, 10000, 0,
false,
},
{
10000, 1000,
100000, 10000, 5000,
100000, 10000, 300,
true,
},
{
3000000, 100,
100000000, 4000, 667,
200000000, 8000, 1334,
false,
},
{
3000000, 100,
100000000, 4000, 0,
200000000, 8000, 1334,
true,
},
} {
f := require.NotPanics
if tc.expectPanic {
f = require.Panics
}
f(t, func() {
keeper.MintingPoolCoinsInvariant(
sdk.NewInt(tc.poolCoinSupply),
sdk.NewInt(tc.mintingPoolCoin),
sdk.NewInt(tc.depositA),
sdk.NewInt(tc.depositB),
sdk.NewInt(tc.reserveA),
sdk.NewInt(tc.reserveB),
sdk.NewInt(tc.refundedA),
sdk.NewInt(tc.refundedB),
)
})
}
}

func TestLiquidityPoolsEscrowAmountInvariant(t *testing.T) {
simapp, ctx := app.CreateTestInput()

Expand Down
101 changes: 37 additions & 64 deletions x/liquidity/keeper/liquidity_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,6 @@ func (k Keeper) ExecuteDeposit(ctx sdk.Context, msg types.DepositMsgState, batch

params := k.GetParams(ctx)

var inputs []banktypes.Input
var outputs []banktypes.Output

reserveCoins := k.GetReserveCoins(ctx, pool)

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

// only two coins are acceptable
if reserveCoins.Len() != msg.Msg.DepositCoins.Len() {
return types.ErrNumOfReserveCoin
}

reserveCoins.Sort()

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

depositCoinA := depositCoins[0]
depositCoinB := depositCoins[1]
depositCoinAmountA := depositCoinA.Amount
depositCoinAmountB := depositCoinB.Amount
depositableCoinAmountA := depositCoinB.Amount.ToDec().MulTruncate(lastReserveRatio).TruncateInt()

refundedCoins := sdk.NewCoins()
refundedCoinA := sdk.ZeroInt()
refundedCoinB := sdk.ZeroInt()

var acceptedCoins sdk.Coins
// handle when depositing coin A amount is less than, greater than or equal to depositable amount
if depositCoinA.Amount.LT(depositableCoinAmountA) {
depositCoinAmountB = depositCoinA.Amount.ToDec().QuoTruncate(lastReserveRatio).TruncateInt()
acceptedCoins = sdk.NewCoins(depositCoinA, sdk.NewCoin(depositCoinB.Denom, depositCoinAmountB))

inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, acceptedCoins))
outputs = append(outputs, banktypes.NewOutput(reserveAcc, acceptedCoins))

refundedCoinB = depositCoinB.Amount.Sub(depositCoinAmountB)

if refundedCoinB.IsPositive() {
refundedCoins = sdk.NewCoins(sdk.NewCoin(depositCoinB.Denom, refundedCoinB))
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, refundedCoins))
outputs = append(outputs, banktypes.NewOutput(depositor, refundedCoins))
}
} else if depositCoinA.Amount.GT(depositableCoinAmountA) {
depositCoinAmountA = depositCoinB.Amount.ToDec().MulTruncate(lastReserveRatio).TruncateInt()
acceptedCoins = sdk.NewCoins(depositCoinB, sdk.NewCoin(depositCoinA.Denom, depositCoinAmountA))

inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, acceptedCoins))
outputs = append(outputs, banktypes.NewOutput(reserveAcc, acceptedCoins))
poolCoinTotalSupply := k.GetPoolCoinTotalSupply(ctx, pool)
poolCoinMintAmt := sdk.MinDec(
poolCoinTotalSupply.ToDec().MulTruncate(depositCoinA.Amount.ToDec()).QuoTruncate(lastReserveCoinA.Amount.ToDec()),
poolCoinTotalSupply.ToDec().MulTruncate(depositCoinB.Amount.ToDec()).QuoTruncate(lastReserveCoinB.Amount.ToDec()),
)
mintRate := poolCoinMintAmt.TruncateDec().QuoTruncate(poolCoinTotalSupply.ToDec())
acceptedCoins := sdk.NewCoins(
sdk.NewCoin(depositCoins[0].Denom, lastReserveCoinA.Amount.ToDec().Mul(mintRate).TruncateInt()),
sdk.NewCoin(depositCoins[1].Denom, lastReserveCoinB.Amount.ToDec().Mul(mintRate).TruncateInt()),
Comment on lines +255 to +256
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sdk.NewCoin(depositCoins[0].Denom, lastReserveCoinA.Amount.ToDec().Mul(mintRate).TruncateInt()),
sdk.NewCoin(depositCoins[1].Denom, lastReserveCoinB.Amount.ToDec().Mul(mintRate).TruncateInt()),
sdk.NewCoin(depositCoins[0].Denom, lastReserveCoinA.Amount.ToDec().MulTruncate(mintRate).TruncateInt()),
sdk.NewCoin(depositCoins[1].Denom, lastReserveCoinB.Amount.ToDec().MulTruncate(mintRate).TruncateInt()),

What do you think about using MulTruncate instead of Mul here? @hallazzang @typark391 @kogisin
I think both will have the same result but that is more intuitive, or is there a case that is not?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using MulTruncate here can cause(very rarely) a depositor to take advantage against the pool.
Since the result of MulTruncate is always less or equal than the result of Mul, I suggest to use Mul here.
How do others think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. Let's keep it Mul first, reproduce the corresponding edge case, analyze it, and specify it on spec through other PRs.

)
refundedCoins := depositCoins.Sub(acceptedCoins)
refundedCoinA := sdk.NewCoin(depositCoinA.Denom, refundedCoins.AmountOf(depositCoinA.Denom))
refundedCoinB := sdk.NewCoin(depositCoinB.Denom, refundedCoins.AmountOf(depositCoinB.Denom))

refundedCoinA = depositCoinA.Amount.Sub(depositCoinAmountA)
mintPoolCoin := sdk.NewCoin(pool.PoolCoinDenom, poolCoinMintAmt.TruncateInt())
mintPoolCoins := sdk.NewCoins(mintPoolCoin)

if refundedCoinA.IsPositive() {
refundedCoins = sdk.NewCoins(sdk.NewCoin(depositCoinA.Denom, refundedCoinA))
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, refundedCoins))
outputs = append(outputs, banktypes.NewOutput(depositor, refundedCoins))
}
} else {
acceptedCoins = sdk.NewCoins(depositCoinA, depositCoinB)
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, acceptedCoins))
outputs = append(outputs, banktypes.NewOutput(reserveAcc, acceptedCoins))
if mintPoolCoins.IsZero() || acceptedCoins.IsZero() {
return fmt.Errorf("pool coin truncated, no accepted coin, refund")
}

// calculate pool token mint amount
poolCoinTotalSupply := k.GetPoolCoinTotalSupply(ctx, pool)
poolCoinAmt := sdk.MinInt(
poolCoinTotalSupply.ToDec().MulTruncate(depositCoinAmountA.ToDec()).QuoTruncate(reserveCoins[0].Amount.ToDec()).TruncateInt(),
poolCoinTotalSupply.ToDec().MulTruncate(depositCoinAmountB.ToDec()).QuoTruncate(reserveCoins[1].Amount.ToDec()).TruncateInt())
mintPoolCoin := sdk.NewCoin(pool.PoolCoinDenom, poolCoinAmt)
mintPoolCoins := sdk.NewCoins(mintPoolCoin)

// mint pool token to the depositor
if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, mintPoolCoins); err != nil {
return err
}

var inputs []banktypes.Input
var outputs []banktypes.Output

if !refundedCoins.IsZero() {
// refund truncated deposit coins
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, refundedCoins))
outputs = append(outputs, banktypes.NewOutput(depositor, refundedCoins))
}

// send accepted deposit coins
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, acceptedCoins))
outputs = append(outputs, banktypes.NewOutput(reserveAcc, acceptedCoins))

// send minted pool coins
inputs = append(inputs, banktypes.NewInput(batchEscrowAcc, mintPoolCoins))
outputs = append(outputs, banktypes.NewOutput(depositor, mintPoolCoins))

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

MintingPoolCoinsInvariant(poolCoinTotalSupply, mintPoolCoin.Amount, depositCoinA.Amount, depositCoinB.Amount,
lastReserveCoinA, lastReserveCoinB, refundedCoinA, refundedCoinB)
DepositInvariant(lastReserveCoinA, lastReserveCoinB, depositCoinA.Amount, depositCoinB.Amount,
afterReserveCoinA, afterReserveCoinB, refundedCoinA, refundedCoinB)
lastReserveCoinA.Amount, lastReserveCoinB.Amount, refundedCoinA.Amount, refundedCoinB.Amount)
DepositInvariant(lastReserveCoinA.Amount, lastReserveCoinB.Amount, depositCoinA.Amount, depositCoinB.Amount,
afterReserveCoinA, afterReserveCoinB, refundedCoinA.Amount, refundedCoinB.Amount)
}

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

reserveCoins = k.GetReserveCoins(ctx, pool)
lastReserveRatio = sdk.NewDecFromInt(reserveCoins[0].Amount).Quo(sdk.NewDecFromInt(reserveCoins[1].Amount))
lastReserveRatio := sdk.NewDecFromInt(reserveCoins[0].Amount).Quo(sdk.NewDecFromInt(reserveCoins[1].Amount))

logger := k.Logger(ctx)
logger.Debug(
Expand Down
Loading