Skip to content

Commit

Permalink
Merge pull request #40 from terminal-fi/curve
Browse files Browse the repository at this point in the history
support for curve pools
  • Loading branch information
zviadm authored Jan 19, 2023
2 parents 2d46242 + dfda61f commit f7f98a0
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 25 deletions.
3 changes: 2 additions & 1 deletion src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { SwappaManager } from '../swappa-manager';
import {
mainnetRegistryMobius, mainnetRegistryMoola, mainnetRegistryMoolaV2,
mainnetRegistrySavingsCELO, mainnetRegistrySushiswap, mainnetRegistryUbeswap,
mainnetRegistryCeloDex, mainnetRegistrySymmetric, mainnetRegistryMisc,
mainnetRegistryCeloDex, mainnetRegistrySymmetric, mainnetRegistryMisc, mainnetRegistryCurve,
} from '../registry-cfg';
import { RegistryMento } from '../registries/mento';
import { Registry } from '../registry';
Expand Down Expand Up @@ -47,6 +47,7 @@ const registriesByName: {[name: string]: (kit: ContractKit) => Registry} = {
"celodex": mainnetRegistryCeloDex,
"symmetric": mainnetRegistrySymmetric,
"misc": mainnetRegistryMisc,
"curve": mainnetRegistryCurve,
}

let chainId: number
Expand Down
210 changes: 210 additions & 0 deletions src/pairs/curve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import Web3 from "web3"
import BigNumber from "bignumber.js"

import { ICurve, ABI as CurveABI } from "../../types/web3-v1-contracts/ICurve"
import { Erc20, ABI as Erc20ABI } from '../../types/web3-v1-contracts/ERC20';

import { Address, Pair, Snapshot, BigNumberString } from "../pair"
import { selectAddress } from "../utils"
import { address as pairCurveAddress } from "../../tools/deployed/mainnet.PairCurve.addr.json"

interface PairCurveSnapshot extends Snapshot {
paused: boolean
tokenPrecisionMultipliers: BigNumberString[]
balancesWithAdjustedPrecision: BigNumberString[]
swapFee: BigNumberString
preciseA: BigNumberString
}

