Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 150acef

Browse files
authored
fix: validation swap msg and offerCoinFee ceiling (tendermint#436)
* fix: validation swap msg and offerCoinFee ceiling * docs: update description and swagger to 2.3.1 * fix: consume all ReservedOfferCoinFee when full matched * fix: apply suggestions of the PR * fix: revert da7d52e pointer argument * fix: reserveOfferCoinFee residual Issue * fix: update comments and revert fee logic
1 parent 33a5ca6 commit 150acef

File tree

14 files changed

+335
-40
lines changed

14 files changed

+335
-40
lines changed

client/docs/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"info": {
44
"title": "Cosmos SDK Liquidity Module - REST and gRPC Gateway docs",
55
"description": "A REST interface for state queries, transactions",
6-
"version": "2.3.0"
6+
"version": "2.3.1"
77
},
88
"apis": [
99
{

client/docs/statik/statik.go

Lines changed: 3 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/docs/swagger-ui/swagger.yaml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ swagger: '2.0'
22
info:
33
title: Cosmos SDK Liquidity Module - REST and gRPC Gateway docs
44
description: 'A REST interface for state queries, transactions'
5-
version: 2.3.0
5+
version: 2.3.1
66
paths:
77
/cosmos/liquidity/v1beta1/params:
88
get:
@@ -1345,8 +1345,8 @@ paths:
13451345
offering
13461346
13471347
and `offer_coin_fee` must be half of offer coin amount *
1348-
current `params.swap_fee_rate` for reservation to pay
1349-
fees.
1348+
current `params.swap_fee_rate` and ceil for reservation
1349+
to pay fees.
13501350
13511351
This request is stacked in the batch of the liquidity
13521352
pool, is not processed
@@ -1693,8 +1693,8 @@ paths:
16931693
offering
16941694
16951695
and `offer_coin_fee` must be half of offer coin amount *
1696-
current `params.swap_fee_rate` for reservation to pay
1697-
fees.
1696+
current `params.swap_fee_rate` and ceil for reservation to
1697+
pay fees.
16981698
16991699
This request is stacked in the batch of the liquidity
17001700
pool, is not processed
@@ -2502,7 +2502,7 @@ definitions:
25022502
`demand_coin_denom` with the coin and the price you're offering
25032503
25042504
and `offer_coin_fee` must be half of offer coin amount * current
2505-
`params.swap_fee_rate` for reservation to pay fees.
2505+
`params.swap_fee_rate` and ceil for reservation to pay fees.
25062506
25072507
This request is stacked in the batch of the liquidity pool, is not
25082508
processed
@@ -3455,7 +3455,7 @@ definitions:
34553455
`demand_coin_denom` with the coin and the price you're offering
34563456
34573457
and `offer_coin_fee` must be half of offer coin amount * current
3458-
`params.swap_fee_rate` for reservation to pay fees.
3458+
`params.swap_fee_rate` and ceil for reservation to pay fees.
34593459
34603460
This request is stacked in the batch of the liquidity pool, is not
34613461
processed
@@ -3665,7 +3665,7 @@ definitions:
36653665
`demand_coin_denom` with the coin and the price you're offering
36663666
36673667
and `offer_coin_fee` must be half of offer coin amount * current
3668-
`params.swap_fee_rate` for reservation to pay fees.
3668+
`params.swap_fee_rate` and ceil for reservation to pay fees.
36693669
36703670
This request is stacked in the batch of the liquidity pool, is
36713671
not processed
@@ -4086,7 +4086,7 @@ definitions:
40864086
`demand_coin_denom` with the coin and the price you're offering
40874087
40884088
and `offer_coin_fee` must be half of offer coin amount * current
4089-
`params.swap_fee_rate` for reservation to pay fees.
4089+
`params.swap_fee_rate` and ceil for reservation to pay fees.
40904090
40914091
This request is stacked in the batch of the liquidity pool, is not
40924092
processed

go.sum

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,6 @@ github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoM
613613
github.com/tendermint/tendermint v0.34.0-rc4/go.mod h1:yotsojf2C1QBOw4dZrTcxbyxmPUrT4hNuOQWX9XUwB4=
614614
github.com/tendermint/tendermint v0.34.0-rc6/go.mod h1:ugzyZO5foutZImv0Iyx/gOFCX6mjJTgbLHTwi17VDVg=
615615
github.com/tendermint/tendermint v0.34.0/go.mod h1:Aj3PIipBFSNO21r+Lq3TtzQ+uKESxkbA3yo/INM4QwQ=
616-
github.com/tendermint/tendermint v0.34.10 h1:wBOc/It8sh/pVH9np2V5fBvRmIyFN/bUrGPx+eAHexs=
617616
github.com/tendermint/tendermint v0.34.10/go.mod h1:aeHL7alPh4uTBIJQ8mgFEE8VwJLXI1VD3rVOmH2Mcy0=
618617
github.com/tendermint/tendermint v0.34.11 h1:q1Yh76oG4QbS07xhmIJh5iAE0fYpJ8P8YKYtjnWfJRY=
619618
github.com/tendermint/tendermint v0.34.11/go.mod h1:aeHL7alPh4uTBIJQ8mgFEE8VwJLXI1VD3rVOmH2Mcy0=

proto/tendermint/liquidity/v1beta1/tx.proto

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ message MsgWithdrawWithinBatchResponse {}
137137
// `MsgSwapWithinBatch` defines an sdk.Msg type that supports submitting a swap offer request to the batch of the liquidity pool.
138138
// Submit swap offer to the liquidity pool batch with the specified the `pool_id`, `swap_type_id`,
139139
// `demand_coin_denom` with the coin and the price you're offering
140-
// and `offer_coin_fee` must be half of offer coin amount * current `params.swap_fee_rate` for reservation to pay fees.
140+
// and `offer_coin_fee` must be half of offer coin amount * current `params.swap_fee_rate` and ceil for reservation to pay fees.
141141
// This request is stacked in the batch of the liquidity pool, is not processed
142142
// immediately, and is processed in the `endblock` at the same time as other requests.
143143
// You must request the same fields as the pool.
@@ -184,7 +184,7 @@ message MsgSwapWithinBatch {
184184
example: "\"denomB\"",
185185
}];
186186

187-
// half of offer coin amount * params.swap_fee_rate for reservation to pay fees.
187+
// half of offer coin amount * params.swap_fee_rate and ceil for reservation to pay fees.
188188
cosmos.base.v1beta1.Coin offer_coin_fee = 6 [
189189
(gogoproto.nullable) = false,
190190
(gogoproto.moretags) = "yaml:\"offer_coin_fee\"",

x/liquidity/keeper/liquidity_pool.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ func (k Keeper) TransactAndRefundSwapLiquidityPool(ctx sdk.Context, swapMsgState
660660
sendCoin(poolReserveAcc, sms.Msg.GetSwapRequester(), sdk.NewCoin(sms.Msg.DemandCoinDenom, receiveAmt))
661661
sendCoin(batchEscrowAcc, poolReserveAcc, sdk.NewCoin(sms.Msg.OfferCoin.Denom, offerCoinFeeAmt))
662662

663-
if sms.RemainingOfferCoin.IsPositive() && sms.OrderExpiryHeight == ctx.BlockHeight() {
663+
if sms.RemainingOfferCoin.Add(sms.ReservedOfferCoinFee).IsPositive() && sms.OrderExpiryHeight == ctx.BlockHeight() {
664664
sendCoin(batchEscrowAcc, sms.Msg.GetSwapRequester(), sms.RemainingOfferCoin.Add(sms.ReservedOfferCoinFee))
665665
}
666666

x/liquidity/keeper/msg_server.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,19 +138,14 @@ func (k msgServer) Swap(goCtx context.Context, msg *types.MsgSwapWithinBatch) (*
138138
return nil, types.ErrCircuitBreakerEnabled
139139
}
140140

141-
params := k.GetParams(ctx)
142-
if msg.OfferCoinFee.IsZero() {
143-
msg.OfferCoinFee = types.GetOfferCoinFee(msg.OfferCoin, params.SwapFeeRate)
144-
}
145-
146141
poolBatch, found := k.GetPoolBatch(ctx, msg.PoolId)
147142
if !found {
148143
return nil, types.ErrPoolBatchNotExists
149144
}
150145

151146
batchMsg, err := k.Keeper.SwapWithinBatch(ctx, msg, types.CancelOrderLifeSpan)
152147
if err != nil {
153-
return &types.MsgSwapWithinBatchResponse{}, err
148+
return nil, err
154149
}
155150

156151
ctx.EventManager().EmitEvents(sdk.Events{

x/liquidity/keeper/msg_server_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/stretchr/testify/require"
99

1010
"github.com/tendermint/liquidity/app"
11+
"github.com/tendermint/liquidity/x/liquidity"
1112
"github.com/tendermint/liquidity/x/liquidity/types"
1213
)
1314

@@ -219,3 +220,213 @@ func TestMsgGetLiquidityPoolMetadata(t *testing.T) {
219220
require.Equal(t, totalSupply, metaData.PoolCoinTotalSupply)
220221
require.Equal(t, creatorBalance, metaData.PoolCoinTotalSupply)
221222
}
223+
224+
func TestMsgSwapWithinBatch(t *testing.T) {
225+
simapp, ctx := app.CreateTestInput()
226+
params := simapp.LiquidityKeeper.GetParams(ctx)
227+
228+
depositCoins := sdk.NewCoins(sdk.NewCoin(DenomX, sdk.NewInt(1_000_000_000)), sdk.NewCoin(DenomY, sdk.NewInt(1_000_000_000)))
229+
depositorAddr := app.AddRandomTestAddr(simapp, ctx, depositCoins.Add(params.PoolCreationFee...))
230+
user := app.AddRandomTestAddr(simapp, ctx, sdk.NewCoins(sdk.NewCoin(DenomX, sdk.NewInt(1_000_000_000)), sdk.NewCoin(DenomY, sdk.NewInt(1_000_000_000))))
231+
232+
pool, err := simapp.LiquidityKeeper.CreatePool(ctx, &types.MsgCreatePool{
233+
PoolCreatorAddress: depositorAddr.String(),
234+
PoolTypeId: types.DefaultPoolTypeID,
235+
DepositCoins: depositCoins,
236+
})
237+
require.NoError(t, err)
238+
239+
cases := []struct {
240+
expectedErr string // empty means no error expected
241+
swapFeeRate sdk.Dec
242+
msg *types.MsgSwapWithinBatch
243+
afterBalance sdk.Coins
244+
}{
245+
{
246+
"",
247+
types.DefaultSwapFeeRate,
248+
&types.MsgSwapWithinBatch{
249+
SwapRequesterAddress: user.String(),
250+
PoolId: pool.Id,
251+
SwapTypeId: pool.TypeId,
252+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(10000)),
253+
OfferCoinFee: types.GetOfferCoinFee(sdk.NewCoin(DenomX, sdk.NewInt(10000)), params.SwapFeeRate),
254+
DemandCoinDenom: DenomY,
255+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
256+
},
257+
types.MustParseCoinsNormalized("999989985denomX,1000009984denomY"),
258+
},
259+
{
260+
// bad offer coin denom
261+
"bad offer coin fee",
262+
types.DefaultSwapFeeRate,
263+
&types.MsgSwapWithinBatch{
264+
SwapRequesterAddress: user.String(),
265+
PoolId: pool.Id,
266+
SwapTypeId: pool.TypeId,
267+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(10000)),
268+
OfferCoinFee: sdk.NewCoin(DenomY, sdk.NewInt(15)),
269+
DemandCoinDenom: DenomY,
270+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
271+
},
272+
types.MustParseCoinsNormalized("1000000000denomX,1000000000denomY"),
273+
},
274+
{
275+
"bad offer coin fee",
276+
types.DefaultSwapFeeRate,
277+
&types.MsgSwapWithinBatch{
278+
SwapRequesterAddress: user.String(),
279+
PoolId: pool.Id,
280+
SwapTypeId: pool.TypeId,
281+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(10000)),
282+
OfferCoinFee: sdk.NewCoin(DenomX, sdk.NewInt(14)),
283+
DemandCoinDenom: DenomY,
284+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
285+
},
286+
types.MustParseCoinsNormalized("1000000000denomX,1000000000denomY"),
287+
},
288+
{
289+
"bad offer coin fee",
290+
types.DefaultSwapFeeRate,
291+
&types.MsgSwapWithinBatch{
292+
SwapRequesterAddress: user.String(),
293+
PoolId: pool.Id,
294+
SwapTypeId: pool.TypeId,
295+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(10000)),
296+
OfferCoinFee: sdk.NewCoin(DenomX, sdk.NewInt(16)),
297+
DemandCoinDenom: DenomY,
298+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
299+
},
300+
types.MustParseCoinsNormalized("1000000000denomX,1000000000denomY"),
301+
},
302+
{
303+
"",
304+
types.DefaultSwapFeeRate,
305+
&types.MsgSwapWithinBatch{
306+
SwapRequesterAddress: user.String(),
307+
PoolId: pool.Id,
308+
SwapTypeId: pool.TypeId,
309+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(10001)),
310+
OfferCoinFee: sdk.NewCoin(DenomX, sdk.NewInt(16)),
311+
DemandCoinDenom: DenomY,
312+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
313+
},
314+
types.MustParseCoinsNormalized("999989983denomX,1000009984denomY"),
315+
},
316+
{
317+
"",
318+
types.DefaultSwapFeeRate,
319+
&types.MsgSwapWithinBatch{
320+
SwapRequesterAddress: user.String(),
321+
PoolId: pool.Id,
322+
SwapTypeId: pool.TypeId,
323+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(100)),
324+
OfferCoinFee: sdk.NewCoin(DenomX, sdk.NewInt(1)),
325+
DemandCoinDenom: DenomY,
326+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
327+
},
328+
types.MustParseCoinsNormalized("999999899denomX,1000000098denomY"),
329+
},
330+
{
331+
"bad offer coin fee",
332+
types.DefaultSwapFeeRate,
333+
&types.MsgSwapWithinBatch{
334+
SwapRequesterAddress: user.String(),
335+
PoolId: pool.Id,
336+
SwapTypeId: pool.TypeId,
337+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(100)),
338+
OfferCoinFee: sdk.NewCoin(DenomX, sdk.NewInt(0)),
339+
DemandCoinDenom: DenomY,
340+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
341+
},
342+
types.MustParseCoinsNormalized("1000000000denomX,1000000000denomY"),
343+
},
344+
{
345+
"",
346+
types.DefaultSwapFeeRate,
347+
&types.MsgSwapWithinBatch{
348+
SwapRequesterAddress: user.String(),
349+
PoolId: pool.Id,
350+
SwapTypeId: pool.TypeId,
351+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(1000)),
352+
OfferCoinFee: sdk.NewCoin(DenomX, sdk.NewInt(2)),
353+
DemandCoinDenom: DenomY,
354+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
355+
},
356+
types.MustParseCoinsNormalized("999998998denomX,1000000997denomY"),
357+
},
358+
{
359+
"bad offer coin fee",
360+
types.DefaultSwapFeeRate,
361+
&types.MsgSwapWithinBatch{
362+
SwapRequesterAddress: user.String(),
363+
PoolId: pool.Id,
364+
SwapTypeId: pool.TypeId,
365+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(1000)),
366+
OfferCoinFee: sdk.NewCoin(DenomX, sdk.NewInt(1)),
367+
DemandCoinDenom: DenomY,
368+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
369+
},
370+
types.MustParseCoinsNormalized("1000000000denomX,1000000000denomY"),
371+
},
372+
{
373+
"",
374+
sdk.ZeroDec(),
375+
&types.MsgSwapWithinBatch{
376+
SwapRequesterAddress: user.String(),
377+
PoolId: pool.Id,
378+
SwapTypeId: pool.TypeId,
379+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(1000)),
380+
OfferCoinFee: sdk.NewCoin(DenomX, sdk.ZeroInt()),
381+
DemandCoinDenom: DenomY,
382+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
383+
},
384+
types.MustParseCoinsNormalized("999999000denomX,1000000999denomY"),
385+
},
386+
{
387+
"does not match the reserve coin of the pool",
388+
types.DefaultSwapFeeRate,
389+
&types.MsgSwapWithinBatch{
390+
SwapRequesterAddress: user.String(),
391+
PoolId: pool.Id,
392+
SwapTypeId: pool.TypeId,
393+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(10000)),
394+
OfferCoinFee: types.GetOfferCoinFee(sdk.NewCoin(DenomX, sdk.NewInt(10000)), params.SwapFeeRate),
395+
DemandCoinDenom: DenomA,
396+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
397+
},
398+
types.MustParseCoinsNormalized("1000000000denomX,1000000000denomY"),
399+
},
400+
{
401+
"can not exceed max order ratio of reserve coins that can be ordered at a order",
402+
types.DefaultSwapFeeRate,
403+
&types.MsgSwapWithinBatch{
404+
SwapRequesterAddress: user.String(),
405+
PoolId: pool.Id,
406+
SwapTypeId: pool.TypeId,
407+
OfferCoin: sdk.NewCoin(DenomX, sdk.NewInt(100_000_001)),
408+
OfferCoinFee: types.GetOfferCoinFee(sdk.NewCoin(DenomX, sdk.NewInt(100_000_001)), params.SwapFeeRate),
409+
DemandCoinDenom: DenomY,
410+
OrderPrice: sdk.MustNewDecFromStr("1.00002"),
411+
},
412+
types.MustParseCoinsNormalized("1000000000denomX,1000000000denomY"),
413+
},
414+
}
415+
416+
for _, tc := range cases {
417+
cacheCtx, _ := ctx.CacheContext()
418+
cacheCtx = cacheCtx.WithBlockHeight(1)
419+
params.SwapFeeRate = tc.swapFeeRate
420+
simapp.LiquidityKeeper.SetParams(cacheCtx, params)
421+
_, err = simapp.LiquidityKeeper.SwapWithinBatch(cacheCtx, tc.msg, types.CancelOrderLifeSpan)
422+
if tc.expectedErr == "" {
423+
require.NoError(t, err)
424+
liquidity.EndBlocker(cacheCtx, simapp.LiquidityKeeper)
425+
} else {
426+
require.EqualError(t, err, tc.expectedErr)
427+
}
428+
moduleAccAddress := simapp.AccountKeeper.GetModuleAddress(types.ModuleName)
429+
require.True(t, simapp.BankKeeper.GetAllBalances(cacheCtx, moduleAccAddress).IsZero())
430+
require.Equal(t, tc.afterBalance, simapp.BankKeeper.GetAllBalances(cacheCtx, user))
431+
}
432+
}

