Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exchange tokens via mento broker #106

Merged
merged 18 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
"scripts": {
"clean": "tsc -b . --clean",
"dev": "ts-node ./bin/dev.js",
"dev": "yarn build && ts-node ./bin/dev.js",
"build": "tsc -b .",
"docs": "./generate_docs.sh",
"lint": "yarn run --top-level eslint -c .eslintrc.js ",
Expand All @@ -47,6 +47,7 @@
"@celo/wallet-local": "^5.1.1",
"@ethereumjs/util": "8.0.5",
"@ledgerhq/hw-transport-node-hid": "^6.27.4",
"@mento-protocol/mento-sdk": "^0.2.2",
"@oclif/core": "^3.18.1",
"@oclif/plugin-autocomplete": "^3.0.5",
"@oclif/plugin-commands": "^3.1.1",
Expand All @@ -60,6 +61,7 @@
"chalk": "^2.4.2",
"command-exists": "^1.2.9",
"debug": "^4.1.1",
"ethers": "5",
"fs-extra": "^8.1.0",
"humanize-duration": "^3.29.0",
"path": "^0.12.7",
Expand Down
62 changes: 42 additions & 20 deletions packages/cli/src/commands/exchange/celo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { Flags } from '@oclif/core'
import BigNumber from 'bignumber.js'
import { BaseCommand } from '../../base'
import { newCheckBuilder } from '../../utils/checks'
import { displaySendTx, failWith } from '../../utils/cli'
import { binaryPrompt, displaySendEthersTxViaCK, displaySendTx } from '../../utils/cli'
import { CustomFlags } from '../../utils/command'
import { checkNotDangerousExchange } from '../../utils/exchange'
import { enumEntriesDupWithLowercase } from '../../utils/helpers'
import { getMentoBroker } from '../../utils/mento-broker-adaptor'
import { consoleLogger } from '@celo/base'

const largeOrderPercentage = 1
const deppegedPricePercentage = 20
const depeggedPricePercentage = 20

const stableTokenOptions = enumEntriesDupWithLowercase(Object.entries(StableToken))
export default class ExchangeCelo extends BaseCommand {
Expand Down Expand Up @@ -49,40 +50,61 @@ export default class ExchangeCelo extends BaseCommand {
const minBuyAmount = res.flags.forAtLeast
const stableToken = stableTokenOptions[res.flags.stableToken]

let exchange
try {
exchange = await kit.contracts.getExchange(stableToken)
} catch {
failWith(`The ${stableToken} token was not deployed yet`)
}

await newCheckBuilder(this).hasEnoughCelo(res.flags.from, sellAmount).runChecks()

const [celoToken, stableTokenAddress, { mento, brokerAddress }] = await Promise.all([
kit.contracts.getGoldToken(),
kit.registry.addressFor(stableTokenInfos[stableToken].contract),
getMentoBroker(kit.connection),
])

async function getQuote(tokenIn: string, tokenOut: string, amount: string) {
const quoteAmountOut = await mento.getAmountOut(tokenIn, tokenOut, amount)
const expectedAmountOut = quoteAmountOut.mul(99).div(100)
return expectedAmountOut
}

consoleLogger(`Fetching Quote`)
const expectedAmountToReceive = await getQuote(
celoToken.address,
stableTokenAddress,
sellAmount.toFixed()
)
if (minBuyAmount.toNumber() === 0) {
const check = await checkNotDangerousExchange(
kit,
sellAmount,
largeOrderPercentage,
deppegedPricePercentage,
true,
new BigNumber(expectedAmountToReceive.toString()),
depeggedPricePercentage,
stableTokenInfos[stableToken]
)

if (!check) {
console.log('Cancelled')
return
}
} else if (expectedAmountToReceive.lt(minBuyAmount.toString())) {
const check = await binaryPrompt(
'Warning: the expected amount to receive is less than the minimum amount to receive. Are you sure you want to continue?',
false
)
if (!check) {
consoleLogger('Cancelled')
return
}
}

const celoToken = await kit.contracts.getGoldToken()

await displaySendTx(
'increaseAllowance',
celoToken.increaseAllowance(exchange.address, sellAmount.toFixed())
celoToken.increaseAllowance(brokerAddress, sellAmount.toFixed())
)

const exchangeTx = exchange.exchange(sellAmount.toFixed(), minBuyAmount!.toFixed(), true)
// Set explicit gas based on github.com/celo-org/celo-monorepo/issues/2541
await displaySendTx('exchange', exchangeTx, { gas: 300000 })
consoleLogger('Swapping', sellAmount.toFixed(), 'for at least', expectedAmountToReceive)
const tx = await mento.swapIn(
celoToken.address,
stableTokenAddress,
sellAmount.toFixed(),
expectedAmountToReceive
)
await displaySendEthersTxViaCK('exchange', tx, kit.connection)
}
}
81 changes: 59 additions & 22 deletions packages/cli/src/exchange-stable-base.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { StableToken } from '@celo/contractkit'
import { consoleLogger } from '@celo/base'
import { CeloContract, StableToken } from '@celo/contractkit'
import { stableTokenInfos } from '@celo/contractkit/lib/celo-tokens'
import BigNumber from 'bignumber.js'
import { BaseCommand } from './base'
import { newCheckBuilder } from './utils/checks'
import { displaySendTx, failWith } from './utils/cli'
import { binaryPrompt, displaySendEthersTxViaCK, displaySendTx } from './utils/cli'
import { CustomFlags } from './utils/command'
import { checkNotDangerousExchange } from './utils/exchange'
import { getMentoBroker } from './utils/mento-broker-adaptor'

const largeOrderPercentage = 1
const deppegedPricePercentage = 20
const depeggedPricePercentage = 20
export default class ExchangeStableBase extends BaseCommand {
static flags = {
...BaseCommand.flags,
Expand All @@ -31,8 +32,8 @@ export default class ExchangeStableBase extends BaseCommand {
async run() {
const kit = await this.getKit()
const res = await this.parse()
const sellAmount = res.flags.value
const minBuyAmount = res.flags.forAtLeast
const sellAmount = res.flags.value as BigNumber
const minBuyAmount = res.flags.forAtLeast as BigNumber

if (!this._stableCurrency) {
throw new Error('Stable currency not set')
Expand All @@ -41,38 +42,74 @@ export default class ExchangeStableBase extends BaseCommand {
.hasEnoughStable(res.flags.from, sellAmount, this._stableCurrency)
.runChecks()

let stableToken
let exchange
try {
stableToken = await kit.contracts.getStableToken(this._stableCurrency)
exchange = await kit.contracts.getExchange(this._stableCurrency)
} catch {
failWith(`The ${this._stableCurrency} token was not deployed yet`)
const [stableToken, celoNativeTokenAddress, { mento, brokerAddress }] = await Promise.all([
kit.contracts.getStableToken(this._stableCurrency),
kit.registry.addressFor(CeloContract.GoldToken),
getMentoBroker(kit.connection),
])

console.info(`Prepare to exchange ${stableToken.address} for ${celoNativeTokenAddress}`)

// note using getAmountIn here to match way rate is shown in the oracles
async function getQuote(tokenIn: string, tokenOut: string, amount: string) {
const quoteAmountOut = await mento.getAmountIn(tokenIn, tokenOut, amount)
const expectedAmountOut = quoteAmountOut.mul(99).div(100)
aaronmgdr marked this conversation as resolved.
Show resolved Hide resolved
return expectedAmountOut
}

// TODO: im unsure how to handle that now with mento we get a quote vs using the forAtLeast param
// at the moment all i do is check if the quote is bigger than the for atLeast.
aaronmgdr marked this conversation as resolved.
Show resolved Hide resolved
// but what if someone wanted to accept less than the quote?
// should I only use the quote if they don't provide a forAtLeast?
consoleLogger('Fetching quote')
const expectedAmountToReceive = await getQuote(
stableToken.address,
celoNativeTokenAddress,
sellAmount.toFixed()
)

// TODO since its not ever zero now what to condition on?
if (minBuyAmount.toNumber() === 0) {
const check = await checkNotDangerousExchange(
kit,
sellAmount,
largeOrderPercentage,
deppegedPricePercentage,
false,
stableTokenInfos[this._stableCurrency]
new BigNumber(expectedAmountToReceive.toString()),
depeggedPricePercentage,
stableTokenInfos[this._stableCurrency as StableToken]
)

if (!check) {
console.log('Cancelled')
consoleLogger('Cancelled')
return
}
} else if (expectedAmountToReceive.lt(minBuyAmount.toString())) {
const check = await binaryPrompt(
'Warning: the expected amount to receive is less than the minimum amount to receive. Are you sure you want to continue?',
false
)
if (!check) {
consoleLogger('Cancelled')
return
}
}

await displaySendTx(
'increaseAllowance',
stableToken.increaseAllowance(exchange.address, sellAmount.toFixed())
stableToken.increaseAllowance(brokerAddress, sellAmount.toFixed())
)

const exchangeTx = exchange.exchange(sellAmount.toFixed(), minBuyAmount!.toFixed(), false)
// Set explicit gas based on github.com/celo-org/celo-monorepo/issues/2541
await displaySendTx('exchange', exchangeTx, { gas: 300000 })
consoleLogger(
'Swapping',
sellAmount.toFixed(),
'for at least',
expectedAmountToReceive.toString()
)
const tx = await mento.swapIn(
stableToken.address,
celoNativeTokenAddress,
sellAmount.toFixed(),
expectedAmountToReceive
)
await displaySendEthersTxViaCK('exchange', tx, kit.connection)
}
}
81 changes: 59 additions & 22 deletions packages/cli/src/utils/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { CeloTransactionObject, CeloTx, EventLog, parseDecodedParams } from '@celo/connect'
import {
CeloTransactionObject,
CeloTx,
Connection,
EventLog,
TransactionResult,
parseDecodedParams,
} from '@celo/connect'
import { Errors, ux } from '@oclif/core'
import BigNumber from 'bignumber.js'
import chalk from 'chalk'
import { ethers } from 'ethers'
import { convertEthersToCeloTx } from './mento-broker-adaptor'

const CLIError = Errors.CLIError

Expand All @@ -12,6 +21,25 @@ export async function displayWeb3Tx(name: string, txObj: any, tx?: Omit<CeloTx,
console.log(result)
ux.action.stop()
}
// allows building a tx with ethers but signing and sending with celo Connection
// cant use displaySendTx because it expects a CeloTransactionObject which isnt really possible to convert to from ethers
export async function displaySendEthersTxViaCK(
name: string,
txData: ethers.providers.TransactionRequest,
connection: Connection,
defaultParams: { gas?: string } = {}
) {
ux.action.start(`Sending Transaction: ${name}`)
const tx = convertEthersToCeloTx(txData, defaultParams)
const txWithPrices = await connection.setFeeMarketGas(tx)
try {
const result = await connection.sendTransaction(txWithPrices)
await innerDisplaySendTx(name, result)
} catch (e) {
ux.action.stop(`failed: ${(e as Error).message}`)
throw e
}
}

export async function displaySendTx<A>(
name: string,
Expand All @@ -22,33 +50,42 @@ export async function displaySendTx<A>(
ux.action.start(`Sending Transaction: ${name}`)
try {
const txResult = await txObj.send(tx)
const txHash = await txResult.getHash()

console.log(chalk`SendTransaction: {red.bold ${name}}`)
printValueMap({ txHash })

const txReceipt = await txResult.waitReceipt()
ux.action.stop()

if (displayEventName && txReceipt.events) {
Object.entries(txReceipt.events)
.filter(
([eventName]) =>
(typeof displayEventName === 'string' && eventName === displayEventName) ||
displayEventName.includes(eventName)
)
.forEach(([eventName, log]) => {
const { params } = parseDecodedParams((log as EventLog).returnValues)
console.log(chalk.magenta.bold(`${eventName}:`))
printValueMap(params, chalk.magenta)
})
}
await innerDisplaySendTx(name, txResult, displayEventName)
} catch (e) {
ux.action.stop(`failed: ${(e as Error).message}`)
throw e
}
}

// to share between displaySendTx and displaySendEthersTxViaCK
async function innerDisplaySendTx(
name: string,
txResult: TransactionResult,
displayEventName?: string | string[] | undefined
) {
const txHash = await txResult.getHash()

console.log(chalk`SendTransaction: {red.bold ${name}}`)
printValueMap({ txHash })

const txReceipt = await txResult.waitReceipt()
ux.action.stop()

if (displayEventName && txReceipt.events) {
Object.entries(txReceipt.events)
.filter(
([eventName]) =>
(typeof displayEventName === 'string' && eventName === displayEventName) ||
displayEventName.includes(eventName)
)
.forEach(([eventName, log]) => {
const { params } = parseDecodedParams((log as EventLog).returnValues)
console.log(chalk.magenta.bold(`${eventName}:`))
printValueMap(params, chalk.magenta)
})
}
}

export function printValueMap(valueMap: Record<string, any>, color = chalk.yellowBright.bold) {
console.log(
Object.keys(valueMap)
Expand Down
41 changes: 41 additions & 0 deletions packages/cli/src/utils/exchange.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { calculateExpectedSlippage } from './exchange'

import BigNumber from 'bignumber.js'

describe('calculateExpectedSlippage', () => {
describe('when amount is the same', () => {
it('gives zero', () => {
const sellingAmount = new BigNumber(100)
const quotedAmountToReceiveWithBuffer = new BigNumber(110)
const oracleMedianRate = new BigNumber('1.1')
const slippage = 0 // % slippage
// (Executed Price – Expected Price) / Expected Price * 100
expect(
calculateExpectedSlippage(sellingAmount, quotedAmountToReceiveWithBuffer, oracleMedianRate)
).toEqual(slippage)
})
})
describe('when quotedAmountToReceiveWithBuffer is less than oracle rate', () => {
it('gives a negative amount', () => {
const sellingAmount = new BigNumber(100)
const quotedAmountToReceiveWithBuffer = new BigNumber(105)
const oracleMedianRate = new BigNumber('1.1')
const slippage = -4.761904761904762 // % slippage
// (Executed Price – Expected Price) / Expected Price * 100
expect(
calculateExpectedSlippage(sellingAmount, quotedAmountToReceiveWithBuffer, oracleMedianRate)
).toEqual(slippage)
})
})
describe('when quotedAmountToReceiveWithBuffer is higher than oracle rate', () => {
it('gives a positive amount', () => {
const sellingAmount = new BigNumber(100)
const quotedAmountToReceiveWithBuffer = new BigNumber(115)
const oracleMedianRate = new BigNumber('1.1')
const slippage = 4.3478260869565215 // % slippage
expect(
calculateExpectedSlippage(sellingAmount, quotedAmountToReceiveWithBuffer, oracleMedianRate)
).toEqual(slippage)
})
})
})
Loading
Loading