export class PairCurve extends Pair {
allowRepeats = false
private curvePool: ICurve

private paused: boolean = false
private tokenPrecisionMultipliers: BigNumber[] = []
private balancesWithAdjustedPrecision: BigNumber[] = []
private swapFee: BigNumber = new BigNumber(0)
private preciseA: BigNumber = new BigNumber(0)

private token0Idx: number
private token1Idx: number
private nCoins: number

static readonly POOL_PRECISION_DECIMALS = 18
static readonly A_PRECISION = 100

constructor(
chainId: number,
private web3: Web3,
private poolAddr: Address,
opts?: {nCoins: number, token0Idx: number, token1Idx: number},
) {
super(selectAddress(chainId, {mainnet: pairCurveAddress}))
this.curvePool = new web3.eth.Contract(CurveABI, poolAddr) as unknown as ICurve
this.nCoins = opts ? opts.nCoins : 2
this.token0Idx = opts ? opts.token0Idx : 0
this.token1Idx = opts ? opts.token1Idx : 1
}

protected async _init() {
const [
tokenA,
tokenB,
] = await Promise.all([
this.curvePool.methods.coins(this.token0Idx).call(),
this.curvePool.methods.coins(this.token1Idx).call(),
])
const erc20A = new this.web3.eth.Contract(Erc20ABI, tokenA) as unknown as Erc20
const erc20B = new this.web3.eth.Contract(Erc20ABI, tokenB) as unknown as Erc20
const [
decimalsA,
decimalsB,
] = await Promise.all([
erc20A.methods.decimals().call(),
erc20B.methods.decimals().call(),
])
this.tokenPrecisionMultipliers = [
new BigNumber(10).pow(PairCurve.POOL_PRECISION_DECIMALS - Number.parseInt(decimalsA)),
new BigNumber(10).pow(PairCurve.POOL_PRECISION_DECIMALS - Number.parseInt(decimalsB)),
]
return {
pairKey: this.poolAddr,
tokenA, tokenB,
}
}

public async refresh() {
const [
paused,
balances,
swapFee,
preciseA,
] = await Promise.all([
false,
Promise.all([...Array(this.nCoins).keys()].map((i) => this.curvePool.methods.balances(i).call())),
this.curvePool.methods.fee().call(),
this.curvePool.methods.A_precise().call(),
])
this.paused = paused
this.balancesWithAdjustedPrecision = balances.map((b, idx) => this.tokenPrecisionMultipliers[idx].multipliedBy(b))
this.swapFee = new BigNumber(swapFee).div(new BigNumber(10).pow(10))
this.preciseA = new BigNumber(preciseA)
}

public outputAmount(inputToken: Address, inputAmount: BigNumber): BigNumber {
if (this.paused) {
return new BigNumber(0)
}

// See: https://github.com/mobiusAMM/mobiusV1/blob/master/contracts/SwapUtils.sol#L617
const [tokenIndexFrom, tokenIndexTo] = inputToken === this.tokenA ?
[this.token0Idx, this.token1Idx] : [this.token1Idx, this.token0Idx]
const x = inputAmount
.multipliedBy(this.tokenPrecisionMultipliers[tokenIndexFrom])
.plus(this.balancesWithAdjustedPrecision[tokenIndexFrom])
const y = this.getY(
x,
this.balancesWithAdjustedPrecision,
this.preciseA)
const outputAmountWithFee = this.balancesWithAdjustedPrecision[tokenIndexTo].minus(y).minus(1)
const fee = outputAmountWithFee.multipliedBy(this.swapFee)
const outputAmount = outputAmountWithFee.minus(fee).div(this.tokenPrecisionMultipliers[tokenIndexTo]).integerValue()
return outputAmount
}

private getY = (x: BigNumber, xp: BigNumber[], a: BigNumber) => {
// See: https://github.com/mobiusAMM/mobiusV1/blob/master/contracts/SwapUtils.sol#L531
const d = this.getD(xp, a)
const nTokens = xp.length
const nA = a.multipliedBy(nTokens)

const s = x
const c = d
.multipliedBy(d).div(x.multipliedBy(nTokens))
.integerValue()
.multipliedBy(d).multipliedBy(PairCurve.A_PRECISION).div(nA.multipliedBy(nTokens))
.integerValue()
const b = s.plus(d.multipliedBy(PairCurve.A_PRECISION).div(nA)).integerValue()

let yPrev
let y = d
for (let i = 0; i < 256; i++) {
yPrev = y
y = y.multipliedBy(y).plus(c).div(
y.multipliedBy(2).plus(b).minus(d))
.integerValue()
if (y.minus(yPrev).abs().lte(1)) {
return y
}
}
throw new Error("SwapPool approximation did not converge!")
}

private getD (xp: BigNumber[], a: BigNumber) {
// See: https://github.com/mobiusAMM/mobiusV1/blob/master/contracts/SwapUtils.sol#L393
const s = BigNumber.sum(...xp)
if (s.eq(0)) {
return s
}

let prevD
let d = s
const nTokens = xp.length
const nA = a.multipliedBy(nTokens)

for (let i = 0; i < 256; i++) {
let dP = d
xp.forEach((x) => {
dP = dP.multipliedBy(d).div(x.multipliedBy(nTokens)).integerValue()
})
prevD = d
d = nA.multipliedBy(s).div(PairCurve.A_PRECISION).plus(dP.multipliedBy(nTokens)).multipliedBy(d).div(
nA.minus(PairCurve.A_PRECISION).multipliedBy(d).div(PairCurve.A_PRECISION).plus(
new BigNumber(nTokens).plus(1).multipliedBy(dP)
)
).integerValue()
if (d.minus(prevD).abs().lte(1)) {
return d
}
}
throw new Error("SwapPool D does not converge!")
}

protected swapExtraData() {
return this.poolAddr
}

public snapshot(): PairCurveSnapshot {
return {
paused: this.paused,
tokenPrecisionMultipliers: this.tokenPrecisionMultipliers.map(n => n.toFixed()),
balancesWithAdjustedPrecision: this.balancesWithAdjustedPrecision.map(n => n.toFixed()),
swapFee: this.swapFee.toFixed(),
preciseA: this.preciseA.toFixed()
}
}

public restore(snapshot: PairCurveSnapshot): void {
this.paused = snapshot.paused
this.tokenPrecisionMultipliers = snapshot.tokenPrecisionMultipliers.map(r => new BigNumber(r))
this.balancesWithAdjustedPrecision = snapshot.balancesWithAdjustedPrecision.map(r => new BigNumber(r))
this.swapFee = new BigNumber(snapshot.swapFee)
this.preciseA = new BigNumber(snapshot.preciseA)
}
}

