Skip to content

Commit

Permalink
add flash mint nav tx builder
Browse files Browse the repository at this point in the history
  • Loading branch information
janndriessen committed Oct 22, 2024
1 parent 9ea52ee commit 618cc84
Show file tree
Hide file tree
Showing 2 changed files with 363 additions and 0 deletions.
247 changes: 247 additions & 0 deletions src/flashmint/builders/nav.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { BigNumber } from '@ethersproject/bignumber'

import { ChainId } from 'constants/chains'
import { Contracts } from 'constants/contracts'
import {
HighYieldETHIndex,
TheUSDCYieldIndex,
USDC,
WETH,
} from 'constants/tokens'
import { LocalhostProvider, LocalhostProviderUrl } from 'tests/utils'
import { getFlashMintNavContract } from 'utils/contracts'
import { wei } from 'utils/numbers'
import { Exchange } from 'utils'

import {
FlashMintNavBuildRequest,
FlashMintNavTransactionBuilder,
} from 'flashmint/builders/nav'

const provider = LocalhostProvider
const rpcUrl = LocalhostProviderUrl

const eth = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
const usdcAddress = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'

const FlashMintNavAddress = Contracts[ChainId.Mainnet].FlashMintNav

describe('FlashMintNavTransactionBuilder()', () => {
const contract = getFlashMintNavContract(provider)

test('returns null for invalid request (no index token)', async () => {
const buildRequest = createBuildRequest()
buildRequest.isMinting = true
buildRequest.outputToken = ''
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
expect(tx).toBeNull()
})

test('returns null for invalid request (no input/output token)', async () => {
const buildRequest = createBuildRequest()
buildRequest.inputToken = ''
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
expect(tx).toBeNull()
})

test('returns null for invalid request (inputTokenAmount = 0)', async () => {
const buildRequest = createBuildRequest()
buildRequest.indexTokenAmount = BigNumber.from(0)
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
expect(tx).toBeNull()
})

test('returns null for invalid request (outputTokenAmount = 0)', async () => {
const buildRequest = createBuildRequest()
buildRequest.inputOutputTokenAmount = BigNumber.from(0)
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
expect(tx).toBeNull()
})

test('returns null for invalid request (invalid swap data debt collateral - no pool)', async () => {
const buildRequest = createBuildRequest()
buildRequest.reserveAssetSwapData = {
exchange: 1,
path: ['', ''],
fees: [],
poolIds: [],
pool: '',
}
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
expect(tx).toBeNull()
})

test('returns null for invalid request (invalid swap data input/output token - no paths)', async () => {
const buildRequest = createBuildRequest()
buildRequest.reserveAssetSwapData = {
exchange: 1,
path: [],
fees: [],
poolIds: [],
pool: '0x0000000000000000000000000000000000000000',
}
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
expect(tx).toBeNull()
})

test('returns null for invalid request (invalid swap data input/output token - univ3 fees)', async () => {
const buildRequest = createBuildRequest()
buildRequest.reserveAssetSwapData = {
exchange: 3,
path: ['', '', ''],
// For UniV3 fees.length has to be path.length - 1
fees: [3000],
poolIds: [],
pool: '0x0000000000000000000000000000000000000000',
}
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
expect(tx).toBeNull()
})

test('returns null for invalid request (invalid path - exchange type none)', async () => {
const buildRequest = createBuildRequest()
buildRequest.reserveAssetSwapData = {
exchange: 0,
path: [],
fees: [500],
poolIds: [],
pool: '0x00000000000000000000000000000000000000',
}
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
expect(tx).toBeNull()
})

test('returns tx for correct swap data with exchange type none', async () => {
const buildRequest = createBuildRequest()
buildRequest.reserveAssetSwapData = {
exchange: 0,
path: [],
fees: [500],
poolIds: [],
pool: '0x0000000000000000000000000000000000000000',
}
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
expect(tx).not.toBeNull()
})

test('returns a tx for minting icUSD (ERC20)', async () => {
const buildRequest = createBuildRequest()
buildRequest.isMinting = true
const indexToken = buildRequest.outputToken
const refTx = await contract.populateTransaction.issueSetFromExactERC20(
indexToken,
buildRequest.indexTokenAmount,
buildRequest.inputToken,
buildRequest.inputOutputTokenAmount,
buildRequest.reserveAssetSwapData
)
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
if (!tx) fail()
expect(tx.to).toBe(FlashMintNavAddress)
expect(tx.data).toEqual(refTx.data)
})

test('returns a tx for minting icUSD (ETH)', async () => {
const buildRequest = createBuildRequest(true, eth, 'ETH')
const indexToken = buildRequest.outputToken
const refTx = await contract.populateTransaction.issueSetFromExactETH(
indexToken,
buildRequest.indexTokenAmount,
buildRequest.reserveAssetSwapData,
{ value: buildRequest.inputOutputTokenAmount }
)
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
if (!tx) fail()
expect(tx.to).toBe(FlashMintNavAddress)
expect(tx.data).toEqual(refTx.data)
expect(tx.value).toEqual(buildRequest.inputOutputTokenAmount)
})

test('returns a tx for redeeming icUSD (ERC20)', async () => {
const buildRequest = createBuildRequest(
false,
TheUSDCYieldIndex.addressArbitrum!,
TheUSDCYieldIndex.symbol,
usdcAddress,
'USDC'
)
const refTx = await contract.populateTransaction.redeemExactSetForERC20(
buildRequest.inputToken,
buildRequest.indexTokenAmount,
buildRequest.outputToken,
buildRequest.inputOutputTokenAmount,
buildRequest.reserveAssetSwapData
)
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
if (!tx) fail()
expect(tx.to).toBe(FlashMintNavAddress)
expect(tx.data).toEqual(refTx.data)
})

test('returns a tx for redeeming icUSD (ETH)', async () => {
const buildRequest = createBuildRequest(
false,
TheUSDCYieldIndex.address!,
TheUSDCYieldIndex.symbol,
eth,
'ETH'
)
const refTx = await contract.populateTransaction.redeemExactSetForETH(
buildRequest.inputToken,
buildRequest.indexTokenAmount,
buildRequest.inputOutputTokenAmount,
buildRequest.reserveAssetSwapData
)
const builder = new FlashMintNavTransactionBuilder(rpcUrl)
const tx = await builder.build(buildRequest)
if (!tx) fail()
expect(tx.to).toBe(FlashMintNavAddress)
expect(tx.data).toEqual(refTx.data)
})
})