x/liquidity/spec/03_state_transitions.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,25 @@ Variables:
221221

222222
### Swap Fee Payment
223223

224-
- Swap fees are calculated after the swap price is calculated.
225-
- Swap fees are proportional to the coins received from matched swap orders.
226-
227-
- `SwapFee` = `ReceivedMatchedCoin` * `SwapFeeRate`
224+
Rather than taking fee solely from `OfferCoin`, liquidity module is designed to take fees half from `OfferCoin`, and the other half from `ExchangedCoin`. This smooths out an impact of the fee payment process.
225+
- **OfferCoin Fee Reservation ( fee before batch process, in OfferCoin )**
226+
- when user orders 100 Xcoin, the swap message demands
227+
- `OfferCoin`(in Xcoin) : 100
228+
- `ReservedOfferCoinFeeAmount`(in Xcoin) = `OfferCoin`*(`SwapFeeRate`/2)
229+
- user needs to have at least 100+100*(`SwapFeeRate`/2) amount of Xcoin to successfully commit this swap message
230+
- the message fails when user's balance is below this amount
231+
- **Actual Fee Payment**
232+
- if 10 Xcoin is executed
233+
- **OfferCoin Fee Payment from Reserved OfferCoin Fee**
234+
- `OfferCoinFeeAmount`(in Xcoin) = (10/100)*`ReservedOfferCoinFeeAmount`
235+
- `ReservedOfferCoinFeeAmount` is reduced from this fee payment
236+
- **ExchangedCoin Fee Payment ( fee after batch process, in ExchangedCoin )**
237+
- `ExchangedCoinFeeAmount`(in Ycoin) = `OfferCoinFeeAmount` / `SwapPrice`
238+
- this is exactly equal value compared to advance fee payment assuming the current SwapPrice, to minimize the pool price impact from fee payment process
228239

229-
- Swap fees are sent to the liquidity pool
240+
- Swap fees are proportional to the coins received from matched swap orders.
241+
- Swap fees are sent to the liquidity pool.
242+
- The decimal points of the swap fees are rounded up.
230243

231244
## Cancel unexecuted swap orders with expired CancelHeight
232245

x/liquidity/spec/04_messages.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,5 @@ The MsgWithdrawWithinBatch message performs validity checks. The transaction tha
9797
- Denoms of `OfferCoin` or `DemandCoin` do not exist in `bank` module
9898
- The balance of `SwapRequester` does not have enough coins for `OfferCoin`
9999
- `OrderPrice` <= zero
100-
- `OfferCoinFee` equals `OfferCoin` _`params.SwapFeeRate`_ `0.5` with truncating Int
100+
- `OfferCoinFee` equals `OfferCoin` * `params.SwapFeeRate` * `0.5` with ceiling
101101
- Has sufficient balance `OfferCoinFee` to reserve offer coin fee

0 commit comments

Comments
 (0)