export async function createCurvePairs(
chainId: number,
web3: Web3,
poolAddr: Address,
nCoins: number,
): Promise<Pair[]> {
const swapPool = new web3.eth.Contract(CurveABI, poolAddr) as unknown as ICurve
const r: Pair[] = []
for (let token0Idx = 0; token0Idx < nCoins - 1; token0Idx++) {
for (let token1Idx = token0Idx+1; token1Idx < nCoins; token1Idx++) {
r.push(new PairCurve(chainId, web3, poolAddr, {nCoins, token0Idx, token1Idx}))
}
}
return r
}
33 changes: 27 additions & 6 deletions src/pairs/stableswap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,31 @@ export class PairStableSwap extends Pair {
private swapFee: BigNumber = new BigNumber(0)
private preciseA: BigNumber = new BigNumber(0)

private token0Idx: number
private token1Idx: number

static readonly POOL_PRECISION_DECIMALS = 18
static readonly A_PRECISION = 100

constructor(
chainId: number,
private web3: Web3,
private swapPoolAddr: Address,
tokenIdxs?: [number, number],
) {
super(selectAddress(chainId, {mainnet: pairStableSwapAddress}))
this.swapPool = new web3.eth.Contract(SwapABI, swapPoolAddr) as unknown as ISwap
this.token0Idx = tokenIdxs ? tokenIdxs[0] : 0
this.token1Idx = tokenIdxs ? tokenIdxs[1] : 1
}

protected async _init() {
const [
tokenA,
tokenB,
] = await Promise.all([
this.swapPool.methods.getToken(0).call(),
this.swapPool.methods.getToken(1).call(),
this.swapPool.methods.getToken(this.token0Idx).call(),
this.swapPool.methods.getToken(this.token1Idx).call(),
])
const erc20A = new this.web3.eth.Contract(Erc20ABI, tokenA) as unknown as Erc20
const erc20B = new this.web3.eth.Contract(Erc20ABI, tokenB) as unknown as Erc20
Expand Down Expand Up @@ -77,9 +83,6 @@ export class PairStableSwap extends Pair {
this.swapPool.methods.getSwapFee().call(),
this.swapPool.methods.getAPrecise().call(),
])
if (balances.length !== 2) {
throw new Error("pool must have only 2 tokens!")
}
this.paused = paused
this.balancesWithAdjustedPrecision = balances.map((b, idx) => this.tokenPrecisionMultipliers[idx].multipliedBy(b))
this.swapFee = new BigNumber(swapFee).div(new BigNumber(10).pow(10))
Expand All @@ -92,7 +95,8 @@ export class PairStableSwap extends Pair {
}

// See: https://github.com/mobiusAMM/mobiusV1/blob/master/contracts/SwapUtils.sol#L617
const [tokenIndexFrom, tokenIndexTo] = inputToken === this.tokenA ? [0, 1] : [1, 0]
const [tokenIndexFrom, tokenIndexTo] = inputToken === this.tokenA ?
[this.token0Idx, this.token1Idx] : [this.token1Idx, this.token0Idx]
const x = inputAmount
.multipliedBy(this.tokenPrecisionMultipliers[tokenIndexFrom])
.plus(this.balancesWithAdjustedPrecision[tokenIndexFrom])
Expand Down Expand Up @@ -186,3 +190,20 @@ export class PairStableSwap extends Pair {
this.preciseA = new BigNumber(snapshot.preciseA)
}
}

export async function createStableSwapPairs(
chainId: number,
web3: Web3,
swapPoolAddr: Address,
): Promise<Pair[]> {
const swapPool = new web3.eth.Contract(SwapABI, swapPoolAddr) as unknown as ISwap
const balances = await swapPool.methods.getBalances().call()
const nTokens = balances.length
const r: Pair[] = []
for (let i = 0; i < nTokens - 1; i++) {
for (let j = i+1; j < nTokens; j++) {
r.push(new PairStableSwap(chainId, web3, swapPoolAddr, [i, j]))
}
}
return r
}
39 changes: 24 additions & 15 deletions src/registry-cfg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { RegistryStatic } from "./registries/static"
import { RegistryUniswapV2 } from "./registries/uniswapv2"
import { RegistryBalancer } from "./registries/balancer"
import { address as registryHelperUniswapV2 } from "../tools/deployed/mainnet.RegistryHelperUniswapV2.addr.json"
import { createCurvePairs } from "./pairs/curve"