function createBuildRequest(
isMinting = true,
inputToken: string = usdcAddress,
inputTokenSymbol = 'USDC',
outputToken: string = HighYieldETHIndex.address!,
outputTokenSymbol: string = HighYieldETHIndex.symbol
): FlashMintNavBuildRequest {
const inputSwapData = {
exchange: Exchange.UniV3,
path: [USDC.address!, WETH.address!],
fees: [500],
poolIds: [],
pool: '0xDC24316b9AE028F1497c275EB9192a3Ea0f67022',
}
const outputSwapData = {
exchange: Exchange.UniV3,
path: [WETH.address!, USDC.address!],
fees: [500],
poolIds: [],
pool: '0xDC24316b9AE028F1497c275EB9192a3Ea0f67022',
}
return {
isMinting,
inputToken,
inputTokenSymbol,
outputToken,
outputTokenSymbol,
indexTokenAmount: wei(1),
inputOutputTokenAmount: BigNumber.from(194235680),
reserveAssetSwapData: isMinting ? inputSwapData : outputSwapData,
}
}
116 changes: 116 additions & 0 deletions src/flashmint/builders/nav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { TransactionRequest } from '@ethersproject/abstract-provider'
import { BigNumber } from '@ethersproject/bignumber'

import { getRpcProvider } from 'utils/rpc-provider'
import { Exchange, SwapDataV3 } from 'utils/swap-data'

import { getFlashMintNavContract } from '../../utils/contracts'
import { TransactionBuilder } from './interface'
import { isEmptyString, isInvalidAmount } from './utils'

export interface FlashMintNavBuildRequest {
isMinting: boolean
inputToken: string
inputTokenSymbol: string
outputToken: string
outputTokenSymbol: string
indexTokenAmount: BigNumber
inputOutputTokenAmount: BigNumber
reserveAssetSwapData: SwapDataV3
}

export class FlashMintNavTransactionBuilder
implements TransactionBuilder<FlashMintNavBuildRequest, TransactionRequest>
{
constructor(private readonly rpcUrl: string) {}

async build(
request: FlashMintNavBuildRequest
): Promise<TransactionRequest | null> {
if (!this.isValidRequest(request)) return null
const provider = getRpcProvider(this.rpcUrl)
const {
inputToken,
inputTokenSymbol,
indexTokenAmount,
inputOutputTokenAmount,
outputToken,
outputTokenSymbol,
isMinting,
reserveAssetSwapData,
} = request
const contract = getFlashMintNavContract(provider)
if (isMinting) {
const inputTokenIsEth = inputTokenSymbol === 'ETH'
if (inputTokenIsEth) {
return await contract.populateTransaction.issueSetFromExactETH(
outputToken,
indexTokenAmount, // _minSetTokenAmount
reserveAssetSwapData,
{ value: inputOutputTokenAmount }
)
} else {
return await contract.populateTransaction.issueSetFromExactERC20(
outputToken,
indexTokenAmount, // _minSetTokenAmount
inputToken,
inputOutputTokenAmount, // _maxAmountInputToken
reserveAssetSwapData
)
}
} else {
const outputTokenIsEth = outputTokenSymbol === 'ETH'
if (outputTokenIsEth) {
return await contract.populateTransaction.redeemExactSetForETH(
inputToken,
indexTokenAmount,
inputOutputTokenAmount, // _minEthAmount
reserveAssetSwapData
)
} else {
return await contract.populateTransaction.redeemExactSetForERC20(
inputToken,
indexTokenAmount,
outputToken,
inputOutputTokenAmount, // _minOutputTokenAmount
reserveAssetSwapData
)
}
}
}

private isValidSwapData(swapData: SwapDataV3): boolean {
if (swapData.exchange === Exchange.None) {
if (swapData.pool.length !== 42) return false
return true
}
if (
swapData.exchange === Exchange.UniV3 &&
swapData.fees.length !== swapData.path.length - 1
)
return false
if (swapData.path.length === 0) return false
if (swapData.pool.length !== 42) return false
return true
}

private isValidRequest(request: FlashMintNavBuildRequest): boolean {
const {
inputToken,
inputTokenSymbol,
outputToken,
outputTokenSymbol,
indexTokenAmount,
inputOutputTokenAmount,
reserveAssetSwapData,
} = request
if (isEmptyString(inputToken)) return false
if (isEmptyString(outputToken)) return false
if (isEmptyString(inputTokenSymbol)) return false
if (isEmptyString(outputTokenSymbol)) return false
if (isInvalidAmount(indexTokenAmount)) return false
if (isInvalidAmount(inputOutputTokenAmount)) return false
if (!this.isValidSwapData(reserveAssetSwapData)) return false
return true
}
}

0 comments on commit 618cc84

Please sign in to comment.