Skip to content

Commit d63889e

Browse files
authored
Add fuzz testing for order placements (#177)
* Add fuzz testing for order placements * add CI * add more fuzzing utils * fix tests * gitignore * gitignore * linter
1 parent 7ff3cc1 commit d63889e

File tree

7 files changed

+362
-1
lines changed

7 files changed

+362
-1
lines changed

.github/workflows/fuzz.yml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Fuzzing
2+
3+
4+
on:
5+
push:
6+
branches: [ master ]
7+
pull_request:
8+
branches: [ master ]
9+
10+
defaults:
11+
run:
12+
shell: bash
13+
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v2
19+
20+
- name: Set up Go
21+
uses: actions/setup-go@v2
22+
with:
23+
go-version: 1.18
24+
25+
- name: Fuzz Place Order Msg
26+
run: go test github.com/sei-protocol/sei-chain/x/dex/keeper/msgserver -fuzz FuzzPlaceOrders -fuzztime 30s

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ package-lock.json
2323

2424
# test coverage output
2525
c.out
26+
27+
# fuzz test data
28+
*/testdata/fuzz/*

testutil/fuzzing/cosmos.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package fuzzing
2+
3+
import (
4+
sdk "github.com/cosmos/cosmos-sdk/types"
5+
)
6+
7+
func FuzzDec(i int64, isNil bool) sdk.Dec {
8+
if isNil {
9+
return sdk.Dec{}
10+
}
11+
return sdk.NewDec(i)
12+
}
13+
14+
func FuzzCoin(denom string, isNil bool, i int64) sdk.Coin {
15+
if isNil {
16+
return sdk.Coin{Denom: denom, Amount: sdk.Int{}}
17+
}
18+
return sdk.Coin{Denom: denom, Amount: sdk.NewInt(i)}
19+
}

testutil/fuzzing/dex.go

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package fuzzing
2+
3+
import (
4+
"fmt"
5+
6+
sdk "github.com/cosmos/cosmos-sdk/types"
7+
"github.com/sei-protocol/sei-chain/x/dex/types"
8+
)
9+
10+
const BaselinePrice = 1234.56
11+
12+
var ValidAccountCorpus = []string{
13+
"sei1h9yjz89tl0dl6zu65dpxcqnxfhq60wxx8s5kag",
14+
"sei1c2q6xm0x684rshrnlg898zm3vpwz92pcfhgmws",
15+
"sei1ewxvf5a9wq9zk5nurtl6m9yfxpnhyp7s7uk5sl",
16+
"sei1lllgxa294pshcsrsrteh7sj6ey0zqgty30sl8a",
17+
"sei1vhn2p3xavts9swus27zz3n56tz98g3f6unavs2",
18+
"sei1jpkqjfydghgrc23chmnj52xln0muz09j5huhkt",
19+
"sei1k98zjg7scsmk6d4ye8hhrv3an6ppykvt660736",
20+
"sei1wxpqjzdmtjm6gwg6555n0a0aqglrvnp3pqh9hs",
21+
"sei1yuyyr3xg7jhk7pjkrp4j6h88t7gv35e29pfvmf",
22+
"sei1vjgdad5v2euf98nj3pwg5d8agflr384k0eks43",
23+
}
24+
25+
var AccountCorpus = append([]string{
26+
"invalid",
27+
}, ValidAccountCorpus...)
28+
29+
var ContractCorpus = []string{
30+
"invalid",
31+
"sei14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sh9m79m",
32+
"sei1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrqms7u8a",
33+
}
34+
35+
var (
36+
MicroTick = sdk.MustNewDecFromStr("0.000001")
37+
MilliTick = sdk.MustNewDecFromStr("0.001")
38+
WholeTick = sdk.OneDec()
39+
PairCorpus = []types.Pair{
40+
{},
41+
{PriceDenom: "SEI"},
42+
{AssetDenom: "ATOM"},
43+
{
44+
PriceDenom: "SEI",
45+
AssetDenom: "ATOM",
46+
},
47+
{
48+
PriceDenom: "SEI",
49+
AssetDenom: "ATOM",
50+
Ticksize: &MicroTick,
51+
},
52+
{
53+
PriceDenom: "SEI",
54+
AssetDenom: "ATOM",
55+
Ticksize: &MilliTick,
56+
},
57+
{
58+
PriceDenom: "SEI",
59+
AssetDenom: "ATOM",
60+
Ticksize: &WholeTick,
61+
},
62+
{
63+
PriceDenom: "USDC",
64+
AssetDenom: "ATOM",
65+
},
66+
{
67+
PriceDenom: "USDC",
68+
AssetDenom: "ATOM",
69+
Ticksize: &MicroTick,
70+
},
71+
{
72+
PriceDenom: "USDC",
73+
AssetDenom: "ATOM",
74+
Ticksize: &MilliTick,
75+
},
76+
{
77+
PriceDenom: "USDC",
78+
AssetDenom: "ATOM",
79+
Ticksize: &WholeTick,
80+
},
81+
}
82+
)
83+
84+
func GetAccount(i int) string {
85+
ui := uint64(i) % uint64(len(AccountCorpus))
86+
return AccountCorpus[int(ui)]
87+
}
88+
89+
func GetValidAccount(i int) string {
90+
ui := uint64(i) % uint64(len(ValidAccountCorpus))
91+
return ValidAccountCorpus[int(ui)]
92+
}
93+
94+
func GetContract(i int) string {
95+
ui := uint64(i) % uint64(len(ContractCorpus))
96+
return ContractCorpus[int(ui)]
97+
}
98+
99+
func GetPair(i int) types.Pair {
100+
ui := uint64(i) % uint64(len(PairCorpus))
101+
return PairCorpus[int(ui)]
102+
}
103+
104+
func GetPlacedOrders(direction types.PositionDirection, orderType types.OrderType, pair types.Pair, prices []byte, quantities []byte) []*types.Order {
105+
// take the shorter slice's length
106+
if len(prices) < len(quantities) {
107+
quantities = quantities[:len(prices)]
108+
} else {
109+
prices = prices[:len(quantities)]
110+
}
111+
res := []*types.Order{}
112+
for i, price := range prices {
113+
var priceDec sdk.Dec
114+
if direction == types.PositionDirection_LONG {
115+
priceDec = sdk.MustNewDecFromStr(fmt.Sprintf("%f", BaselinePrice+float64(price)))
116+
} else {
117+
priceDec = sdk.MustNewDecFromStr(fmt.Sprintf("%f", BaselinePrice-float64(price)))
118+
}
119+
quantity := sdk.NewDec(int64(quantities[i]))
120+
res = append(res, &types.Order{
121+
Id: uint64(i),
122+
Status: types.OrderStatus_PLACED,
123+
Price: priceDec,
124+
Quantity: quantity,
125+
PositionDirection: direction,
126+
OrderType: orderType,
127+
PriceDenom: pair.PriceDenom,
128+
AssetDenom: pair.AssetDenom,
129+
})
130+
}
131+
return res
132+
}
133+
134+
func GetOrderBookEntries(buy bool, priceDenom string, assetDenom string, entryWeights []byte, allAccountIndices []byte, allWeights []byte) []types.OrderBook {
135+
res := []types.OrderBook{}
136+
totalPriceWeights := uint64(0)
137+
for _, entryWeight := range entryWeights {
138+
totalPriceWeights += uint64(entryWeight)
139+
}
140+
sliceStartAccnt, sliceStartWeights := 0, 0
141+
cumWeights := uint64(0)
142+
for i, entryWeight := range entryWeights {
143+
var price sdk.Dec
144+
if buy {
145+
price = sdk.MustNewDecFromStr(fmt.Sprintf("%f", BaselinePrice-float64(i)))
146+
} else {
147+
price = sdk.MustNewDecFromStr(fmt.Sprintf("%f", BaselinePrice+float64(i)))
148+
}
149+
cumWeights += uint64(entryWeight)
150+
nextSliceStartAccnt := int(cumWeights * uint64(len(allAccountIndices)) / totalPriceWeights)
151+
nextSliceStartWeights := int(cumWeights * uint64(len(allWeights)) / totalPriceWeights)
152+
entry := types.OrderEntry{
153+
Price: price,
154+
Quantity: sdk.NewDec(int64(uint64((entryWeight)))),
155+
PriceDenom: priceDenom,
156+
AssetDenom: assetDenom,
157+
Allocations: GetAllocations(
158+
int64(uint64((entryWeight))),
159+
allAccountIndices[sliceStartAccnt:nextSliceStartAccnt],
160+
allWeights[sliceStartWeights:nextSliceStartWeights],
161+
),
162+
}
163+
if buy {
164+
res = append(res, &types.LongBook{
165+
Price: price,
166+
Entry: &entry,
167+
})
168+
} else {
169+
res = append(res, &types.ShortBook{
170+
Price: price,
171+
Entry: &entry,
172+
})
173+
}
174+
sliceStartAccnt, sliceStartWeights = nextSliceStartAccnt, nextSliceStartWeights
175+
}
176+
return res
177+
}
178+
179+
func GetAllocations(totalQuantity int64, accountIndices []byte, weights []byte) []*types.Allocation {
180+
// take the shorter slice's length
181+
if len(accountIndices) < len(weights) {
182+
weights = weights[:len(accountIndices)]
183+
} else {
184+
accountIndices = accountIndices[:len(weights)]
185+
}
186+
// dedupe and aggregate
187+
aggregatedAccountsToWeights := map[string]uint64{}
188+
totalWeight := uint64(0)
189+
for i, accountIdx := range accountIndices {
190+
account := GetValidAccount(int(accountIdx))
191+
weight := uint64(weights[i])
192+
if old, ok := aggregatedAccountsToWeights[account]; !ok {
193+
aggregatedAccountsToWeights[account] = weight
194+
} else {
195+
aggregatedAccountsToWeights[account] = old + weight
196+
}
197+
totalWeight += weight
198+
}
199+
200+
quantityDec := sdk.NewDec(totalQuantity)
201+
totalWeightDec := sdk.NewDec(int64(totalWeight))
202+
res := []*types.Allocation{}
203+
for account, weight := range aggregatedAccountsToWeights {
204+
res = append(res, &types.Allocation{
205+
Account: account,
206+
Quantity: quantityDec.Mul(sdk.NewDec(int64(weight))).Quo(totalWeightDec),
207+
})
208+
}
209+
return res
210+
}

testutil/keeper/dex.go

+35-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@ import (
99
"github.com/cosmos/cosmos-sdk/store"
1010
storetypes "github.com/cosmos/cosmos-sdk/store/types"
1111
sdk "github.com/cosmos/cosmos-sdk/types"
12+
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
13+
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
14+
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
1215
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
16+
paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper"
1317
typesparams "github.com/cosmos/cosmos-sdk/x/params/types"
1418
"github.com/sei-protocol/sei-chain/app"
1519
"github.com/sei-protocol/sei-chain/x/dex/keeper"
1620
"github.com/sei-protocol/sei-chain/x/dex/types"
21+
epochkeeper "github.com/sei-protocol/sei-chain/x/epoch/keeper"
22+
epochtypes "github.com/sei-protocol/sei-chain/x/epoch/types"
1723
"github.com/stretchr/testify/require"
1824
"github.com/tendermint/tendermint/libs/log"
1925
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
@@ -42,12 +48,26 @@ func TestApp() *app.App {
4248

4349
func DexKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) {
4450
storeKey := sdk.NewKVStoreKey(types.StoreKey)
51+
keyAcc := sdk.NewKVStoreKey(authtypes.StoreKey)
52+
keyBank := sdk.NewKVStoreKey(banktypes.StoreKey)
53+
keyParams := sdk.NewKVStoreKey(typesparams.StoreKey)
54+
tKeyParams := sdk.NewTransientStoreKey(typesparams.TStoreKey)
55+
keyEpochs := sdk.NewKVStoreKey(epochtypes.StoreKey)
4556
memStoreKey := storetypes.NewMemoryStoreKey(types.MemStoreKey)
4657

58+
blackListAddrs := map[string]bool{}
59+
60+
maccPerms := map[string][]string{}
61+
4762
db := tmdb.NewMemDB()
4863
stateStore := store.NewCommitMultiStore(db)
64+
stateStore.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db)
65+
stateStore.MountStoreWithDB(keyBank, sdk.StoreTypeIAVL, db)
4966
stateStore.MountStoreWithDB(storeKey, sdk.StoreTypeIAVL, db)
5067
stateStore.MountStoreWithDB(memStoreKey, sdk.StoreTypeMemory, nil)
68+
stateStore.MountStoreWithDB(keyEpochs, sdk.StoreTypeIAVL, db)
69+
stateStore.MountStoreWithDB(tKeyParams, sdk.StoreTypeTransient, db)
70+
stateStore.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db)
5171
require.NoError(t, stateStore.LoadLatestVersion())
5272

5373
registry := codectypes.NewInterfaceRegistry()
@@ -59,17 +79,31 @@ func DexKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) {
5979
memStoreKey,
6080
"DexParams",
6181
)
62-
k := keeper.NewPlainKeeper(
82+
paramsKeeper := paramskeeper.NewKeeper(cdc, codec.NewLegacyAmino(), keyParams, tKeyParams)
83+
accountKeeper := authkeeper.NewAccountKeeper(cdc, keyAcc, paramsKeeper.Subspace(authtypes.ModuleName), authtypes.ProtoBaseAccount, maccPerms)
84+
bankKeeper := bankkeeper.NewBaseKeeper(cdc, keyBank, accountKeeper, paramsKeeper.Subspace(banktypes.ModuleName), blackListAddrs)
85+
epochKeeper := epochkeeper.NewKeeper(cdc, keyEpochs, memStoreKey, paramsKeeper.Subspace(epochtypes.ModuleName))
86+
k := keeper.NewKeeper(
6387
cdc,
6488
storeKey,
6589
memStoreKey,
6690
paramsSubspace,
91+
*epochKeeper,
92+
bankKeeper,
6793
)
6894

6995
ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger())
7096

7197
// Initialize params
7298
k.SetParams(ctx, types.DefaultParams())
99+
bankParams := banktypes.DefaultParams()
100+
bankParams.SendEnabled = []*banktypes.SendEnabled{
101+
{
102+
Denom: TestPriceDenom,
103+
Enabled: true,
104+
},
105+
}
106+
bankKeeper.SetParams(ctx, bankParams)
73107

74108
return k, ctx
75109
}

x/dex/keeper/msgserver/msg_server_place_orders.go

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ func (k msgServer) transferFunds(goCtx context.Context, msg *types.MsgPlaceOrder
2525
if err != nil {
2626
return err
2727
}
28+
for _, fund := range msg.Funds {
29+
if fund.Amount.IsNil() {
30+
return errors.New("deposit amount cannot be nil")
31+
}
32+
}
2833
if err := k.BankKeeper.IsSendEnabledCoins(ctx, msg.Funds...); err != nil {
2934
return err
3035
}

0 commit comments

Comments
 (0)