export const mainnetRegistryMoola =
(kit: ContractKit) => new RegistryAave("moola", kit, "0x7AAaD5a5fa74Aec83b74C2a098FBC86E17Ce4aEA")
Expand All @@ -31,29 +32,28 @@ export const mainnetRegistryMobius =
(kit: ContractKit) => {
const web3 = kit.web3 as unknown as Web3
return new RegistryStatic("mobius", web3.eth.getChainId().then(chainId => [
// Source: https://github.com/mobiusAMM/mobiusV1
new PairStableSwap(chainId, web3, "0x0ff04189Ef135b6541E56f7C638489De92E9c778"), // cUSD <-> bUSDC
new PairStableSwap(chainId, web3, "0xdBF27fD2a702Cc02ac7aCF0aea376db780D53247"), // cUSD <-> cUSDT
new PairStableSwap(chainId, web3, "0xE0F2cc70E52f05eDb383313393d88Df2937DA55a"), // cETH <-> WETH
new PairStableSwap(chainId, web3, "0x19260b9b573569dDB105780176547875fE9fedA3"), // BTC <-> WBTC
new PairStableSwap(chainId, web3, "0xA5037661989789d0310aC2B796fa78F1B01F195D"), // cUSD <-> USDC
new PairStableSwap(chainId, web3, "0x2080AAa167e2225e1FC9923250bA60E19a180Fb2"), // cUSD <-> pUSDC
new PairStableSwap(chainId, web3, "0x63C1914bf00A9b395A2bF89aaDa55A5615A3656e"), // cUSD <-> asUSDC
new PairStableSwap(chainId, web3, "0x382Ed834c6b7dBD10E4798B08889eaEd1455E820"), // cEUR <-> pEUR
new PairStableSwap(chainId, web3, "0x413FfCc28e6cDDE7e93625Ef4742810fE9738578"), // CELO <-> pCELO
new PairStableSwap(chainId, web3, "0x02Db089fb09Fda92e05e92aFcd41D9AAfE9C7C7C"), // cUSD <-> pUSD
new PairStableSwap(chainId, web3, "0x0986B42F5f9C42FeEef66fC23eba9ea1164C916D"), // cUSD <-> aaUSDC
// Opticsv2: https://github.com/mobiusAMM/mobius-interface/blob/main/src/constants/StablePools.ts
// Source: https://github.com/mobiusAMM/mobius-interface/blob/main/src/constants/StablePools.ts
new PairStableSwap(chainId, web3, "0xC0BA93D4aaf90d39924402162EE4a213300d1d60"), // cUSD <-> wUSDC
new PairStableSwap(chainId, web3, "0x9F4AdBD0af281C69a582eB2E6fa2A594D4204CAe"), // cUSD <-> atUST
new PairStableSwap(chainId, web3, "0x9906589Ea8fd27504974b7e8201DF5bBdE986b03"), // cUSD <-> USDCv2
new PairStableSwap(chainId, web3, "0xF3f65dFe0c8c8f2986da0FEc159ABE6fd4E700B4"), // cUSD <-> DAIv2
new PairStableSwap(chainId, web3, "0x74ef28D635c6C5800DD3Cd62d4c4f8752DaACB09"), // cETH <-> WETHv2
new PairStableSwap(chainId, web3, "0xaEFc4e8cF655a182E8346B24c8AbcE45616eE0d2"), // cBTC <-> WBTCv2
new PairStableSwap(chainId, web3, "0xcCe0d62Ce14FB3e4363Eb92Db37Ff3630836c252"), // cUSD <-> pUSDCv2
new PairStableSwap(chainId, web3, "0xA5037661989789d0310aC2B796fa78F1B01F195D"), // cUSD <-> USDCv1
new PairStableSwap(chainId, web3, "0x0986B42F5f9C42FeEef66fC23eba9ea1164C916D"), // cUSD <-> aaUSDC
new PairStableSwap(chainId, web3, "0xa2F0E57d4cEAcF025E81C76f28b9Ad6E9Fbe8735"), // cUSD <-> pUSDv2
new PairStableSwap(chainId, web3, "0xFc9e2C63370D8deb3521922a7B2b60f4Cff7e75a"), // CELO <-> pCELOv2
new PairStableSwap(chainId, web3, "0x23C95678862a229fAC088bd9705622d78130bC3e"), // cEUR <-> pEURv2
new PairStableSwap(chainId, web3, "0x9F4AdBD0af281C69a582eB2E6fa2A594D4204CAe"), // cUSD <-> atUST
new PairStableSwap(chainId, web3, "0xC0BA93D4aaf90d39924402162EE4a213300d1d60"), // cUSD <-> wUSDC
new PairStableSwap(chainId, web3, "0x02Db089fb09Fda92e05e92aFcd41D9AAfE9C7C7C"), // cUSD <-> pUSD
new PairStableSwap(chainId, web3, "0x63C1914bf00A9b395A2bF89aaDa55A5615A3656e"), // cUSD <-> asUSDC
new PairStableSwap(chainId, web3, "0x2080AAa167e2225e1FC9923250bA60E19a180Fb2"), // cUSD <-> pUSDC
new PairStableSwap(chainId, web3, "0x19260b9b573569dDB105780176547875fE9fedA3"), // BTC <-> WBTC
new PairStableSwap(chainId, web3, "0xE0F2cc70E52f05eDb383313393d88Df2937DA55a"), // cETH <-> WETH
new PairStableSwap(chainId, web3, "0xdBF27fD2a702Cc02ac7aCF0aea376db780D53247"), // cUSD <-> cUSDT
new PairStableSwap(chainId, web3, "0x0ff04189Ef135b6541E56f7C638489De92E9c778"), // cUSD <-> bUSDC
new PairStableSwap(chainId, web3, "0x413FfCc28e6cDDE7e93625Ef4742810fE9738578"), // CELO <-> pCELO
new PairStableSwap(chainId, web3, "0x382Ed834c6b7dBD10E4798B08889eaEd1455E820"), // cEUR <-> pEUR
]))
}
export const mainnetRegistryMisc =
Expand Down Expand Up @@ -83,6 +83,15 @@ export const mainnetRegistryCeloDex =
{ registryHelperAddr: registryHelperUniswapV2 })
export const mainnetRegistrySymmetric =
(kit: ContractKit) => new RegistryBalancer("symmetric", kit.web3 as unknown as Web3, "0x3E30b138ecc85cD89210e1A19a8603544A917372")
export const mainnetRegistryCurve =
(kit: ContractKit) => {
const web3 = kit.web3 as unknown as Web3
const pairs = web3.eth.getChainId().then(chainId =>
Promise.all([
createCurvePairs(chainId, web3, "0xf4cab10dc19695aace14b7a16d7705b600ad5f73", 2), // cUSD <-> USDC
]).then(r => r.flat()))
return new RegistryStatic("curve", pairs)
}

// mainnetRegistriesWhitelist contains list of more established protocols with
// overall higher TVL.
Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1522,9 +1522,9 @@
"@types/node" "*"

"@ubeswap/default-token-list@^4.0.11":
version "4.1.42"
resolved "https://registry.yarnpkg.com/@ubeswap/default-token-list/-/default-token-list-4.1.42.tgz#9917bf221a96d033032acc2fa0e66607f7efbc3c"
integrity sha512-GnbAZ5Qb33NG1/yPw8yHRKNfRre3UOKhTrlM4pop8hflxMeIWLyIbGJujeGZqUG77nvkBcQfrO8nlp3DV6qXoA==
version "4.1.57"
resolved "https://registry.yarnpkg.com/@ubeswap/default-token-list/-/default-token-list-4.1.57.tgz#fcd343ea6cbd629ab02043fb8959d2cf3bbc59d7"
integrity sha512-rPFsGh/8OpzlZ+td+5ohTcsXF+cyxTgl6OKspiWJL6C/HS3/ER2Eb6J2IZosz0oDj1XsXVlLNPE8Gfvzz/d80Q==

"@wry/equality@^0.1.2":
version "0.1.11"
Expand Down

0 comments on commit f7f98a0

Please sign in to comment.