/
amm.go
242 lines (219 loc) · 9.66 KB
/
amm.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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
package balancer
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/osmosis-labs/osmosis/v13/osmomath"
"github.com/osmosis-labs/osmosis/v13/x/gamm/types"
)
// subPoolAssetWeights subtracts the weights of two different pool asset slices.
// It assumes that both pool assets have the same token denominations,
// with the denominations in the same order.
// Returned weights can (and probably will have some) be negative.
func subPoolAssetWeights(base []PoolAsset, other []PoolAsset) []PoolAsset {
weightDifference := make([]PoolAsset, len(base))
// TODO: Consider deleting these panics for performance
if len(base) != len(other) {
panic("subPoolAssetWeights called with invalid input, len(base) != len(other)")
}
for i, asset := range base {
if asset.Token.Denom != other[i].Token.Denom {
panic(fmt.Sprintf("subPoolAssetWeights called with invalid input, "+
"expected other's %vth asset to be %v, got %v",
i, asset.Token.Denom, other[i].Token.Denom))
}
curWeightDiff := asset.Weight.Sub(other[i].Weight)
weightDifference[i] = PoolAsset{Token: asset.Token, Weight: curWeightDiff}
}
return weightDifference
}
// addPoolAssetWeights adds the weights of two different pool asset slices.
// It assumes that both pool assets have the same token denominations,
// with the denominations in the same order.
// Returned weights can be negative.
func addPoolAssetWeights(base []PoolAsset, other []PoolAsset) []PoolAsset {
weightSum := make([]PoolAsset, len(base))
// TODO: Consider deleting these panics for performance
if len(base) != len(other) {
panic("addPoolAssetWeights called with invalid input, len(base) != len(other)")
}
for i, asset := range base {
if asset.Token.Denom != other[i].Token.Denom {
panic(fmt.Sprintf("addPoolAssetWeights called with invalid input, "+
"expected other's %vth asset to be %v, got %v",
i, asset.Token.Denom, other[i].Token.Denom))
}
curWeightSum := asset.Weight.Add(other[i].Weight)
weightSum[i] = PoolAsset{Token: asset.Token, Weight: curWeightSum}
}
return weightSum
}
// assumes 0 < d < 1
func poolAssetsMulDec(base []PoolAsset, d sdk.Dec) []PoolAsset {
newWeights := make([]PoolAsset, len(base))
for i, asset := range base {
// TODO: This can adversarially panic at the moment! (as can Pool.TotalWeight)
// Ensure this won't be able to panic in the future PR where we bound
// each assets weight, and add precision
newWeight := d.MulInt(asset.Weight).RoundInt()
newWeights[i] = PoolAsset{Token: asset.Token, Weight: newWeight}
}
return newWeights
}
// ValidateUserSpecifiedWeight ensures that a weight that is provided from user-input anywhere
// for creating a pool obeys the expected guarantees.
// Namely, that the weight is in the range [1, MaxUserSpecifiedWeight)
func ValidateUserSpecifiedWeight(weight sdk.Int) error {
if !weight.IsPositive() {
return sdkerrors.Wrap(types.ErrNotPositiveWeight, weight.String())
}
if weight.GTE(MaxUserSpecifiedWeight) {
return sdkerrors.Wrap(types.ErrWeightTooLarge, weight.String())
}
return nil
}
// solveConstantFunctionInvariant solves the constant function of an AMM
// that determines the relationship between the differences of two sides
// of assets inside the pool.
// For fixed balanceXBefore, balanceXAfter, weightX, balanceY, weightY,
// we could deduce the balanceYDelta, calculated by:
// balanceYDelta = balanceY * (1 - (balanceXBefore/balanceXAfter)^(weightX/weightY))
// balanceYDelta is positive when the balance liquidity decreases.
// balanceYDelta is negative when the balance liquidity increases.
//
// panics if tokenWeightUnknown is 0.
func solveConstantFunctionInvariant(
tokenBalanceFixedBefore,
tokenBalanceFixedAfter,
tokenWeightFixed,
tokenBalanceUnknownBefore,
tokenWeightUnknown sdk.Dec,
) sdk.Dec {
// weightRatio = (weightX/weightY)
weightRatio := tokenWeightFixed.Quo(tokenWeightUnknown)
// y = balanceXBefore/balanceXAfter
y := tokenBalanceFixedBefore.Quo(tokenBalanceFixedAfter)
// amountY = balanceY * (1 - (y ^ weightRatio))
yToWeightRatio := osmomath.Pow(y, weightRatio)
paranthetical := sdk.OneDec().Sub(yToWeightRatio)
amountY := tokenBalanceUnknownBefore.Mul(paranthetical)
return amountY
}
// balancer notation: pAo - pool shares amount out, given single asset in
// the second argument requires the tokenWeightIn / total token weight.
func calcPoolSharesOutGivenSingleAssetIn(
tokenBalanceIn,
normalizedTokenWeightIn,
poolShares,
tokenAmountIn,
swapFee sdk.Dec,
) sdk.Dec {
// deduct swapfee on the in asset.
// We don't charge swap fee on the token amount that we imagine as unswapped (the normalized weight).
// So effective_swapfee = swapfee * (1 - normalized_token_weight)
tokenAmountInAfterFee := tokenAmountIn.Mul(feeRatio(normalizedTokenWeightIn, swapFee))
// To figure out the number of shares we add, first notice that in balancer we can treat
// the number of shares as linearly related to the `k` value function. This is due to the normalization.
// e.g.
// if x^.5 y^.5 = k, then we `n` x the liquidity to `(nx)^.5 (ny)^.5 = nk = k'`
// We generalize this linear relation to do the liquidity add for the not-all-asset case.
// Suppose we increase the supply of x by x', so we want to solve for `k'/k`.
// This is `(x + x')^{weight} * old_terms / (x^{weight} * old_terms) = (x + x')^{weight} / (x^{weight})`
// The number of new shares we need to make is then `old_shares * ((k'/k) - 1)`
// Whats very cool, is that this turns out to be the exact same `solveConstantFunctionInvariant` code
// with the answer's sign reversed.
poolAmountOut := solveConstantFunctionInvariant(
tokenBalanceIn.Add(tokenAmountInAfterFee),
tokenBalanceIn,
normalizedTokenWeightIn,
poolShares,
sdk.OneDec()).Neg()
return poolAmountOut
}
// getPoolAssetsByDenom return a mapping from pool asset
// denom to the pool asset itself. There must be no duplicates.
// Returns error, if any found.
func getPoolAssetsByDenom(poolAssets []PoolAsset) (map[string]PoolAsset, error) {
poolAssetsByDenom := make(map[string]PoolAsset)
for _, poolAsset := range poolAssets {
_, ok := poolAssetsByDenom[poolAsset.Token.Denom]
if ok {
return nil, fmt.Errorf(formatRepeatingPoolAssetsNotAllowedErrFormat, poolAsset.Token.Denom)
}
poolAssetsByDenom[poolAsset.Token.Denom] = poolAsset
}
return poolAssetsByDenom, nil
}
// updateIntermediaryPoolAssetsLiquidity updates poolAssetsByDenom with liquidity.
//
// all liquidity coins must exist in poolAssetsByDenom. Returns error, if not.
//
// This is a helper function that is useful for updating the pool asset amounts
// as an intermediary step in a multi-join methods such as CalcJoinPoolShares.
// In CalcJoinPoolShares with multi-asset joins, we first attempt to do
// a MaximalExactRatioJoin that might leave out some tokens in.
// Then, for every remaining tokens in, we attempt to do a single asset join.
// Since the first step (MaximalExactRatioJoin) affects the pool liqudity due to slippage,
// we would like to account for that in the subsequent steps of single asset join.
func updateIntermediaryPoolAssetsLiquidity(liquidity sdk.Coins, poolAssetsByDenom map[string]PoolAsset) error {
for _, coin := range liquidity {
poolAsset, ok := poolAssetsByDenom[coin.Denom]
if !ok {
return fmt.Errorf(failedInterimLiquidityUpdateErrFormat, coin.Denom)
}
poolAsset.Token.Amount = poolAssetsByDenom[coin.Denom].Token.Amount.Add(coin.Amount)
poolAssetsByDenom[coin.Denom] = poolAsset
}
return nil
}
// feeRatio returns the fee ratio that is defined as follows:
// 1 - ((1 - normalizedTokenWeightOut) * swapFee)
func feeRatio(normalizedWeight, swapFee sdk.Dec) sdk.Dec {
return sdk.OneDec().Sub((sdk.OneDec().Sub(normalizedWeight)).Mul(swapFee))
}
// calcSingleAssetInGivenPoolSharesOut returns token amount in with fee included
// given the swapped out shares amount, using solveConstantFunctionInvariant
func calcSingleAssetInGivenPoolSharesOut(
tokenBalanceIn,
normalizedTokenWeightIn,
totalPoolSharesSupply,
sharesAmountOut,
swapFee sdk.Dec,
) sdk.Dec {
// delta balanceIn is negative(tokens inside the pool increases)
// pool weight is always 1
tokenAmountIn := solveConstantFunctionInvariant(totalPoolSharesSupply.Add(sharesAmountOut), totalPoolSharesSupply, sdk.OneDec(), tokenBalanceIn, normalizedTokenWeightIn).Neg()
// deduct swapfee on the in asset
tokenAmountInFeeIncluded := tokenAmountIn.Quo(feeRatio(normalizedTokenWeightIn, swapFee))
return tokenAmountInFeeIncluded
}
// calcPoolSharesInGivenSingleAssetOut returns pool shares amount in, given single asset out.
// the returned shares in have the fee included in them.
// the second argument requires the tokenWeightOut / total token weight.
func calcPoolSharesInGivenSingleAssetOut(
tokenBalanceOut,
normalizedTokenWeightOut,
totalPoolSharesSupply,
tokenAmountOut,
swapFee,
exitFee sdk.Dec,
) sdk.Dec {
tokenAmountOutFeeIncluded := tokenAmountOut.Quo(feeRatio(normalizedTokenWeightOut, swapFee))
// delta poolSupply is positive(total pool shares decreases)
// pool weight is always 1
sharesIn := solveConstantFunctionInvariant(tokenBalanceOut.Sub(tokenAmountOutFeeIncluded), tokenBalanceOut, normalizedTokenWeightOut, totalPoolSharesSupply, sdk.OneDec())
// charge exit fee on the pool token side
// pAi = pAiAfterExitFee/(1-exitFee)
sharesInFeeIncluded := sharesIn.Quo(sdk.OneDec().Sub(exitFee))
return sharesInFeeIncluded
}
// ensureDenomInPool check to make sure the input denoms exist in the provided pool asset map
func ensureDenomInPool(poolAssetsByDenom map[string]PoolAsset, tokensIn sdk.Coins) error {
for _, coin := range tokensIn {
_, ok := poolAssetsByDenom[coin.Denom]
if !ok {
return sdkerrors.Wrapf(types.ErrDenomNotFoundInPool, invalidInputDenomsErrFormat, coin.Denom)
}
}
return nil
}