forked from neutron-org/neutron
-
Notifications
You must be signed in to change notification settings - Fork 0
/
multihop_swap.go
166 lines (145 loc) · 4.29 KB
/
multihop_swap.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package keeper
import (
sdkerrors "cosmossdk.io/errors"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
math_utils "github.com/MonikaCat/neutron/v2/utils/math"
"github.com/MonikaCat/neutron/v2/x/dex/types"
)
type MultihopStep struct {
RemainingBestPrice math_utils.PrecDec
tradePairID *types.TradePairID
}
func (k Keeper) HopsToRouteData(
ctx sdk.Context,
hops []string,
) ([]MultihopStep, error) {
nPairs := len(hops) - 1
routeArr := make([]MultihopStep, nPairs)
priceAcc := math_utils.OnePrecDec()
for i := range routeArr {
index := len(routeArr) - 1 - i
tokenIn := hops[index]
tokenOut := hops[index+1]
tradePairID, err := types.NewTradePairID(tokenIn, tokenOut)
if err != nil {
return routeArr, err
}
price, found := k.GetCurrPrice(ctx, tradePairID)
if !found {
return routeArr, types.ErrInsufficientLiquidity
}
priceAcc = priceAcc.Mul(price)
routeArr[index] = MultihopStep{
tradePairID: tradePairID,
RemainingBestPrice: priceAcc,
}
}
return routeArr, nil
}
type StepResult struct {
Ctx *types.BranchableCache
CoinOut sdk.Coin
Err error
}
type multihopCacheKey struct {
TokenIn string
TokenOut string
InAmount math.Int
}
func newCacheKey(tokenIn, tokenOut string, inAmount math.Int) multihopCacheKey {
return multihopCacheKey{
TokenIn: tokenIn,
TokenOut: tokenOut,
InAmount: inAmount,
}
}
func (k Keeper) MultihopStep(
bctx *types.BranchableCache,
step MultihopStep,
inCoin sdk.Coin,
stepCache map[multihopCacheKey]StepResult,
) (sdk.Coin, *types.BranchableCache, error) {
cacheKey := newCacheKey(step.tradePairID.TakerDenom, step.tradePairID.MakerDenom, inCoin.Amount)
val, ok := stepCache[cacheKey]
if ok {
ctxBranchCopy := val.Ctx.Branch()
return val.CoinOut, ctxBranchCopy, val.Err
}
// TODO: Due to rounding on swap it is possible to leak tokens at each hop.
// In these cases the user will lose trace amounts of tokens from intermediary steps.
// To fix this we would have to pre-calculate the route such that the amount
// in will be used completely at each step.
// As an intermediary fix, we should credit the unswapped coins back to the user's account.
coinOut, err := k.SwapFullAmountIn(bctx.Ctx, step.tradePairID, inCoin.Amount)
ctxBranch := bctx.Branch()
stepCache[cacheKey] = StepResult{Ctx: bctx, CoinOut: coinOut, Err: err}
if err != nil {
return sdk.Coin{}, bctx, err
}
return coinOut, ctxBranch, nil
}
func (k Keeper) RunMultihopRoute(
ctx sdk.Context,
route types.MultiHopRoute,
initialInCoin sdk.Coin,
exitLimitPrice math_utils.PrecDec,
stepCache map[multihopCacheKey]StepResult,
) (sdk.Coin, func(), error) {
routeData, err := k.HopsToRouteData(ctx, route.Hops)
if err != nil {
return sdk.Coin{}, nil, err
}
currentPrice := math_utils.OnePrecDec()
var currentOutCoin sdk.Coin
inCoin := initialInCoin
bCacheCtx := types.NewBranchableCache(ctx)
for _, step := range routeData {
// If we can't hit the best possible price we can greedily abort
priceUpperbound := currentPrice.Mul(step.RemainingBestPrice)
if exitLimitPrice.GT(priceUpperbound) {
return sdk.Coin{}, bCacheCtx.WriteCache, types.ErrExitLimitPriceHit
}
currentOutCoin, bCacheCtx, err = k.MultihopStep(
bCacheCtx,
step,
inCoin,
stepCache,
)
inCoin = currentOutCoin
if err != nil {
return sdk.Coin{}, nil, sdkerrors.Wrapf(
err,
"Failed at pair: %s",
step.tradePairID.MustPairID().CanonicalString(),
)
}
currentPrice = math_utils.NewPrecDecFromInt(currentOutCoin.Amount).
Quo(math_utils.NewPrecDecFromInt(initialInCoin.Amount))
}
if exitLimitPrice.GT(currentPrice) {
return sdk.Coin{}, nil, types.ErrExitLimitPriceHit
}
return currentOutCoin, bCacheCtx.WriteCache, nil
}
// NOTE: SwapFullAmountIn does not ensure that 100% of amountIn is used. Due to rounding it is possible that
// a dust amount of AmountIn remains unswapped. It is the caller's responsibility to handle this appropriately.
func (k Keeper) SwapFullAmountIn(ctx sdk.Context,
tradePairID *types.TradePairID,
amountIn math.Int,
) (totalOut sdk.Coin, err error) {
_, swapAmountMakerDenom, orderFilled, err := k.Swap(
ctx,
tradePairID,
amountIn,
nil,
nil,
)
if err != nil {
return sdk.Coin{}, err
}
if !orderFilled {
return sdk.Coin{}, types.ErrInsufficientLiquidity
}
return swapAmountMakerDenom, err
}