From 75a5e861a9bb049910d89e021d59fd3a497adf68 Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Fri, 24 Mar 2023 12:58:09 +0000 Subject: [PATCH 01/16] Add tokenRates param to GyroE pool --- src/pools/gyroEPool/gyroEPool.ts | 10 ++++++++-- src/types.ts | 1 + test/lib/onchainData.ts | 19 ++++++++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/pools/gyroEPool/gyroEPool.ts b/src/pools/gyroEPool/gyroEPool.ts index 1827a18c..85c198dc 100644 --- a/src/pools/gyroEPool/gyroEPool.ts +++ b/src/pools/gyroEPool/gyroEPool.ts @@ -74,6 +74,7 @@ export class GyroEPool implements PoolBase { totalShares: BigNumber; gyroEParams: GyroEParams; derivedGyroEParams: DerivedGyroEParams; + tokenRates: [BigNumber, BigNumber]; static fromPool(pool: SubgraphPoolBase): GyroEPool { const { @@ -91,6 +92,7 @@ export class GyroEPool implements PoolBase { w, z, dSq, + tokenRates, } = pool; const gyroEParams = { @@ -129,7 +131,8 @@ export class GyroEPool implements PoolBase { pool.tokens as GyroEPoolToken[], pool.tokensList, gyroEParams as GyroEParamsFromSubgraph, - derivedGyroEParams as DerivedGyroEParamsFromSubgraph + derivedGyroEParams as DerivedGyroEParamsFromSubgraph, + tokenRates ?? [ONE, ONE] ); } @@ -141,7 +144,8 @@ export class GyroEPool implements PoolBase { tokens: GyroEPoolToken[], tokensList: string[], gyroEParams: GyroEParamsFromSubgraph, - derivedGyroEParams: DerivedGyroEParamsFromSubgraph + derivedGyroEParams: DerivedGyroEParamsFromSubgraph, + tokenRates: [BigNumber, BigNumber] ) { this.id = id; this.address = address; @@ -173,6 +177,8 @@ export class GyroEPool implements PoolBase { z: safeParseFixed(derivedGyroEParams.z, 38), dSq: safeParseFixed(derivedGyroEParams.dSq, 38), }; + + this.tokenRates = tokenRates; } parsePoolPairData(tokenIn: string, tokenOut: string): GyroEPoolPairData { diff --git a/src/types.ts b/src/types.ts index bc9c62b0..acd05652 100644 --- a/src/types.ts +++ b/src/types.ts @@ -117,6 +117,7 @@ export interface SubgraphPoolBase { w?: string; z?: string; dSq?: string; + tokenRates?: [BigNumber, BigNumber]; // FxPool delta?: string; diff --git a/test/lib/onchainData.ts b/test/lib/onchainData.ts index 58f451b3..caff4b20 100644 --- a/test/lib/onchainData.ts +++ b/test/lib/onchainData.ts @@ -1,5 +1,5 @@ import { JsonRpcProvider } from '@ethersproject/providers'; -import { formatFixed } from '@ethersproject/bignumber'; +import { formatFixed, BigNumber } from '@ethersproject/bignumber'; import { Provider } from '@ethersproject/providers'; import { isSameAddress } from '../../src/utils'; @@ -147,6 +147,13 @@ export async function getOnChainBalances( pool.address, 'getSwapFeePercentage' ); + if (pool.poolType.toString() === 'GyroE') { + multiPool.call( + `${pool.id}.tokenRates`, + pool.address, + 'getTokenRates' + ); + } } }); @@ -165,6 +172,7 @@ export async function getOnChainBalances( totalSupply: string; virtualSupply?: string; actualSupply?: string; + tokenRates?: [BigNumber, BigNumber]; } >; @@ -183,6 +191,7 @@ export async function getOnChainBalances( totalSupply: string; virtualSupply?: string; actualSupply?: string; + tokenRates?: [BigNumber, BigNumber]; } >; } catch (err) { @@ -201,6 +210,7 @@ export async function getOnChainBalances( virtualSupply, actualSupply, totalSupply, + tokenRates, } = onchainData; if ( @@ -297,6 +307,13 @@ export async function getOnChainBalances( } else { subgraphPools[index].totalShares = formatFixed(totalSupply, 18); } + + if (subgraphPools[index].poolType === 'GyroE') { + if (tokenRates && tokenRates.length) { + subgraphPools[index].tokenRates = tokenRates; + } + } + onChainPools.push(subgraphPools[index]); } catch (err) { throw `Issue with pool onchain data: ${err}`; From 7bd58e44148e58139ed63ee9867298e170364a1d Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Fri, 24 Mar 2023 16:29:30 +0000 Subject: [PATCH 02/16] Add GyroEV2 pool --- src/pools/gyroEPool/gyroEPool.ts | 10 +- .../gyroEV2Pool/GyroscopeBalancerLicense.pdf | Bin 0 -> 184467 bytes src/pools/gyroEV2Pool/LICENSE | 5 + src/pools/gyroEV2Pool/gyroEV2Abi.json | 1968 +++++++++++++++++ .../gyroEV2Pool/gyroEV2Math/constants.ts | 6 + .../gyroEV2Pool/gyroEV2Math/gyroEV2Math.ts | 250 +++ .../gyroEV2Math/gyroEV2MathFunctions.ts | 411 ++++ .../gyroEV2Math/gyroEV2MathHelpers.ts | 501 +++++ src/pools/gyroEV2Pool/gyroEV2Pool.ts | 542 +++++ src/pools/index.ts | 5 + src/types.ts | 7 +- test/lib/onchainData.ts | 22 +- tsconfig.json | 3 +- 13 files changed, 3712 insertions(+), 18 deletions(-) create mode 100644 src/pools/gyroEV2Pool/GyroscopeBalancerLicense.pdf create mode 100644 src/pools/gyroEV2Pool/LICENSE create mode 100644 src/pools/gyroEV2Pool/gyroEV2Abi.json create mode 100644 src/pools/gyroEV2Pool/gyroEV2Math/constants.ts create mode 100644 src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2Math.ts create mode 100644 src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathFunctions.ts create mode 100644 src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts create mode 100644 src/pools/gyroEV2Pool/gyroEV2Pool.ts diff --git a/src/pools/gyroEPool/gyroEPool.ts b/src/pools/gyroEPool/gyroEPool.ts index 85c198dc..1827a18c 100644 --- a/src/pools/gyroEPool/gyroEPool.ts +++ b/src/pools/gyroEPool/gyroEPool.ts @@ -74,7 +74,6 @@ export class GyroEPool implements PoolBase { totalShares: BigNumber; gyroEParams: GyroEParams; derivedGyroEParams: DerivedGyroEParams; - tokenRates: [BigNumber, BigNumber]; static fromPool(pool: SubgraphPoolBase): GyroEPool { const { @@ -92,7 +91,6 @@ export class GyroEPool implements PoolBase { w, z, dSq, - tokenRates, } = pool; const gyroEParams = { @@ -131,8 +129,7 @@ export class GyroEPool implements PoolBase { pool.tokens as GyroEPoolToken[], pool.tokensList, gyroEParams as GyroEParamsFromSubgraph, - derivedGyroEParams as DerivedGyroEParamsFromSubgraph, - tokenRates ?? [ONE, ONE] + derivedGyroEParams as DerivedGyroEParamsFromSubgraph ); } @@ -144,8 +141,7 @@ export class GyroEPool implements PoolBase { tokens: GyroEPoolToken[], tokensList: string[], gyroEParams: GyroEParamsFromSubgraph, - derivedGyroEParams: DerivedGyroEParamsFromSubgraph, - tokenRates: [BigNumber, BigNumber] + derivedGyroEParams: DerivedGyroEParamsFromSubgraph ) { this.id = id; this.address = address; @@ -177,8 +173,6 @@ export class GyroEPool implements PoolBase { z: safeParseFixed(derivedGyroEParams.z, 38), dSq: safeParseFixed(derivedGyroEParams.dSq, 38), }; - - this.tokenRates = tokenRates; } parsePoolPairData(tokenIn: string, tokenOut: string): GyroEPoolPairData { diff --git a/src/pools/gyroEV2Pool/GyroscopeBalancerLicense.pdf b/src/pools/gyroEV2Pool/GyroscopeBalancerLicense.pdf new file mode 100644 index 0000000000000000000000000000000000000000..531fd47e92bd6d9a92e0b12ce115f326daf32f6a GIT binary patch literal 184467 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

Zq;X2GWn?73IbM%)O^eRj;`1y@ zZZF&^3jZDpYJK&9hBlabAl@v*zr&m>w5x=%EnK?HRRJ$MS|jzAFD?W?=sq%nbmw~n z-0d#Ct)YBcfbaWZpDwkO<&-_o_eH;dny*>KV{kuN!FP^fI z)WTlwd(2IqYKi%0*R&@jSs4y@x~Se9nq~f#IhVFrq(OM(mCc%Fcs(lC_v>p^^7C5f zd7l@lHJ$d_-3qV!V5Mr~`+EC~C|=8RBk4Zzoxnbmrv)FenIx)iLY+okZtz|LeU9eS z9sIoNYTA(-i0b_wI$M=luhvJr;SXoln3}qeWBce>`s6B0pk+kS0*p6P1dzd0jS=ypW&*}*j^2fGg z%J*1trynL1XS5ApT~6xUnvwgXk(*x8wa(9SPt@$gbw~x>a8s66?Pj<*_hZ-#zrXD> zH9@kXs5&9c|ApuDfm$IIO-uk6J>^6nK|^U zf{LR<)|L+zg8<*EoUXv0p}I#WdAWVttRx14Nw$|RJsfAA#W|AN2?NDABBUCL|}y`5DYG&O~;W`0*>p{8LL z^sLA`$-CrBa!8LTV=|5#d^iqTnMZdUnai!O(V|aUL35fyn6l++;VWif4`46|L}{T) zsy9u0c0lf4jlHRmHIY(TFK)H^C|Fr)9U?qR|KqD;kBJNy2DK7f! zyT0(ouUW>;F*Bo`Jhz+QG@9y4diFJtsO1pq(qc#$av2tJHO-zyW~vFkW-RN1-nupl zl42rQ*yF}7Ji<4QAae$iHZKlDLsV|PB<|F|*wcfW5sjLT4NzrmY?VcR4B#s=X#xIf zvW3F?&0hO`ka6^dr4(|mg`}}d22vgJWb`Uek1;liNNZRHY*xNuvl-ku8di`DCQykn+&2{N9r(C_Yw~+E^_xIjl*lcSC2toTZNiDM1@pu*&Sxo~Q2-ITcY_qX5L+F#(WjHZ&N zBU+=w3BPSr^Yf$F+u=3Mip&@q^VB+P8HX}cYY)Hyb|qF+d8!LPUaIQdvsxZ2tyfWE!I@Kjf?U*ip}Wtx4}%q*I3qMP ze?hr#->@z0TXdb@tEVDL!p;yeZv*qAC?W_q#KVqTMaUIhFiMTrey-Kh9Sq+*XZ_&$ zguUak9N4IoU9Z4p^s)8| zF%qYqx}9XN^6HIP1^p^JqaVV!-oq`-{0e%FXcyxV^Ooi+t=VYVegWBn$u(td3gjt7 zHD|-?oO7FTL%N1cXJ?2W>L{I+Zp+BYM?a-{%c!AO-NDs|7WK&~X5`6@NMM9Vnx_PHj4@}jc?dIRteFLwE8<0OJpt+!G4cZvE)_JZ9T}Vv)a@;XJ!8<_pg0?-P`M~Om*zRfG*&5P2 zu)Xd4_{A(I4=gEhP-~B@bQ^y<&W! zeQ|h!=B#2ZeT}3Af6m2%XRTrl@iHttGeMf7gnK@?B)X(KUFHb!i1YsOh;8D#p#gxB z0y6nYmV@F>nA0opVSlBdRtr)lZbB(ybRmal~)r)+eHsi-Ej zqc_l`#Ne5un$|+d%YIr+@0t7jZ~AWbfz)M2_>sXR=Lt!!P$o?D`~|)%JGz*rBT8=n zd3W#_xhbC4zniXjeMQ8Iz}gUE#o_{5wNG^hn_D-%uR5P`pP-tN3VRwJkvWfQan)(d z*9+LR_<`4Z_Sx@D**EZILBv&GBsULX)%H}Z1oItn6GgbtR5-oy=Gjt0xKotdjOvD) zlrTWsYE4Ef4T{c#q=ey&kyDs&27QgyZ9!dlZu;%$A3_;EHi;;04n4O?1LE{14kxbM z1ldhOHhJV+tJ|D)7zvo$zw?_R_RMPjh4xiQ%x6qB>{RUgQUNUZi zh2+VSjj@=e`QB!jM6cQ=WKS3%kC!W*2^P#+zi0{=Vhd4*Nfqt4jZbbkl@@+`%KWuw zvt>}Fkb0vnNxl8@q&+<|Iij>szr9*CZq|^sK&4)hJP@NX-4I5^Xc5lT;87LKB<0OU z#LwlhT2e}~y6Xq_S4zkw{TS#mZ*{-6v3w| zh78OnCPp_l$w~O=Rm3j+&CUM5?2wV1s7sKeW{}hviG_scAbHhLv}*XBg^GDSAeCk{ z(?b+biUwNz=g!TNhvbz*u~JH!lkC5adPV+Xgnxdjv=68vVSY^Qx_~Po7*Py*en}>n z^Badp{4s{%uF-@@PT~s^$)g7$lpyl_X8@8Mz_ne{>|~M#=$=cWa2+M@#_ezteALNT zjZa@#q>F%3X;8q0poYabpQFx{68O++V&@RVBv~~$eNZEqcK}3#cZI~f<~cfQo|1RN z3M3MII5K;#Q2?h8Bvw3*9YEnvy)qeeR7Ui4!+pbY{)X0@#AQ~Ld6L~CTtrL->ZAU5~-qs)E)6vUJR3!Y7u-A*Qmvd=@N z{(gYBfPhGey&EqJU}#9RBz?gz*HAm@NG?onhLH*6v;;%V%b*vsG3#~csT0tB{n@o> zg-K;N6(%QS`<5(dz5eXI-hHlhgWG`KeZ2*I ziJC!2;(~^PYGGImjG1tDm=i=e=Ts%DRsQ%lL(Xzevr#CcO|gH>E|^-|9fB@`AZtDu zuWrkb)>B^5B8St<`x{AzvLuirNr)7!BV5C>2etB#GbG6rR<3qp04n*h_gPf zs;|cG&tk%aHpi@R$?hguMw#45wq1i>LtF-$E8qB0=UU=7)_1mge!Cqhw0?~06FSe> zzclJ_$p8^&A;;qJJlW<>ZA2#C(mHW@9<#9p!r~sQ+q6lv!M;|Fx_?<(3i8em3eF7` zU?rPa^La5jW$Wh`GA_l7U2{|Gmpm?4&ByDln}(O@lx27+ZD#o`UK}&L4*&=AT z`~_zHHy@I#mWQgIbtc7~>atc&bG3xyD1IdaRA3R+Wh`TjDb6V_QY!#C2A6o4morVcqbj=Jb)u{G zMUs_Ym_wy!%VAbRt9af{uF`5}OBa0bDu`Wszq|H2deN##JmW3O9TlFqVw!5#66eUkawL37^JvBOez!1w zj`fJ0;)6*GXvH|%z3uRq*OSXzRyeB`tAW!xHG$H)(n@u2t)_JKKBYMtc~mZ%I%mwu z_zP#4R=9qd(Wy3mu} z(UTl?E#X`JEnRSp;{^Z_G$8gq(3{^VB1)r!c3Z$+Mb1bCzHdMkBi@hyN`m(!E{*Z$ zd3u!Qp}W|Ex2;z8T02fE1#rFfYlzZ2?@(Qw=Z3l zbwn#g?~mEfPCjMvhilp{Sud*-0DT0jWMC!I9k|{rn4oq?M~}u(I8h_&-L#6hCmyJ{ zy|u@~e4W<+DtfH2X>)%_7uoza*KFL|u}u#pNqc`9(zPE<-oFotG0xwNT*hjsxE}9n zUV)%stLgGmoP9BVu2q<)^#wex-goWjdal7rwnK%vJyL!b;muF z8i$S^L`AZZ44I{e46qQ>?k?ZLm6S^RGIO|~cJA+$2f~C5!#cw+1@=dPpP#P!4J;JA z2FAi31zIWwqaTQgphwG^B%MjZ6@SJ3D=C3D2`(yR#S(*%H7h-(U^uk!0Kn|XP)eTf zMdm0@!k1c?g~^yFqD&?f49h>vILCU=+(?iAwrr~JJyyTo;e}wrnaCF#MCV8#MXM_y z_EG`vnuV6Y_`5 zdz|K}Uf;O4-@V*Qpx@s}z=pn0)8X&zLT^39{gA#85&Q4o$>bN-giCbhD6(H5 zR5>p}%)4NuN`G`b1V_V=+`wJjO&Oug{3`PP3Nu(T1k_w^&8k!on>~12xTYuD$1?K* z9M?LVxqVGHKi0}GIuP~XY0GOk>)U2bj(xFRrmf+Y`8JmupVpX^B#~%XSauiZZOddv zZ%U>A;VnLAwD)AHu>RE)Fdf}NOX~pMiUYH~k%#td$OHh zTRISq8iA)wYoQEG-YwuNEY^BnkWt{4v`n+tw={4-sCtqjR;BEFFp|tmEEw2|cqS9C z92=6^5V=j=miGkPj7_y&zP72m3lw;1Drw|yZ-HE>uB^C~hZ%T;pvfnWi;`_Rn>{O% zGO+Na0-xjb#OYU?=h@-WRJu`t5CPuP*wF`ZsHEeg90JFd#E;M{51zg8NlR}2qOR5w z8wHx+KeI!NDjA9l3gxV6ZDd?WSjA1w+7aU2$Aoowqrh2-LINI*)W08>E|0g0azD1? z1gX2C26M;|gcZn+mI3S&1doo27MDN@B56hJLwtju?mB>;`Tpf|seR=B`O7gg=4?!5 z(x}voLaH=dOLb`Fpu-c->+A^Ow`ZU?-NR*v;Uv4uEdVt+8D?;5U&QSNg-edx`IOxR zfU|ggH_Ti{)tMo(d3b1l%doK@6B`~8O{%6$O@#iePBZJm+HbS-ivX?9)R1BJClJ)^ zwt!vSOX=#jaHb&-tEB-4>nUfJ0zk)|fyvNK8ePfROxcR`c_Q!WCC3eq&a3Z?JVH)d zeQoks(RdOqNrpW@XfJ*UV2g$P9FHz%v>yN{6hHKv3ozIIoK~~Sk17X)GQIsXIEXp~ zR=W${@tnNX>2}&TF~(3BspnaBUH`r?(RN;)&iKNic3QNC5W`&+g&uFum=o-IL(P7R z=Tgdh<@l6-*uHYTw}Ad~jO9x9H(YgVCj*U(rmNSJtb5!k^-xwU^-<<4=c(&aX4q`a ztYkX7dYGE-o-Z?}@nbfhrb*kjZKzom6Or}g8>_Q28_lai(`ba;r0hAd;8?QxRKe5lROg@>XWQLnmB`Lcrs9zQ2C(6P(=`2U|B3U!`y@y@>!$Q zDNJJq%}1+uVRtLA6LKw%O=_dkcDcA}R~px=7YZ5NR6OV0p4_3_Snk?*-DKN^9!QBP zP5A4Me}zcWcF8AdGr2{lM?%7C_Tw(!MBm#~G$=I8+|0g?CQOQ5&Xd&u#4PpAwr486Ne8Zu&}1qI-PxB{-ka9I- z_~hX)^shZ1_5Vx}LUCK;bzCEZML1*#iM8elYlQ4aR-6?8nJ9*)$?28GiPvN%4D7!N z0b|ce2Q-eUGJrp0`7*7nuE3>p)rGD^3ygVOMslh79K z3WqEGNJt6PsH$ipJ{Xrs0HV{P+Ao&2LWyv(PBWEeW$!ge z6YUlva+~tsK8D1KQ40WpA+HX8{md1^0b*Hz zi^ADuZR%XlsWP{e1vL$;>PIcJhyk280or}vJayZ?3EFaV`Sp2eb6L9B@GWip*CXw= z(}NXK_liPV3$vXa4qoS)k0*k+EyIW)S|LK05!l)YctheQyCl5Buj~U*QHiJ$@_k4` zX=ZxSf=oldx)s&fodrb84=Jw+xTsQgec(Zm&LsVhFta5X>4Y@t@F9dGL5Uva4x^7h zyjfsxPCC6{zQeF!K+5ZO=Z zl=>%iOQ{0xjT$qQdTgg1AIl8Nj#K#YjPgz^^W+N`%Uq4$u|$@@V-M+H`mQ3=4EQXG z<6mYHSu)W*Xq*Ucyp!}@c`(FN`K0@fx1YK@!OSpjh?C&D)VD}~fu;)Y{qIkK_nH0{ zbM$~Qqq|Y)n0raWoD%L3wqdlW@0mD3*+ES*@#YsVKzC%J{YsViV9|m8fTLl4nni7Z zY|-9>Zku>XQ=LQFNp3)KL)C%r(A;BbfOVt!TzkRR5mWo62E%RKp(U{kWfzgYqM#$`wE6kZdgBXOzVh_(&L zjcpUMLu{|J0p1ON6SgDQhJiN^g1EX=%m2~|`$nJ*8_kV{2hNH}rr+iRs2#Q)optFpkn2ohPc{nUR>`*1h6rvC9yCa9zU&UZ zIn0g5j@AQ?H5jj)9LRSV@`KhxzyO~@^DSwo__`n6tjo@L!@OJI7xgXb8_x}0158V% zho9#O=1#=}#J;mfbPiaT>@;T0EcOHUjZ8QO&{=S9FQJW8S1KtmuH^?3!FBF|%AJTir4^6k|Zvjuh z-9@fZYA4qZw03-*5ZwINAm0M`zYym1{6kNKcGP>KFGX{JEXT~hI?#D@JhUU zu1}D6o*yV~Al@e2!hMRS{l5$7{KQVAb`;*YY7t%tF&pq-q@=%F)4rmkPPa$Wz9!`i z-}p?VzmI=-nX$x|#^`?$iP4+dPw9WtV`7?*_|QWjV+no|!)IR9DTIkMAGy(+0OmZx zSelRg&;v;Kz{9ty^!JM|&OBcN!zU5uyr+rubuvSz+Ry{S&;4&w)4sODPPxADV@_*6 zkmkSDjNS}>x{s&%s1H5#|J*r+;q!)$;q$va?d$f3f7(k)U#GPJ`pUTlGUM4>{d|gC zh=ZLD!)5F1`LBVUJ&EClAz<2~@r-4uk{(-|Q7bp{tpw@Gg&#|9Rtr3oN_AEKSnQTT zPGZwqtRPvidO~}lO6ZRZ?V)VuW`;Q-&v_K-h?9~xg!2kB#-W%L-pRKNK|sX{$~y&% zxhG*XcqO?IVTwaH_@X$vC&520m(85*%2o)$e%BsIs^WpGG0y}yAXzZY*$_t`fh_;m z)Bz#?l3;I4Xe`hmby~$h2`>*-T)Q>2ApM2BMXa@Ap2XVG)y&DtUw2p0rj(OdmcRt~ z@sE92C#Dq}ve@&Q-Ibk1-+Cne1R5%6A7`K%duL;M=pBat3@;s*G)s>{+91pXG3Vo6xDzRq=5_r!% zxpMivx{B8%N)o8|%9AE`98^p*dv>nUUrg1?9RR6@y@8TrCF%$c?VWpDH7ARY#cb8~ z8C&NSRJB^AR#gTg%V!FzS*l+2PNg~n6Ke-jC9bNe0B2Nq6Uq_}RMT4lF>tEs#2-%- zf-*wUa#cmUKQ|QU6y@xAFG*M?aYeBd2!=!t6wj`vB`+RLRLzprT0!!#e58AtMx5CS zU_7X*Ca{`9W6|n<*oQmkoJ?Y9@**V(GOyw|th)Xi(5Da9BaH@&Yqg5j>gBBDi==d)yT8lqKWNSdJyc<#HZ_68UYTBhJ;=uTAO5$GZI)}a)be-k;Y6a<-US8%D? z-MMfTzz(^RgKzGGC_awQ=>L%23(0?_{|E+$)~=;zH{pK>I8@4^<=~C_$F$C! z6EHD4b`O&Opm!D~MtAf-uzR>8!5zzqIPL{NP;Xol$E*|`L=jXk)GziPygxC!Y&1QvtDAsGE1vI`>quk;^rv9I;c!pFFb z{s(pqfrUB_4N}88QN-+D=d!PQx>!CW70voJhg7S-CDdkoBvR*fC2I2EC47aji9cC% zNKP3k9M>$8+rQr4PNFGH7>LIl^&Kg0HB(n@^`C%FdN!M=^VBBXW$grBjqJUBVD9 z!EU~iy9s6zdudyVfy69;5O^kX{$tJG23-zX{_E$10y0W=?l9A>D|e0F3PJ%x0XIq) zlIM0S>du+PGOuP}qD3K<6|thE-YS!q z+27@D{-qPdjre2@v5N78>dzcfAL5^dJmoQ{wrgybD2H09^nQvmp*1sVu+8k0iSh&o z1Qk-xC}P{uS`XU*Z}7-&@h+_E02dR50{JfLUli(HT+kd080cd{K9qc9CKgDRC3mXL zT&$38eusatdAx*p-!@Qh8Rq(;MKyL6Mb9i*_u_KE=RbzlYw06>Gx9w-DJ2ldy|W%^ z*>a^bu8XSbYWME@wXp)-wV> z>+f>(<_gMK=s9~Wsp{f{#EI1~4)QKFbCN){^h$kq(o3mX=oVT@6%{>5Ik@_bmKQK` zB4n_g6-B$dZfd?BVYwB{DP*=b`P86)Af6eSt_|uwZ zGV>IH4FTfXQ~s95z(vC@xui#r<|pV|k7&EfdP5kovF5Q3sJSv^Xl@U}DmYsMUVQ)H zMpbtbv}R&Rbv%WHfZq-2sb0~z#)eZ=mA_${pA92-@tpE7WMZ`yx=4DT+%AO3`;TCh z!!_np4f?{I9?foO=qyeaf?AYat7^GBJsK<2G^w)Wt@EzJZQ%slB#}V2X$&RkISFxN zQneXG6Mi!G#`stSWB!=`#^JRs0*N!4Po)Ui`L$H)He=$83}XBZpe~ty@~mPn`&eHk9!?6qk-zy1C&* z@5)#*Z0~^Y{t6STIA?ghjt`An1oaHyMH+FLe3sP7+Yr^Mhj*?Nx~uPL>U0?oeh?Mh2dl@0w-~d>lz%sN-fqr$t^e}tJ1V>Oit4KI zF)F$)%z~1k3?2(7s(()&8dgFxs0L$HXw#({sZ68#=&5ZR~&u}{KyH! zN=j`Fu|Bc2%HtwqaNUr=B4rP#rT-POP%2r5QG+_2~Omk1d0rV3I8j^q4ZaXXaK!p!W4EkdTMmoI+(3EiW#_Z(uYQ7u zb0&|ohutGStYzRhRam-Q_QuLRq-!N8*mlrO8K$$LWx2M?4PW3SZpfXN$Bp}AYE&C^ z-GLid`j4wt^s1RFnC?&4U&tl4X)aT9V8`8F7X3QEp6+M7djmKorFs!arMtoRD`2MD zAAW>eKAa%=C;9!PNNKWMzeuQRxI5u{?%AY;blH8UIzF$=7^} zwG3mkH8!USN2uvYD}E2|)Ly4|uTn7ESluvx9DeoOi9I0mgG0mrNr#=Qq`}_RD)Zm= zNBmSF55!DcL&i`OZ2t8zic-hyd(?X!Ei{j;9|S#|%k7gs<}K@PYLCzK(5seF?=`iW zH`(d*>YhiV&g;uy4s)E_iPLa@RN3;YA<-fLrR6+u9^IF7UH4v&gV^!EdaN7zXSk^w;v7;FZ+4ndjlX}IYR zJ&qo*Gd-%kM-zO29f(Hk>ByXpA_*z(ob@i99lY}xQX_`&7+!p{L#)}y02DGo@Z&ml zXMBz{WwellR)jM@xE3b?&!h*muaAS(>E0WTT5W`BuQ!GRt!EM7CZ@I(b; zyw=POnL@!rxA2AuyWHD6Z6n&D=ct?-lSaUjrpagNCC>Wz6RUH1-{3-YP<&Ag_ zpC`H-`il!V8dw+X7M{mXo~2i*hmlb=Cd;Fhln=h)Z(X|%`b4*XC!M;}XtuN*bgw4Y z+mD#Ks#-pp+X^pT3hoWtg`c+AH`JD-#C9BO~ z4b7gev*hAIJbPkDL+WhSKVU0E;|7RLH}3R!>xSSHVy;YY)IdX%pYernf*c{S$GlSd zU{8yRQN;$=*|A?R^jBbESV;vO=@}tBY(K#j!Woa-<<*I|c!O!EX+nBlEY!V%IZtwq z*#6ar;!IhAUmj5JH{8?XN6nCq>R|?xs~ef_*0h3I5ZBrp9eWn6&~P@KXYg2y@So zxqYNUELpKv5Vz|(9^5N_r`Gh`;I4V!n;5+4pI5sE)4kr}(|y-t^$nwLOrUAeLpP4E z5tHe$^@n_Llax3Oi8FY?rZ`8Zf}ZRdiw7Jn{@kgp`2DjX=k(u;6ZkT?_4StxNd1<# z?Z6N1@TY%L`u&=44S#90a^Mul_K=v`>KHm`oF3f)+btj;Y$h_F3KUXnaQku$U52kZ zX>dm{0qvFq`4%V0&WSE1T8;`b1}+F-=?c^u!Eoa0Q36tu z$Ah~0h_U21)Tg9yE0v;%w+4n$0Y8aRRGOC_Q-U{HIk8N>efiQkc3;)Mg3B|f7kXMx zx3r6n`yvA2GwaVpK&L`V_l`ORX}+R{dZHKlK+bG5b5cCMuQ$z~Sxk@IogOS2L>Z+M zZTkCXOblJEHDokAz#mR~++Y~C1f!aN@6#NZ{C4|-&$;HjKm@uq?tsUu0aD5wwS48x z>|~VIM7?qI@cDDezqRSl{QwC%0Q#3C?iFW}z_b+I1isGBbe^uRhN~W2AK8jDQlBX% zmFED`=aD$(=LZ~~7yJ8Wbm7bLNEP_S>hZ2<5{!~O^ipq)#4jlo@MioLD4NB+1Oo(bII8crIR~8lfBx+~R4)xrFlRjf7U3`#;>E z>%qrNIxEWpX_41m6U%d4>D~FtSzyim**A!-lMSfFcbMeu%`n|Utxo7ujKuYS9C4(6 z2S(Px5{`9x8?(t6p55Dv-X&56A*PLiq%TeAMxl!Gm=SD%af$mfW(*Z+q~E4{?+kL& z_9|Zov95&Jl6?Se5k)u=-|HVig4)sIMn0bUsg2kWY&y%dr9i=36&oJB*QwX=^*P>P zP5Jm1ND+(ti&LC4NoTVUdKkR51LqFV!z2DKP}}uvys2cTWWqU)PPU`6w0>dok`5?} zk|t&{XV4M6Mocl!W-Mi-i4R`6iOgoFB8;5#rutq?#p~o{A^~mLIm|fb7tjB zs9ISrnqj}tCBj=wbYk_U^%j0(d8=^iypL1)sKGwJ0UO)j9hL}7oH&}!-d6LcVzlZ& z77*JZ3O{S$gry_hjfe(49>f>cf16Fo8o4Y{QIrWAI`??mdD>m{om@Ur88AA6u&jS- zE7m&W?{!Dn5qT|k0~H?@ymGTB9wkkdy6=hFn!X{&8ZRaRade0E8ssI=Me*jqV@pMOLhPwk>4#(#0)7_t6UNM_+o>~ix#+7VFEQ8z_U<>``}L@-RdMquo4P3#+&$xS z{^zM3C*pSfiC0@xb!qb~{2Lk%Ee2SmK03ExIK8Gh$`K4zkW?_7V8spK#{VYVPMY1{ zR+KqXfk96?Ov1i_U8_UBL+nHQLpy_{li1so9(#(dlF0%B zuPZWNw6VdW8>9|0FX5;?@0%`jQeKBNUP5U>+Lu6f<$s}Rc5ENI>Y~*_RZhgRMtozI z@v&sTvmgwx%KvP`w7lgZ$3^}NlH27}k244+bfjj`vWE%w_(R)fpW#@CA2L9cd9ZAiw`ZQf2vp9jXo49RzzV; z5aUN>fJ!~C4U#JPxOZ+01ZfP867jCkrbD0{#?VkBTFwY#vc`(D$t4dDi#${qi_dZF zbP#z;PH@%t44;Hd=rBWwvu~D%;t9qHJ8w=#9cKkv4F4CLuDCQW%Ln6Xg4z#;u~~p( zEO0TMihfY$2D36y#@OV|VPTMv-GP_5RI2c#Vy;Y@LPu!3Sm`uG=^b6GJ?>>!%F8!4 zCci~hov17NO~KmK`lx1Pvs1orE1c=U!tR&lE6WkR=n7o&DU_3xX_B8M$SF};mOC*r ziK%)T2LfrItlJ2AFOCt?drNXh%ws3c(L6;uYTw5Lp%p~dwESFkUsDI7jdXMxueJI= zmy@`R-oSqls*l%d8newEm2wu>#uD~51;Pdb+KYBj$^gGY)JT|HG`D@mygNCf5 zqJhNFN&MIGw+b?vmIdW1(gVX$6ed?MI1g#_{e|T!LNyAdLl%n}Zp@bpX%tcJQ{98o zrwmG)4B`J4`DH9@t&O!UH`UOWIY%c(AH}z|(QRim5ga|ZOP3wjBAlp0@Xd)nVa^*= z=Zd1OaGhuSSco#u=v!&f?!#WfUXSLsY6qZsAC>^19*!oAOjm1_c|z z_nnEfKyEoz161?v4zk><5=ob1D`5r=;fg!8M#EZf>$M1YI~H>e6!bx-OBU%17Dwu5 z(dyQEWY$u)+LtRYm3J>niWup5ZKo>VR1IW7#{1(3yv{YfQ*m!{oecM;5FaZ%LqdXB zh(05kkF@Je8<*Pb9lWP!F#1Elr9rxjg9|g3qWUVA4T)kh{~6dgnFx7PX@qP1tt72f z_4qs*7GumUacopo@^bUq&2Y@DwG6P-G#D$cYtpB0DDJ?=>T{K1KGyQm2XY+9UT8PxqM7W{ zuzcAUGREjjQRX5&w^%c2VigHVlt>g&tz2D{P;0xd#dRd71$US7;!NLBpu|d(BNlkF z!TqpdWuWbubnohkxplgBO*;!a*8+Gsz*vaN+K`~#ks#-kx`+gQ?$ccfhod#H;w?v& zWA<~p+Czs39sV!L);FPB7ap(8U$2dW^vAUk7May1T((P;R6O2VE^4X9Hqiwbf{bEw z@typUr1~}@Cb7;rO0+#b5VJG)Y*EbCg!kXp$`xzOqQl^h)KB<63+WtvU3*KT(||!M z+$8Qro)?s7ib16%NOk=z%3+zzR@3O$Bzj{Op8h%w7!JIW#o-3TDtcToQ-ZJai3_|D zX`wQfU>Vi`5&p0FPQa8BVF9aefCwp z=MpvUYLZdb)I?0CU{&D~tG*TuaiE>NJVqCZtW8=6iHJ=aXT485blxU?RaZ0;XMu%V ze+n>c#dbANQ45_(*F+)*^O2uDDAlT{2_9bq57OFlALXt&Fa17D-s(m(bJbW%a0N;jDgK zG)hz$0BFV{X;Rd?dnRuf9S&TQgtGoxI8dJ-b^KSv-~-bscD)>}ccE3E#)L7FU%mwR z5u+wOFv8&~CfElV1}MzCtZ=Gv0&%V8q?NDcnXKNxvGJQnm%d@F)zAZjO5$n|URvy; z`I+o?n#^{Zl-|HYeC;|7+5GSae-gbn$GcOhZPO076$6ZZ3XSO?>HAE9hEF**huQz5jklnd0i|PeL z`K9!Zv{1euJjGP9a-9kOoBYK(XrN@R-%P<;F>Jb@5A8s(G^1Z9-a;&|@fnqtIcU@5 zw|#638+S>bUPpLjILW)})mzOezwW>Ro2RJ*p9&4XcvSm-@h^w#D0UUonsqb}V}rE< z&Unzg|4Hc&iVG7sz1iw-3*;liY2G}9KX#m(Hl_Xh3B4B2t=#Z-Tq>zV7j|c@MqZV9 zvo>rPR^W8+Q^tvxs;P#79bwPLZ>9~#OeHm@x!P~Esl)pYZ``7G<-D=eV0aJqBHFZg z{|vp_|Be0o%e)o^^pw3yTcL`;>R0lL38D_L3TObc+@0WEb?Mi=d*#0JLwFy@yaVIj zxo`2po7|tZ4Al;UGk0N+7G1<};Kb2akWp94#5H^cO6j`OE4_Yv`_y%vu?4PSIzaH~ zy?%V@d?lNA-LrH#nsW^p?4279p_C3<-OBM1^@jyv=bTj6{Tyw=1_jQS?@2{x6zD;Q zI|Tcslh04MGj9oD6Fa`o>d7sdIF4bmO(9!9jMx{Elw=;&d%;b(&o&s+`T~%f)|}czXzE0-wkHNeRX7 zQThAlAw2|`u_8tK!5F{_di&G`P-ajgpTb4m%zy8{2MB19Q9}h)gzpMKpNHs1gJNZ^?=v?NS7u%C z>~aLP+U4KC_X7fnzyxUrSj99TTOyAByx_oqfP#Vmb%}UfXu@I<5-`8&f&idfc7$mC z4|!t4*aM9!0!s@#pJUk7dqI)j->_HzfDs$&1#KU`?Bv`d8(FI91g*V;T4!JpDP&~BJIUo z3bRkPUQk+EUS3+%u9 zj2kzQF<2=6G||CqNK!A zQsQtV9JTDbK*Cc}*uS60-6s%mPgDu>nhpsLTvnqgH? zQ`N7gYU;oNJhTtSFX_|A-=~k$m2{3~--SzjeToN{6v3=BM=}PBW!VtvD6&^&_Q4ST z3>h+f_>h5Ph6iCt<+$NvhQRP4)x(FJIdljQt;G0!Dl0?%D&3wk_jvYQv`?_IbXaLA z%$qxoF<3#C4UvvvEe2*E&<7BV7%^tdh#?ckgki+MsbeOLfH5P6ju|m`_;4ON5aU-2 z9GF)%(Ce-6PGjH2D#8Q%j>h)e4%38Q?FoPMwVgsS>_kwlgaCdUV7vK(557T@K_fsEu?)>bG_%pHLvt@01CPgimY}&B%`P-Y zct}d{wcS>UW%sq+#I5#o&#U0hIl6OJBX_o+(V*MoC+l|ISUuVxJzC#>x_6)9x?R*; zx1EI*EzCAsD|Qd_3SO+F7Yn`=kaqqCx=qKp6 z9IqLVjCe-)N`cZ>w1p?WCW!Ysu)wVdeBqXhlP@ByVEQZ-Rzq5}GD7OVJpR zdJCzypaso(G#k;hqCqZ0RMBYkMKmv?c?-(Xq|i)4Bh%N~Fq=;LYFlD>beNaELZ3q+ z5T*O*^W1)!KFjTw=%2a$0@@L@U!>2rMWQgwg5iMQsA#KbmtnX>|I}XKjAn*8=q_x# zC}J6!S~L^U%to^gje+i>`EB!}PE6!!co9{TC~bo;xcxBPFGK3wXeu!R8Bk-87&HVa zgnPAn6Ev0Ba3?w$ByPD4DF%t_*CNFraoIIUF-V+u5mF2i^Ug(zL1N}?q!=V7PD2Wz zliu@GK~c17;$or=v(t;Ql`h6sx)@vOVi4)Y?1djhR<_@^m6TwM-IYofmqc53kd~dK zWeRDzpR~*;Emx72Ye-8CX*r9uBuPt{v_wctinKgU24YjRkW|Mh&gzt(w7f`KHj|cS z(vl!8g`}l`v}mL%)k$M*V=K8el3VS=*q}pu$WW9%b{fMLjUms+P$uj`d=*WGyHXfQ z%hw|ZBFxTjFR9hteFjx588(JKgC9PF-S7;2ghs>;dIs6(8BF9Eb|{8ei)J>O-Dti+ zlR;y^*!fuSbsX)8WoT;A%tmuHns3k;cyYc#Lt#m_h)?o@m1PS(k-6wI__BkD7>%X! z)UcXV#|Z1f#2z6NBbf-T;`1vw9kQd7ShxMl`u)GGU>atkx6pMsfko;1ti7)7hrDPf zxuY%dbaa@9{01T-aviCL1Sv#&AT)D#e+bLW?gwG|INB9$;Tiauy)Dr@x`Wu*SKFdL zgbzf&2zOE>4~3tOzO8kNq%Hba^m%++^v&>%(HF`(W%SvZ=p<6@ zV8C5%(W{uXEqY~mbaZi;2h7(4&T2+i${wAPm>C^|>D7hLj;5M1&27=z@LACsJyw7A z&9-Pc79pvVC0N+vFwZm|;R#N!>Ld$Oy^R};^~Qb-Ft<`%_7F2JJTR3q$P4n}En^l>$G~=Eu~O$P`Y4R+li2j+;81jBDLJ z7sj2fwH})q?G2BjdyH4WER2V1lR_S^&}8EAM8e{2X)YL9x22$f z$MI><%;Plsw4QNZEW|j4g*=Y81zzNFUi7xGIITl@lyDfMM8Z4@2|}1h36mg?GUH^F zvTT$ayQAF5a}vnOIAJ}Gb$>37bw9>Q{-@9U;mIUvAKWl^7TbJoijSO+rm6L&ix&D@ zTh7+BEpr>PyV8lKv*#{k_MG{x4e|MPt#jjb+LpnyekOPp3mzP=+XAyjPOINCD>c8a zZE$Mv$oQPPhW62u`c<8h*NxqI^_%oFiA`dO^<#OBuKJnaDi%DN~66Ym>_!u5D0rDK_C>>OuzJ% zWjA~;XV}JDCe605GF`aRH}iWB7DNRUL@QVkZ5-`jLsY?z=s-NdHWd(E;6!wT3(dR1rJj56(wm(2VXozvd?{Re# zLo9@R#028Mpa|lKy`TWG7!rsj9FtI#IR?F<7h);LK2U;K21&$nC`Igx_)n;SK8Tf2 zhS(3vGe@C6#{tk6u?i{?2SUHhcTml75cEeJ%&`UrAP#{l#Gx<{u@aVFwnI1?r#JqxBH zHo`Q-vpCL%>6yPn6U;!I17{$fjrb+Zg$BfVFcWb;oQZf2%*uQL3pg%>M#M#MR^||# z3r$Eb=6D{=K|CMMM*j<7F5(i7OJQE-b65uR5tqXP#Ac2wU}5GUTnLL0FM@LsFGlX4rZDMTpmPTnQIv zK7mzm3F2zF6!8YQ4Dm*|Jo7QEfh!Pif-BKyE#k*;GhBsu3tWx34z595kGLOhg=-OS zgX<7~1=nXjf?spI9abW4fYpeIvZ5r4<=9=HYZ zURZ~?5!NH#hxh^954R#d0JkAN2){yn2!5UU8$1lRXWoZLU<2YN_zmKt93O)_5Fdv- z5ubqHB5vmRd$=3%N%$RNE8K&)1@S#-gL@I%VIyJ(+@Ef6S9C0^1f%qrbjJOAWkN6BcnRyHLLM!5*VN2$( z@GQsYpbhbPXwSR}FF*(4i?9{(CD?}eGCYO24>}QFf$fN|a(oSTApQk*BEAlP$h-k> zAifT7!XFX;3cC>Bg584$znrhonu znSSs;km(1yW%}|{WcsJSAk#nPGX2w^$n;ORO#h^ZO#g(-^iQ}<|D=aZ|M({|eLt7! z`?*Zt|36Tszx#hkroZ{WGW~yL`u{kY{y$w$|DP+$~9)Ysl`N1 zotfP!kJaAK^dS+%8cI@DM?Vr!mObK6PEd~~YsPogs9nj%nywnCtx@sqszD$QXLU6* zealHQ=@R-^dW1@j>nrw^e$aPcxzJ90M~&TJXk zmf63R(-j9tzoG=bm0q9MqtdQxu&wfk6kT@B&Y9z1O;0BK$;UglZ&)+)^&h+5`#Sw? zn*D0+vGj-JIvfXPn6T9>fbnrdCz+H=5TS-rVkR|UrUE*^Fwi(?BFgY3CS635sAZ@gR=WfoBsM@w~(is)i1B8A1mfe&XADD9{ z<~xjZ(sSwgAVBX_U@2WH(D7tEW*G+>l$K(IfVlK##uEn`)q_wr{&3%NScaACAM*^O z#iVoVR@TQm5LRR91t?7UDJyJ^Uc@Is+=$^DMPABdjg8n&hxJ13*tc&VKN!h;POGth z1em%Vgv^I+?rPeZ`7ouqtA8U9D%>kPDNx}eAa0CJ*>9lD!XcoCu-_iV%*FQ0Fy$Kc z=wVgwgVj=>WaE|UUe*`Mq=!@z^62_>eL(u^I728jwLi71r3Xa+Wv%&~aCav+S~Iy3t@7a{Rw z%E8zO!is`uW1NJ(M^XX1-7pQA_f&Hg`-hoY1vp>649iTM5Tab2dN<|LGOZP zNo>UIPAxjvD23WuUo~;C5!nchI1cy|i#rSkV}I=QO8R5Rus5dt=997&;^jlHi2i=` zi?gwmHP~`SY=sE*ZwF)MPzP%PnK3@AzOfU17{Ps5BcRXGc1CP5unVs#lS8%oT`t3P zD+Y8p9Gv(%Wl~k7B5o;yDWrTXLL|aMBVikcL=;xrNGE+drBJiar$toyC7;5jVG$8ukeApzS(r=<=5U# zw!6On%NxhWJhJEhS&_|~ht$OxUy^j^0F{3+IX{xA?WHj)ZDNO9b3wJY|B*<9?2gvCg4SSq&0 zt%^T@a$Qr|Xw!@do+d_5lqL7Ef9wTkb!{z5$HUn9ho4oSbyll;lNHRc&hI5zz1ES| zRaSAN;|#||Az_O5JoQ}nJnx0pOWmuiYuq=69V>L>5NvhsAd)6(3z>fA+Z-5s$ne}Bn~i_e*N?fUegxBiqN*ZJ>U z-Fod6_qp$(ms~mb+Uu{^wm!F@ZQks=`$Yb5%kK0)53;`|5k!flqP}Cs_I+wQE?bYM zo?z``HE_n^BHX~yq>FQo5*OzNjwW5Sb2Q3a;Bhp{T-3?BKWku_qr}m`(Zo@o7M%6U zLgg;yQRM|iI$b#3`fE{e;_wE=AQ&aHMKI!gVzs^~h;Bg;1uIa+DjJ2S>C^15m5_}o zGrL8vDDa|L?4;*BB}wK~UbLS%H*p|wkZT=CeBHtN)!a#{QdVOsKi<#S66MZ9 zSgh`TKvk+yfwGU;H%J`V#=fCjZJlH-@9(d0vf%T^Q8uh=)PpLYHq@gv$7&qa)m*7t z-6tucJh0pC*xmd<(uz~Dvl^xCn<-0WwUA$0Er@w}H7tn+WCo0raw{oIwbC-FT1h3U zmHaT;rPW+^G?1S;?}lW|5$jJX9hIKALvRqfq3e43yI(!~Y)863nf;Kk?ZjyhrSC&g zc6--iCMTw6J{M!s!w?}SQZ5(oPA5kT7aAru#aI}1Qi!B{jB+V_XmJ$$yz6rlnjVQ! zE`|3cM~k2LH;cqO`&22k9SF9zl{GI3+sqMqVR0ZF9M8b}NdTl#3N4W!s3_sLh`8kZhrzvWw&mCW>%+NjX^Y9JAFW z>ecExRaF09go>XF6+Z#L>ZkskQ1P$VoI6Q>u#?+yx1FPjeNeNjF_!PuvAF>QJwx1l7_mnp%WLz;%Oh7>c9=FkeB4+v@Ja?XV zZO7)dXRPh@=q>c!uBRqmf9q}{uekZ>^IfDxU31gk`|oO-SnH+#cp`n#tn{(JJa=o` zekM`IBY$~t9?F9fDr^TQs{gEQ9GzK0%e9uABZ_it6z^uE80Rk|M;m7*9-B)Jrm2{D z+p5aRIW1}@Q8JqdB-AUCvJ$HmwV05UA8}jF5dwuO`zpX#NR9YZ##25%qxlrB#UNYp z>FmvY>YsDG)p%Il+sJsUba8;x8B?CRK%F+zIZazE%rnlD&vnkzR>&8ISIVoxZ^>_Z z9Y&3HY>_TA46HIZoiQrL1B@)7NQ-MR7T{o|oMfd~&JcNn>5iRjO3Rg*un7mY7Ur6$ zFvnSi-JDg}ta8r6RhNna4l8)%DW?0X>wBYQ8<=uM_^6I>!5rZtHquF|Q~9;N*}f&d zt9_zRWkEjPiasy<*5_p<^>xyM_T+P%@3>CP_^l`F9o8o`KAB-NW%-OnWfNLYDFx_d z#8n&4X{g+=nas&Fop?St)G9Ww5qA$Z6n<>?_a3`=#<1yU(_uRobaY+(+Vvl&5B%=N zLz_S7s+xGqgyj$3fB9vPiBoOomX9wV`t{%DHl_dd`kKR6k#Xb-^5~y7?K$y5<6{k- z_uTR1lh~qjaBlNTkAM}HrfhqyM8tno7EL&LFhNsJiD*)+&A5bT4LOnPd;$&H{UGc*K$5wI%5Ddon z0H?ERj<9uY`tY~`_U*#8-`yx4-@Nwrv@`u<=lh$Lf_$4=HapS#OAmge-l;%?cUI2ql-Iys+@q;R)8$lb-cfmh1YWmFjt zNO06YXN;52C3(2p9pvufoMh*pbB(%;2PFI_#e9bXp-3M#vEsUgPVdvl8H*UCUJ<7? z!ZZrXriG*1*dR0|?Mz0nv1!jOG#J4H?S3J*ks#z^3Eg8_DDWsOx`(Ie;bYp99n(Ev zUv6k853|jdkiP1AiWkI?D~Lfa$bk;FoV4dA{%rot4~Y}0eCElrWnX&utmEM|Jz2k8 zW|$W#%N8JKm{_Um?lBt|r30IOU#1--~%W~yH>t(*1U=3L-u9B~@ zT(7LM-t2qX@vO_4kK&^(tOc2+X=ThR)e=mm1d25UB7RV?7#sVL9xa^{BaxgKi7+w3 zg=nOCmx-8m&;^jpMoQ))CA(!!ve}-}P}bN9*i}2VcamFEfeJqtFn%sz{9M5Jn_C4U zbkYSWPXP}s;DH4^u%OwKTZn6(l!toO_kAumxA6&$AsbsgoY^*PnA?ka_wWA*nGGhm9AA{0>`)!2BtJo0)x>+4K<=rLkyys)}S&2lE5YC!GX2sWC>6sqrYOWhM4S!M3~ z+zL=9MAoOilUlk#)OGc2q~p4HkPQq)HomxelJj{01lBlt*?&IgSz8j2d2X*gW+upsaCEZ5GAJ=M`I<7S`d1ifwXcuJTj?59Z zLCwZO16jVT@fXJdK5;zCSt#e_tW!6?wmztH**9`E|?eeaTg+&L;gTq*51K8oy2 z*U_0|!}g1BzKP9B8*m2w0{P3qv^lbJJBZj*#TJWUx+so{&xoHBZ#G?TGAs&SC@nQL zTdtF?vlxoJCc$4+67lAla`RI6I)N`PLVPh`aycUC^rmU6Mw z3KL40rKXr!8VcjUB9?*@?2YB=p*WdY?1;uUA+_ud-DVvEHI+&y03CeWXgTS{nf)~LbdB-=_$I@r+w(RU2kUSy>s)?+)s zM&l$_q3tda3!G1uNH`(ekIY>0gl{*9<&u+^B2sNxx6dKPxd} z=Adb}uT5{3b~J3Cf7Zj5MLSy-Y-#E%OmTS689#PO@sAsg%D}~=rd-N*m(Iz2E?p$O zfqd}I*17cDJWBLF<#j!jn$0K;Dy(y1DXhqAf$Q_u!(Gzj!b8^WLWlJ^>uYcz@4GyQ z&6(%O%M(fry&NTBEjrpd!+nNlMqr_|IPY@jP0qW7J8gG`H<1VFCdXfGE^tFob*n*< zZC$tZs^*(jBYRb=b|7L16l{1htqHW_DM2g&)-Pz!MiH={OZw9XPtj=)dQbU+V`6u|3of${WCIs%l?;2 zH{N!C`kPyyPJgjxCz~7YLHRus$Cn-QNFe3ZqGW`uj|Ybuv4iZ(aj-AP!9K>p`Pqt@ zNR;p6nK)yb%#15de(u8=nv2w6G*9L2q4N1&<@2)oA2am7at!@_j-ewz!_Zl0<0)L+ zw|vBvz43gwV(Z6$ug@mogAl(6H>vDJLI8FMdU2mMVvxG0mPg>S_l!!1%!u{gAju+ zViQmUHW892DlEv2q=MW?DqtgtuYC(NLBN(MYT`S72Y5}nmXh82dyn@Y*HS83+H`qK z5h=>!O_s--ERQ!?UP3byGZ!^xehg&}B<7xZil`Z{HXh5ig~}U)t)RPCCD-vCoxd($ z+A@Cf^%HNncW?UpmN`|#!TRz+lzRDW2h1*KnZ1Wp^kxUJW+j?+lC{aY)G7{c@FyFWbss8rOG&-+lQnwZ^Mh4f z+i+1yaAL&z>%Ww4>``I`jA_+TJxDOS%i2?;315f#jzGl-Eb#NR50~ z)|ge(f8fo9#rr{D)l-R8SKx@jKXxzzgMUo%c3{GQvQyIbfkXIdX?tZq-7YQH?Ow&Y z9WT`Fyohf5gZk-eiB;{VN$aI2B^*kqbFYJq&Tt6S@3o9}$)&ONL>ckN+n zEw&tLveFdBbJUfJ2>DgjvT0C}xxZn6d9?WqVU_TtSvG-kK4Q3onGeuVQNSK^DHbJ zBAWt%Avn5BW|M4|1X0u^vs;qTg-mV0^@+i328oIU`&0(mEKmypv6Bu;*~=xeQEHWT zOZz2JI!$JNmU1J}aJ|uL6pWp8RVwmxT>jtHUB5j!D{W#+nB>XHy$ctoY=O!oNe$!R zni^(Mda-p3JN!i3&rdy!vRWh8;Ht%stCkS1TG;a8oq-Mdt_yP_mjtrLmW1??RLn^AAt~@K5O@~||FMd2>?i7$ z;pfz?gN?dIz~)Uhb7G-iM^684Gb`U600ujjPZfMAMJ};m0Vv1Z!HbLojLS3GW_9qH z4M!dl61!~Bi&2V`1xt2CFae>m$zlW9M9mfhtARyjrM6(Hx3O3j71-HYwr=0&>UKh} z+sO?$9G|;)tFOJbn;m~9lX{mzb`vfYHS)o3;3#kuIZ7Pa9FU4LigJYmXG1m<+fMEp zm^m7An+oiXMJmeAZzZB=W@kS;2T2it*igNbQO{!)XQ^?5C-O9%0ZvfqjFdI2*%)#i z!jlEIm6tqPc9iS6$ohI^8hc0{oo7R-tAW~OHx0?+ManAWd2C8$tTNUv6pMw{-nM$- zOz|S?CAQU8*+M0`+B(2Ck&YAUj464%b-2xZ2fb6+VB8>Y5*{%coYZc!l}i-m1eF!5 zwOo>sl$9y=DI`TGm1Xvayx1SBQdvJWIa{36xr1&3E9u)NX>up&oAN3qGvBt+H)+gT zO1auXEIY8mHe$h8v=c2m;Y^sFK91?T$)MSnszmLiGoI3WAV7H_i zYy4e|E&2Wix`I8N2O7bTx*osomV1NhVZQIN`bxg-fmYvgIJv#?2T(FUBA31eH1ihU z-5A%ZU|26MM68+bx7f_gH+!P`=C)Y1t#^!{s&1>QwpCPda%(C2W=~F&4a=7`;!wv) zgAIKgGQ{f}P(@-m+mbkO+(8P+ndROJ!q(x50VLqtiFfYwukaDx5)UD%4`p`rjB$eRfqfxUE%`(Z-WLauyu}BuV zTa8z^8qf6`D;DZM)91yXsyN;He2-~3*_f>1o!q$W=)dbUPPSQFUAu#PL)umw~~SJd$VUvyh$hf}JOBPcROU^-Wu@0UCNpxIlRB z$>myZW2CXWx5syH$FqH?Wzg8zxXh(OtU_eVa=w75QlYW}9Q|k_o)2@$4P?MeWYpsw z>214SP4C$BJjr|eeGB>IPbN)WL0k}+W+HAAt zmiBxlfU7dTF(VQT7291^KNVSI{iZW26Fk%dm2ygdwu#wX@|`@f7|&_ zr@OkI5XjoAXIWVR^#YUV1!ZV}i$md5z6Tc)Wb2M_qCGl>~ zT~__TNZ%Ocge6B{!+z544E96&NPB)!KL>N?74=iIwmoZO_&e=+2|XO+t67_cr^X^t zXge)@nl{xkD|~)|mGBT2|-ECllEW3=< zyOpmW`|*{ef$tZ;bCX{q)SyJ<~5Z zqck?|V{+fh4HJIzK)PJoG4c6J?|v(1Cwv^=k)Oj9nKBl~tFHUbu?QsX+;vpw5E>Vtz z<6L2`*et9N#loWgLUnkAFxGf_-pFWO!Kk9CLW6Nu-Wk1ablKvJc~~D8%xUyrO4g*hUTBZ?Rkj$%|VYodS_2t|bh?ET_(g(J&mYBS=~3(vEhYh7$R$35SF zspT^3W%eu83k#YHR|#t@H(J-&Z&p_pTvvFTb%T9_Cz4g6OJfOVC=oOzib(>XIOr5B z`X*pL`=72#FAd!oqM<^swKP&xND3vd#N?lT+!iS{MIv5-3-_eMS>32_LNTk6-&HI- ztiPdDX<>oQYLQ~$yhunk8bpB_NMS)f`Wd81s5F>jtiBFq>0vLF^4&PDt*AsJlSmU; zO4btt=_IWwTPe$uWsN01&6GQ!Hs!{ssr!K1lz?JV%%(e=jj}rjM^jOjW^u40#`m&g zd^E=R88fy3NjRC(#3DO$!_e7%2I!o|Mr)vNc3-Y>{DCC9+mby7$jx)vvjDZLF?oQ& zQPv7L&e+i$JBmTw1=zLm+(UXg_@HxDMQCM3c9*)Kh#y4rdz{(x1CQJ16@7eA8F21Q z%zDZ?`}r%EJT`UGtikE?CNElW)wjQX;D=Sx4*TXuTkoqLNZze)xop*szk4qI-JRrZ z^@5wv7~WhrazWfTCt3Bt{3U;yx9H_-Y&YF<&6yJ`D;M_~y!E0BUu|B&e<@Oq^XLwK zM*LwaVBqr0$kD)ei;TatXJp{JMaG}mGjcFWq7m$U;D5s3)JdD$HT_KVDT7968M_5Q z$X1fwUpthtaQP@_h2yum5VXqhKY>?aPEM~SD& z7ug?{4%v+gPzSs2ZZNrXGlx6Jl}H@h#%k|Q(41yYyESvFX7`Err~2@fYNO^Qns<_y zHhGtNTfBnzXA}&+XPjF$n6>OxRec&U=lIv$J&l;f?0Qw7M$FyQh`G_j)bo?mh`wJl zUPV2vr>1dO|2<6-8cC%itGM+?MOeQE;Sihl%uE0H<}2yrOZSZ4eC1o)q#Y-=e2_lz zz%9i3g)s3%+peu=?;&n%RTJtKqu9-1ayaFZdUKZH0#)>9_iDR$Z;x{VdZ@MC`?=rh z%%*RaP+NBBf7y|}^7o~F-^k2wp|EWReHI`E3@Jx*Fq`J{a~M?vJx4omfzY6r3%U1rg~M+reqzxmr}}n9GWyqqUE^MUN7yIjz|(Nw{K{-G;PwC+soKb zR+#at`pZjC6b;>x;G-;Z0re~fsPg(3)A+1m@_t+&5+g5VQDB&pQwAxzE zS3W+2GWLiFq9jp+5!u6x)itrd{H?fMPN&D~^ZRiHSko?nUt_l7bTB(L(<7TSkSN79 zyqo`hC|Qnp{BDoO?^H~th{uVf)1lbyn(A5kgWWPtUJ5&^6SR_gCt9HA|B+C@b z-s&bT9OK%nPK_r8BI{iI%I9#0k3tL&!bp%b)wGAUM!W zd|$pkF&-{G_)PkOr_(PL8GY{b3n=+&e{=ia3xp55g6XfmyQxF?{qa#^<63S0=pP?| ztOOj3{OuC7?Vx6N&Fp;Fj8P7LG0N6`iAujY&%~iF^CbO}75BAv-^_G%->gqno>W*- ztqBmXg%&%DT?0v#Fi;+78fYD4>+h^`nVl|n7~y1K%bq}3vv$vuiPU-QXhO>!UR+Er zw$OxFZ0uzzu_c@X#6j{P3rlf~JWXtrXIW<2ra2do`Qo|qV#^}ieCLJYWinGmFLqw+ zS|zSAt})*(cFIpVpA}z_-xlAI-?hEv{9HUFAF>^E_BQa}=_wA>{k#k@cuJ*P)b+QK|u`6-70rd^3r$751t=iW&{N1&KE)7g>0FywiR!tGyiXxzx76eLNR;!|bT;?Gj91xn4 zQmE2NW6q+&ViekvmgnLq|%Tq`s;ODn?3~H3+ypdtyu?Cnp?b!=%yw zS>SNv;YK9*NNGG}sH~Pw85QhyK0bCk8wfS>-kz2|5cW23X{+xBbh9*Hv{~5ZSD>1( z1)D!q?PPajLe(za64`~>P_>*Ns%F1NZVR)m(A}wMxY~t_I{}T==JnOMoL=7$8Q0J? zf{2ubZK(BeVw%osi;@=`LLe_zV=*&|GD>mz(9h*VKSoicP90=DM;)#%|6L)jBqwK$ z-0(BesH`Wc3b((iAR5n5b7cawXN;t)%i_pzvFX3P;jI4>_*SIHH(UM^5C_k>k0=$G7_L z5Cl74P;wnv`58@Fzb$DRB#49YqtDx&J~q@oPuY+V0|){G6Wa^%^HKOtYA_OFF9atJ zKDoqPW)l~Zg@%Qe4-Fz?YlCbw84Mo`wuO_^{?ap$%z zQWmb7D_M2?ldOVH{oAazET3~Nl4;?%F4C=oObf3;nxBH;Jg~Y?GAX0-A@R$Mj9SgE zFscaST74+7!|d{+te6yWN9HI9nWOwRFJDd(zE5G|M|)_Hkw4rLU|SFkKg(n}o&O|X zIq2Z7m%bvgNh61!MZ%wSJw?wK#-~SJaYgfb^5lv3u3w?d&U~C+glmJpg8;ZzCAIAL z*bop$4CB89&ixi!z;#4ad^El2+H2V^+G&|XVptpsy`YM2O7%8bO(g+qu%y^pQc`Ul z;He4?Dj8eSXl*Px*Se^rseFxfRqvhiuz2rJCVfVAywSL2Nr}N|m*#1mrPh?co7xgE5m-MR^t9y?X$Ml{dHzd!I7bP!J zRxAHcV_yPZRduet*PiF;%tLZcPR>a}P9#812*GH!1A-!xC^bqH6;LK6h=_^`*8!(0 zwZBz6fx~sGEde4R)l2?Xd0K}byjA;@7NytNqLs?;O}Ppr`Pcf^-r)q>=lLPoXPt4M zto5yLc)#!c_HXrXjBkogOUXpM-Iy=M##M#mSB|=26gw*2XttY=m>bO@Q`l&3Hor7E zbGXFU)O{}g=K(vv->V`#V-t$mO_7wkn>Az?nULi$%`_L=$-d+nad7tIkA>4|i5dRt z%%oa%T$AY+B9Jc*!uNaYI}LFIWOc6m~SL(2K{G-3c? z@=qj$#3T4#`5<7-?_@9c%vz5yHnTNjYBtvhtppMz(SOa(p$~&WKVZQ%V_P8?QdwOZ z)7sn0ZfZrXG4cm2As-!sBIsBlo@)dwp%EO*MsPSA^Si~pBAXH0MOF+$tP`d*Mu?bh;aZ8fsP;oDlD1+B0@!wICjFxsu039r)Wd5Xk&$W6oR(l_zjTa;-{aIn60na zet4kRN7>cG@9m;MO+YAR0Q&>F7t>WBxB{RKE%3j&R7;s&XR-i8L?e-KG*(^1iIT}i zpdcQf+6>O}=qT^OF77qNYjr-FZtzcl`Q&_uu!@Ok0WNUQ5TWe&dF@h>a>9fNm(%AHMF&B4Qd;Az?o zV2>sm({Qv!LV;y_5!!$@p>A{p@fp;PW*`oc$eQdMMD|n#k#Gc}hX8*-E)hKhqDTC# z9wOWD(kS_QC=TUuP$G>Im6KaWUvs zD)-Zz#94$E7bWY{6 zc)w)cX(3BPr1)q#xrmpW)}(lv*N_>JWHMnAOqc|8A;~5aCc(yf_x(K-`Fo>t9Qh@% zz+^>7ne;iKb7HeYvt#o@^I|_@f5iRJc-DS4smn$}UCb`x77I7&%Z;_hbNVaFcJ&oq zkLvg8pRk;nyVAPBy3^t;M097(7^sMvhriYaW)pLeIfA1Ida179JErkh4+PR80tvEW zhUM-DHmEITz>8OB|mA$R-FX0AU z-w7S~*%key(tSlx^60cS+MRv)2LM$pN&)DAVw5FVatUfW44M*|;@Y-UeEEz0ga5PQ zv-^Mkr_AQWotNGB(zACjei+>o+p!l_AoXR$uG{=nYRS@H|L((I{TtRf=V4j;nDQ9V zZf}js@)%y_*DFB^cdEq zX|MFLG$ipvK4wUqR0;bB?4DFf_Z7$tI1*A4WF}Ernpvi4NO%B%KM2RdN0a*}A7;-Knlf!-_*)ELr!F7N)ZZ+08!u z7?y#f5GDrDG%4ov`Rr{2U2Q)7&F8&9yvs580~y$L=+rqxSkN)@uf=~h}r z5w;i(@tnx08^b6(6oNSndAv_Xfys>N)Wzf(j(rqCWzM#-6G~8_Q%q+{8yJ-E$nH&e z2Hw4Xcvc6YS&r;yroj);zVfrmVLz+WNUVO{I#k5_aYd6; z-Q3h%Y9Z+@tfz^Zj*wOqTQ_bDCGT2w;oQ`OaTlGtcQ5zDhq{)O&iju06ZO1#S3h)O zA>Mgs56m^E6EeH)w_Mh1#UBs8l2>8nl{lLv^b*K74_8UU-glsdk-q zO#Mg199`W|duH{SwHMZIXxP*sjjtL%s=eVn?YycfXNnncYEHefS@mi*r*~j2WhP~ahg>>3dC*{(*8jUm+n#!97 zeHCykzxHY&@lc4z_r;JM^J4R2Yh!!^UJ~|_2GGVa(8jT0Z5#t_Oz8FS=;u@$lUEVC zy|0a9v}h6r$AZ0n%)1UGXQhP-OfCb$lmTJNfG}n9yRE&}$JUU=XRLN>2KF@IaSL>{ z1$)#=5?QfwK%`ESGt)j97U*UxQEXUQMbvX~`pIqDqGO&jnR^#a{P;6EQ(;Ds{-hYv*&c6@hFLpkzty*|n zc4ITY4<5*JJG`f$_^DvLh^7F~8Bf^a>wIEjnKP)5@^i^U4B9nHziU7yM)#nv0BpVL!Ypri6>=b?DB zeyfU!m$py~Vba~pdmRpNjC7)#L-FF%BrQGR6#fMm2=LG+xJ<)M=VsEqZgoZh8WvQm z^DP1ZmKMPTf?ouC45G+T>_dJ+W;CuOKnGl|G>Ry4)kK1UDVPgWy6T;eL@@K7!Re~- zVQ4xjAz|v-2s!Yp!M*Z&9syFTm#gZrc^+ti`$V(~{kYw4MrZ~i%0KA>uE>!FdIL6I z5<8Ry)*lGZ!-2uMg(nc$4r^}oF9a8pcu?r`?@h)9FPjJYqvFw9;fIU_Y>AC6!YAMp9S%o11ev8!|3_zE zLl!{-qR169>j`!+EMT;vKp7t0ho~p~=GgNsuyb_+Z%-p+=KkZBC#N(YS8Do_jt_2P zrsD9E8y%9D9@#6-JoKgB`A#=9C4VGjbSmf`ZRCn||@O9jL!wwbnxQT%8N zywO3^BKWQ_a%BsPjoT>{8uLju6{5{c$coYwL$fkYk`id?2EtfD@r^uZ%Z_$4yUuNQ z=_z+SGG;tJygzH6H{OmLiN|lT_l~i49!Be41w?nc``w`56FFDfai)Cf!Ei1UYTN3o z>L%Vj0l50R)8UST6Ufty-&*XH4wAhiCCUDl^zs3JPF_F|=-PrhG)#0DumIsdM_F4^ z{Px8>ZmXzzf#NDS_qU?)5ZrZlhfqxzbye<|pA?@=uSuQxUc$+F$O|cW z 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 ME7t**SfynL3tto*Gp6GFPmzlv>rz%G2K_OP@M zMZF8qIdl(19GTAJPsKZDzT9$KVY$4L^4(f}CB&C{ete{NRMMeChu&t&V4qUQ?|K#^ zW}qmJ_Qm%_iGC7~mzUUU+Vn}B?v47;^!G}5qq7n|(Wh*fa^SkGaIMo4SNS!VM`o2DG*Ik{~THmy`Vy+`qFij8@Ea$<6dReW-K))On6_&A3H)~4A<$L3o%tujh! zxl?zoy87Oo8`o~!QPfLmscyU`R?e8XdCJ_2^StMI?W)6P2}Gwb#c9(+zQ#$qMDE+= zxG_KNSXjv1@|cg;v$I8Cso&N$G_Jd=q^opGiFtue4L?O6Gf-K5?T-1_3OOPBJr{;# z_Gm@u=bD;{>qVJKeC+d2_TUr?17FEpDCZDu)&YqftZzg=EeeeKev$h$&w zWTs9Gy0-J+3rB3I;l?%}S}bQ7MGwQJKj;NZ2TFCjMAdN^F(L_?+0t^#=0CPn@`>=df^Ph`7X(U9{NK z)M}CW6Wvzrjf}f~ZO+LM*G}A!6~4V^7W=|5+$! z@_{z-WAX`6-X?BBc*t|Dl}FEqDC$gG7&RwKxNRt?Z0+ITA+@*V{hmiUzXW$reJeA5 z+x`n^)b0?gP@Pu^l~KEVI1h$7$}B(#krFY~;8XFb0N( zYYQxfeI*`VxDu%mW2qAQ;Hl*!^F3YO>v6?fB1Mmk9YhbDomsa1NZ;obn`3(}Hzf*` zO5ZMCp8ou3)yNCUn+2!TE=o29IQFN$RByZO!MSUqh4g+msyCk(cwfl)vcEMFxP7x- z;@zvCHr%kr8yu1+*|epnAMm;SCSXkjx5l##bi)oy(X+z3iuw{C6&6Gibl)J)p9?s1t9t%?~Q$#Rc_-k5!qMWpQL( zoBu{_Z3~Nup_NKuG85Oh)g4o`yU~7j+tRg}6-xuQWiJhzI&lkSy~m8cj~~5D%y&nZ z`mFLQjthz0vqrGdJ9vFa+8Wc$HS1jtr|r@<5_kx7^VS#%c=2f`kL84*trNIXF_@u{ zroO(u(2~jGL96zfnTf?r5nFvLX;0yPJk=~p4{bsdX6d7Sed6==_0f+}sJ^@eeHGJn zZgpgzjm_M-Qps};ADNppS39zAuJzoc@t0-8PKIf(*Sv3Q7_C=5qrCdowf6GrhB4w@ zJQp5@S-pK*VtUHGDYruOT?TKt&BI5M{XT&MUrOBNY!8=~+~{1D*Ay8&{$glhZ)LaK z5w7ZGTNjU*^AhPnpSE1?Gv1)}oXI@oEeH7GJF+!pwF%j5hkv&^ZU2i1yM$^rd5Ui3@F zXYG;At)jktInQo?n{46BjraPaaxG4&e6xJ8^@R7y&0m#|O;K3Leqo?4v8C6+V=`-< zzkP4+Tbg+6_FxaQ?1ikZj991Mn=HMTjslL4aaj}fn7_~9yP#=P@1F1brs2|3ep^0J ziqq+2SdhKRg)+wP;oisp;F!Opc>nI0G=#?ZH5J=!KglkpcY)>Hhj-aUa%=lm_OdnQ z)@obC$LTcf)iaxX&{{hg&vmj=TJz=Xi4)io-TeOgOTB&S##ztOsveI#iVf`e7Tiz} z+~BEEws6L>te})(P3McnDf5pVJHfjWRQ^$AuD7?#-tLIZs;@4)_nHPu3N4l1Ch1tJ zer)phjGW_$+v@i>_8;Bk6B;+USaD!jnE$>k_w^_5FCP%$$&(8{g^ZNF7?-|DK_lII zLGbMB>XW-BeW=(yJy`cs-y6?WGlMFh#HOR?sqa5KI6av$Q>|@BXNvGb&(Qsph2uwr zf}_3)w04YfKi4#Ak=VOowOzZ!(_GdGuahINRx7n^mJ8s5M>5%(>03kr$zfI7Xv&jj<2=-G(NQqqubwZUL zvWK2RdsR6_08qW3x#89<=b{5XZ~8n6xFrSvg630vhF}Q#Pk;i^TV8wCf=ATnW4X-( z?4Q3}y=zen9D&d9*?&_aS7M~RwjX&TAoTsO1b*Rc=Zel}OW9A@F9;z?ku<}8V*JN% zb_V~wvqHztS|ri-9nF5nPH+6tM?sIRmDk7GsV}^$aAG<)J(2+1Af#lb#{!I}S=Fn3 zyHOZoHmFfAZfF`eHbsj0BKBwq44^N0zTc%O-%`U;wfdc#c;N~Fu(J^HX_929{|B(E zBa-}&C9F8T^5VmT;aMNmwsTVw@)?rdGXRiYo!p%8JNxqmbW*7`$P|5YPFL^(gZ~OEz$LjmT*W8r&6xareA~1xm z-c0B1uvdE|7^=O#der-ya7rZm2PFh_D>H)JXXis-M>lnPY`6V)VS}dwM@bCDUP78; z2n=D2?U>%_%#$*+f{5U04yPt`;6|X>RR~!O^AEyczhf^PM;PpxkhSekEjX?AdiSfx zUHqozqeBGOonc5mq@B5jD!I-I=gYdDtxBIfE*sJgI1x%f{NIkXCq~J4YSYwQT*4;szgfxb{lB-PJ$l8O5#o) z0z$-yF`KDozi&y`3i7j7gu)4W_6TdKOG8t*Yq_wo^7+BWPA&IGTR+*Qmi`MJ7dr;K zmm~&Y_)ukq>yoNf`|tg3=XUW_3kMMKh3xfUNCtCUF|j`vMxIHJv(ZpQS|ZItL-m8ANJ9H4Pw@OzK{_S@VNT&bv`aD`aGCA zJj!%uL2r(Mvj@(w6BP1sJ7zOkYuheE^0Gqs{d7OA@qOTpV1JXQ1q_Z}d5fHxB2|G(ty zY$Gh2r!4DSJaxM6RzCJ=1J!0PrcKw?yb>1??gz1j)?tLNZopmgZt+@J6Pn*GCxwJv~(KBT4+``5|Ml? zzn`2nDYfYJ=X+5r7T3^AH~c~ph2>r}Bg>81T~%Au^PY8!C|6-(sc+3*MJQ2NZp^3I zJrn$cxdN`;CUs3LX3x@l6J>mI1iX(T2&M8LNCdj4IZ7 zgSZ{LG=?N;%w%a7!GcCY|M)TS^?`k|I#_VqA?8L918(=93Xs%y;6V3l?Kkpxdf&51 zboof{J6`CLvh+=hunJJeL)UKk2A$20-}JQnO5*#PHn~7EG=a($6`6t=6$DDmw7E zUXw%eOUS%fPA6n2LPk2XJ7%nt`g&5p$(Z@i4({4_e0VQ(Eg3$`>zIFC8maHt^%zE` zB+NhHX!OozuFK@>%isox!JPsEhx^3aMi}cme`@=TmyX+B#mYZIvO)kZ7a%5f1)>_| zTy4{!jwO#bOpc80I|@n%J~(rNz%j=TZx>}6n%b@&k=VR=t;%q}HUOWba8xB%&|$r^ z&A`wMBFE(LqOQA;j>Im7JzN1sgYjHe#BVod%!VJv%l(g>Dlda4%btROT|bT!DK70~ zKG^g_Oyc-&`?k|_SHewYg$S6K_+Oja_8mL6uAFMLZcgXYEk)1E#rInRrKAx<;^cS7 zT@h=$9Y0o)+gSGUP5ynljSTrRbjz*Iv7mU(jmm`!zGoR+xMzE`xXp4QgbdDdnfv*& zKuxPT{6JF03c-d{LZY{dSqx4?72H=S)s|Tu|1Y&g$kxkI5-S3NP@Vgh?CLF-`_raD!(it=jbyF8-;yHBr>`6_WZm|v+1L(-uv`S*)OrMShaz{Et;vmH9APx zc=X6|r^2ugJUJ*qNftm7vZ@@r@p2>5U+#ODOP2HsU*dXjuotpwzR*bL&j8p%GA*p_* z*J2Ek_b>_TiHyubp{Yt~lbd^j4H*7$31$YU8l@f-VY&U$adbHi8&aSxRWL|EYy#)*r3}9IMsgt~B zhc#?w%-2d`s5MHz{4xev8~rrTu}Q(XC+_D@kFu z!+ulMimamNjhUC8SCo(U%|jAi0MsU|fb$+~r8$3d@d4{jCMPGq`Iyu=~%&Z%S#Il$VY#Jz*6)4`x zdxdisw{yH(zSg(%C$voV+}XY0V`e>YUH|QGjDBC(JS0lqRX%wG0#M+(V^7_~Y3Fv0 zx`Mt#>$~oVTXOFVC)8Mlix9J#4-OFVyU;LV)BDxNen0Qs_rHD=8C4XZTRv_|0V_@0 z+gV?kqe!a^QOnynTEh0(KS&C*k-{nK{6{{uo^(iap5bi^b!VxIOwhOy{I8$CW~da| z#o%U-*hf{$!o>YUY~Lo4(!5Luv;gMZ7|( z__@PYwWgx8HK}j?yQB#HTJI%0F9D5XMm<$0?Q%OXwBMKfWs8E}f6PLRkhMWr2yP7> zuZ(|{Vz=wHf5btj#S3!l+Sb4VgcyE5G-mfHlaf_&6Gko`AK?+5C~=Pn8G;XY-ud?9 z!QXgYwcnIq>s~RW?s==d&8i?G5@$JSPBnd<-eW|#+0y%-`Ysztq_D6ugk-RCP}ob@ z&&}ShVMA(?+@Q3UsYpukIVA_j5xtJ8U$~f*(k&+Om-xGqoGtXfu(Kv9n#S$Cb}+T& z#_i)ZrEZnFib?OH+D2dpzzI(8)ekKne(R8ZKDzY9@fq{Zwvsw)Qj%Dof$}P)OMN`# zcFJEZc*NK}-Q4OFV{fY%dzl=T6IZ|3}bhlr3oBC+w@n+Uh)3h zBdTo6^vYMt-;quv2A)7#h&cy=vc}%Regl;Ae7vb1Nvixdt&1l9pP)H_=eNTT#Dfk! zU60oa8{1rL<9na#q ze1w9^uvUwn1RemKYHzb8Ct}!%+;iqPul+V@>NpBIC)pc7u{fMKacbun9ITig>^pDW zk=>SuYLNT}esv1FhMoZluYI5ReyfQeqmjL>c%?+b5x}R00ce<;KH2rK-sNb%(~hN| zUT$gy@I@?R!Ey2#rq63lr_aZqtqIbwu$r?!Z;Bd60H1rJ9ZRIQ%qVVy|BNo$I zX-P-`MG{9#iV4{r!{6=t&YbNsPETum2Z;jDCutg|NQUrU3~rwI@S^IK`#xcsmGH)i zI1d+1;$Rgb4BvUqgop#sJ5I5>2Wy7QN(F#;>DBk^!z2mjT>#EC<9-2%o5A;uvxrv<%07Arw{$Jv@Ck*7Z#=Uc~P>xNb zOOEXPRT@44-3!2CIWlKOtk13Ur6V*Hvu4gtnA;fJb(TcLGA!SNR*HN@eP&GF3v{6S z`bkZby^)w{_Ft{2;P107iE=+%<$&>oMCg$BA6_-RK zz(L@XWrtw36`CbZ+z>4>=ksdm5*|dPJ%Ez}CNM=2IKE}jH}Y`w?Ch}+us2_NXJun} zV2JCLdW~Hh$6G_hG#zeIz#75CS2#{LwzQ}H=j%1U(VU9loLEIr{1>Si?VivO^I_#T z2%s)-BZ$D~gcuuH*2w0)oWk4Sfflh2+75uqNHHlR`MG`SALEvCe%%j$^s&G%x-0-d?Cq@e9WXqEZ?G>-k`h-0(NcZ06?K)1qfE9GEFd8 z{p?4S0R(g_LUZB+QO@9q+^T=c_GF`@4G^$%Asm4VVF)Ok7##FNxz@(A%awQ!>uZBu zyGJ4Y1?WgZ5=Vgj%dg+H-&y1RcxX_jLBYAb$iWG0cbdelVUdsC^j!7t5FmI zgx~PSzSYqBq1Bla!@XzivPrzSO9$ER0uU3IRTjWOETM;VHw~P03l5Po7LgOsNMypX za$ty|zs7$h>jR%afYYtkZ36xn8Y`n(PC67ewp;oTA~ripUkU~n@$J9 zN0UE7`ACI51UN!+0_=&EfecO^4mmia>*|nRMi8(m8jZlpSyI5qGPJZV?QSWm8&yC= zQdt|*q*=`#$23oOKIWLCcji1olt?4~Ka|WJo|6MLEH+)d4IKp!P70VM>>t>jEqXaH zBD!;OGejicl^Q}QOCJDnNZ)h4kNW^Qh**D*8-pN$oyuUTT=41|SvT<%x8E)#M*IiPArX+6vM~rZnsm-+#_ss z5n}y7xKki_%n}LqCRI%Auetl zyC9XwK@nbWmPZNLRbW^BXpqLEDLlV&v}zT!YW6fJnSd=fM9^E4wH>z?Bds{`JDL;C z3Ox#c6#LIs4i60oM(Sj&6;eOMEc0h%Hx?j{WzAZ!!X#oAL@X0SS-n-iyr$IoR01d&K1B?LcJ+ZpE^h-#WEv*0>q<_%B zfr|CdOx|1Hw@bdkR|tr|j4mqy{LeH_H_%Ort~}UYMQcWS&vuz=eO60Hpa`tiRkv^k zy;ObLF*L*iB=v+KS4K7m2OySSar08%S&Aqs@^CBXWMLv2$JM5GuOHPY_Ui%xOSW;# z7P8wUWF-uiGp6=YW4)17@IZ{!XH{NvT_8SNY zyCwyY@fty%cF3?ddKND-inADJ6sSXRo-PV077A;2nWfUIo+2rzb6>}Dcl zKLkYekpl1;9AJuhPC&uy7kkhmO3}WuY8Oj^Sl<6oeffg`0~=&1hK3>pt5D!#aR8B9 zVC2Ab|Bdc;Bwu<`6n3MIO|btusdoG+G{(r*F+gYy?;LpR_}x}`x=##3?2xc>U@YJm z^N1}=4~ZgMgfMdEHzhZCPpKB85wK|s}379ixDQ$Tyf>5kTkubWrZWJ1l(`{R5Lz<$aH>95$s z&a_oHr+hH{CPK{P_LXMkHc82t7Tgd%4vj?v4M<2G-pBiKLxAY9h~i^ykW?8}S{;&joS{l$ZML6+4J|{QZV!ZrIb9JWXQ=~( zRxy*3J91&{VRJv^l$&r4Op4E?O0c2@Yj9W3zSrSxvD+tz=#xYVXx1ZhKQy?#ALvUX@WNurO0mM58DG%e23SEN=kmQU zK3;%G7eBZ(C93u$M9i1wfA=}RP_wQH?5!7++G&r9spQ3RW!#+M`Vq)%gq}Qnd8%fBeuCt0f z!{Am?QBdNssq4E0xVN{$xV@#obtMH9R^~M>yK{clPT%fu3~Nhn86Yi)q%qNHfcQRZ z>g~OTw-|_+919&Lz#^suG**qOJpb|jgFd@2K}4VBP~8Ms-y|jp3LuW}Gz>Sh`iR^( zHk_(PFu-@fZWvA`g?j>$a*nnqgO4 z@&O?pNO3j+fzyNgTc=jdc4uNCKuL)k0=Pp#GADKwC-ym6o-+(Z3BXpD;7+j&2c+F$ zg+|)E8!OL8ygmUD-DgXrCCDkIaF?%ShrG$9`{6b4z`tqzq3SR=BCOq4Q!>wxSocrZr#V~D%P(O!e#bpX%&DN>kLoABAGyyee)Q*#~mI=`Bf>s5R=;m5Q*3CjhK7?vnxcLAH=bA zKFQkOi~yU57?|O{YWCx!=x)frkX|=lqk0l8USDWzY6yoYJIoCMBFL=3{C}H|8)Ayv zfKwfuMvX_5p3JSAHN3gVr4TnnT)?L{-byc_3Fn<(28Q6$l3Ct^^(hA5fBpN^8$W2c zdf(m65juQKMbaYN5X+hp%X4&dkl)L14U`(}8ia(H!YWj!-puGS=8DN0IL2i|34>-= zg7t*hR6JPf{y=#d0`!*V=oFSGoNz8Eay>KVA|k?5e-I%q01)}ax}vAnSrogmO+q9E zVk;DZ)#_^}FYrr~w`&h4Ro@P=1eW0f-zaXUipPy5jDpuA-9^t;;=TiELRcdF_Oa`T zA#r<9sBUB#(r#fMh6r~xT6T69%XJM;g^2cIZdN2qw&3+7R)(kF;b)1X>XAaMSq;|# z(qVy92X`6=T$?)Ssf|LX(L-_?AWs4-{bD_MmcgSYE5ISNV$l%brGyA?2rSv0zhmEx zK4s|qS|t3I#(8Ks)zf0E>U80X!w_LGvCR<)g0om(mRH?Xg%`|9xQ-WRq)465$JtEk zJC=RAr{sYA?S_w}G!crAQ&v50G*#3s$aRAwoG##QpOE7^Vyr|Cy+8HRwua)XaD@KI z8s!T~pw;72fd(B8DLzH{BBef11Qjsuy%B{M8jl%O&6VFmpvT3#Wj@jp#Oz9h_-Ou; zkLvn`v2ml3dZY!dslwd8MlzS5s#ctUOw-4xN=oMx;Z2_tlWe~JzU7USmQFGM9kk+- znkFw<>ZD%11(^T^kPCt&Y+N=FXMd0Wt-q}9J&N}mbw*<_T#6DdUoo_sVD-q-Rr}ys zJRc^-KS*q-#I4yjdzS4pH8{eonE%W(mGQeB3Of%+E}zxKoXNAb`@KS zOKlJK*@9y0#-k8I$k8lTgf&ktot{?u8(Gt%mTcNHowlM%=Q_mNtr%p`k02hA!nZ!dQCI}kqSxl{? z9JS7`3qdBsc!&U3gOLddDsfzG@0J3ymrK#@8}$ZC7(gTxpTt=V-A#79Db87y2m!v) zC^d&QLrI~mi+-fMw!T&VHqB?fKE9jPVCUWVWG+ew0p2n$`u3lB`cmHk8SPA0WLs4A zfMfTGZ@tGLk&VF$lBdg5g0G&>2}FokshiK>ZO7x4b{X$P&k=R@34%H`0W4ZEgA*hM z#GubY*V?xb5MGY785-n^Gg$tWtKwO^KkFVep?pG5OG`I~#<0l2D|2ox9B>8cCZRRb zaW-t)j@RkP*8yFV(6U2sNdx#;!ueHKGW&w!^G1YN-WG4-^YPZ{(@@WAGgePP33f}W zVSoUHMpyxgi&|)Wj0+x_n=dhRJLe_SfN|I$O#3( zXzoD)PEU-Qcq?w;NXJMchq;;_z44-0gWbyi@vU(;PBBEYPB6&h~PFOuLv39_H zbFcPWEtpmJ*Wi@1MG*3k#pk(7%8$AS={s#c3|9FoLyD-R8 z@id~r-zZ2$g8(MhwMX)9?5LXVWsA1)+W1fu9nk39#Eq1X!u$-T0geA`zWV(_8qI1 zPA`m1XnlmkssA`_e!%O3vR~_q z+zwuAg$(u+2IpYxZ{S!Ev+DM%a~-#?w$Hv%h~8j25^PS25s&*B(!g+>$ap1UIY=U!E1dJErt(mxO6PX_9n{)kbU5f9=->El-q9# zpIy6Eb$9drnWo4vo*j=Y9|B}jV^aeA-gWU;y<8SiIc?APn|)s09~ zm%_hXyJdE&FzfhaZUnF|pi&Ty71`NuTE4r8GQV<$7x^K%su5l#PWcxSSmW)Wrf0_2 z4F)KaX&Q>xapf(^DgT-k4 z=jyHhxZ!tJy3Rx;mmS`^aJJlIHyNirw>;$^xkp>9AE=EV{Ki;s%tLj!AK*nkOJF!v zP4ru1xn{YZ)t-Dlh@xPMb_N{4v~8NCZoO?PjduQyaZYj!kR_Z=P78%~L0*TGO&hmBI`!-UAj z-ea6|oDSDSF84yxg3pEZab#xlHD`aWANeLrA?UE$h{!0o2C!~ggsoSizq_A9_)eAF zIij5pKj@rZfsPa~KoR!ASD#O2g@mB?dB_&!R5RmjH^~S<*5jm3lmCWG5%F__mGV#&wNf7&N$M3lO7rYyx7H8zU!49|7ohm;=vOuY+fDdQ@#pO zD|l(a$EIr%>l3?;O`UpSf9JA#XV>Q2a0HH=#8}xpv*m+h*c7Lb>o?0T-<-LcC$$Ed zcXZqc5iY6PavD#Q=bpLoLg#gKCn*5M#{q`i-=^~*Xivb(n&0Y-<+mSg9EcP$IKa3U zhT_GMtHxgV8nedm?v~L!SS*Ko3y4iAG_}9;I6%qw3-M$7Arm_}(lCb=6TEG|P)Q;@rIJ`ycy? zb7)O!KEJ=yrzbPwwe9rGjm&G%zKXzOXabkJ;puQDcGT1}Tc)kr{$uFw@-Xyh0{A=e zkNe%1GhNr#9a*;K<8D8{l_Bo%i43bG5j0+X?`yXE;iqkt?K@<6EmCz2%$7`v&xlBz zPr~HO@D8yVGpoPa)!z4HBC62{6e#$KEg#@gb~7hZW9+fPcZZByv2^)z1OPNE#OJV; z?70!*8n+oecMbR|F}1mzG@MK}E#)72;uJovvrdN6_~-BcI62&$y1!Ny_i>&--!x=U z$A;1eQ~wEw3fkD`&5~ZWD#L%ZteIry01pE24Vu9gC39chaq8WovB>qj{Cmu{oF#3>7<`3SGUt27-2oYARTl{eN zpd~MVo~oEWuFG-FKDMnMIfAC}W#F7T7k>=DY8_$`=|1L&do3wF0(R8{_XnNV?C7r4 zD_!-`j)s8jKLFUWT)(s{Cf({#r*QMG@wcrT4*vnbP06ilPxi$0N@tQ z4EC(7%3HK+rKfuStce+@iU=v@9wA~`)76(pb$(=DMoA>1VN&V?&BrC&+p318ud?lX z2m;O(AdLpxQkirc=;yf~UOxGmNdtN`R-Dqd3UKc*s~GZoiNb@^*C(M%r+|WWVo-93 z#OV(Kj9Jk`hZaMl%skWM<8o_|(}re^KAOU9?D)uAUdvm3j1cd)+s6-9`OTsZ^{%0IEJoc>3# zS@a#MKBy~PxlZoV(QD|ZWYG&UpZ+`cu_u3tWnQp}6Bk=ska5)P)896%g+ z@*+j|Qy6+HdTc+E<*b4P3Y%fhY3SZ5=c)r!&|76cQ%QhS#Qf!5&Jw?)9alJaegqMD zNhpvEg1ihLJEGM$e2HL63 zXs3X^Phpdf)Gyw9Qn~ShF`5%B6u~9RGct7rATRp3Rd!1$dk&c4MOaz1@)Nd!w-8d zLWMz2y+;}=OHZ&So$L@_m+p@~Y!T@X5rPdcvxHSz!SgNL5QB;yAAMTyg`ziSFG&d! zOszjKMA>V_z{a`lcc4kVg=`+Az|r`UztauYxh|Vt8GE+p+CZbLfnWZRRXD)3l;|r5J0tF0_2QT;Ijs53RaCIy;az zM&sYjq%rI787B;#2@w%C$Zt-9r|DQNW1h3x<7(G&o4_}xdeu@Bj zfAB|~s1lT%HEax;JMHfLYpQ+hZ2Ci;6~-8_&PmsP@t+nyR*!i0b>;VI#f91kAmoD( z7j8=ZO)tr-7JW0{dhfAeK%#ks#QiD$OQk0Fs+8hpyDffpuTxH?=~zftn#ETLisG-W zy3!-(``FJV?>DvZ{CI7(poCb@qwT^iO$-_a&kC897&^;{vWsV|6jJJFg#UzPTYc^xLZK?>dq3$c0ODzAaoaf4%Yk!)F>F z5#F^vkfghI&{n8g5!`$Ke-v%}<2&GnyM8Y%X`j8n$Wrdv9j)I%m+-Ws zX7RfZ_%9099~zS;f7bP%o@h$oSt9YsYNsY{%!JO9!gpl2{8razRRmj4=&Jbt<&^ z!`8(SzL&@Fu3_^Yqqn++j?w@=E|JSi>>0hfTW_SN7rIIVNUU;}{!xE!>D%Zj2r==$ z=L8V%lwUg(Cj6i_cxj1`T#@dJ%!yxp9xL}Y%i3!H~~nSj)OlEF%q4q992 zc~&aX4wKB+^*ri!4IUjZ{3rodgXkTww(`}|0}1hadMZYn7sD&WRSNq{+$KoLxn*8N z?SY@_ne#6<%sS75mCZr!1VQ3_>j4Y%*IE}cD~{)X_-^U4Iks&L48DZIu)6-W&6yf% zoBf)-icUSKmH=pq#HY*dPBhY;uQ|E?QnAy6#PB+~eEo=@(s@(op2mtyF2(ige4omiJkqf&T zF>{w`e3Od*_|}L5aIIneMee@KFV6J5`e|Hy*I`x8n?Iu{5y;i1uz)ddnIge=-|+H} zd4p`OiZmqv@CR;RHO9N2`#Sp3C;2SR(>~cr91lBt9(PA(~jN~ z)x?m2bG1S#ybCMj?wD(1`zF8o{(A9Q(WTv0yw)HfXhMO@$_UrnH9NZE&Y

KA(?m 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 parseFixed('1', d)); + + return balances.map((bal, index) => + bal.mul(ONE).div(scalingFactors[index]) + ); +} + +export function balancesFromTokenInOut( + balanceTokenIn: BigNumber, + balanceTokenOut: BigNumber, + tokenInIsToken0: boolean +): [BigNumber, BigNumber] { + return tokenInIsToken0 + ? [balanceTokenIn, balanceTokenOut] + : [balanceTokenOut, balanceTokenIn]; +} + +///////// +/// INVARIANT CALC +///////// + +export function calcAtAChi( + x: BigNumber, + y: BigNumber, + p: GyroEParams, + d: DerivedGyroEParams +): BigNumber { + const dSq2 = mulXpU(d.dSq, d.dSq); + + // (cx - sy) * (w/lambda + z) / lambda + // account for 2 factors of dSq (4 s,c factors) + const termXp = divXpU( + divDownMagU(divDownMagU(d.w, p.lambda).add(d.z), p.lambda), + dSq2 + ); + + let val = mulDownXpToNpU( + mulDownMagU(x, p.c).sub(mulDownMagU(y, p.s)), + termXp + ); + + // (x lambda s + y lambda c) * u, note u > 0 + let termNp = mulDownMagU(mulDownMagU(x, p.lambda), p.s).add( + mulDownMagU(mulDownMagU(y, p.lambda), p.c) + ); + val = val.add(mulDownXpToNpU(termNp, divXpU(d.u, dSq2))); + + // (sx+cy) * v, note v > 0 + termNp = mulDownMagU(x, p.s).add(mulDownMagU(y, p.c)); + val = val.add(mulDownXpToNpU(termNp, divXpU(d.v, dSq2))); + + return val; +} + +export function calcInvariantSqrt( + x: BigNumber, + y: BigNumber, + p: GyroEParams, + d: DerivedGyroEParams +): [BigNumber, BigNumber] { + let val = calcMinAtxAChiySqPlusAtxSq(x, y, p, d).add( + calc2AtxAtyAChixAChiy(x, y, p, d) + ); + val = val.add(calcMinAtyAChixSqPlusAtySq(x, y, p, d)); + const err = mulUpMagU(x, x).add(mulUpMagU(y, y)).div(ONE_XP); + val = val.gt(0) ? sqrt(val, BigNumber.from(5)) : BigNumber.from(0); + return [val, err]; +} + +function calcMinAtxAChiySqPlusAtxSq( + x: BigNumber, + y: BigNumber, + p: GyroEParams, + d: DerivedGyroEParams +) { + let termNp = mulUpMagU(mulUpMagU(mulUpMagU(x, x), p.c), p.c).add( + mulUpMagU(mulUpMagU(mulUpMagU(y, y), p.s), p.s) + ); + termNp = termNp.sub( + mulDownMagU(mulDownMagU(mulDownMagU(x, y), p.c.mul(2)), p.s) + ); + const termXp = mulXpU(d.u, d.u) + .add(divDownMagU(mulXpU(d.u.mul(2), d.v), p.lambda)) + .add(divDownMagU(divDownMagU(mulXpU(d.v, d.v), p.lambda), p.lambda)); + + let val = mulDownXpToNpU(termNp.mul(-1), termXp); + val = val.add( + mulDownXpToNpU( + divDownMagU(divDownMagU(termNp.sub(9), p.lambda), p.lambda), + divXpU(ONE_XP, d.dSq) + ) + ); + return val; +} + +function calc2AtxAtyAChixAChiy( + x: BigNumber, + y: BigNumber, + p: GyroEParams, + d: DerivedGyroEParams +) { + let termNp = mulDownMagU( + mulDownMagU(mulDownMagU(x, x).sub(mulUpMagU(y, y)), p.c.mul(2)), + p.s + ); + + const xy = mulDownMagU(y, x.mul(2)); + termNp = termNp + .add(mulDownMagU(mulDownMagU(xy, p.c), p.c)) + .sub(mulDownMagU(mulDownMagU(xy, p.s), p.s)); + let termXp = mulXpU(d.z, d.u).add( + divDownMagU(divDownMagU(mulXpU(d.w, d.v), p.lambda), p.lambda) + ); + termXp = termXp.add( + divDownMagU(mulXpU(d.w, d.u).add(mulXpU(d.z, d.v)), p.lambda) + ); + termXp = divXpU(termXp, mulXpU(mulXpU(mulXpU(d.dSq, d.dSq), d.dSq), d.dSq)); + const val = mulDownXpToNpU(termNp, termXp); + return val; +} + +function calcMinAtyAChixSqPlusAtySq( + x: BigNumber, + y: BigNumber, + p: GyroEParams, + d: DerivedGyroEParams +) { + let termNp = mulUpMagU(mulUpMagU(mulUpMagU(x, x), p.s), p.s).add( + mulUpMagU(mulUpMagU(mulUpMagU(y, y), p.c), p.c) + ); + termNp = termNp.add(mulUpMagU(mulUpMagU(mulUpMagU(x, y), p.s.mul(2)), p.c)); + let termXp = mulXpU(d.z, d.z).add( + divDownMagU(divDownMagU(mulXpU(d.w, d.w), p.lambda), p.lambda) + ); + termXp = termXp.add(divDownMagU(mulXpU(d.z.mul(2), d.w), p.lambda)); + termXp = divXpU(termXp, mulXpU(mulXpU(mulXpU(d.dSq, d.dSq), d.dSq), d.dSq)); + let val = mulDownXpToNpU(termNp.mul(-1), termXp); + val = val.add(mulDownXpToNpU(termNp.sub(9), divXpU(ONE_XP, d.dSq))); + return val; +} + +export function calcAChiAChiInXp( + p: GyroEParams, + d: DerivedGyroEParams +): BigNumber { + const dSq3 = mulXpU(mulXpU(d.dSq, d.dSq), d.dSq); + let val = mulUpMagU(p.lambda, divXpU(mulXpU(d.u.mul(2), d.v), dSq3)); + val = val.add( + mulUpMagU( + mulUpMagU(divXpU(mulXpU(d.u.add(1), d.u.add(1)), dSq3), p.lambda), + p.lambda + ) + ); + val = val.add(divXpU(mulXpU(d.v, d.v), dSq3)); + const termXp = divUpMagU(d.w, p.lambda).add(d.z); + val = val.add(divXpU(mulXpU(termXp, termXp), dSq3)); + return val; +} + +///////// +/// SWAP AMOUNT CALC +///////// + +export function checkAssetBounds( + params: GyroEParams, + derived: DerivedGyroEParams, + invariant: Vector2, + newBal: BigNumber, + assetIndex: number +): void { + if (assetIndex === 0) { + const xPlus = maxBalances0(params, derived, invariant); + if (newBal.gt(MAX_BALANCES) || newBal.gt(xPlus)) + throw new Error('ASSET BOUNDS EXCEEDED'); + } else { + const yPlus = maxBalances1(params, derived, invariant); + if (newBal.gt(MAX_BALANCES) || newBal.gt(yPlus)) + throw new Error('ASSET BOUNDS EXCEEDED'); + } +} + +function maxBalances0(p: GyroEParams, d: DerivedGyroEParams, r: Vector2) { + const termXp1 = divXpU(d.tauBeta.x.sub(d.tauAlpha.x), d.dSq); + const termXp2 = divXpU(d.tauBeta.y.sub(d.tauAlpha.y), d.dSq); + let xp = mulDownXpToNpU( + mulDownMagU(mulDownMagU(r.y, p.lambda), p.c), + termXp1 + ); + xp = xp.add( + termXp2.gt(BigNumber.from(0)) + ? mulDownMagU(r.y, p.s) + : mulDownXpToNpU(mulUpMagU(r.x, p.s), termXp2) + ); + return xp; +} + +function maxBalances1(p: GyroEParams, d: DerivedGyroEParams, r: Vector2) { + const termXp1 = divXpU(d.tauBeta.x.sub(d.tauAlpha.x), d.dSq); + const termXp2 = divXpU(d.tauBeta.y.sub(d.tauAlpha.y), d.dSq); + let yp = mulDownXpToNpU( + mulDownMagU(mulDownMagU(r.y, p.lambda), p.s), + termXp1 + ); + yp = yp.add( + termXp2.gt(BigNumber.from(0)) + ? mulDownMagU(r.y, p.c) + : mulDownXpToNpU(mulUpMagU(r.x, p.c), termXp2) + ); + return yp; +} + +export function calcYGivenX( + x: BigNumber, + params: GyroEParams, + d: DerivedGyroEParams, + r: Vector2 +): BigNumber { + const ab: Vector2 = { + x: virtualOffset0(params, d, r), + y: virtualOffset1(params, d, r), + }; + + const y = solveQuadraticSwap( + params.lambda, + x, + params.s, + params.c, + r, + ab, + d.tauBeta, + d.dSq + ); + return y; +} + +export function calcXGivenY( + y: BigNumber, + params: GyroEParams, + d: DerivedGyroEParams, + r: Vector2 +): BigNumber { + const ba: Vector2 = { + x: virtualOffset1(params, d, r), + y: virtualOffset0(params, d, r), + }; + const x = solveQuadraticSwap( + params.lambda, + y, + params.c, + params.s, + r, + ba, + { + x: d.tauAlpha.x.mul(-1), + y: d.tauAlpha.y, + }, + d.dSq + ); + return x; +} + +export function virtualOffset0( + p: GyroEParams, + d: DerivedGyroEParams, + r: Vector2, + switchTau?: boolean +): BigNumber { + const tauValue = switchTau ? d.tauAlpha : d.tauBeta; + const termXp = divXpU(tauValue.x, d.dSq); + + let a = tauValue.x.gt(BigNumber.from(0)) + ? mulUpXpToNpU(mulUpMagU(mulUpMagU(r.x, p.lambda), p.c), termXp) + : mulUpXpToNpU(mulDownMagU(mulDownMagU(r.y, p.lambda), p.c), termXp); + + a = a.add(mulUpXpToNpU(mulUpMagU(r.x, p.s), divXpU(tauValue.y, d.dSq))); + + return a; +} + +export function virtualOffset1( + p: GyroEParams, + d: DerivedGyroEParams, + r: Vector2, + switchTau?: boolean +): BigNumber { + const tauValue = switchTau ? d.tauBeta : d.tauAlpha; + const termXp = divXpU(tauValue.x, d.dSq); + + let b = tauValue.x.lt(BigNumber.from(0)) + ? mulUpXpToNpU(mulUpMagU(mulUpMagU(r.x, p.lambda), p.s), termXp.mul(-1)) + : mulUpXpToNpU( + mulDownMagU(mulDownMagU(r.y.mul(-1), p.lambda), p.s), + termXp + ); + + b = b.add(mulUpXpToNpU(mulUpMagU(r.x, p.c), divXpU(tauValue.y, d.dSq))); + return b; +} + +function solveQuadraticSwap( + lambda: BigNumber, + x: BigNumber, + s: BigNumber, + c: BigNumber, + r: Vector2, + ab: Vector2, + tauBeta: Vector2, + dSq: BigNumber +): BigNumber { + const lamBar: Vector2 = { + x: ONE_XP.sub(divDownMagU(divDownMagU(ONE_XP, lambda), lambda)), + y: ONE_XP.sub(divUpMagU(divUpMagU(ONE_XP, lambda), lambda)), + }; + const q: QParams = { + a: BigNumber.from(0), + b: BigNumber.from(0), + c: BigNumber.from(0), + }; + const xp = x.sub(ab.x); + if (xp.gt(BigNumber.from(0))) { + q.b = mulUpXpToNpU( + mulDownMagU(mulDownMagU(xp.mul(-1), s), c), + divXpU(lamBar.y, dSq) + ); + } else { + q.b = mulUpXpToNpU( + mulUpMagU(mulUpMagU(xp.mul(-1), s), c), + divXpU(lamBar.x, dSq).add(1) + ); + } + const sTerm: Vector2 = { + x: divXpU(mulDownMagU(mulDownMagU(lamBar.y, s), s), dSq), + y: divXpU(mulUpMagU(mulUpMagU(lamBar.x, s), s), dSq.add(1)).add(1), + }; + sTerm.x = ONE_XP.sub(sTerm.x); + sTerm.y = ONE_XP.sub(sTerm.y); + + q.c = calcXpXpDivLambdaLambda(x, r, lambda, s, c, tauBeta, dSq).mul(-1); + q.c = q.c.add(mulDownXpToNpU(mulDownMagU(r.y, r.y), sTerm.y)); // r.y === currentInv + err + q.c = q.c.gt(BigNumber.from(0)) + ? sqrt(q.c, BigNumber.from(5)) + : BigNumber.from(0); + if (q.b.sub(q.c).gt(BigNumber.from(0))) { + q.a = mulUpXpToNpU(q.b.sub(q.c), divXpU(ONE_XP, sTerm.y).add(1)); + } else { + q.a = mulUpXpToNpU(q.b.sub(q.c), divXpU(ONE_XP, sTerm.x)); + } + return q.a.add(ab.y); +} + +export function calcXpXpDivLambdaLambda( + x: BigNumber, + r: Vector2, + lambda: BigNumber, + s: BigNumber, + c: BigNumber, + tauBeta: Vector2, + dSq: BigNumber +): BigNumber { + const sqVars = { + x: mulXpU(dSq, dSq), + y: mulUpMagU(r.x, r.x), + }; + const q: QParams = { + a: BigNumber.from(0), + b: BigNumber.from(0), + c: BigNumber.from(0), + }; + let termXp = divXpU(mulXpU(tauBeta.x, tauBeta.y), sqVars.x); + if (termXp.gt(BigNumber.from(0))) { + q.a = mulUpMagU(sqVars.y, s.mul(2)); + q.a = mulUpXpToNpU(mulUpMagU(q.a, c), termXp.add(7)); + } else { + q.a = mulDownMagU(mulDownMagU(r.y, r.y), s.mul(2)); // r.y === currentInv + err + q.a = mulUpXpToNpU(mulDownMagU(q.a, c), termXp); + } + + if (tauBeta.x.lt(BigNumber.from(0))) { + q.b = mulUpXpToNpU( + mulUpMagU(mulUpMagU(r.x, x), c.mul(2)), + divXpU(tauBeta.x, dSq).mul(-1).add(3) + ); + } else { + q.b = mulUpXpToNpU( + mulDownMagU(mulDownMagU(r.y.mul(-1), x), c.mul(2)), + divXpU(tauBeta.x, dSq) + ); + } + q.a = q.a.add(q.b); + termXp = divXpU(mulXpU(tauBeta.y, tauBeta.y), sqVars.x).add(7); + q.b = mulUpMagU(sqVars.y, s); + q.b = mulUpXpToNpU(mulUpMagU(q.b, s), termXp); + + q.c = mulUpXpToNpU( + mulDownMagU(mulDownMagU(r.y.mul(-1), x), s.mul(2)), + divXpU(tauBeta.y, dSq) + ); + q.b = q.b.add(q.c).add(mulUpMagU(x, x)); + q.b = q.b.gt(BigNumber.from(0)) + ? divUpMagU(q.b, lambda) + : divDownMagU(q.b, lambda); + + q.a = q.a.add(q.b); + q.a = q.a.gt(BigNumber.from(0)) + ? divUpMagU(q.a, lambda) + : divDownMagU(q.a, lambda); + + termXp = divXpU(mulXpU(tauBeta.x, tauBeta.x), sqVars.x).add(7); + const val = mulUpMagU(mulUpMagU(sqVars.y, c), c); + return mulUpXpToNpU(val, termXp).add(q.a); +} + +///////// +/// LINEAR ALGEBRA OPERATIONS +///////// + +export function mulA(params: GyroEParams, tp: Vector2): Vector2 { + return { + x: divDownMagU(mulDownMagU(params.c, tp.x), params.lambda).sub( + divDownMagU(mulDownMagU(params.s, tp.y), params.lambda) + ), + y: mulDownMagU(params.s, tp.x).add(mulDownMagU(params.c, tp.y)), + }; +} + +export function scalarProd(t1: Vector2, t2: Vector2): BigNumber { + const ret = mulDownMagU(t1.x, t2.x).add(mulDownMagU(t1.y, t2.y)); + return ret; +} diff --git a/src/pools/gyroEV2Pool/gyroEV2Pool.ts b/src/pools/gyroEV2Pool/gyroEV2Pool.ts new file mode 100644 index 00000000..339dec6d --- /dev/null +++ b/src/pools/gyroEV2Pool/gyroEV2Pool.ts @@ -0,0 +1,542 @@ +import { getAddress } from '@ethersproject/address'; +import { WeiPerEther as ONE, Zero } from '@ethersproject/constants'; +import { formatFixed, BigNumber } from '@ethersproject/bignumber'; +import { BigNumber as OldBigNumber, bnum, ZERO } from '../../utils/bignumber'; + +import { + PoolBase, + PoolPairBase, + PoolTypes, + SubgraphToken, + SwapTypes, + SubgraphPoolBase, +} from '../../types'; +import { + GyroEParams, + DerivedGyroEParams, + Vector2, + normalizeBalances, + balancesFromTokenInOut, + reduceFee, + addFee, + virtualOffset0, + virtualOffset1, +} from './gyroEV2Math/gyroEV2MathHelpers'; +import { isSameAddress, safeParseFixed } from '../../utils'; +import { mulDown, divDown } from '../gyroHelpers/gyroSignedFixedPoint'; +import { + calculateInvariantWithError, + calcOutGivenIn, + calcInGivenOut, + calcSpotPriceAfterSwapOutGivenIn, + calcSpotPriceAfterSwapInGivenOut, + calcDerivativePriceAfterSwapOutGivenIn, + calcDerivativeSpotPriceAfterSwapInGivenOut, +} from './gyroEV2Math/gyroEV2Math'; +import { SWAP_LIMIT_FACTOR } from '../gyroHelpers/constants'; +import { universalNormalizedLiquidity } from '../liquidity'; + +export type GyroEPoolPairData = PoolPairBase & { + tokenInIsToken0: boolean; +}; + +export type GyroEPoolToken = Pick< + SubgraphToken, + 'address' | 'balance' | 'decimals' +>; + +type GyroEParamsFromSubgraph = { + alpha: string; + beta: string; + c: string; + s: string; + lambda: string; +}; +type DerivedGyroEParamsFromSubgraph = { + tauAlphaX: string; + tauAlphaY: string; + tauBetaX: string; + tauBetaY: string; + u: string; + v: string; + w: string; + z: string; + dSq: string; +}; + +export class GyroEV2Pool implements PoolBase { + poolType: PoolTypes = PoolTypes.GyroE; + id: string; + address: string; + tokensList: string[]; + tokens: GyroEPoolToken[]; + swapFee: BigNumber; + totalShares: BigNumber; + gyroEParams: GyroEParams; + derivedGyroEParams: DerivedGyroEParams; + tokenRates: BigNumber[]; + + static fromPool(pool: SubgraphPoolBase): GyroEV2Pool { + const { + alpha, + beta, + c, + s, + lambda, + tauAlphaX, + tauAlphaY, + tauBetaX, + tauBetaY, + u, + v, + w, + z, + dSq, + tokenRates, + } = pool; + + const gyroEParams = { + alpha, + beta, + c, + s, + lambda, + }; + + const derivedGyroEParams = { + tauAlphaX, + tauAlphaY, + tauBetaX, + tauBetaY, + u, + v, + w, + z, + dSq, + }; + + if ( + !Object.values(gyroEParams).every((el) => el) || + !Object.values(derivedGyroEParams).every((el) => el) + ) + throw new Error( + 'Pool missing GyroE params and/or GyroE derived params' + ); + + if (!tokenRates) throw new Error('GyroEV2 Pool missing tokenRates'); + + return new GyroEV2Pool( + pool.id, + pool.address, + pool.swapFee, + pool.totalShares, + pool.tokens as GyroEPoolToken[], + pool.tokensList, + gyroEParams as GyroEParamsFromSubgraph, + derivedGyroEParams as DerivedGyroEParamsFromSubgraph, + tokenRates + ); + } + + constructor( + id: string, + address: string, + swapFee: string, + totalShares: string, + tokens: GyroEPoolToken[], + tokensList: string[], + gyroEParams: GyroEParamsFromSubgraph, + derivedGyroEParams: DerivedGyroEParamsFromSubgraph, + tokenRates: string[] + ) { + this.id = id; + this.address = address; + this.swapFee = safeParseFixed(swapFee, 18); + this.totalShares = safeParseFixed(totalShares, 18); + this.tokens = tokens; + this.tokensList = tokensList; + this.tokenRates = [ + safeParseFixed(tokenRates[0], 18), + safeParseFixed(tokenRates[1], 18), + ]; + + this.gyroEParams = { + alpha: safeParseFixed(gyroEParams.alpha, 18), + beta: safeParseFixed(gyroEParams.beta, 18), + c: safeParseFixed(gyroEParams.c, 18), + s: safeParseFixed(gyroEParams.s, 18), + lambda: safeParseFixed(gyroEParams.lambda, 18), + }; + + this.derivedGyroEParams = { + tauAlpha: { + x: safeParseFixed(derivedGyroEParams.tauAlphaX, 38), + y: safeParseFixed(derivedGyroEParams.tauAlphaY, 38), + }, + tauBeta: { + x: safeParseFixed(derivedGyroEParams.tauBetaX, 38), + y: safeParseFixed(derivedGyroEParams.tauBetaY, 38), + }, + u: safeParseFixed(derivedGyroEParams.u, 38), + v: safeParseFixed(derivedGyroEParams.v, 38), + w: safeParseFixed(derivedGyroEParams.w, 38), + z: safeParseFixed(derivedGyroEParams.z, 38), + dSq: safeParseFixed(derivedGyroEParams.dSq, 38), + }; + } + + parsePoolPairData(tokenIn: string, tokenOut: string): GyroEPoolPairData { + const tokenInIndex = this.tokens.findIndex( + (t) => getAddress(t.address) === getAddress(tokenIn) + ); + if (tokenInIndex < 0) throw 'Pool does not contain tokenIn'; + const tI = this.tokens[tokenInIndex]; + const balanceIn = tI.balance; + const decimalsIn = tI.decimals; + + const tokenOutIndex = this.tokens.findIndex( + (t) => getAddress(t.address) === getAddress(tokenOut) + ); + if (tokenOutIndex < 0) throw 'Pool does not contain tokenOut'; + const tO = this.tokens[tokenOutIndex]; + const balanceOut = tO.balance; + const decimalsOut = tO.decimals; + + const tokenInIsToken0 = tokenInIndex === 0; + + const poolPairData: GyroEPoolPairData = { + id: this.id, + address: this.address, + poolType: this.poolType, + tokenIn: tokenIn, + tokenOut: tokenOut, + decimalsIn: Number(decimalsIn), + decimalsOut: Number(decimalsOut), + balanceIn: safeParseFixed(balanceIn, decimalsIn), + balanceOut: safeParseFixed(balanceOut, decimalsOut), + swapFee: this.swapFee, + tokenInIsToken0, + }; + + return poolPairData; + } + + getNormalizedLiquidity(poolPairData: GyroEPoolPairData): OldBigNumber { + return universalNormalizedLiquidity( + this._derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData, + ZERO + ) + ); + } + + getLimitAmountSwap( + poolPairData: GyroEPoolPairData, + swapType: SwapTypes + ): OldBigNumber { + if (swapType === SwapTypes.SwapExactIn) { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const virtualOffsetFunc = poolPairData.tokenInIsToken0 + ? virtualOffset0 + : virtualOffset1; + const maxAmountInAssetInPool = virtualOffsetFunc( + this.gyroEParams, + this.derivedGyroEParams, + invariant + ).sub( + virtualOffsetFunc( + this.gyroEParams, + this.derivedGyroEParams, + invariant, + true + ) + ); + const limitAmountIn = maxAmountInAssetInPool.sub( + normalizedBalances[0] + ); + const limitAmountInPlusSwapFee = divDown( + limitAmountIn, + ONE.sub(poolPairData.swapFee) + ); + return bnum( + formatFixed( + mulDown(limitAmountInPlusSwapFee, SWAP_LIMIT_FACTOR), + 18 + ) + ); + } else { + return bnum( + formatFixed( + mulDown(poolPairData.balanceOut, SWAP_LIMIT_FACTOR), + poolPairData.decimalsOut + ) + ); + } + } + + // Updates the balance of a given token for the pool + updateTokenBalanceForPool(token: string, newBalance: BigNumber): void { + // token is BPT + if (isSameAddress(this.address, token)) { + this.updateTotalShares(newBalance); + } else { + // token is underlying in the pool + const T = this.tokens.find((t) => isSameAddress(t.address, token)); + if (!T) throw Error('Pool does not contain this token'); + T.balance = formatFixed(newBalance, T.decimals); + } + } + + updateTotalShares(newTotalShares: BigNumber): void { + this.totalShares = newTotalShares; + } + + _exactTokenInForTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const inAmount = safeParseFixed(amount.toString(), 18); + const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); + const outAmount = calcOutGivenIn( + orderedNormalizedBalances, + inAmountLessFee, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant + ); + return bnum(formatFixed(outAmount, 18)); + } + + _tokenInForExactTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const outAmount = safeParseFixed(amount.toString(), 18); + + const inAmountLessFee = calcInGivenOut( + orderedNormalizedBalances, + outAmount, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant + ); + const inAmount = addFee(inAmountLessFee, poolPairData.swapFee); + return bnum(formatFixed(inAmount, 18)); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _calcTokensOutGivenExactBptIn(bptAmountIn: BigNumber): BigNumber[] { + // Missing maths for this + return new Array(this.tokens.length).fill(Zero); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _calcBptOutGivenExactTokensIn(amountsIn: BigNumber[]): BigNumber { + // Missing maths for this + return Zero; + } + + _spotPriceAfterSwapExactTokenInForTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const inAmount = safeParseFixed(amount.toString(), 18); + const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); + const newSpotPrice = calcSpotPriceAfterSwapOutGivenIn( + orderedNormalizedBalances, + inAmountLessFee, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + return bnum(formatFixed(newSpotPrice, 18)); + } + + _spotPriceAfterSwapTokenInForExactTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const outAmount = safeParseFixed(amount.toString(), 18); + const newSpotPrice = calcSpotPriceAfterSwapInGivenOut( + orderedNormalizedBalances, + outAmount, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + return bnum(formatFixed(newSpotPrice, 18)); + } + + _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const inAmount = safeParseFixed(amount.toString(), 18); + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + + const derivative = calcDerivativePriceAfterSwapOutGivenIn( + [ + orderedNormalizedBalances[0].add( + reduceFee(inAmount, poolPairData.swapFee) + ), + orderedNormalizedBalances[1], + ], + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + return bnum(formatFixed(derivative, 18)); + } + + _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const outAmount = safeParseFixed(amount.toString(), 18); + const derivative = calcDerivativeSpotPriceAfterSwapInGivenOut( + [ + orderedNormalizedBalances[0], + orderedNormalizedBalances[1].sub(outAmount), + ], + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + return bnum(formatFixed(derivative, 18)); + } +} diff --git a/src/pools/index.ts b/src/pools/index.ts index 6315385b..f58cbb03 100644 --- a/src/pools/index.ts +++ b/src/pools/index.ts @@ -8,6 +8,7 @@ import { ComposableStablePool } from './composableStable/composableStablePool'; import { Gyro2Pool } from './gyro2Pool/gyro2Pool'; import { Gyro3Pool } from './gyro3Pool/gyro3Pool'; import { GyroEPool } from './gyroEPool/gyroEPool'; +import { GyroEV2Pool } from './gyroEV2Pool/gyroEV2Pool'; import { FxPool } from './xaveFxPool/fxPool'; import { BigNumber as OldBigNumber, @@ -37,6 +38,7 @@ export function parseNewPool( | Gyro2Pool | Gyro3Pool | GyroEPool + | GyroEV2Pool | FxPool | undefined { // We're not interested in any pools which don't allow swapping @@ -53,6 +55,7 @@ export function parseNewPool( | Gyro2Pool | Gyro3Pool | GyroEPool + | GyroEV2Pool | FxPool; try { @@ -76,6 +79,8 @@ export function parseNewPool( else if (pool.poolType === 'Gyro2') newPool = Gyro2Pool.fromPool(pool); else if (pool.poolType === 'Gyro3') newPool = Gyro3Pool.fromPool(pool); else if (pool.poolType === 'GyroE') newPool = GyroEPool.fromPool(pool); + else if (pool.poolType === 'GyroEV2') + newPool = GyroEV2Pool.fromPool(pool); else if (pool.poolType === 'FX') newPool = FxPool.fromPool(pool); else { console.error( diff --git a/src/types.ts b/src/types.ts index acd05652..f7a88fb8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,7 +102,7 @@ export interface SubgraphPoolBase { // Gyro3 specific field root3Alpha?: string; - // GyroE specific fields + // GyroE and GyroEV2 specific fields alpha?: string; beta?: string; c?: string; @@ -117,7 +117,9 @@ export interface SubgraphPoolBase { w?: string; z?: string; dSq?: string; - tokenRates?: [BigNumber, BigNumber]; + + // GyroEV2 specific fields + tokenRates?: string[]; // FxPool delta?: string; @@ -196,6 +198,7 @@ export enum PoolFilter { Gyro2 = 'Gyro2', Gyro3 = 'Gyro3', GyroE = 'GyroE', + GyroEV2 = 'GyroEV2', // Linear Pools defined below all operate the same mathematically but have different factories and names in Subgraph AaveLinear = 'AaveLinear', Linear = 'Linear', diff --git a/test/lib/onchainData.ts b/test/lib/onchainData.ts index caff4b20..c911cddb 100644 --- a/test/lib/onchainData.ts +++ b/test/lib/onchainData.ts @@ -1,5 +1,5 @@ import { JsonRpcProvider } from '@ethersproject/providers'; -import { formatFixed, BigNumber } from '@ethersproject/bignumber'; +import { formatFixed } from '@ethersproject/bignumber'; import { Provider } from '@ethersproject/providers'; import { isSameAddress } from '../../src/utils'; @@ -12,6 +12,7 @@ import composableStablePoolAbi from '../../src/pools/composableStable/Composable import elementPoolAbi from '../../src/pools/elementPool/ConvergentCurvePool.json'; import linearPoolAbi from '../../src/pools/linearPool/linearPoolAbi.json'; import fxPoolAbi from '../../src/pools/xaveFxPool/fxPoolAbi.json'; +import gyroEV2Abi from '../../src/pools/gyroEV2Pool/gyroEV2Abi.json'; import { PoolFilter, SubgraphPoolBase, PoolDataService } from '../../src'; import { Multicaller } from './multicaller'; import { Fragment, JsonFragment } from '@ethersproject/abi/lib/fragments'; @@ -37,6 +38,7 @@ export async function getOnChainBalances( ...linearPoolAbi, ...composableStablePoolAbi, ...fxPoolAbi, + ...gyroEV2Abi, ].map((row) => [row.name, row]) ) ); @@ -147,7 +149,7 @@ export async function getOnChainBalances( pool.address, 'getSwapFeePercentage' ); - if (pool.poolType.toString() === 'GyroE') { + if (pool.poolType.toString() === 'GyroEV2') { multiPool.call( `${pool.id}.tokenRates`, pool.address, @@ -172,7 +174,7 @@ export async function getOnChainBalances( totalSupply: string; virtualSupply?: string; actualSupply?: string; - tokenRates?: [BigNumber, BigNumber]; + tokenRates?: string[]; } >; @@ -191,7 +193,7 @@ export async function getOnChainBalances( totalSupply: string; virtualSupply?: string; actualSupply?: string; - tokenRates?: [BigNumber, BigNumber]; + tokenRates?: string[]; } >; } catch (err) { @@ -308,10 +310,16 @@ export async function getOnChainBalances( subgraphPools[index].totalShares = formatFixed(totalSupply, 18); } - if (subgraphPools[index].poolType === 'GyroE') { - if (tokenRates && tokenRates.length) { - subgraphPools[index].tokenRates = tokenRates; + if (subgraphPools[index].poolType === 'GyroEV2') { + if (!Array.isArray(tokenRates) || tokenRates.length !== 2) { + console.error( + `GyroEV2 pool with missing or invalid tokenRates: ${poolId}` + ); + return; } + subgraphPools[index].tokenRates = tokenRates.map((rate) => + formatFixed(rate, 18) + ); } onChainPools.push(subgraphPools[index]); diff --git a/tsconfig.json b/tsconfig.json index c9120c5c..62deea93 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,8 @@ "src/pools/elementPool/ConvergentCurvePool.json", "src/pools/linearPool/linearPoolAbi.json", "src/pools/composableStable/ComposableStable.json", - "src/pools/xaveFxPool/fxPoolAbi.json" + "src/pools/xaveFxPool/fxPoolAbi.json", + "src/pools/gyroEV2Pool/gyroEV2Abi.json" ], "files": ["hardhat.config.ts"] } From 6a969bc0fab8f946e266d64f47afb194291f7435 Mon Sep 17 00:00:00 2001 From: Steffen Schuldenzucker Date: Sat, 25 Mar 2023 15:47:30 +0100 Subject: [PATCH 03/16] Implement rate scaling in math for GyroEV2 pool --- .../gyroEV2Math/gyroEV2MathHelpers.ts | 9 +- src/pools/gyroEV2Pool/gyroEV2Pool.ts | 93 +++++++++++++++---- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts b/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts index e2363f7f..ea9aa7e8 100644 --- a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts +++ b/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts @@ -68,12 +68,13 @@ export function addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { //////// export function normalizeBalances( balances: BigNumber[], - decimals: number[] + decimals: number[], + tokenRates: BigNumber[] ): BigNumber[] { const scalingFactors = decimals.map((d) => parseFixed('1', d)); return balances.map((bal, index) => - bal.mul(ONE).div(scalingFactors[index]) + bal.mul(ONE).div(scalingFactors[index]).mul(tokenRates[index]) ); } @@ -87,6 +88,10 @@ export function balancesFromTokenInOut( : [balanceTokenOut, balanceTokenIn]; } +// Alias for code readability. +export const valuesInOutFrom01 = balancesFromTokenInOut; + + ///////// /// INVARIANT CALC ///////// diff --git a/src/pools/gyroEV2Pool/gyroEV2Pool.ts b/src/pools/gyroEV2Pool/gyroEV2Pool.ts index 339dec6d..07196fd8 100644 --- a/src/pools/gyroEV2Pool/gyroEV2Pool.ts +++ b/src/pools/gyroEV2Pool/gyroEV2Pool.ts @@ -17,6 +17,7 @@ import { Vector2, normalizeBalances, balancesFromTokenInOut, + valuesInOutFrom01, reduceFee, addFee, virtualOffset0, @@ -235,9 +236,15 @@ export class GyroEV2Pool implements PoolBase { swapType: SwapTypes ): OldBigNumber { if (swapType === SwapTypes.SwapExactIn) { + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); const normalizedBalances = normalizeBalances( [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut] + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut ); const orderedNormalizedBalances = balancesFromTokenInOut( normalizedBalances[0], @@ -270,6 +277,8 @@ export class GyroEV2Pool implements PoolBase { ); const limitAmountIn = maxAmountInAssetInPool.sub( normalizedBalances[0] + ).div( + tokenRateInOut[0] ); const limitAmountInPlusSwapFee = divDown( limitAmountIn, @@ -312,9 +321,15 @@ export class GyroEV2Pool implements PoolBase { poolPairData: GyroEPoolPairData, amount: OldBigNumber ): OldBigNumber { + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); const normalizedBalances = normalizeBalances( [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut] + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut ); const orderedNormalizedBalances = balancesFromTokenInOut( normalizedBalances[0], @@ -333,14 +348,16 @@ export class GyroEV2Pool implements PoolBase { }; const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); - const outAmount = calcOutGivenIn( + const inAmountLessFeeScaled = inAmountLessFee.mul(tokenRateInOut[0]); + const outAmountScaled = calcOutGivenIn( orderedNormalizedBalances, - inAmountLessFee, + inAmountLessFeeScaled, poolPairData.tokenInIsToken0, this.gyroEParams, this.derivedGyroEParams, invariant ); + const outAmount = outAmountScaled.div(tokenRateInOut[1]); return bnum(formatFixed(outAmount, 18)); } @@ -348,9 +365,15 @@ export class GyroEV2Pool implements PoolBase { poolPairData: GyroEPoolPairData, amount: OldBigNumber ): OldBigNumber { + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); const normalizedBalances = normalizeBalances( [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut] + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut ); const orderedNormalizedBalances = balancesFromTokenInOut( normalizedBalances[0], @@ -367,15 +390,17 @@ export class GyroEV2Pool implements PoolBase { y: currentInvariant, }; const outAmount = safeParseFixed(amount.toString(), 18); + const outAmountScaled = outAmount.mul(tokenRateInOut[1]); - const inAmountLessFee = calcInGivenOut( + const inAmountScaledLessFee = calcInGivenOut( orderedNormalizedBalances, - outAmount, + outAmountScaled, poolPairData.tokenInIsToken0, this.gyroEParams, this.derivedGyroEParams, invariant ); + const inAmountLessFee = inAmountScaledLessFee.div(tokenRateInOut[0]); const inAmount = addFee(inAmountLessFee, poolPairData.swapFee); return bnum(formatFixed(inAmount, 18)); } @@ -396,9 +421,15 @@ export class GyroEV2Pool implements PoolBase { poolPairData: GyroEPoolPairData, amount: OldBigNumber ): OldBigNumber { + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); const normalizedBalances = normalizeBalances( [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut] + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut ); const orderedNormalizedBalances = balancesFromTokenInOut( normalizedBalances[0], @@ -416,15 +447,17 @@ export class GyroEV2Pool implements PoolBase { }; const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); - const newSpotPrice = calcSpotPriceAfterSwapOutGivenIn( + const inAmountLessFeeScaled = inAmountLessFee.mul(tokenRateInOut[0]); + const newSpotPriceScaled = calcSpotPriceAfterSwapOutGivenIn( orderedNormalizedBalances, - inAmountLessFee, + inAmountLessFeeScaled, poolPairData.tokenInIsToken0, this.gyroEParams, this.derivedGyroEParams, invariant, poolPairData.swapFee ); + const newSpotPrice = newSpotPriceScaled.mul(tokenRateInOut[1]).div(tokenRateInOut[0]); return bnum(formatFixed(newSpotPrice, 18)); } @@ -432,9 +465,15 @@ export class GyroEV2Pool implements PoolBase { poolPairData: GyroEPoolPairData, amount: OldBigNumber ): OldBigNumber { + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); const normalizedBalances = normalizeBalances( [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut] + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut ); const orderedNormalizedBalances = balancesFromTokenInOut( normalizedBalances[0], @@ -451,15 +490,17 @@ export class GyroEV2Pool implements PoolBase { y: currentInvariant, }; const outAmount = safeParseFixed(amount.toString(), 18); - const newSpotPrice = calcSpotPriceAfterSwapInGivenOut( + const outAmountScaled = outAmount.mul(tokenRateInOut[1]); + const newSpotPriceScaled = calcSpotPriceAfterSwapInGivenOut( orderedNormalizedBalances, - outAmount, + outAmountScaled, poolPairData.tokenInIsToken0, this.gyroEParams, this.derivedGyroEParams, invariant, poolPairData.swapFee ); + const newSpotPrice = newSpotPriceScaled.mul(tokenRateInOut[1]).div(tokenRateInOut[0]); return bnum(formatFixed(newSpotPrice, 18)); } @@ -468,9 +509,15 @@ export class GyroEV2Pool implements PoolBase { amount: OldBigNumber ): OldBigNumber { const inAmount = safeParseFixed(amount.toString(), 18); + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); const normalizedBalances = normalizeBalances( [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut] + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut ); const orderedNormalizedBalances = balancesFromTokenInOut( normalizedBalances[0], @@ -487,10 +534,10 @@ export class GyroEV2Pool implements PoolBase { y: currentInvariant, }; - const derivative = calcDerivativePriceAfterSwapOutGivenIn( + const derivativeScaled = calcDerivativePriceAfterSwapOutGivenIn( [ orderedNormalizedBalances[0].add( - reduceFee(inAmount, poolPairData.swapFee) + reduceFee(inAmount.mul(tokenRateInOut[0]), poolPairData.swapFee) ), orderedNormalizedBalances[1], ], @@ -500,6 +547,7 @@ export class GyroEV2Pool implements PoolBase { invariant, poolPairData.swapFee ); + const derivative = derivativeScaled.mul(tokenRateInOut[1]); return bnum(formatFixed(derivative, 18)); } @@ -507,9 +555,15 @@ export class GyroEV2Pool implements PoolBase { poolPairData: GyroEPoolPairData, amount: OldBigNumber ): OldBigNumber { + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); const normalizedBalances = normalizeBalances( [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut] + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut ); const orderedNormalizedBalances = balancesFromTokenInOut( normalizedBalances[0], @@ -526,10 +580,10 @@ export class GyroEV2Pool implements PoolBase { y: currentInvariant, }; const outAmount = safeParseFixed(amount.toString(), 18); - const derivative = calcDerivativeSpotPriceAfterSwapInGivenOut( + const derivativeScaled = calcDerivativeSpotPriceAfterSwapInGivenOut( [ orderedNormalizedBalances[0], - orderedNormalizedBalances[1].sub(outAmount), + orderedNormalizedBalances[1].sub(outAmount.mul(tokenRateInOut[1])), ], poolPairData.tokenInIsToken0, this.gyroEParams, @@ -537,6 +591,7 @@ export class GyroEV2Pool implements PoolBase { invariant, poolPairData.swapFee ); + const derivative = derivativeScaled.mul(tokenRateInOut[1].pow(2)).div(tokenRateInOut[0]); return bnum(formatFixed(derivative, 18)); } } From 1ff318a112c9176a03012c7e8674a7c8555cb90e Mon Sep 17 00:00:00 2001 From: Steffen Schuldenzucker Date: Mon, 27 Mar 2023 11:07:18 +0200 Subject: [PATCH 04/16] Add tests for GyroEV2Pool --- test/gyroEV2Pool.spec.ts | 196 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 test/gyroEV2Pool.spec.ts diff --git a/test/gyroEV2Pool.spec.ts b/test/gyroEV2Pool.spec.ts new file mode 100644 index 00000000..a7094541 --- /dev/null +++ b/test/gyroEV2Pool.spec.ts @@ -0,0 +1,196 @@ +// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyroEPool.spec.ts + +import { GyroEPoolPairData } from '../src/pools/gyroEV2Pool/gyroEV2Pool'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { expect } from 'chai'; +import { GyroEV2Pool } from '../src/pools/gyroEV2Pool/gyroEV2Pool'; +import { SwapTypes } from '../src/types'; +import { bnum } from '../src/utils/bignumber'; +import { reduceFee } from '../src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers'; + +const TEST_POOL_PAIR_DATA: GyroEPoolPairData = { + id: '123', + address: '123', + poolType: 1, + swapFee: ONE.mul(9).div(100), + tokenIn: '123', + tokenOut: '123', + decimalsIn: 18, + decimalsOut: 18, + balanceIn: ONE.mul(100), + balanceOut: ONE.mul(100), + tokenInIsToken0: true, +}; + +const POOL = GyroEV2Pool.fromPool({ + id: '1', + address: '1', + poolType: 'GyroE', + swapFee: '0.09', + swapEnabled: true, + totalShares: '100', + tokens: [ + { + address: '1', + balance: '66.66666666666667', // ~ 100/1.5 so that the rate-scaled balances are about the same + decimals: 18, + priceRate: '1', + weight: null, + }, + { + address: '2', + balance: '100', + decimals: 18, + priceRate: '1', + weight: null, + }, + ], + tokensList: ['1', '2'], + tokenRates: ['1.5', '1'], + // GYRO E-CLP PARAMS + alpha: '0.050000000000020290', + beta: '0.397316269897841178', + c: '0.9551573261744535', + s: '0.29609877111408056', + lambda: '748956.475000000000000000', + // GYRO E-CLP DERIVED PARAMS + tauAlphaX: '-0.99999999998640216827321822090250869512', + tauAlphaY: '0.00000521494821273352387635736307999088', + tauBetaX: '0.99999999985251225321221463296419833612', + tauBetaY: '0.00001717485123551095031292618834391386', + u: '0.56564182095617502122541689600223111041', + v: '0.00000626352651807875756896296543790835', + w: '0.00000338251066240397957902003753652350', + z: '0.82465103535609803284538786438983276111', + dSq: '1.00000000000000002140811391783216360000', +}); + +describe('gyroEPool tests', () => { + const poolPairData = TEST_POOL_PAIR_DATA; + + context('normalized liquidity', () => { + it(`should correctly calculate normalized liquidity`, async () => { + const normalizedLiquidity = + POOL.getNormalizedLiquidity(poolPairData); + + expect(Number(normalizedLiquidity)).to.be.approximately( + 8521784.473067058, + 0.00001 + ); + }); + }); + + context('limit amount swap', () => { + it(`should correctly calculate limit amount for swap exact in`, async () => { + const limitAmount = POOL.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactIn + ); + + expect(Number(limitAmount)).to.be.approximately( + 237.276550936135363786, + 0.00001 + ); + }); + + it(`should correctly calculate limit amount for swap exact out`, async () => { + const limitAmount = POOL.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactOut + ); + + expect(Number(limitAmount)).to.be.approximately(99.9999, 0.00001); + }); + }); + + context('swap amounts', () => { + it(`should correctly calculate swap amount for swap exact in`, async () => { + const swapAmount = POOL._exactTokenInForTokenOut( + poolPairData, + bnum('10') + ); + + expect(Number(swapAmount)).to.be.approximately( + 4.231511373250766852, + 0.00001 + ); + }); + + it(`should correctly calculate swap amount for swap exact out`, async () => { + const swapAmount = POOL._tokenInForExactTokenOut( + poolPairData, + bnum('10') + ); + + const reduced = formatFixed( + reduceFee( + parseFixed(swapAmount.toString(), 18), + poolPairData.swapFee + ), + 18 + ); + + expect(Number(reduced)).to.be.approximately( + 23.632225157442760466, + 0.00001 + ); + }); + }); + + context('prices', () => { + it(`should correctly calculate price after swap exact in`, async () => { + const priceAfterSwap = + POOL._spotPriceAfterSwapExactTokenInForTokenOut( + poolPairData, + bnum('10') + ); + + expect(Number(priceAfterSwap)).to.be.approximately( + 2.363222355995745212, + 0.00001 + ); + }); + + it(`should correctly calculate price after swap exact out`, async () => { + const priceAfterSwap = + POOL._spotPriceAfterSwapTokenInForExactTokenOut( + poolPairData, + bnum('10') + ); + + expect(Number(priceAfterSwap)).to.be.approximately( + 2.363223669898799537, + 0.00001 + ); + }); + }); + + context('derivative of price', () => { + it(`should correctly calculate derivative of price after swap exact in`, async () => { + const priceDerivative = + POOL._derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData, + bnum('10') + ); + + expect(Number(priceDerivative)).to.be.approximately( + 1.03343127137E-7, + 0.00001 + ); + }); + + it(`should correctly calculate derivative of price after swap exact out`, async () => { + const priceDerivative = + POOL._derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + poolPairData, + bnum('10') + ); + + expect(Number(priceDerivative)).to.be.approximately( + 2.13539511340E-7, + 0.00001 + ); + }); + }); +}); From c9c72d94fd4997f8514246e034d09aecf4609ba2 Mon Sep 17 00:00:00 2001 From: Steffen Schuldenzucker Date: Wed, 29 Mar 2023 13:52:22 +0200 Subject: [PATCH 05/16] Fix GyroEV2Pool: Use the right math fcts --- .../gyroEV2Math/gyroEV2MathHelpers.ts | 2 +- src/pools/gyroEV2Pool/gyroEV2Pool.ts | 29 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts b/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts index ea9aa7e8..32617b66 100644 --- a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts +++ b/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts @@ -74,7 +74,7 @@ export function normalizeBalances( const scalingFactors = decimals.map((d) => parseFixed('1', d)); return balances.map((bal, index) => - bal.mul(ONE).div(scalingFactors[index]).mul(tokenRates[index]) + mulDown(bal.mul(ONE).div(scalingFactors[index]), tokenRates[index]) ); } diff --git a/src/pools/gyroEV2Pool/gyroEV2Pool.ts b/src/pools/gyroEV2Pool/gyroEV2Pool.ts index 07196fd8..fffa6bcd 100644 --- a/src/pools/gyroEV2Pool/gyroEV2Pool.ts +++ b/src/pools/gyroEV2Pool/gyroEV2Pool.ts @@ -275,9 +275,8 @@ export class GyroEV2Pool implements PoolBase { true ) ); - const limitAmountIn = maxAmountInAssetInPool.sub( - normalizedBalances[0] - ).div( + const limitAmountIn = divDown( + maxAmountInAssetInPool.sub(normalizedBalances[0]), tokenRateInOut[0] ); const limitAmountInPlusSwapFee = divDown( @@ -348,7 +347,7 @@ export class GyroEV2Pool implements PoolBase { }; const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); - const inAmountLessFeeScaled = inAmountLessFee.mul(tokenRateInOut[0]); + const inAmountLessFeeScaled = mulDown(inAmountLessFee, tokenRateInOut[0]); const outAmountScaled = calcOutGivenIn( orderedNormalizedBalances, inAmountLessFeeScaled, @@ -357,7 +356,7 @@ export class GyroEV2Pool implements PoolBase { this.derivedGyroEParams, invariant ); - const outAmount = outAmountScaled.div(tokenRateInOut[1]); + const outAmount = divDown(outAmountScaled, tokenRateInOut[1]); return bnum(formatFixed(outAmount, 18)); } @@ -390,7 +389,7 @@ export class GyroEV2Pool implements PoolBase { y: currentInvariant, }; const outAmount = safeParseFixed(amount.toString(), 18); - const outAmountScaled = outAmount.mul(tokenRateInOut[1]); + const outAmountScaled = mulDown(outAmount, tokenRateInOut[1]); const inAmountScaledLessFee = calcInGivenOut( orderedNormalizedBalances, @@ -400,7 +399,7 @@ export class GyroEV2Pool implements PoolBase { this.derivedGyroEParams, invariant ); - const inAmountLessFee = inAmountScaledLessFee.div(tokenRateInOut[0]); + const inAmountLessFee = divDown(inAmountScaledLessFee, tokenRateInOut[0]); const inAmount = addFee(inAmountLessFee, poolPairData.swapFee); return bnum(formatFixed(inAmount, 18)); } @@ -447,7 +446,7 @@ export class GyroEV2Pool implements PoolBase { }; const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); - const inAmountLessFeeScaled = inAmountLessFee.mul(tokenRateInOut[0]); + const inAmountLessFeeScaled = mulDown(inAmountLessFee, tokenRateInOut[0]); const newSpotPriceScaled = calcSpotPriceAfterSwapOutGivenIn( orderedNormalizedBalances, inAmountLessFeeScaled, @@ -457,7 +456,7 @@ export class GyroEV2Pool implements PoolBase { invariant, poolPairData.swapFee ); - const newSpotPrice = newSpotPriceScaled.mul(tokenRateInOut[1]).div(tokenRateInOut[0]); + const newSpotPrice = divDown(mulDown(newSpotPriceScaled, tokenRateInOut[1]), tokenRateInOut[0]); return bnum(formatFixed(newSpotPrice, 18)); } @@ -490,7 +489,7 @@ export class GyroEV2Pool implements PoolBase { y: currentInvariant, }; const outAmount = safeParseFixed(amount.toString(), 18); - const outAmountScaled = outAmount.mul(tokenRateInOut[1]); + const outAmountScaled = mulDown(outAmount, tokenRateInOut[1]); const newSpotPriceScaled = calcSpotPriceAfterSwapInGivenOut( orderedNormalizedBalances, outAmountScaled, @@ -500,7 +499,7 @@ export class GyroEV2Pool implements PoolBase { invariant, poolPairData.swapFee ); - const newSpotPrice = newSpotPriceScaled.mul(tokenRateInOut[1]).div(tokenRateInOut[0]); + const newSpotPrice = divDown(mulDown(newSpotPriceScaled, tokenRateInOut[1]), tokenRateInOut[0]); return bnum(formatFixed(newSpotPrice, 18)); } @@ -537,7 +536,7 @@ export class GyroEV2Pool implements PoolBase { const derivativeScaled = calcDerivativePriceAfterSwapOutGivenIn( [ orderedNormalizedBalances[0].add( - reduceFee(inAmount.mul(tokenRateInOut[0]), poolPairData.swapFee) + reduceFee(mulDown(inAmount, tokenRateInOut[0]), poolPairData.swapFee) ), orderedNormalizedBalances[1], ], @@ -547,7 +546,7 @@ export class GyroEV2Pool implements PoolBase { invariant, poolPairData.swapFee ); - const derivative = derivativeScaled.mul(tokenRateInOut[1]); + const derivative = mulDown(derivativeScaled, tokenRateInOut[1]); return bnum(formatFixed(derivative, 18)); } @@ -583,7 +582,7 @@ export class GyroEV2Pool implements PoolBase { const derivativeScaled = calcDerivativeSpotPriceAfterSwapInGivenOut( [ orderedNormalizedBalances[0], - orderedNormalizedBalances[1].sub(outAmount.mul(tokenRateInOut[1])), + orderedNormalizedBalances[1].sub(mulDown(outAmount, tokenRateInOut[1])), ], poolPairData.tokenInIsToken0, this.gyroEParams, @@ -591,7 +590,7 @@ export class GyroEV2Pool implements PoolBase { invariant, poolPairData.swapFee ); - const derivative = derivativeScaled.mul(tokenRateInOut[1].pow(2)).div(tokenRateInOut[0]); + const derivative = divDown(mulDown(derivativeScaled, tokenRateInOut[1].pow(2)), tokenRateInOut[0]); return bnum(formatFixed(derivative, 18)); } } From e7a0604d64c190cd1737274e4390d4540bf6a506 Mon Sep 17 00:00:00 2001 From: Steffen Schuldenzucker Date: Fri, 31 Mar 2023 22:30:17 +0200 Subject: [PATCH 06/16] Fix wrong arith function --- src/pools/gyroEV2Pool/gyroEV2Pool.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pools/gyroEV2Pool/gyroEV2Pool.ts b/src/pools/gyroEV2Pool/gyroEV2Pool.ts index fffa6bcd..56319cf4 100644 --- a/src/pools/gyroEV2Pool/gyroEV2Pool.ts +++ b/src/pools/gyroEV2Pool/gyroEV2Pool.ts @@ -590,7 +590,8 @@ export class GyroEV2Pool implements PoolBase { invariant, poolPairData.swapFee ); - const derivative = divDown(mulDown(derivativeScaled, tokenRateInOut[1].pow(2)), tokenRateInOut[0]); + const rateAdjFactor = divDown(mulDown(tokenRateInOut[1], tokenRateInOut[1]), tokenRateInOut[0]); + const derivative = mulDown(derivativeScaled, rateAdjFactor); return bnum(formatFixed(derivative, 18)); } } From b77e7463c539206a05945dcaa83956e2589bab55 Mon Sep 17 00:00:00 2001 From: Steffen Schuldenzucker Date: Fri, 31 Mar 2023 22:31:03 +0200 Subject: [PATCH 07/16] Fix gyroEV2Pool tests, tighten bounds --- test/gyroEV2Pool.spec.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/test/gyroEV2Pool.spec.ts b/test/gyroEV2Pool.spec.ts index a7094541..cae42130 100644 --- a/test/gyroEV2Pool.spec.ts +++ b/test/gyroEV2Pool.spec.ts @@ -2,11 +2,11 @@ import { GyroEPoolPairData } from '../src/pools/gyroEV2Pool/gyroEV2Pool'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import {BigNumber, formatFixed, parseFixed} from '@ethersproject/bignumber'; import { expect } from 'chai'; import { GyroEV2Pool } from '../src/pools/gyroEV2Pool/gyroEV2Pool'; import { SwapTypes } from '../src/types'; -import { bnum } from '../src/utils/bignumber'; +import {bnum, ZERO} from '../src/utils/bignumber'; import { reduceFee } from '../src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers'; const TEST_POOL_PAIR_DATA: GyroEPoolPairData = { @@ -18,7 +18,7 @@ const TEST_POOL_PAIR_DATA: GyroEPoolPairData = { tokenOut: '123', decimalsIn: 18, decimalsOut: 18, - balanceIn: ONE.mul(100), + balanceIn: BigNumber.from('66666666666666672128'), // ~ 100/1.5 so that the rate-scaled balances are about the same balanceOut: ONE.mul(100), tokenInIsToken0: true, }; @@ -87,9 +87,8 @@ describe('gyroEPool tests', () => { poolPairData, SwapTypes.SwapExactIn ); - expect(Number(limitAmount)).to.be.approximately( - 237.276550936135363786, + 236.323201823051507893, 0.00001 ); }); @@ -132,8 +131,8 @@ describe('gyroEPool tests', () => { ); expect(Number(reduced)).to.be.approximately( - 23.632225157442760466, - 0.00001 + 21.505324893272912024, + 0.0001 ); }); }); @@ -176,10 +175,23 @@ describe('gyroEPool tests', () => { expect(Number(priceDerivative)).to.be.approximately( 1.03343127137E-7, - 0.00001 + 1E-12 ); }); + it(`should correctly calculate derivative of price after swap exact in at 0`, async () => { + const priceDerivative = + POOL._derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData, + bnum('0') + ); + + expect(Number(priceDerivative)).to.be.approximately( + 1.17346314397E-7, + 1E-12 + ); + }) + it(`should correctly calculate derivative of price after swap exact out`, async () => { const priceDerivative = POOL._derivativeSpotPriceAfterSwapTokenInForExactTokenOut( @@ -189,7 +201,7 @@ describe('gyroEPool tests', () => { expect(Number(priceDerivative)).to.be.approximately( 2.13539511340E-7, - 0.00001 + 1E-12 ); }); }); From d00161d25b71ec1eeb9664af5061b27810a89c99 Mon Sep 17 00:00:00 2001 From: Steffen Schuldenzucker Date: Fri, 31 Mar 2023 22:31:28 +0200 Subject: [PATCH 08/16] GyroEPool tests: Tighten bounds for small number --- test/gyroEPool.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/gyroEPool.spec.ts b/test/gyroEPool.spec.ts index 8dd8d864..54af86ae 100644 --- a/test/gyroEPool.spec.ts +++ b/test/gyroEPool.spec.ts @@ -188,7 +188,7 @@ describe('gyroEPool tests', () => { expect(Number(priceDerivative)).to.be.approximately( 3.2030926701e-7, - 0.00001 + 1E-12 ); }); }); From e8a72ba29e919a079c7523004ffee1a80766108a Mon Sep 17 00:00:00 2001 From: Steffen Schuldenzucker Date: Fri, 31 Mar 2023 22:49:39 +0200 Subject: [PATCH 09/16] Remove redundant gyroEV2Math libraries These were mostly exact copies of the gyroEMath libraries. All the scaling happens in the pool file. --- .../gyroEV2Pool/gyroEV2Math/constants.ts | 6 - .../gyroEV2Pool/gyroEV2Math/gyroEV2Math.ts | 250 --------- .../gyroEV2Math/gyroEV2MathFunctions.ts | 411 --------------- .../gyroEV2Math/gyroEV2MathHelpers.ts | 489 ------------------ src/pools/gyroEV2Pool/gyroEV2Pool.ts | 12 +- test/gyroEV2Pool.spec.ts | 2 +- 6 files changed, 9 insertions(+), 1161 deletions(-) delete mode 100644 src/pools/gyroEV2Pool/gyroEV2Math/constants.ts delete mode 100644 src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2Math.ts delete mode 100644 src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathFunctions.ts diff --git a/src/pools/gyroEV2Pool/gyroEV2Math/constants.ts b/src/pools/gyroEV2Pool/gyroEV2Math/constants.ts deleted file mode 100644 index 558f164e..00000000 --- a/src/pools/gyroEV2Pool/gyroEV2Math/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { BigNumber } from '@ethersproject/bignumber'; - -export const MAX_BALANCES = BigNumber.from(10).pow(34); // 1e16 in normal precision - -// Invariant calculation -export const MAX_INVARIANT = BigNumber.from(10).pow(37).mul(3); // 3e19 in normal precision diff --git a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2Math.ts b/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2Math.ts deleted file mode 100644 index b6b9cc47..00000000 --- a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2Math.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { BigNumber } from '@ethersproject/bignumber'; -import { WeiPerEther as ONE } from '@ethersproject/constants'; -import { MAX_BALANCES, MAX_INVARIANT } from './constants'; -import { ONE_XP, SMALL } from '../../gyroHelpers/constants'; -import { - GyroEParams, - DerivedGyroEParams, - Vector2, - calcAtAChi, - calcInvariantSqrt, - calcAChiAChiInXp, - calcXGivenY, - calcYGivenX, - checkAssetBounds, -} from './gyroEV2MathHelpers'; -import { - mulDown, - divDown, - mulUpMagU, - divUpMagU, - mulUpXpToNpU, - mulDownXpToNpU, - divXpU, - sqrt, -} from '../../gyroHelpers/gyroSignedFixedPoint'; -import { - normalizedLiquidityXIn, - normalizedLiquidityYIn, - calcSpotPriceXGivenY, - calcSpotPriceYGivenX, - dPxDXOut, - dPxDYIn, - dPyDXIn, - dPyDYOut, -} from './gyroEV2MathFunctions'; - -export function calculateNormalizedLiquidity( - balances: BigNumber[], - params: GyroEParams, - derived: DerivedGyroEParams, - r: Vector2, - fee: BigNumber, - tokenInIsToken0: boolean -): BigNumber { - if (tokenInIsToken0) { - return normalizedLiquidityXIn(balances, params, derived, fee, r); - } else { - return normalizedLiquidityYIn(balances, params, derived, fee, r); - } -} - -export function calculateInvariantWithError( - balances: BigNumber[], - params: GyroEParams, - derived: DerivedGyroEParams -): [BigNumber, BigNumber] { - const [x, y] = balances; - - if (x.add(y).gt(MAX_BALANCES)) throw new Error('MAX ASSETS EXCEEDED'); - const AtAChi = calcAtAChi(x, y, params, derived); - - const invariantResult = calcInvariantSqrt(x, y, params, derived); - const square_root = invariantResult[0]; - let err = invariantResult[1]; - - if (square_root.gt(0)) { - err = divUpMagU(err.add(1), square_root.mul(2)); - } else { - err = err.gt(0) - ? sqrt(err, BigNumber.from(5)) - : BigNumber.from(10).pow(9); - } - - err = mulUpMagU(params.lambda, x.add(y)) - .div(ONE_XP) - .add(err) - .add(1) - .mul(20); - - const mulDenominator = divXpU( - ONE_XP, - calcAChiAChiInXp(params, derived).sub(ONE_XP) - ); - const invariant = mulDownXpToNpU( - AtAChi.add(square_root).sub(err), - mulDenominator - ); - err = mulUpXpToNpU(err, mulDenominator); - - err = err - .add( - mulUpXpToNpU(invariant, mulDenominator) - .mul( - params.lambda - .mul(params.lambda) - .div(BigNumber.from(10).pow(36)) - ) - .mul(40) - .div(ONE_XP) - ) - .add(1); - - if (invariant.add(err).gt(MAX_INVARIANT)) - throw new Error('MAX INVARIANT EXCEEDED'); - - return [invariant, err]; -} - -export function calcOutGivenIn( - balances: BigNumber[], - amountIn: BigNumber, - tokenInIsToken0: boolean, - params: GyroEParams, - derived: DerivedGyroEParams, - invariant: Vector2 -): BigNumber { - if (amountIn.lt(SMALL)) return BigNumber.from(0); - - const ixIn = Number(!tokenInIsToken0); - const ixOut = Number(tokenInIsToken0); - - const calcGiven = tokenInIsToken0 ? calcYGivenX : calcXGivenY; - - const balInNew = balances[ixIn].add(amountIn); - - checkAssetBounds(params, derived, invariant, balInNew, ixIn); - const balOutNew = calcGiven(balInNew, params, derived, invariant); - const amountOut = balances[ixOut].sub(balOutNew); - if (amountOut.lt(0)) { - // Should never happen; check anyways to catch a numerical bug. - throw new Error('ASSET BOUNDS EXCEEDED 1'); - } - - return amountOut; -} - -export function calcInGivenOut( - balances: BigNumber[], - amountOut: BigNumber, - tokenInIsToken0: boolean, - params: GyroEParams, - derived: DerivedGyroEParams, - invariant: Vector2 -): BigNumber { - if (amountOut.lt(SMALL)) return BigNumber.from(0); - - const ixIn = Number(!tokenInIsToken0); - const ixOut = Number(tokenInIsToken0); - - const calcGiven = tokenInIsToken0 ? calcXGivenY : calcYGivenX; - - if (amountOut.gt(balances[ixOut])) - throw new Error('ASSET BOUNDS EXCEEDED 2'); - const balOutNew = balances[ixOut].sub(amountOut); - - const balInNew = calcGiven(balOutNew, params, derived, invariant); - checkAssetBounds(params, derived, invariant, balInNew, ixIn); - const amountIn = balInNew.sub(balances[ixIn]); - - if (amountIn.lt(0)) - // Should never happen; check anyways to catch a numerical bug. - throw new Error('ASSET BOUNDS EXCEEDED 3'); - return amountIn; -} - -export function calcSpotPriceAfterSwapOutGivenIn( - balances: BigNumber[], - amountIn: BigNumber, - tokenInIsToken0: boolean, - params: GyroEParams, - derived: DerivedGyroEParams, - invariant: Vector2, - swapFee: BigNumber -): BigNumber { - const ixIn = Number(!tokenInIsToken0); - const f = ONE.sub(swapFee); - - const calcSpotPriceGiven = tokenInIsToken0 - ? calcSpotPriceYGivenX - : calcSpotPriceXGivenY; - - const balInNew = balances[ixIn].add(amountIn); - const newSpotPriceFactor = calcSpotPriceGiven( - balInNew, - params, - derived, - invariant - ); - return divDown(ONE, mulDown(newSpotPriceFactor, f)); -} - -export function calcSpotPriceAfterSwapInGivenOut( - balances: BigNumber[], - amountOut: BigNumber, - tokenInIsToken0: boolean, - params: GyroEParams, - derived: DerivedGyroEParams, - invariant: Vector2, - swapFee: BigNumber -): BigNumber { - const ixOut = Number(tokenInIsToken0); - const f = ONE.sub(swapFee); - - const calcSpotPriceGiven = tokenInIsToken0 - ? calcSpotPriceXGivenY - : calcSpotPriceYGivenX; - - const balOutNew = balances[ixOut].sub(amountOut); - const newSpotPriceFactor = calcSpotPriceGiven( - balOutNew, - params, - derived, - invariant - ); - return divDown(newSpotPriceFactor, f); -} - -export function calcDerivativePriceAfterSwapOutGivenIn( - balances: BigNumber[], - tokenInIsToken0: boolean, - params: GyroEParams, - derived: DerivedGyroEParams, - invariant: Vector2, - swapFee: BigNumber -): BigNumber { - const ixIn = Number(!tokenInIsToken0); - - const newDerivativeSpotPriceFactor = ixIn - ? dPxDYIn(balances, params, derived, swapFee, invariant) - : dPyDXIn(balances, params, derived, swapFee, invariant); - - return newDerivativeSpotPriceFactor; -} - -export function calcDerivativeSpotPriceAfterSwapInGivenOut( - balances: BigNumber[], - tokenInIsToken0: boolean, - params: GyroEParams, - derived: DerivedGyroEParams, - invariant: Vector2, - swapFee: BigNumber -): BigNumber { - const ixIn = Number(!tokenInIsToken0); - - const newDerivativeSpotPriceFactor = ixIn - ? dPxDXOut(balances, params, derived, swapFee, invariant) - : dPyDYOut(balances, params, derived, swapFee, invariant); - - return newDerivativeSpotPriceFactor; -} diff --git a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathFunctions.ts b/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathFunctions.ts deleted file mode 100644 index 72f5a83d..00000000 --- a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathFunctions.ts +++ /dev/null @@ -1,411 +0,0 @@ -import { BigNumber } from '@ethersproject/bignumber'; -import { WeiPerEther as ONE } from '@ethersproject/constants'; -import { - GyroEParams, - DerivedGyroEParams, - Vector2, - QParams, - virtualOffset0, - virtualOffset1, -} from './gyroEV2MathHelpers'; -import { ONE_XP } from '../../gyroHelpers/constants'; -import { - mulDown, - divDown, - mulDownMagU, - divDownMagU, - mulUpMagU, - divUpMagU, - mulUpXpToNpU, - mulDownXpToNpU, - divXpU, - sqrt, -} from '../../gyroHelpers/gyroSignedFixedPoint'; -import { calcXpXpDivLambdaLambda } from './gyroEV2MathHelpers'; - -///////// -/// SPOT PRICE AFTER SWAP CALCULATIONS -///////// - -export function calcSpotPriceYGivenX( - x: BigNumber, - params: GyroEParams, - d: DerivedGyroEParams, - r: Vector2 -): BigNumber { - const ab: Vector2 = { - x: virtualOffset0(params, d, r), - y: virtualOffset1(params, d, r), - }; - const newSpotPriceFactor = solveDerivativeQuadraticSwap( - params.lambda, - x, - params.s, - params.c, - r, - ab, - d.tauBeta, - d.dSq - ); - return newSpotPriceFactor; -} - -export function calcSpotPriceXGivenY( - y: BigNumber, - params: GyroEParams, - d: DerivedGyroEParams, - r: Vector2 -): BigNumber { - const ba: Vector2 = { - x: virtualOffset1(params, d, r), - y: virtualOffset0(params, d, r), - }; - const newSpotPriceFactor = solveDerivativeQuadraticSwap( - params.lambda, - y, - params.c, - params.s, - r, - ba, - { - x: d.tauAlpha.x.mul(-1), - y: d.tauAlpha.y, - }, - d.dSq - ); - return newSpotPriceFactor; -} - -function solveDerivativeQuadraticSwap( - lambda: BigNumber, - x: BigNumber, - s: BigNumber, - c: BigNumber, - r: Vector2, - ab: Vector2, - tauBeta: Vector2, - dSq: BigNumber -): BigNumber { - const lamBar: Vector2 = { - x: ONE_XP.sub(divDownMagU(divDownMagU(ONE_XP, lambda), lambda)), - y: ONE_XP.sub(divUpMagU(divUpMagU(ONE_XP, lambda), lambda)), - }; - const q: QParams = { - a: BigNumber.from(0), - b: BigNumber.from(0), - c: BigNumber.from(0), - }; - const xp = x.sub(ab.x); - q.b = mulUpXpToNpU(mulDownMagU(s, c), divXpU(lamBar.y, dSq)); - - const sTerm: Vector2 = { - x: divXpU(mulDownMagU(mulDownMagU(lamBar.y, s), s), dSq), - y: divXpU(mulUpMagU(mulUpMagU(lamBar.x, s), s), dSq.add(1)).add(1), - }; - sTerm.x = ONE_XP.sub(sTerm.x); - sTerm.y = ONE_XP.sub(sTerm.y); - - q.c = calcXpXpDivLambdaLambda(x, r, lambda, s, c, tauBeta, dSq).mul(-1); - q.c = q.c.add(mulDownXpToNpU(mulDownMagU(r.y, r.y), sTerm.y)); // r.y === currentInv + err - q.c = q.c.gt(BigNumber.from(0)) - ? sqrt(q.c, BigNumber.from(5)) - : BigNumber.from(0); - - q.c = mulDown(mulDown(q.c, lambda), lambda); - q.c = divDown(xp, q.c); - - if (q.b.sub(q.c).gt(BigNumber.from(0))) { - q.a = mulUpXpToNpU(q.b.sub(q.c), divXpU(ONE_XP, sTerm.y).add(1)); - } else { - q.a = mulUpXpToNpU(q.b.sub(q.c), divXpU(ONE_XP, sTerm.x)); - } - return q.a; -} - -///////// -/// SPOT PRICE DERIVATIVE CALCULATIONS -///////// - -function setup( - balances, - params: GyroEParams, - derived: DerivedGyroEParams, - fee: BigNumber, - rVec: Vector2, - ixVar: number -) { - const r = rVec.y; - const { c, s, lambda } = params; - const [x0, y0] = balances; - const a = virtualOffset0(params, derived, rVec); - const b = virtualOffset1(params, derived, rVec); - const ls = ONE.sub(divDown(ONE, mulDown(lambda, lambda))); - const f = ONE.sub(fee); - - let R: BigNumber; - if (ixVar === 0) { - R = sqrt( - mulDown(mulDown(r, r), ONE.sub(mulDown(ls, mulDown(s, s)))).sub( - divDown(mulDown(x0.sub(a), x0.sub(a)), mulDown(lambda, lambda)) - ), - BigNumber.from(5) - ); - } else { - R = sqrt( - mulDown(mulDown(r, r), ONE.sub(mulDown(ls, mulDown(c, c)))).sub( - divDown(mulDown(y0.sub(b), y0.sub(b)), mulDown(lambda, lambda)) - ), - BigNumber.from(5) - ); - } - - return { x0, y0, c, s, lambda, a, b, ls, f, r, R }; -} - -export function normalizedLiquidityYIn( - balances: BigNumber[], - params: GyroEParams, - derived: DerivedGyroEParams, - fee: BigNumber, - rVec: Vector2 -): BigNumber { - const { y0, c, s, lambda, b, ls, R } = setup( - balances, - params, - derived, - fee, - rVec, - 1 - ); - - const returnValue = divDown( - mulDown( - divDown(ONE, ONE.sub(mulDown(ls, mulDown(c, c)))), - mulDown( - R, - mulDown( - mulDown( - mulDown( - mulDown(mulDown(ls, s), c), - mulDown(lambda, lambda) - ), - R - ).sub(y0.sub(b)), - mulDown( - mulDown( - mulDown(mulDown(ls, s), c), - mulDown(lambda, lambda) - ), - R - ).sub(y0.sub(b)) - ) - ) - ), - mulDown(mulDown(lambda, lambda), mulDown(R, R)).add( - mulDown(y0.sub(b), y0.sub(b)) - ) - ); - - return returnValue; -} - -export function normalizedLiquidityXIn( - balances: BigNumber[], - params: GyroEParams, - derived: DerivedGyroEParams, - fee: BigNumber, - rVec: Vector2 -): BigNumber { - const { x0, c, s, lambda, a, ls, R } = setup( - balances, - params, - derived, - fee, - rVec, - 0 - ); - - const returnValue = divDown( - mulDown( - divDown(ONE, ONE.sub(mulDown(ls, mulDown(s, s)))), - mulDown( - R, - mulDown( - mulDown( - mulDown( - mulDown(mulDown(ls, s), c), - mulDown(lambda, lambda) - ), - R - ).sub(x0.sub(a)), - mulDown( - mulDown( - mulDown(mulDown(ls, s), c), - mulDown(lambda, lambda) - ), - R - ).sub(x0.sub(a)) - ) - ) - ), - mulDown(mulDown(lambda, lambda), mulDown(R, R)).add( - mulDown(x0.sub(a), x0.sub(a)) - ) - ); - - return returnValue; -} - -export function dPyDXIn( - balances: BigNumber[], - params: GyroEParams, - derived: DerivedGyroEParams, - fee: BigNumber, - rVec: Vector2 -): BigNumber { - const { x0, c, s, lambda, a, ls, R } = setup( - balances, - params, - derived, - fee, - rVec, - 0 - ); - - const returnValue = divDown( - mulDown( - ONE.sub(mulDown(ls, mulDown(s, s))), - divDown(ONE, mulDown(mulDown(lambda, lambda), R)).add( - divDown( - mulDown(x0.sub(a), x0.sub(a)), - mulDown( - mulDown( - mulDown(lambda, lambda), - mulDown(lambda, lambda) - ), - mulDown(R, mulDown(R, R)) - ) - ) - ) - ), - mulDown( - mulDown(mulDown(ls, s), c).sub( - divDown(x0.sub(a), mulDown(mulDown(lambda, lambda), R)) - ), - mulDown(mulDown(ls, s), c).sub( - divDown(x0.sub(a), mulDown(mulDown(lambda, lambda), R)) - ) - ) - ); - - return returnValue; -} - -export function dPxDYIn( - balances: BigNumber[], - params: GyroEParams, - derived: DerivedGyroEParams, - fee: BigNumber, - rVec: Vector2 -): BigNumber { - const { y0, c, s, lambda, b, ls, R } = setup( - balances, - params, - derived, - fee, - rVec, - 1 - ); - - const returnValue = divDown( - mulDown( - ONE.sub(mulDown(ls, mulDown(c, c))), - divDown(ONE, mulDown(mulDown(lambda, lambda), R)).add( - divDown( - mulDown(y0.sub(b), y0.sub(b)), - mulDown( - mulDown( - mulDown(lambda, lambda), - mulDown(lambda, lambda) - ), - mulDown(R, mulDown(R, R)) - ) - ) - ) - ), - mulDown( - mulDown(mulDown(ls, s), c).sub( - divDown(y0.sub(b), mulDown(mulDown(lambda, lambda), R)) - ), - mulDown(mulDown(ls, s), c).sub( - divDown(y0.sub(b), mulDown(mulDown(lambda, lambda), R)) - ) - ) - ); - - return returnValue; -} - -export function dPxDXOut( - balances: BigNumber[], - params: GyroEParams, - derived: DerivedGyroEParams, - fee: BigNumber, - rVec: Vector2 -): BigNumber { - const { x0, s, lambda, a, ls, R, f } = setup( - balances, - params, - derived, - fee, - rVec, - 0 - ); - - const returnValue = mulDown( - divDown(ONE, mulDown(f, ONE.sub(mulDown(ls, mulDown(s, s))))), - divDown(ONE, mulDown(mulDown(lambda, lambda), R)).add( - divDown( - mulDown(x0.sub(a), x0.sub(a)), - mulDown( - mulDown(mulDown(lambda, lambda), mulDown(lambda, lambda)), - mulDown(mulDown(R, R), R) - ) - ) - ) - ); - - return returnValue; -} - -export function dPyDYOut( - balances: BigNumber[], - params: GyroEParams, - derived: DerivedGyroEParams, - fee: BigNumber, - rVec: Vector2 -): BigNumber { - const { y0, c, lambda, b, ls, R, f } = setup( - balances, - params, - derived, - fee, - rVec, - 1 - ); - - const returnValue = mulDown( - divDown(ONE, mulDown(f, ONE.sub(mulDown(ls, mulDown(c, c))))), - divDown(ONE, mulDown(mulDown(lambda, lambda), R)).add( - divDown( - mulDown(y0.sub(b), y0.sub(b)), - mulDown( - mulDown(mulDown(lambda, lambda), mulDown(lambda, lambda)), - mulDown(mulDown(R, R), R) - ) - ) - ) - ); - - return returnValue; -} diff --git a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts b/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts index 32617b66..5c067235 100644 --- a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts +++ b/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts @@ -1,71 +1,9 @@ import { BigNumber, parseFixed } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import { ONE_XP } from '../../gyroHelpers/constants'; import { mulDown, - divDown, - mulDownMagU, - divDownMagU, - mulUpMagU, - divUpMagU, - mulUpXpToNpU, - mulDownXpToNpU, - mulXpU, - divXpU, - sqrt, } from '../../gyroHelpers/gyroSignedFixedPoint'; -import { MAX_BALANCES } from './constants'; -///////// -/// TYPES -///////// - -export type GyroEParams = { - alpha: BigNumber; - beta: BigNumber; - c: BigNumber; - s: BigNumber; - lambda: BigNumber; -}; - -// terms in this struct are stored in extra precision (38 decimals) with final decimal rounded down -export type DerivedGyroEParams = { - tauAlpha: Vector2; - tauBeta: Vector2; - u: BigNumber; - v: BigNumber; - w: BigNumber; - z: BigNumber; - dSq: BigNumber; -}; - -export type Vector2 = { - x: BigNumber; - y: BigNumber; -}; - -export type QParams = { - a: BigNumber; - b: BigNumber; - c: BigNumber; -}; - -///////// -/// FEE CALCULATION -///////// - -export function reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - const feeAmount = mulDown(amountIn, swapFee); - return amountIn.sub(feeAmount); -} - -export function addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - return divDown(amountIn, ONE.sub(swapFee)); -} - -//////// -/// BALANCE CALCULATION -//////// export function normalizeBalances( balances: BigNumber[], decimals: number[], @@ -77,430 +15,3 @@ export function normalizeBalances( mulDown(bal.mul(ONE).div(scalingFactors[index]), tokenRates[index]) ); } - -export function balancesFromTokenInOut( - balanceTokenIn: BigNumber, - balanceTokenOut: BigNumber, - tokenInIsToken0: boolean -): [BigNumber, BigNumber] { - return tokenInIsToken0 - ? [balanceTokenIn, balanceTokenOut] - : [balanceTokenOut, balanceTokenIn]; -} - -// Alias for code readability. -export const valuesInOutFrom01 = balancesFromTokenInOut; - - -///////// -/// INVARIANT CALC -///////// - -export function calcAtAChi( - x: BigNumber, - y: BigNumber, - p: GyroEParams, - d: DerivedGyroEParams -): BigNumber { - const dSq2 = mulXpU(d.dSq, d.dSq); - - // (cx - sy) * (w/lambda + z) / lambda - // account for 2 factors of dSq (4 s,c factors) - const termXp = divXpU( - divDownMagU(divDownMagU(d.w, p.lambda).add(d.z), p.lambda), - dSq2 - ); - - let val = mulDownXpToNpU( - mulDownMagU(x, p.c).sub(mulDownMagU(y, p.s)), - termXp - ); - - // (x lambda s + y lambda c) * u, note u > 0 - let termNp = mulDownMagU(mulDownMagU(x, p.lambda), p.s).add( - mulDownMagU(mulDownMagU(y, p.lambda), p.c) - ); - val = val.add(mulDownXpToNpU(termNp, divXpU(d.u, dSq2))); - - // (sx+cy) * v, note v > 0 - termNp = mulDownMagU(x, p.s).add(mulDownMagU(y, p.c)); - val = val.add(mulDownXpToNpU(termNp, divXpU(d.v, dSq2))); - - return val; -} - -export function calcInvariantSqrt( - x: BigNumber, - y: BigNumber, - p: GyroEParams, - d: DerivedGyroEParams -): [BigNumber, BigNumber] { - let val = calcMinAtxAChiySqPlusAtxSq(x, y, p, d).add( - calc2AtxAtyAChixAChiy(x, y, p, d) - ); - val = val.add(calcMinAtyAChixSqPlusAtySq(x, y, p, d)); - const err = mulUpMagU(x, x).add(mulUpMagU(y, y)).div(ONE_XP); - val = val.gt(0) ? sqrt(val, BigNumber.from(5)) : BigNumber.from(0); - return [val, err]; -} - -function calcMinAtxAChiySqPlusAtxSq( - x: BigNumber, - y: BigNumber, - p: GyroEParams, - d: DerivedGyroEParams -) { - let termNp = mulUpMagU(mulUpMagU(mulUpMagU(x, x), p.c), p.c).add( - mulUpMagU(mulUpMagU(mulUpMagU(y, y), p.s), p.s) - ); - termNp = termNp.sub( - mulDownMagU(mulDownMagU(mulDownMagU(x, y), p.c.mul(2)), p.s) - ); - const termXp = mulXpU(d.u, d.u) - .add(divDownMagU(mulXpU(d.u.mul(2), d.v), p.lambda)) - .add(divDownMagU(divDownMagU(mulXpU(d.v, d.v), p.lambda), p.lambda)); - - let val = mulDownXpToNpU(termNp.mul(-1), termXp); - val = val.add( - mulDownXpToNpU( - divDownMagU(divDownMagU(termNp.sub(9), p.lambda), p.lambda), - divXpU(ONE_XP, d.dSq) - ) - ); - return val; -} - -function calc2AtxAtyAChixAChiy( - x: BigNumber, - y: BigNumber, - p: GyroEParams, - d: DerivedGyroEParams -) { - let termNp = mulDownMagU( - mulDownMagU(mulDownMagU(x, x).sub(mulUpMagU(y, y)), p.c.mul(2)), - p.s - ); - - const xy = mulDownMagU(y, x.mul(2)); - termNp = termNp - .add(mulDownMagU(mulDownMagU(xy, p.c), p.c)) - .sub(mulDownMagU(mulDownMagU(xy, p.s), p.s)); - let termXp = mulXpU(d.z, d.u).add( - divDownMagU(divDownMagU(mulXpU(d.w, d.v), p.lambda), p.lambda) - ); - termXp = termXp.add( - divDownMagU(mulXpU(d.w, d.u).add(mulXpU(d.z, d.v)), p.lambda) - ); - termXp = divXpU(termXp, mulXpU(mulXpU(mulXpU(d.dSq, d.dSq), d.dSq), d.dSq)); - const val = mulDownXpToNpU(termNp, termXp); - return val; -} - -function calcMinAtyAChixSqPlusAtySq( - x: BigNumber, - y: BigNumber, - p: GyroEParams, - d: DerivedGyroEParams -) { - let termNp = mulUpMagU(mulUpMagU(mulUpMagU(x, x), p.s), p.s).add( - mulUpMagU(mulUpMagU(mulUpMagU(y, y), p.c), p.c) - ); - termNp = termNp.add(mulUpMagU(mulUpMagU(mulUpMagU(x, y), p.s.mul(2)), p.c)); - let termXp = mulXpU(d.z, d.z).add( - divDownMagU(divDownMagU(mulXpU(d.w, d.w), p.lambda), p.lambda) - ); - termXp = termXp.add(divDownMagU(mulXpU(d.z.mul(2), d.w), p.lambda)); - termXp = divXpU(termXp, mulXpU(mulXpU(mulXpU(d.dSq, d.dSq), d.dSq), d.dSq)); - let val = mulDownXpToNpU(termNp.mul(-1), termXp); - val = val.add(mulDownXpToNpU(termNp.sub(9), divXpU(ONE_XP, d.dSq))); - return val; -} - -export function calcAChiAChiInXp( - p: GyroEParams, - d: DerivedGyroEParams -): BigNumber { - const dSq3 = mulXpU(mulXpU(d.dSq, d.dSq), d.dSq); - let val = mulUpMagU(p.lambda, divXpU(mulXpU(d.u.mul(2), d.v), dSq3)); - val = val.add( - mulUpMagU( - mulUpMagU(divXpU(mulXpU(d.u.add(1), d.u.add(1)), dSq3), p.lambda), - p.lambda - ) - ); - val = val.add(divXpU(mulXpU(d.v, d.v), dSq3)); - const termXp = divUpMagU(d.w, p.lambda).add(d.z); - val = val.add(divXpU(mulXpU(termXp, termXp), dSq3)); - return val; -} - -///////// -/// SWAP AMOUNT CALC -///////// - -export function checkAssetBounds( - params: GyroEParams, - derived: DerivedGyroEParams, - invariant: Vector2, - newBal: BigNumber, - assetIndex: number -): void { - if (assetIndex === 0) { - const xPlus = maxBalances0(params, derived, invariant); - if (newBal.gt(MAX_BALANCES) || newBal.gt(xPlus)) - throw new Error('ASSET BOUNDS EXCEEDED'); - } else { - const yPlus = maxBalances1(params, derived, invariant); - if (newBal.gt(MAX_BALANCES) || newBal.gt(yPlus)) - throw new Error('ASSET BOUNDS EXCEEDED'); - } -} - -function maxBalances0(p: GyroEParams, d: DerivedGyroEParams, r: Vector2) { - const termXp1 = divXpU(d.tauBeta.x.sub(d.tauAlpha.x), d.dSq); - const termXp2 = divXpU(d.tauBeta.y.sub(d.tauAlpha.y), d.dSq); - let xp = mulDownXpToNpU( - mulDownMagU(mulDownMagU(r.y, p.lambda), p.c), - termXp1 - ); - xp = xp.add( - termXp2.gt(BigNumber.from(0)) - ? mulDownMagU(r.y, p.s) - : mulDownXpToNpU(mulUpMagU(r.x, p.s), termXp2) - ); - return xp; -} - -function maxBalances1(p: GyroEParams, d: DerivedGyroEParams, r: Vector2) { - const termXp1 = divXpU(d.tauBeta.x.sub(d.tauAlpha.x), d.dSq); - const termXp2 = divXpU(d.tauBeta.y.sub(d.tauAlpha.y), d.dSq); - let yp = mulDownXpToNpU( - mulDownMagU(mulDownMagU(r.y, p.lambda), p.s), - termXp1 - ); - yp = yp.add( - termXp2.gt(BigNumber.from(0)) - ? mulDownMagU(r.y, p.c) - : mulDownXpToNpU(mulUpMagU(r.x, p.c), termXp2) - ); - return yp; -} - -export function calcYGivenX( - x: BigNumber, - params: GyroEParams, - d: DerivedGyroEParams, - r: Vector2 -): BigNumber { - const ab: Vector2 = { - x: virtualOffset0(params, d, r), - y: virtualOffset1(params, d, r), - }; - - const y = solveQuadraticSwap( - params.lambda, - x, - params.s, - params.c, - r, - ab, - d.tauBeta, - d.dSq - ); - return y; -} - -export function calcXGivenY( - y: BigNumber, - params: GyroEParams, - d: DerivedGyroEParams, - r: Vector2 -): BigNumber { - const ba: Vector2 = { - x: virtualOffset1(params, d, r), - y: virtualOffset0(params, d, r), - }; - const x = solveQuadraticSwap( - params.lambda, - y, - params.c, - params.s, - r, - ba, - { - x: d.tauAlpha.x.mul(-1), - y: d.tauAlpha.y, - }, - d.dSq - ); - return x; -} - -export function virtualOffset0( - p: GyroEParams, - d: DerivedGyroEParams, - r: Vector2, - switchTau?: boolean -): BigNumber { - const tauValue = switchTau ? d.tauAlpha : d.tauBeta; - const termXp = divXpU(tauValue.x, d.dSq); - - let a = tauValue.x.gt(BigNumber.from(0)) - ? mulUpXpToNpU(mulUpMagU(mulUpMagU(r.x, p.lambda), p.c), termXp) - : mulUpXpToNpU(mulDownMagU(mulDownMagU(r.y, p.lambda), p.c), termXp); - - a = a.add(mulUpXpToNpU(mulUpMagU(r.x, p.s), divXpU(tauValue.y, d.dSq))); - - return a; -} - -export function virtualOffset1( - p: GyroEParams, - d: DerivedGyroEParams, - r: Vector2, - switchTau?: boolean -): BigNumber { - const tauValue = switchTau ? d.tauBeta : d.tauAlpha; - const termXp = divXpU(tauValue.x, d.dSq); - - let b = tauValue.x.lt(BigNumber.from(0)) - ? mulUpXpToNpU(mulUpMagU(mulUpMagU(r.x, p.lambda), p.s), termXp.mul(-1)) - : mulUpXpToNpU( - mulDownMagU(mulDownMagU(r.y.mul(-1), p.lambda), p.s), - termXp - ); - - b = b.add(mulUpXpToNpU(mulUpMagU(r.x, p.c), divXpU(tauValue.y, d.dSq))); - return b; -} - -function solveQuadraticSwap( - lambda: BigNumber, - x: BigNumber, - s: BigNumber, - c: BigNumber, - r: Vector2, - ab: Vector2, - tauBeta: Vector2, - dSq: BigNumber -): BigNumber { - const lamBar: Vector2 = { - x: ONE_XP.sub(divDownMagU(divDownMagU(ONE_XP, lambda), lambda)), - y: ONE_XP.sub(divUpMagU(divUpMagU(ONE_XP, lambda), lambda)), - }; - const q: QParams = { - a: BigNumber.from(0), - b: BigNumber.from(0), - c: BigNumber.from(0), - }; - const xp = x.sub(ab.x); - if (xp.gt(BigNumber.from(0))) { - q.b = mulUpXpToNpU( - mulDownMagU(mulDownMagU(xp.mul(-1), s), c), - divXpU(lamBar.y, dSq) - ); - } else { - q.b = mulUpXpToNpU( - mulUpMagU(mulUpMagU(xp.mul(-1), s), c), - divXpU(lamBar.x, dSq).add(1) - ); - } - const sTerm: Vector2 = { - x: divXpU(mulDownMagU(mulDownMagU(lamBar.y, s), s), dSq), - y: divXpU(mulUpMagU(mulUpMagU(lamBar.x, s), s), dSq.add(1)).add(1), - }; - sTerm.x = ONE_XP.sub(sTerm.x); - sTerm.y = ONE_XP.sub(sTerm.y); - - q.c = calcXpXpDivLambdaLambda(x, r, lambda, s, c, tauBeta, dSq).mul(-1); - q.c = q.c.add(mulDownXpToNpU(mulDownMagU(r.y, r.y), sTerm.y)); // r.y === currentInv + err - q.c = q.c.gt(BigNumber.from(0)) - ? sqrt(q.c, BigNumber.from(5)) - : BigNumber.from(0); - if (q.b.sub(q.c).gt(BigNumber.from(0))) { - q.a = mulUpXpToNpU(q.b.sub(q.c), divXpU(ONE_XP, sTerm.y).add(1)); - } else { - q.a = mulUpXpToNpU(q.b.sub(q.c), divXpU(ONE_XP, sTerm.x)); - } - return q.a.add(ab.y); -} - -export function calcXpXpDivLambdaLambda( - x: BigNumber, - r: Vector2, - lambda: BigNumber, - s: BigNumber, - c: BigNumber, - tauBeta: Vector2, - dSq: BigNumber -): BigNumber { - const sqVars = { - x: mulXpU(dSq, dSq), - y: mulUpMagU(r.x, r.x), - }; - const q: QParams = { - a: BigNumber.from(0), - b: BigNumber.from(0), - c: BigNumber.from(0), - }; - let termXp = divXpU(mulXpU(tauBeta.x, tauBeta.y), sqVars.x); - if (termXp.gt(BigNumber.from(0))) { - q.a = mulUpMagU(sqVars.y, s.mul(2)); - q.a = mulUpXpToNpU(mulUpMagU(q.a, c), termXp.add(7)); - } else { - q.a = mulDownMagU(mulDownMagU(r.y, r.y), s.mul(2)); // r.y === currentInv + err - q.a = mulUpXpToNpU(mulDownMagU(q.a, c), termXp); - } - - if (tauBeta.x.lt(BigNumber.from(0))) { - q.b = mulUpXpToNpU( - mulUpMagU(mulUpMagU(r.x, x), c.mul(2)), - divXpU(tauBeta.x, dSq).mul(-1).add(3) - ); - } else { - q.b = mulUpXpToNpU( - mulDownMagU(mulDownMagU(r.y.mul(-1), x), c.mul(2)), - divXpU(tauBeta.x, dSq) - ); - } - q.a = q.a.add(q.b); - termXp = divXpU(mulXpU(tauBeta.y, tauBeta.y), sqVars.x).add(7); - q.b = mulUpMagU(sqVars.y, s); - q.b = mulUpXpToNpU(mulUpMagU(q.b, s), termXp); - - q.c = mulUpXpToNpU( - mulDownMagU(mulDownMagU(r.y.mul(-1), x), s.mul(2)), - divXpU(tauBeta.y, dSq) - ); - q.b = q.b.add(q.c).add(mulUpMagU(x, x)); - q.b = q.b.gt(BigNumber.from(0)) - ? divUpMagU(q.b, lambda) - : divDownMagU(q.b, lambda); - - q.a = q.a.add(q.b); - q.a = q.a.gt(BigNumber.from(0)) - ? divUpMagU(q.a, lambda) - : divDownMagU(q.a, lambda); - - termXp = divXpU(mulXpU(tauBeta.x, tauBeta.x), sqVars.x).add(7); - const val = mulUpMagU(mulUpMagU(sqVars.y, c), c); - return mulUpXpToNpU(val, termXp).add(q.a); -} - -///////// -/// LINEAR ALGEBRA OPERATIONS -///////// - -export function mulA(params: GyroEParams, tp: Vector2): Vector2 { - return { - x: divDownMagU(mulDownMagU(params.c, tp.x), params.lambda).sub( - divDownMagU(mulDownMagU(params.s, tp.y), params.lambda) - ), - y: mulDownMagU(params.s, tp.x).add(mulDownMagU(params.c, tp.y)), - }; -} - -export function scalarProd(t1: Vector2, t2: Vector2): BigNumber { - const ret = mulDownMagU(t1.x, t2.x).add(mulDownMagU(t1.y, t2.y)); - return ret; -} diff --git a/src/pools/gyroEV2Pool/gyroEV2Pool.ts b/src/pools/gyroEV2Pool/gyroEV2Pool.ts index 56319cf4..03efc802 100644 --- a/src/pools/gyroEV2Pool/gyroEV2Pool.ts +++ b/src/pools/gyroEV2Pool/gyroEV2Pool.ts @@ -15,14 +15,12 @@ import { GyroEParams, DerivedGyroEParams, Vector2, - normalizeBalances, balancesFromTokenInOut, - valuesInOutFrom01, reduceFee, addFee, virtualOffset0, virtualOffset1, -} from './gyroEV2Math/gyroEV2MathHelpers'; +} from '../gyroEPool/gyroEMath/gyroEMathHelpers'; import { isSameAddress, safeParseFixed } from '../../utils'; import { mulDown, divDown } from '../gyroHelpers/gyroSignedFixedPoint'; import { @@ -33,10 +31,16 @@ import { calcSpotPriceAfterSwapInGivenOut, calcDerivativePriceAfterSwapOutGivenIn, calcDerivativeSpotPriceAfterSwapInGivenOut, -} from './gyroEV2Math/gyroEV2Math'; +} from '../gyroEPool/gyroEMath/gyroEMath'; import { SWAP_LIMIT_FACTOR } from '../gyroHelpers/constants'; import { universalNormalizedLiquidity } from '../liquidity'; +import { normalizeBalances } from './gyroEV2Math/gyroEV2MathHelpers'; + +// Alias for code readability. Observe that `balancesFromTokenInOut()` is its own inverse. +const valuesInOutFrom01 = balancesFromTokenInOut; + + export type GyroEPoolPairData = PoolPairBase & { tokenInIsToken0: boolean; }; diff --git a/test/gyroEV2Pool.spec.ts b/test/gyroEV2Pool.spec.ts index cae42130..72d96188 100644 --- a/test/gyroEV2Pool.spec.ts +++ b/test/gyroEV2Pool.spec.ts @@ -7,7 +7,7 @@ import { expect } from 'chai'; import { GyroEV2Pool } from '../src/pools/gyroEV2Pool/gyroEV2Pool'; import { SwapTypes } from '../src/types'; import {bnum, ZERO} from '../src/utils/bignumber'; -import { reduceFee } from '../src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers'; +import { reduceFee } from '../src/pools/gyroEPool/gyroEMath/gyroEMathHelpers'; const TEST_POOL_PAIR_DATA: GyroEPoolPairData = { id: '123', From 64a103d1b79df70ba7c52bc05aff2044c4b548db Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Tue, 18 Apr 2023 12:51:53 +0100 Subject: [PATCH 10/16] Use poolTypeVersion --- .../gyroEV2Math/gyroEV2MathHelpers.ts | 4 +- src/pools/gyroEV2Pool/gyroEV2Pool.ts | 40 ++++++++++++++----- src/pools/index.ts | 11 +++-- src/types.ts | 2 +- test/gyroEPool.spec.ts | 2 +- test/gyroEV2Pool.spec.ts | 18 ++++----- test/lib/onchainData.ts | 10 ++++- test/lib/subgraphPoolDataService.ts | 2 + 8 files changed, 60 insertions(+), 29 deletions(-) diff --git a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts b/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts index 5c067235..441351f0 100644 --- a/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts +++ b/src/pools/gyroEV2Pool/gyroEV2Math/gyroEV2MathHelpers.ts @@ -1,8 +1,6 @@ import { BigNumber, parseFixed } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import { - mulDown, -} from '../../gyroHelpers/gyroSignedFixedPoint'; +import { mulDown } from '../../gyroHelpers/gyroSignedFixedPoint'; export function normalizeBalances( balances: BigNumber[], diff --git a/src/pools/gyroEV2Pool/gyroEV2Pool.ts b/src/pools/gyroEV2Pool/gyroEV2Pool.ts index 03efc802..c471013d 100644 --- a/src/pools/gyroEV2Pool/gyroEV2Pool.ts +++ b/src/pools/gyroEV2Pool/gyroEV2Pool.ts @@ -40,7 +40,6 @@ import { normalizeBalances } from './gyroEV2Math/gyroEV2MathHelpers'; // Alias for code readability. Observe that `balancesFromTokenInOut()` is its own inverse. const valuesInOutFrom01 = balancesFromTokenInOut; - export type GyroEPoolPairData = PoolPairBase & { tokenInIsToken0: boolean; }; @@ -351,7 +350,10 @@ export class GyroEV2Pool implements PoolBase { }; const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); - const inAmountLessFeeScaled = mulDown(inAmountLessFee, tokenRateInOut[0]); + const inAmountLessFeeScaled = mulDown( + inAmountLessFee, + tokenRateInOut[0] + ); const outAmountScaled = calcOutGivenIn( orderedNormalizedBalances, inAmountLessFeeScaled, @@ -403,7 +405,10 @@ export class GyroEV2Pool implements PoolBase { this.derivedGyroEParams, invariant ); - const inAmountLessFee = divDown(inAmountScaledLessFee, tokenRateInOut[0]); + const inAmountLessFee = divDown( + inAmountScaledLessFee, + tokenRateInOut[0] + ); const inAmount = addFee(inAmountLessFee, poolPairData.swapFee); return bnum(formatFixed(inAmount, 18)); } @@ -450,7 +455,10 @@ export class GyroEV2Pool implements PoolBase { }; const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); - const inAmountLessFeeScaled = mulDown(inAmountLessFee, tokenRateInOut[0]); + const inAmountLessFeeScaled = mulDown( + inAmountLessFee, + tokenRateInOut[0] + ); const newSpotPriceScaled = calcSpotPriceAfterSwapOutGivenIn( orderedNormalizedBalances, inAmountLessFeeScaled, @@ -460,7 +468,10 @@ export class GyroEV2Pool implements PoolBase { invariant, poolPairData.swapFee ); - const newSpotPrice = divDown(mulDown(newSpotPriceScaled, tokenRateInOut[1]), tokenRateInOut[0]); + const newSpotPrice = divDown( + mulDown(newSpotPriceScaled, tokenRateInOut[1]), + tokenRateInOut[0] + ); return bnum(formatFixed(newSpotPrice, 18)); } @@ -503,7 +514,10 @@ export class GyroEV2Pool implements PoolBase { invariant, poolPairData.swapFee ); - const newSpotPrice = divDown(mulDown(newSpotPriceScaled, tokenRateInOut[1]), tokenRateInOut[0]); + const newSpotPrice = divDown( + mulDown(newSpotPriceScaled, tokenRateInOut[1]), + tokenRateInOut[0] + ); return bnum(formatFixed(newSpotPrice, 18)); } @@ -540,7 +554,10 @@ export class GyroEV2Pool implements PoolBase { const derivativeScaled = calcDerivativePriceAfterSwapOutGivenIn( [ orderedNormalizedBalances[0].add( - reduceFee(mulDown(inAmount, tokenRateInOut[0]), poolPairData.swapFee) + reduceFee( + mulDown(inAmount, tokenRateInOut[0]), + poolPairData.swapFee + ) ), orderedNormalizedBalances[1], ], @@ -586,7 +603,9 @@ export class GyroEV2Pool implements PoolBase { const derivativeScaled = calcDerivativeSpotPriceAfterSwapInGivenOut( [ orderedNormalizedBalances[0], - orderedNormalizedBalances[1].sub(mulDown(outAmount, tokenRateInOut[1])), + orderedNormalizedBalances[1].sub( + mulDown(outAmount, tokenRateInOut[1]) + ), ], poolPairData.tokenInIsToken0, this.gyroEParams, @@ -594,7 +613,10 @@ export class GyroEV2Pool implements PoolBase { invariant, poolPairData.swapFee ); - const rateAdjFactor = divDown(mulDown(tokenRateInOut[1], tokenRateInOut[1]), tokenRateInOut[0]); + const rateAdjFactor = divDown( + mulDown(tokenRateInOut[1], tokenRateInOut[1]), + tokenRateInOut[0] + ); const derivative = mulDown(derivativeScaled, rateAdjFactor); return bnum(formatFixed(derivative, 18)); } diff --git a/src/pools/index.ts b/src/pools/index.ts index f58cbb03..43f35b24 100644 --- a/src/pools/index.ts +++ b/src/pools/index.ts @@ -78,10 +78,13 @@ export function parseNewPool( newPool = ComposableStablePool.fromPool(pool); else if (pool.poolType === 'Gyro2') newPool = Gyro2Pool.fromPool(pool); else if (pool.poolType === 'Gyro3') newPool = Gyro3Pool.fromPool(pool); - else if (pool.poolType === 'GyroE') newPool = GyroEPool.fromPool(pool); - else if (pool.poolType === 'GyroEV2') - newPool = GyroEV2Pool.fromPool(pool); - else if (pool.poolType === 'FX') newPool = FxPool.fromPool(pool); + else if (pool.poolType === 'GyroE') { + if (pool.poolTypeVersion === 2) { + newPool = GyroEV2Pool.fromPool(pool); + } else { + newPool = GyroEPool.fromPool(pool); + } + } else if (pool.poolType === 'FX') newPool = FxPool.fromPool(pool); else { console.error( `Unknown pool type or type field missing: ${pool.poolType} ${pool.id}` diff --git a/src/types.ts b/src/types.ts index f7a88fb8..0c21fc81 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,6 +71,7 @@ export interface SubgraphPoolBase { id: string; address: string; poolType: string; + poolTypeVersion?: number; swapFee: string; swapEnabled: boolean; totalShares: string; @@ -198,7 +199,6 @@ export enum PoolFilter { Gyro2 = 'Gyro2', Gyro3 = 'Gyro3', GyroE = 'GyroE', - GyroEV2 = 'GyroEV2', // Linear Pools defined below all operate the same mathematically but have different factories and names in Subgraph AaveLinear = 'AaveLinear', Linear = 'Linear', diff --git a/test/gyroEPool.spec.ts b/test/gyroEPool.spec.ts index 54af86ae..8e3c0ce7 100644 --- a/test/gyroEPool.spec.ts +++ b/test/gyroEPool.spec.ts @@ -188,7 +188,7 @@ describe('gyroEPool tests', () => { expect(Number(priceDerivative)).to.be.approximately( 3.2030926701e-7, - 1E-12 + 1e-12 ); }); }); diff --git a/test/gyroEV2Pool.spec.ts b/test/gyroEV2Pool.spec.ts index 72d96188..c63b3819 100644 --- a/test/gyroEV2Pool.spec.ts +++ b/test/gyroEV2Pool.spec.ts @@ -2,11 +2,11 @@ import { GyroEPoolPairData } from '../src/pools/gyroEV2Pool/gyroEV2Pool'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import {BigNumber, formatFixed, parseFixed} from '@ethersproject/bignumber'; +import { BigNumber, formatFixed, parseFixed } from '@ethersproject/bignumber'; import { expect } from 'chai'; import { GyroEV2Pool } from '../src/pools/gyroEV2Pool/gyroEV2Pool'; import { SwapTypes } from '../src/types'; -import {bnum, ZERO} from '../src/utils/bignumber'; +import { bnum } from '../src/utils/bignumber'; import { reduceFee } from '../src/pools/gyroEPool/gyroEMath/gyroEMathHelpers'; const TEST_POOL_PAIR_DATA: GyroEPoolPairData = { @@ -174,8 +174,8 @@ describe('gyroEPool tests', () => { ); expect(Number(priceDerivative)).to.be.approximately( - 1.03343127137E-7, - 1E-12 + 1.03343127137e-7, + 1e-12 ); }); @@ -187,10 +187,10 @@ describe('gyroEPool tests', () => { ); expect(Number(priceDerivative)).to.be.approximately( - 1.17346314397E-7, - 1E-12 + 1.17346314397e-7, + 1e-12 ); - }) + }); it(`should correctly calculate derivative of price after swap exact out`, async () => { const priceDerivative = @@ -200,8 +200,8 @@ describe('gyroEPool tests', () => { ); expect(Number(priceDerivative)).to.be.approximately( - 2.13539511340E-7, - 1E-12 + 2.1353951134e-7, + 1e-12 ); }); }); diff --git a/test/lib/onchainData.ts b/test/lib/onchainData.ts index c911cddb..c600023c 100644 --- a/test/lib/onchainData.ts +++ b/test/lib/onchainData.ts @@ -149,7 +149,10 @@ export async function getOnChainBalances( pool.address, 'getSwapFeePercentage' ); - if (pool.poolType.toString() === 'GyroEV2') { + if ( + pool.poolType.toString() === 'GyroE' && + pool.poolTypeVersion == 2 + ) { multiPool.call( `${pool.id}.tokenRates`, pool.address, @@ -310,7 +313,10 @@ export async function getOnChainBalances( subgraphPools[index].totalShares = formatFixed(totalSupply, 18); } - if (subgraphPools[index].poolType === 'GyroEV2') { + if ( + subgraphPools[index].poolType === 'GyroE' && + subgraphPools[index].poolTypeVersion == 2 + ) { if (!Array.isArray(tokenRates) || tokenRates.length !== 2) { console.error( `GyroEV2 pool with missing or invalid tokenRates: ${poolId}` diff --git a/test/lib/subgraphPoolDataService.ts b/test/lib/subgraphPoolDataService.ts index c93a4cf3..19267f65 100644 --- a/test/lib/subgraphPoolDataService.ts +++ b/test/lib/subgraphPoolDataService.ts @@ -14,6 +14,7 @@ const queryWithLinear = ` id address poolType + poolTypeVersion swapFee totalShares tokens (orderBy: index) { @@ -68,6 +69,7 @@ const queryWithLinear = ` id address poolType + poolTypeVersion swapFee totalShares tokens (orderBy: index) { From b712719086220fbe62b796ce537ab417bc1bcb1e Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 26 Apr 2023 11:45:04 +0100 Subject: [PATCH 11/16] Disable FX pools. --- src/pools/index.ts | 9 +++++++-- src/types.ts | 4 ++-- test/lib/onchainData.ts | 2 +- test/testScripts/swapExample.ts | 4 ++-- test/xaveFxPool.integration.spec.ts | 12 +++++++++--- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/pools/index.ts b/src/pools/index.ts index 6315385b..1ced02d4 100644 --- a/src/pools/index.ts +++ b/src/pools/index.ts @@ -21,6 +21,7 @@ import { SwapTypes, PoolPairBase, PoolTypes, + PoolFilter, } from '../types'; export function parseNewPool( @@ -56,6 +57,11 @@ export function parseNewPool( | FxPool; try { + const isLinear = pool.poolType.toString().includes('Linear'); + if (!isLinear && !(pool.poolType in PoolFilter)) { + console.error(`Unsupported pool type: ${pool.poolType} ${pool.id}`); + return undefined; + } if (pool.poolType === 'Weighted' || pool.poolType === 'Investment') { newPool = WeightedPool.fromPool(pool, false); } else if (pool.poolType === 'LiquidityBootstrapping') { @@ -67,8 +73,7 @@ export function parseNewPool( } else if (pool.poolType === 'Element') { newPool = ElementPool.fromPool(pool); newPool.setCurrentBlockTimestamp(currentBlockTimestamp); - } else if (pool.poolType.toString().includes('Linear')) - newPool = LinearPool.fromPool(pool); + } else if (isLinear) newPool = LinearPool.fromPool(pool); else if (pool.poolType === 'StablePhantom') newPool = PhantomStablePool.fromPool(pool); else if (pool.poolType === 'ComposableStable') diff --git a/src/types.ts b/src/types.ts index 889436a2..474936e1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -187,7 +187,7 @@ export enum PoolFilter { Weighted = 'Weighted', Stable = 'Stable', MetaStable = 'MetaStable', - LBP = 'LiquidityBootstrapping', + LiquidityBootstrapping = 'LiquidityBootstrapping', Investment = 'Investment', Element = 'Element', StablePhantom = 'StablePhantom', @@ -207,7 +207,7 @@ export enum PoolFilter { SiloLinear = 'SiloLinear', TetuLinear = 'TetuLinear', YearnLinear = 'YearnLinear', - FxPool = 'FX', + // FX = 'FX', } export interface PoolBase { diff --git a/test/lib/onchainData.ts b/test/lib/onchainData.ts index 58f451b3..d931d1ef 100644 --- a/test/lib/onchainData.ts +++ b/test/lib/onchainData.ts @@ -47,7 +47,7 @@ export async function getOnChainBalances( const subgraphPools: SubgraphPoolBase[] = []; subgraphPoolsOriginal.forEach((pool) => { if (!supportedPoolTypes.includes(pool.poolType)) { - console.error(`Unknown pool type: ${pool.poolType} ${pool.id}`); + console.error(`Unknown pool type OC: ${pool.poolType} ${pool.id}`); return; } diff --git a/test/testScripts/swapExample.ts b/test/testScripts/swapExample.ts index 4f5d22b4..01450ec5 100644 --- a/test/testScripts/swapExample.ts +++ b/test/testScripts/swapExample.ts @@ -58,7 +58,7 @@ function setUp(networkId: Network, provider: JsonRpcProvider): SOR { } export async function swap(): Promise { - const networkId = Network.POLYGON; + const networkId = Network.MAINNET; const provider = new JsonRpcProvider(PROVIDER_URLS[networkId]); // gasPrice is used by SOR as a factor to determine how many pools to swap against. // i.e. higher cost means more costly to trade against lots of different pools. @@ -66,7 +66,7 @@ export async function swap(): Promise { // This determines the max no of pools the SOR will use to swap. const maxPools = 4; const tokenIn = ADDRESSES[networkId].DAI; - const tokenOut = ADDRESSES[networkId].XSGD; + const tokenOut = ADDRESSES[networkId].USDC; const swapType: SwapTypes = SwapTypes.SwapExactIn; const swapAmount = parseFixed('100', 18); diff --git a/test/xaveFxPool.integration.spec.ts b/test/xaveFxPool.integration.spec.ts index 2c092452..b8489908 100644 --- a/test/xaveFxPool.integration.spec.ts +++ b/test/xaveFxPool.integration.spec.ts @@ -1,7 +1,7 @@ // yarn test:only test/xaveFxPool.integration.spec.ts import dotenv from 'dotenv'; import { JsonRpcProvider } from '@ethersproject/providers'; -import { bnum, SOR, SubgraphPoolBase, SwapTypes } from '../src'; +import { bnum, PoolFilter, SOR, SubgraphPoolBase, SwapTypes } from '../src'; import { ADDRESSES, Network, vaultAddr } from './testScripts/constants'; import { parseFixed } from '@ethersproject/bignumber'; import { expect } from 'chai'; @@ -73,6 +73,8 @@ const xaveFxPoolDAI_USDC_MAINNET: SubgraphPoolBase = { epsilon: '0.0015', }; +const test = 'FX' in PoolFilter; + describe('xaveFxPool: DAI-USDC integration tests', () => { context('test swaps vs queryBatchSwap', () => { // Setup chain @@ -98,7 +100,9 @@ describe('xaveFxPool: DAI-USDC integration tests', () => { toInternalBalance: false, }; - it('ExactIn', async () => { + it('ExactIn', async function () { + if (!test) this.skip(); + const swapType = SwapTypes.SwapExactIn; // swapAmount is tokenIn, expect tokenOut const swapAmount = parseFixed(SWAP_AMOUNT_IN_NUMERAIRE, 6); @@ -129,7 +133,9 @@ describe('xaveFxPool: DAI-USDC integration tests', () => { ); }); - it('ExactOut', async () => { + it('ExactOut', async function () { + if (!test) this.skip(); + const swapType = SwapTypes.SwapExactOut; // swapAmount is tokenOut, expect tokenIn const swapAmount = parseFixed(SWAP_AMOUNT_IN_NUMERAIRE, 18); From fcd04456a0d4b645125f79fa52715e667d181ebc Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Mon, 1 May 2023 12:04:12 +0100 Subject: [PATCH 12/16] Add GyroEV2 integration test --- test/gyroEV2.integration.spec.ts | 163 +++++++++++++++++++++++++++++++ test/testScripts/constants.ts | 10 ++ 2 files changed, 173 insertions(+) create mode 100644 test/gyroEV2.integration.spec.ts diff --git a/test/gyroEV2.integration.spec.ts b/test/gyroEV2.integration.spec.ts new file mode 100644 index 00000000..f31845d3 --- /dev/null +++ b/test/gyroEV2.integration.spec.ts @@ -0,0 +1,163 @@ +// yarn test:only test/gyroEV2.integration.spec.ts +import dotenv from 'dotenv'; +import { JsonRpcProvider } from '@ethersproject/providers'; +import { bnum, SOR, SubgraphPoolBase, SwapTypes } from '../src'; +import { ADDRESSES, Network, vaultAddr } from './testScripts/constants'; +import { parseFixed } from '@ethersproject/bignumber'; +import { expect } from 'chai'; +import { Vault__factory } from '@balancer-labs/typechain'; +import { AddressZero } from '@ethersproject/constants'; +import { setUp } from './testScripts/utils'; +import { scale } from '../src/utils/bignumber'; + +/* + * Testing Notes: + * - Run node on terminal: npx hardhat node --tsconfig tsconfig.testing.json --fork https://polygon-rpc.com --fork-block-number 42173266 + */ + +// accuracy test: https://app.warp.dev/block/bcbBMkR8Da96QHQ2phmHZN + +dotenv.config(); + +let sor: SOR; +const networkId = Network.POLYGON; +const jsonRpcUrl = 'https://polygon-rpc.com'; +const rpcUrl = 'http://127.0.0.1:8545'; +const provider = new JsonRpcProvider(rpcUrl, networkId); +const blocknumber = 42173266; + +const inaccuracyLimit = 1e-14; + +const vault = Vault__factory.connect(vaultAddr, provider); +const SWAP_AMOUNT_IN_NUMERAIRE = '0.1'; + +const gyroEV2PoolWMATIC_stMATIC_POLYGON: SubgraphPoolBase = { + id: '0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2000200000000000000000b49', + address: '0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2', + poolType: 'GyroE', + swapFee: '0.0002', + swapEnabled: true, + totalWeight: '0', + totalShares: '5.366644050391084161', + tokensList: [ + '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', + '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4', + ], + tokens: [ + { + address: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', + balance: '1.123393517620917161', + decimals: 18, + priceRate: '1', + weight: null, + }, + { + address: '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4', + balance: '3.973745355066743187', + decimals: 18, + priceRate: '1', + weight: null, + }, + ], + alpha: '0.997', + beta: '1.00300902708124', + c: '0.707106781186547524', + s: '0.707106781186547524', + lambda: '2000', + tauAlphaX: '-0.9488255257963911869756698523798861', + tauAlphaY: '0.3158007625025655333984021158671054', + tauBetaX: '0.9488255257962740264038592402880117', + tauBetaY: '0.3158007625029175431284287745394428', + u: '0.948825525796332605614025003860828', + v: '0.3158007625027415379053734674832487', + w: '0.00000000000017600486501332933596912057', + z: '-0.00000000000005858028590530604587080878', + dSq: '0.9999999999999999988662409334210612', +}; + +describe('gyroEV2: WMATIC-stMATIC integration tests', () => { + context('test swaps vs queryBatchSwap', () => { + // Setup chain + before(async function () { + sor = await setUp( + networkId, + provider, + [gyroEV2PoolWMATIC_stMATIC_POLYGON], + jsonRpcUrl as string, + blocknumber + ); + + await sor.fetchPools(); + }); + + const tokenIn = ADDRESSES[Network.POLYGON].WMATIC.address; + const tokenOut = ADDRESSES[Network.POLYGON].stMATIC.address; + + const funds = { + sender: AddressZero, + recipient: AddressZero, + fromInternalBalance: false, + toInternalBalance: false, + }; + + it('ExactIn', async () => { + const swapType = SwapTypes.SwapExactIn; + // swapAmount is tokenIn, expect tokenOut + const swapAmount = parseFixed(SWAP_AMOUNT_IN_NUMERAIRE, 6); + + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); + + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + + expect(queryResult[0].toString()).to.eq( + swapInfo.swapAmount.toString() + ); + + expect( + bnum(queryResult[1].abs().toString()).toNumber() + ).to.be.closeTo( + bnum(swapInfo.returnAmount.toString()).toNumber(), + scale(bnum(inaccuracyLimit), 18).toNumber() + ); + }); + + it('ExactOut', async () => { + const swapType = SwapTypes.SwapExactOut; + // swapAmount is tokenOut, expect tokenIn + const swapAmount = parseFixed(SWAP_AMOUNT_IN_NUMERAIRE, 18); + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); + + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + + expect( + bnum(queryResult[0].abs().toString()).toNumber() + ).to.be.closeTo( + bnum(swapInfo.returnAmount.toString()).toNumber(), + scale(bnum(inaccuracyLimit), 6).toNumber() + ); + expect(queryResult[1].abs().toString()).to.eq( + swapInfo.swapAmount.toString() + ); + }); + }); +}); diff --git a/test/testScripts/constants.ts b/test/testScripts/constants.ts index e3b1e0bf..0ea522fe 100644 --- a/test/testScripts/constants.ts +++ b/test/testScripts/constants.ts @@ -402,6 +402,16 @@ export const ADDRESSES = { decimals: 6, symbol: 'XSGD', }, + WMATIC: { + address: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', + decimals: 18, + symbol: 'WMATIC', + }, + stMATIC: { + address: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', + decimals: 18, + symbol: 'stMATIC', + }, }, [Network.ARBITRUM]: { WETH: { From b39b6c6105488dc5413e8952a02d4bf90d51cd87 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Tue, 2 May 2023 10:35:28 +0100 Subject: [PATCH 13/16] Small typo fixes. Add guards for errors. --- src/pools/gyroEV2Pool/gyroEV2Pool.ts | 528 ++++++++++++++------------- test/gyroEV2Pool.spec.ts | 2 +- test/lib/onchainData.ts | 2 +- 3 files changed, 278 insertions(+), 254 deletions(-) diff --git a/src/pools/gyroEV2Pool/gyroEV2Pool.ts b/src/pools/gyroEV2Pool/gyroEV2Pool.ts index c471013d..7636f1d6 100644 --- a/src/pools/gyroEV2Pool/gyroEV2Pool.ts +++ b/src/pools/gyroEV2Pool/gyroEV2Pool.ts @@ -323,94 +323,102 @@ export class GyroEV2Pool implements PoolBase { poolPairData: GyroEPoolPairData, amount: OldBigNumber ): OldBigNumber { - const tokenRateInOut = valuesInOutFrom01( - this.tokenRates[0], - this.tokenRates[1], - poolPairData.tokenInIsToken0 - ); - const normalizedBalances = normalizeBalances( - [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut], - tokenRateInOut - ); - const orderedNormalizedBalances = balancesFromTokenInOut( - normalizedBalances[0], - normalizedBalances[1], - poolPairData.tokenInIsToken0 - ); - const [currentInvariant, invErr] = calculateInvariantWithError( - orderedNormalizedBalances, - this.gyroEParams, - this.derivedGyroEParams - ); + try { + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); - const invariant: Vector2 = { - x: currentInvariant.add(invErr.mul(2)), - y: currentInvariant, - }; - const inAmount = safeParseFixed(amount.toString(), 18); - const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); - const inAmountLessFeeScaled = mulDown( - inAmountLessFee, - tokenRateInOut[0] - ); - const outAmountScaled = calcOutGivenIn( - orderedNormalizedBalances, - inAmountLessFeeScaled, - poolPairData.tokenInIsToken0, - this.gyroEParams, - this.derivedGyroEParams, - invariant - ); - const outAmount = divDown(outAmountScaled, tokenRateInOut[1]); - return bnum(formatFixed(outAmount, 18)); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const inAmount = safeParseFixed(amount.toString(), 18); + const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); + const inAmountLessFeeScaled = mulDown( + inAmountLessFee, + tokenRateInOut[0] + ); + const outAmountScaled = calcOutGivenIn( + orderedNormalizedBalances, + inAmountLessFeeScaled, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant + ); + const outAmount = divDown(outAmountScaled, tokenRateInOut[1]); + return bnum(formatFixed(outAmount, 18)); + } catch (err) { + return ZERO; + } } _tokenInForExactTokenOut( poolPairData: GyroEPoolPairData, amount: OldBigNumber ): OldBigNumber { - const tokenRateInOut = valuesInOutFrom01( - this.tokenRates[0], - this.tokenRates[1], - poolPairData.tokenInIsToken0 - ); - const normalizedBalances = normalizeBalances( - [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut], - tokenRateInOut - ); - const orderedNormalizedBalances = balancesFromTokenInOut( - normalizedBalances[0], - normalizedBalances[1], - poolPairData.tokenInIsToken0 - ); - const [currentInvariant, invErr] = calculateInvariantWithError( - orderedNormalizedBalances, - this.gyroEParams, - this.derivedGyroEParams - ); - const invariant: Vector2 = { - x: currentInvariant.add(invErr.mul(2)), - y: currentInvariant, - }; - const outAmount = safeParseFixed(amount.toString(), 18); - const outAmountScaled = mulDown(outAmount, tokenRateInOut[1]); - - const inAmountScaledLessFee = calcInGivenOut( - orderedNormalizedBalances, - outAmountScaled, - poolPairData.tokenInIsToken0, - this.gyroEParams, - this.derivedGyroEParams, - invariant - ); - const inAmountLessFee = divDown( - inAmountScaledLessFee, - tokenRateInOut[0] - ); - const inAmount = addFee(inAmountLessFee, poolPairData.swapFee); - return bnum(formatFixed(inAmount, 18)); + try { + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const outAmount = safeParseFixed(amount.toString(), 18); + const outAmountScaled = mulDown(outAmount, tokenRateInOut[1]); + + const inAmountScaledLessFee = calcInGivenOut( + orderedNormalizedBalances, + outAmountScaled, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant + ); + const inAmountLessFee = divDown( + inAmountScaledLessFee, + tokenRateInOut[0] + ); + const inAmount = addFee(inAmountLessFee, poolPairData.swapFee); + return bnum(formatFixed(inAmount, 18)); + } catch (err) { + return ZERO; + } } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -429,195 +437,211 @@ export class GyroEV2Pool implements PoolBase { poolPairData: GyroEPoolPairData, amount: OldBigNumber ): OldBigNumber { - const tokenRateInOut = valuesInOutFrom01( - this.tokenRates[0], - this.tokenRates[1], - poolPairData.tokenInIsToken0 - ); - const normalizedBalances = normalizeBalances( - [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut], - tokenRateInOut - ); - const orderedNormalizedBalances = balancesFromTokenInOut( - normalizedBalances[0], - normalizedBalances[1], - poolPairData.tokenInIsToken0 - ); - const [currentInvariant, invErr] = calculateInvariantWithError( - orderedNormalizedBalances, - this.gyroEParams, - this.derivedGyroEParams - ); - const invariant: Vector2 = { - x: currentInvariant.add(invErr.mul(2)), - y: currentInvariant, - }; - const inAmount = safeParseFixed(amount.toString(), 18); - const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); - const inAmountLessFeeScaled = mulDown( - inAmountLessFee, - tokenRateInOut[0] - ); - const newSpotPriceScaled = calcSpotPriceAfterSwapOutGivenIn( - orderedNormalizedBalances, - inAmountLessFeeScaled, - poolPairData.tokenInIsToken0, - this.gyroEParams, - this.derivedGyroEParams, - invariant, - poolPairData.swapFee - ); - const newSpotPrice = divDown( - mulDown(newSpotPriceScaled, tokenRateInOut[1]), - tokenRateInOut[0] - ); - return bnum(formatFixed(newSpotPrice, 18)); + try { + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const inAmount = safeParseFixed(amount.toString(), 18); + const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); + const inAmountLessFeeScaled = mulDown( + inAmountLessFee, + tokenRateInOut[0] + ); + const newSpotPriceScaled = calcSpotPriceAfterSwapOutGivenIn( + orderedNormalizedBalances, + inAmountLessFeeScaled, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + const newSpotPrice = divDown( + mulDown(newSpotPriceScaled, tokenRateInOut[1]), + tokenRateInOut[0] + ); + return bnum(formatFixed(newSpotPrice, 18)); + } catch (err) { + return ZERO; + } } _spotPriceAfterSwapTokenInForExactTokenOut( poolPairData: GyroEPoolPairData, amount: OldBigNumber ): OldBigNumber { - const tokenRateInOut = valuesInOutFrom01( - this.tokenRates[0], - this.tokenRates[1], - poolPairData.tokenInIsToken0 - ); - const normalizedBalances = normalizeBalances( - [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut], - tokenRateInOut - ); - const orderedNormalizedBalances = balancesFromTokenInOut( - normalizedBalances[0], - normalizedBalances[1], - poolPairData.tokenInIsToken0 - ); - const [currentInvariant, invErr] = calculateInvariantWithError( - orderedNormalizedBalances, - this.gyroEParams, - this.derivedGyroEParams - ); - const invariant: Vector2 = { - x: currentInvariant.add(invErr.mul(2)), - y: currentInvariant, - }; - const outAmount = safeParseFixed(amount.toString(), 18); - const outAmountScaled = mulDown(outAmount, tokenRateInOut[1]); - const newSpotPriceScaled = calcSpotPriceAfterSwapInGivenOut( - orderedNormalizedBalances, - outAmountScaled, - poolPairData.tokenInIsToken0, - this.gyroEParams, - this.derivedGyroEParams, - invariant, - poolPairData.swapFee - ); - const newSpotPrice = divDown( - mulDown(newSpotPriceScaled, tokenRateInOut[1]), - tokenRateInOut[0] - ); - return bnum(formatFixed(newSpotPrice, 18)); + try { + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const outAmount = safeParseFixed(amount.toString(), 18); + const outAmountScaled = mulDown(outAmount, tokenRateInOut[1]); + const newSpotPriceScaled = calcSpotPriceAfterSwapInGivenOut( + orderedNormalizedBalances, + outAmountScaled, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + const newSpotPrice = divDown( + mulDown(newSpotPriceScaled, tokenRateInOut[1]), + tokenRateInOut[0] + ); + return bnum(formatFixed(newSpotPrice, 18)); + } catch (err) { + return ZERO; + } } _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( poolPairData: GyroEPoolPairData, amount: OldBigNumber ): OldBigNumber { - const inAmount = safeParseFixed(amount.toString(), 18); - const tokenRateInOut = valuesInOutFrom01( - this.tokenRates[0], - this.tokenRates[1], - poolPairData.tokenInIsToken0 - ); - const normalizedBalances = normalizeBalances( - [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut], - tokenRateInOut - ); - const orderedNormalizedBalances = balancesFromTokenInOut( - normalizedBalances[0], - normalizedBalances[1], - poolPairData.tokenInIsToken0 - ); - const [currentInvariant, invErr] = calculateInvariantWithError( - orderedNormalizedBalances, - this.gyroEParams, - this.derivedGyroEParams - ); - const invariant: Vector2 = { - x: currentInvariant.add(invErr.mul(2)), - y: currentInvariant, - }; + try { + const inAmount = safeParseFixed(amount.toString(), 18); + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; - const derivativeScaled = calcDerivativePriceAfterSwapOutGivenIn( - [ - orderedNormalizedBalances[0].add( - reduceFee( - mulDown(inAmount, tokenRateInOut[0]), - poolPairData.swapFee - ) - ), - orderedNormalizedBalances[1], - ], - poolPairData.tokenInIsToken0, - this.gyroEParams, - this.derivedGyroEParams, - invariant, - poolPairData.swapFee - ); - const derivative = mulDown(derivativeScaled, tokenRateInOut[1]); - return bnum(formatFixed(derivative, 18)); + const derivativeScaled = calcDerivativePriceAfterSwapOutGivenIn( + [ + orderedNormalizedBalances[0].add( + reduceFee( + mulDown(inAmount, tokenRateInOut[0]), + poolPairData.swapFee + ) + ), + orderedNormalizedBalances[1], + ], + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + const derivative = mulDown(derivativeScaled, tokenRateInOut[1]); + return bnum(formatFixed(derivative, 18)); + } catch (err) { + return ZERO; + } } _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( poolPairData: GyroEPoolPairData, amount: OldBigNumber ): OldBigNumber { - const tokenRateInOut = valuesInOutFrom01( - this.tokenRates[0], - this.tokenRates[1], - poolPairData.tokenInIsToken0 - ); - const normalizedBalances = normalizeBalances( - [poolPairData.balanceIn, poolPairData.balanceOut], - [poolPairData.decimalsIn, poolPairData.decimalsOut], - tokenRateInOut - ); - const orderedNormalizedBalances = balancesFromTokenInOut( - normalizedBalances[0], - normalizedBalances[1], - poolPairData.tokenInIsToken0 - ); - const [currentInvariant, invErr] = calculateInvariantWithError( - orderedNormalizedBalances, - this.gyroEParams, - this.derivedGyroEParams - ); - const invariant: Vector2 = { - x: currentInvariant.add(invErr.mul(2)), - y: currentInvariant, - }; - const outAmount = safeParseFixed(amount.toString(), 18); - const derivativeScaled = calcDerivativeSpotPriceAfterSwapInGivenOut( - [ - orderedNormalizedBalances[0], - orderedNormalizedBalances[1].sub( - mulDown(outAmount, tokenRateInOut[1]) - ), - ], - poolPairData.tokenInIsToken0, - this.gyroEParams, - this.derivedGyroEParams, - invariant, - poolPairData.swapFee - ); - const rateAdjFactor = divDown( - mulDown(tokenRateInOut[1], tokenRateInOut[1]), - tokenRateInOut[0] - ); - const derivative = mulDown(derivativeScaled, rateAdjFactor); - return bnum(formatFixed(derivative, 18)); + try { + const tokenRateInOut = valuesInOutFrom01( + this.tokenRates[0], + this.tokenRates[1], + poolPairData.tokenInIsToken0 + ); + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut], + tokenRateInOut + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const outAmount = safeParseFixed(amount.toString(), 18); + const derivativeScaled = calcDerivativeSpotPriceAfterSwapInGivenOut( + [ + orderedNormalizedBalances[0], + orderedNormalizedBalances[1].sub( + mulDown(outAmount, tokenRateInOut[1]) + ), + ], + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + const rateAdjFactor = divDown( + mulDown(tokenRateInOut[1], tokenRateInOut[1]), + tokenRateInOut[0] + ); + const derivative = mulDown(derivativeScaled, rateAdjFactor); + return bnum(formatFixed(derivative, 18)); + } catch (err) { + return ZERO; + } } } diff --git a/test/gyroEV2Pool.spec.ts b/test/gyroEV2Pool.spec.ts index c63b3819..2dabac79 100644 --- a/test/gyroEV2Pool.spec.ts +++ b/test/gyroEV2Pool.spec.ts @@ -1,4 +1,4 @@ -// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyroEPool.spec.ts +// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyroEV2Pool.spec.ts import { GyroEPoolPairData } from '../src/pools/gyroEV2Pool/gyroEV2Pool'; import { WeiPerEther as ONE } from '@ethersproject/constants'; diff --git a/test/lib/onchainData.ts b/test/lib/onchainData.ts index c600023c..5eba7362 100644 --- a/test/lib/onchainData.ts +++ b/test/lib/onchainData.ts @@ -151,7 +151,7 @@ export async function getOnChainBalances( ); if ( pool.poolType.toString() === 'GyroE' && - pool.poolTypeVersion == 2 + pool.poolTypeVersion === 2 ) { multiPool.call( `${pool.id}.tokenRates`, From 92d3967d27babeb034a3e18be31768d2ff0dda08 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Tue, 2 May 2023 10:36:06 +0100 Subject: [PATCH 14/16] Add Polygon integration testing. --- hardhat.config.polygon.ts | 12 +++ package.json | 3 +- test/gyroEV2.integration.spec.ts | 176 +++++++++++++++++++++---------- test/testScripts/constants.ts | 2 +- test/testScripts/swapExample.ts | 4 +- 5 files changed, 135 insertions(+), 62 deletions(-) create mode 100644 hardhat.config.polygon.ts diff --git a/hardhat.config.polygon.ts b/hardhat.config.polygon.ts new file mode 100644 index 00000000..bebf1020 --- /dev/null +++ b/hardhat.config.polygon.ts @@ -0,0 +1,12 @@ +import '@nomiclabs/hardhat-ethers'; + +/** + * @type import('hardhat/config').HardhatUserConfig + */ +export default { + networks: { + hardhat: { + chainId: 137, + }, + }, +}; diff --git a/package.json b/package.json index 8f7627a2..0b58aede 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "test:only": "TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register --timeout 20000", "coverage": "nyc report --reporter=text-lcov | coveralls", "lint": "eslint ./src ./test --ext .ts --max-warnings 0", - "node": "npx hardhat node --tsconfig tsconfig.testing.json --fork $(grep ALCHEMY_URL .env | cut -d '=' -f2) --fork-block-number 14828550" + "node": "npx hardhat node --tsconfig tsconfig.testing.json --fork $(grep ALCHEMY_URL .env | cut -d '=' -f2) --fork-block-number 14828550", + "node:polygon": "npx hardhat --tsconfig tsconfig.testing.json --config hardhat.config.polygon.ts node --fork $(. ./.env && echo $ALCHEMY_URL_POLYGON) --port 8137" }, "husky": { "hooks": { diff --git a/test/gyroEV2.integration.spec.ts b/test/gyroEV2.integration.spec.ts index f31845d3..095da3ca 100644 --- a/test/gyroEV2.integration.spec.ts +++ b/test/gyroEV2.integration.spec.ts @@ -1,40 +1,29 @@ // yarn test:only test/gyroEV2.integration.spec.ts import dotenv from 'dotenv'; +import { expect } from 'chai'; import { JsonRpcProvider } from '@ethersproject/providers'; -import { bnum, SOR, SubgraphPoolBase, SwapTypes } from '../src'; -import { ADDRESSES, Network, vaultAddr } from './testScripts/constants'; import { parseFixed } from '@ethersproject/bignumber'; -import { expect } from 'chai'; -import { Vault__factory } from '@balancer-labs/typechain'; import { AddressZero } from '@ethersproject/constants'; +import { Vault__factory } from '@balancer-labs/typechain'; +import { bnum, SOR, SubgraphPoolBase, SwapTypes } from '../src'; +import { ADDRESSES, Network, vaultAddr } from './testScripts/constants'; import { setUp } from './testScripts/utils'; -import { scale } from '../src/utils/bignumber'; - -/* - * Testing Notes: - * - Run node on terminal: npx hardhat node --tsconfig tsconfig.testing.json --fork https://polygon-rpc.com --fork-block-number 42173266 - */ - -// accuracy test: https://app.warp.dev/block/bcbBMkR8Da96QHQ2phmHZN dotenv.config(); -let sor: SOR; const networkId = Network.POLYGON; -const jsonRpcUrl = 'https://polygon-rpc.com'; -const rpcUrl = 'http://127.0.0.1:8545'; +const { ALCHEMY_URL_POLYGON: jsonRpcUrl } = process.env; +const rpcUrl = 'http://127.0.0.1:8137'; const provider = new JsonRpcProvider(rpcUrl, networkId); const blocknumber = 42173266; -const inaccuracyLimit = 1e-14; - const vault = Vault__factory.connect(vaultAddr, provider); -const SWAP_AMOUNT_IN_NUMERAIRE = '0.1'; const gyroEV2PoolWMATIC_stMATIC_POLYGON: SubgraphPoolBase = { id: '0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2000200000000000000000b49', address: '0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2', poolType: 'GyroE', + poolTypeVersion: 2, swapFee: '0.0002', swapEnabled: true, totalWeight: '0', @@ -76,35 +65,72 @@ const gyroEV2PoolWMATIC_stMATIC_POLYGON: SubgraphPoolBase = { }; describe('gyroEV2: WMATIC-stMATIC integration tests', () => { - context('test swaps vs queryBatchSwap', () => { - // Setup chain - before(async function () { - sor = await setUp( - networkId, - provider, - [gyroEV2PoolWMATIC_stMATIC_POLYGON], - jsonRpcUrl as string, - blocknumber - ); - - await sor.fetchPools(); - }); + let sor: SOR; + const funds = { + sender: AddressZero, + recipient: AddressZero, + fromInternalBalance: false, + toInternalBalance: false, + }; + + // Setup chain + before(async function () { + sor = await setUp( + networkId, + provider, + [gyroEV2PoolWMATIC_stMATIC_POLYGON], + jsonRpcUrl as string, + blocknumber + ); + + await sor.fetchPools(); + }); + context('ExactIn', async () => { + const swapType = SwapTypes.SwapExactIn; - const tokenIn = ADDRESSES[Network.POLYGON].WMATIC.address; - const tokenOut = ADDRESSES[Network.POLYGON].stMATIC.address; + it('should return no swaps when above limit', async () => { + const tokenIn = ADDRESSES[Network.POLYGON].WMATIC.address; + const tokenOut = ADDRESSES[Network.POLYGON].stMATIC.address; + const swapAmount = parseFixed('33.33333333333333', 18); + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); - const funds = { - sender: AddressZero, - recipient: AddressZero, - fromInternalBalance: false, - toInternalBalance: false, - }; + expect(swapInfo.swaps.length).to.eq(0); + expect(swapInfo.returnAmount.toString()).to.eq('0'); + }); + it('token > LSD, getSwaps result should match queryBatchSwap', async () => { + const tokenIn = ADDRESSES[Network.POLYGON].WMATIC.address; + const tokenOut = ADDRESSES[Network.POLYGON].stMATIC.address; + const swapAmount = parseFixed('1.12345678', 18); + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); - it('ExactIn', async () => { - const swapType = SwapTypes.SwapExactIn; - // swapAmount is tokenIn, expect tokenOut - const swapAmount = parseFixed(SWAP_AMOUNT_IN_NUMERAIRE, 6); + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + expect(queryResult[0].toString()).to.eq( + swapInfo.swapAmount.toString() + ); + expect(bnum(queryResult[1].abs().toString()).toNumber()).to.eq( + bnum(swapInfo.returnAmount.toString()).toNumber() + ); + }); + it('LSD > token, getSwaps result should match queryBatchSwap', async () => { + const tokenIn = ADDRESSES[Network.POLYGON].stMATIC.address; + const tokenOut = ADDRESSES[Network.POLYGON].WMATIC.address; + const swapAmount = parseFixed('0.999', 18); const swapInfo = await sor.getSwaps( tokenIn, tokenOut, @@ -122,19 +148,33 @@ describe('gyroEV2: WMATIC-stMATIC integration tests', () => { expect(queryResult[0].toString()).to.eq( swapInfo.swapAmount.toString() ); - - expect( - bnum(queryResult[1].abs().toString()).toNumber() - ).to.be.closeTo( - bnum(swapInfo.returnAmount.toString()).toNumber(), - scale(bnum(inaccuracyLimit), 18).toNumber() + expect(bnum(queryResult[1].abs().toString()).toNumber()).to.eq( + bnum(swapInfo.returnAmount.toString()).toNumber() ); }); + }); + + context('ExactOut', async () => { + const swapType = SwapTypes.SwapExactOut; + + it('should return no swaps when above limit', async () => { + const tokenIn = ADDRESSES[Network.POLYGON].WMATIC.address; + const tokenOut = ADDRESSES[Network.POLYGON].stMATIC.address; + const swapAmount = parseFixed('100', 18); + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); - it('ExactOut', async () => { - const swapType = SwapTypes.SwapExactOut; - // swapAmount is tokenOut, expect tokenIn - const swapAmount = parseFixed(SWAP_AMOUNT_IN_NUMERAIRE, 18); + expect(swapInfo.swaps.length).to.eq(0); + expect(swapInfo.returnAmount.toString()).to.eq('0'); + }); + it('token > LSD, getSwaps result should match queryBatchSwap', async () => { + const tokenIn = ADDRESSES[Network.POLYGON].WMATIC.address; + const tokenOut = ADDRESSES[Network.POLYGON].stMATIC.address; + const swapAmount = parseFixed('1.987654321', 18); const swapInfo = await sor.getSwaps( tokenIn, tokenOut, @@ -148,12 +188,32 @@ describe('gyroEV2: WMATIC-stMATIC integration tests', () => { swapInfo.tokenAddresses, funds ); + expect(bnum(queryResult[0].abs().toString()).toNumber()).to.eq( + bnum(swapInfo.returnAmount.toString()).toNumber() + ); + expect(queryResult[1].abs().toString()).to.eq( + swapInfo.swapAmount.toString() + ); + }); + it('LSD > token, getSwaps result should match queryBatchSwap', async () => { + const tokenIn = ADDRESSES[Network.POLYGON].stMATIC.address; + const tokenOut = ADDRESSES[Network.POLYGON].WMATIC.address; + const swapAmount = parseFixed('0.999', 18); + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); - expect( - bnum(queryResult[0].abs().toString()).toNumber() - ).to.be.closeTo( - bnum(swapInfo.returnAmount.toString()).toNumber(), - scale(bnum(inaccuracyLimit), 6).toNumber() + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + expect(bnum(queryResult[0].abs().toString()).toNumber()).to.eq( + bnum(swapInfo.returnAmount.toString()).toNumber() ); expect(queryResult[1].abs().toString()).to.eq( swapInfo.swapAmount.toString() diff --git a/test/testScripts/constants.ts b/test/testScripts/constants.ts index 0ea522fe..f96e827a 100644 --- a/test/testScripts/constants.ts +++ b/test/testScripts/constants.ts @@ -141,7 +141,7 @@ export const SUBGRAPH_URLS = { [Network.GOERLI]: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-goerli-v2', [Network.POLYGON]: - 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-polygon-v2', + 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-polygon-v2-beta', [Network.ARBITRUM]: `https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-arbitrum-v2`, [Network.GNOSIS]: `https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-gnosis-chain-v2`, }; diff --git a/test/testScripts/swapExample.ts b/test/testScripts/swapExample.ts index 4f5d22b4..48872d87 100644 --- a/test/testScripts/swapExample.ts +++ b/test/testScripts/swapExample.ts @@ -65,8 +65,8 @@ export async function swap(): Promise { const gasPrice = BigNumber.from('14000000000'); // This determines the max no of pools the SOR will use to swap. const maxPools = 4; - const tokenIn = ADDRESSES[networkId].DAI; - const tokenOut = ADDRESSES[networkId].XSGD; + const tokenIn = ADDRESSES[networkId].WMATIC; + const tokenOut = ADDRESSES[networkId].stMATIC; const swapType: SwapTypes = SwapTypes.SwapExactIn; const swapAmount = parseFixed('100', 18); From b83e930316ba1da9d5fdb27fac03442d64527312 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 4 May 2023 14:29:39 +0100 Subject: [PATCH 15/16] Update to version 4.1.1-beta.9. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 40341eed..443cd925 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@balancer-labs/sor", - "version": "4.1.1-beta.8", + "version": "4.1.1-beta.9", "license": "GPL-3.0-only", "main": "dist/index.js", "module": "dist/index.esm.js", From a4a0700f11bdf8ba0793ef767a6d2ba134cb8e4b Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 4 May 2023 14:31:29 +0100 Subject: [PATCH 16/16] Update node command. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 443cd925..94885684 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test:only": "TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register --timeout 20000", "coverage": "nyc report --reporter=text-lcov | coveralls", "lint": "eslint ./src ./test --ext .ts --max-warnings 0", - "node": "npx hardhat node --tsconfig tsconfig.testing.json --fork $(grep ALCHEMY_URL .env | cut -d '=' -f2) --fork-block-number 14828550", + "node": "npx hardhat node --tsconfig tsconfig.testing.json --fork $(. ./.env && echo $ALCHEMY_URL)", "node:polygon": "npx hardhat --tsconfig tsconfig.testing.json --config hardhat.config.polygon.ts node --fork $(. ./.env && echo $ALCHEMY_URL_POLYGON) --port 8137" }, "husky": {