diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fe0756..efc6d1f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,8 @@ import ladleAbi from "./abi/Ladle.json"; import { Balances, emptyVaults, + loadVaults, + SeriesDefinition, Vaults, VaultsAndBalances, } from "./objects/Vault"; @@ -23,16 +25,17 @@ import { ContractContext as Ladle } from "./abi/Ladle"; import yieldLeverAbi from "./generated/abi/YieldLever.json"; import yieldLeverDeployed from "./generated/deployment.json"; import { ExternalProvider } from "@ethersproject/providers"; +import { + SeriesResponse as Series +} from "./abi/Cauldron"; const YIELD_LEVER_CONTRACT_ADDRESS: string = yieldLeverDeployed.deployedTo; const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; -const POOL_CONTRACT = "0xEf82611C6120185D3BF6e020D1993B49471E7da0"; const CAULDRON_CONTRACT = "0xc88191F8cb8e6D4a668B047c1C8503432c3Ca867"; const LADLE_CONTRACT = "0x6cB18fF2A33e981D1e38A663Ca056c0a5265066A"; const YEARN_STRATEGY = "0xa354F35829Ae975e850e23e9615b11Da1B3dC4DE"; -export const SERIES_ID = "0x303230360000"; export const ILK_ID = "0x303900000000"; type YearnApiJson = { address: string; apy: { net_apy: number } }[]; @@ -43,12 +46,14 @@ interface State { usdcBalance?: BigNumber; vaults: VaultsAndBalances; yearn_apy?: number; + series: SeriesDefinition[]; + seriesInfo: { [seriesId: string]: Series }; } export interface Contracts { usdcContract: ERC20; yieldLeverContract: YieldLever; - poolContract: Pool; + poolContracts: { [poolAddress: string]: Pool }; cauldronContract: Cauldron; ladleContract: Ladle; } @@ -70,6 +75,8 @@ export class App extends React.Component, State> { selectedAddress: undefined, usdcBalance: undefined, vaults: emptyVaults(), + series: [], + seriesInfo: {} }; this.state = this.initialState; } @@ -114,6 +121,8 @@ export class App extends React.Component, State> { contracts={this.contracts} account={this.state.selectedAddress} yearnApi={this.state.yearn_apy} + seriesDefinitions={this.state.series} + seriesInfo={this.state.seriesInfo} />, ...vaultIds.map((vaultId) => ( , State> { yieldLeverAbi.abi, this._provider.getSigner(0) ) as any as YieldLever, - poolContract: new ethers.Contract( - POOL_CONTRACT, - poolAbi, - this._provider.getSigner(0) - ) as any as Pool, + poolContracts: Object.create(null) as { [poolAddress: string]: Pool }, cauldronContract: new ethers.Contract( CAULDRON_CONTRACT, cauldronAbi, @@ -235,8 +240,8 @@ export class App extends React.Component, State> { ) as any as Ladle, }; - // if (this.state.selectedAddress !== undefined) - // loadVaults(this.contracts.cauldronContract, this.state.selectedAddress, this._provider); + if (this.state.selectedAddress !== undefined) + void loadVaults(this.contracts.cauldronContract, this.state.selectedAddress, this._provider, (vaultId) => void this.addVault(vaultId), (series) => void this.addSeries(series)); const vaultsBuiltFilter = this.contracts.cauldronContract.filters.VaultBuilt( @@ -271,6 +276,21 @@ export class App extends React.Component, State> { return error.message; } + private async addSeries(series: SeriesDefinition) { + if (this.contracts === undefined || this._provider === undefined) + throw new Error('Race condition'); + const seriesInfo = await this.contracts.cauldronContract.series(series.seriesId); + this.contracts.poolContracts[series.seriesId] = new ethers.Contract( + series.poolAddress, + poolAbi, + this._provider.getSigner(0) + ) as any as Pool; + this.setState({ + series: [...this.state.series, series], + seriesInfo: {...this.state.seriesInfo, [series.seriesId]: seriesInfo } + }) + } + // This method resets the state resetState() { this.setState(this.initialState); diff --git a/frontend/src/components/Invest.tsx b/frontend/src/components/Invest.tsx index 1981ccd..f4af517 100644 --- a/frontend/src/components/Invest.tsx +++ b/frontend/src/components/Invest.tsx @@ -1,6 +1,6 @@ import { BigNumber, utils } from "ethers"; import React from "react"; -import { Contracts, ILK_ID, SERIES_ID } from "../App"; +import { Contracts, ILK_ID } from "../App"; import "./Invest.scss"; import Slippage, { addSlippage, SLIPPAGE_OPTIONS } from "./Slippage"; import UsdcInput from "./UsdcInput"; @@ -10,6 +10,7 @@ import { SeriesResponse as Series, ContractContext as Cauldron, } from "../abi/Cauldron"; +import { SeriesDefinition } from '../objects/Vault'; const UNITS_USDC = 6; const UNITS_LEVERAGE = 2; @@ -20,6 +21,8 @@ interface Properties { label: string; contracts: Readonly; yearnApi?: number; + seriesDefinitions: SeriesDefinition[]; + seriesInfo: { [seriesId: string]: Series }; } enum ApprovalState { @@ -37,13 +40,14 @@ interface State { approvalState: ApprovalState; fyTokens?: BigNumber; slippage: number; + selectedSeriesId?: string; interest?: number; + seriesInterest: { [seriesId: string]: number } } export default class Invest extends React.Component { private readonly contracts: Readonly; private readonly account: string; - private series?: Promise; constructor(props: Properties) { super(props); @@ -55,6 +59,7 @@ export default class Invest extends React.Component { leverage: BigNumber.from(300), approvalState: ApprovalState.Loading, slippage: SLIPPAGE_OPTIONS[1].value, + seriesInterest: {}, }; } @@ -125,6 +130,21 @@ export default class Invest extends React.Component { valueType={ValueType.Usdc} value={this.state.usdcBalance} /> + { /> )} - {this.state.interest !== undefined ? ( + {this.state.selectedSeriesId !== undefined && this.state.seriesInterest[this.state.selectedSeriesId] !== undefined ? ( ) : null} @@ -225,12 +245,27 @@ export default class Invest extends React.Component { } private async checkApprovalState() { + let seriesId: string; + if (this.state.selectedSeriesId === undefined) { + const series = this.props.seriesDefinitions.find((ser) => !Invest.isPastMaturity(this.props.seriesInfo[ser.seriesId])); + if (series === undefined) { + return; + } else { + seriesId = series.seriesId; + this.setState({ selectedSeriesId: series.seriesId }); + } + } else { + seriesId = this.state.selectedSeriesId; + } + + void this.computeSeriesInterests(); + // First, set to loading this.setState({ approvalState: ApprovalState.Loading, }); - const series = await this.loadSeries(); + const series = this.props.seriesInfo[seriesId]; const allowance: BigNumber = await this.contracts.usdcContract.allowance( this.account, this.contracts.yieldLeverContract.address @@ -271,23 +306,36 @@ export default class Invest extends React.Component { approvalState: ApprovalState.Undercollateralized, }); } else { - const interest = await this.computeInterest(); + const toBorrow = this.totalToInvest().sub(this.state.usdcToInvest); + const interest = await this.computeInterest(seriesId, toBorrow); if (allowance.lt(this.state.usdcToInvest)) { this.setState({ fyTokens, approvalState: ApprovalState.ApprovalRequired, - interest, + interest }); } else { this.setState({ fyTokens, approvalState: ApprovalState.Transactable, - interest, + interest }); } } } + private async computeSeriesInterests() { + for (let i = 0; i < this.props.seriesDefinitions.length; i++) { + const series = this.props.seriesDefinitions[i]; + if (!Invest.isPastMaturity(this.props.seriesInfo[series.seriesId])) { + console.log(series.seriesId); + const toBorrow = BigNumber.from(100_000_000); + const interest = await this.computeInterest(series.seriesId, toBorrow); + this.setState({ seriesInterest: { ...this.state.seriesInterest, [series.seriesId]: interest } }); + } + } + } + private collateralizationRatio(fyTokens: BigNumber): BigNumber { return this.totalToInvest().div(fyTokens.div(1_000_000)); } @@ -313,44 +361,42 @@ export default class Invest extends React.Component { * @returns */ private async fyTokens(): Promise { - if (this.totalToInvest().eq(0)) return BigNumber.from(0); + if (this.totalToInvest().eq(0) || this.state.selectedSeriesId === undefined) return BigNumber.from(0); const leverage = this.totalToInvest().sub(this.state.usdcToInvest); + const poolContract = this.props.contracts.poolContracts[this.state.selectedSeriesId]; return addSlippage( - await this.contracts.poolContract.buyBasePreview(leverage), + await poolContract.buyBasePreview(leverage), this.state.slippage ); } private async transact() { + if (this.state.selectedSeriesId === undefined) { + return; + } const leverage = this.totalToInvest().sub(this.state.usdcToInvest); const maxFy = await this.fyTokens(); console.log( this.state.usdcToInvest.toString(), leverage.toString(), maxFy.toString(), - SERIES_ID + this.state.selectedSeriesId ); const tx = await this.contracts.yieldLeverContract.invest( this.state.usdcToInvest, leverage, maxFy, - SERIES_ID + this.state.selectedSeriesId ); await tx.wait(); } - private async loadSeries(): Promise { - if (this.series === undefined) - this.series = this.contracts.cauldronContract.series(SERIES_ID); - return this.series; - } - - private async computeInterest(): Promise { - const series = await this.loadSeries(); + private async computeInterest(seriesId: string, toBorrow: BigNumber): Promise { + const series = this.props.seriesInfo[seriesId]; + const poolContract = this.props.contracts.poolContracts[seriesId]; const currentTime = Date.now() / 1000; const maturityTime = series.maturity; - const toBorrow = this.totalToInvest().sub(this.state.usdcToInvest); - const fyTokens = await this.contracts.poolContract.buyBasePreview(toBorrow); + const fyTokens = await poolContract.buyBasePreview(toBorrow); const year = 356.2425 * 24 * 60 * 60; const result_in_period = toBorrow.mul(1_000_000).div(fyTokens).toNumber() / 1_000_000; @@ -360,4 +406,8 @@ export default class Invest extends React.Component { ); return Math.round(10000 * (1 - interest_per_year)) / 100; } + + private static isPastMaturity(seriesInfo: Series): boolean { + return seriesInfo.maturity <= Date.now() / 1000; + } } diff --git a/frontend/src/components/Vault.tsx b/frontend/src/components/Vault.tsx index c776949..83a379b 100644 --- a/frontend/src/components/Vault.tsx +++ b/frontend/src/components/Vault.tsx @@ -1,6 +1,6 @@ import { BigNumber, utils } from "ethers"; import React from "react"; -import { Contracts, SERIES_ID } from "../App"; +import { Contracts } from "../App"; import { Balance, Vault as VaultI } from "../objects/Vault"; import Slippage, { addSlippage, SLIPPAGE_OPTIONS } from "./Slippage"; import ValueDisplay, { ValueType } from "./ValueDisplay"; @@ -46,7 +46,7 @@ export default class Vault extends React.Component { /> { try { console.log( `Expected FY:\t${utils.formatUnits( - await this.props.contracts.poolContract.buyFYTokenPreview( + await this.props.contracts.poolContracts[this.props.vault.seriesId].buyFYTokenPreview( balance.art ), 6 )} USDC` ); return addSlippage( - await this.props.contracts.poolContract.buyFYTokenPreview(balance.art), + await this.props.contracts.poolContracts[this.props.vault.seriesId].buyFYTokenPreview(balance.art), this.state.slippage ); } catch (e) { @@ -111,7 +111,7 @@ export default class Vault extends React.Component { private async unwind() { const [poolAddress, balances] = await Promise.all([ - this.props.contracts.ladleContract.pools(SERIES_ID), + this.props.contracts.ladleContract.pools(this.props.vault.seriesId), this.props.contracts.cauldronContract.balances(this.props.vaultId), ]); // Sanity check @@ -130,7 +130,7 @@ export default class Vault extends React.Component { poolAddress, balances.ink, balances.art, - SERIES_ID + this.props.vault.seriesId ); const tx = await this.props.contracts.yieldLeverContract.unwind( this.props.vaultId, @@ -138,7 +138,7 @@ export default class Vault extends React.Component { poolAddress, balances.ink, balances.art, - SERIES_ID + this.props.vault.seriesId ); await tx.wait(); await Promise.all([this.props.pollData(), this.updateToBorrow()]); diff --git a/frontend/src/objects/Vault.tsx b/frontend/src/objects/Vault.tsx index 1238980..0532877 100644 --- a/frontend/src/objects/Vault.tsx +++ b/frontend/src/objects/Vault.tsx @@ -1,4 +1,11 @@ -import { BigNumber, Contract, ethers } from "ethers"; +import { BigNumber, ethers } from "ethers"; +import { ContractContext as Cauldron } from "../abi/Cauldron"; +import { ILK_ID } from "../App"; + +export interface SeriesDefinition { + poolAddress: string; + seriesId: string; +} export interface Vault { ilkId: string; @@ -24,33 +31,58 @@ export interface VaultsAndBalances { balances: Balances; } -const CAULDRON_CREATED_BLOCK_NUMBER = 13461506; -const BLOCK_STEPS = 10; +/** Don't look prior to this block number. */ +const CAULDRON_CREATED_BLOCK_NUMBER = 0; +const BLOCK_STEPS = 10000; +/** + * Look for vaults that have been created on or transferred to the address. + */ export async function loadVaults( - cauldron: Contract, + cauldron: Cauldron, account: string, - provider: ethers.providers.Web3Provider + provider: ethers.providers.Web3Provider, + vaultDiscseriesDiscoveredovered: (vaultId: string) => void, + seriesDiscovered: (series: SeriesDefinition) => void ) { - const currentBlock: number = await provider.getBlockNumber(); - const vaultsBuiltFilter = cauldron.filters.VaultBuilt(null, account, null); - const vaultsReceivedFilter = cauldron.filters.VaultGiven(null, account); - for ( - let start = CAULDRON_CREATED_BLOCK_NUMBER; - start < currentBlock; - start += BLOCK_STEPS - ) { - const end = Math.min(start + BLOCK_STEPS, currentBlock); - console.log(start, end); - console.log( - (start - CAULDRON_CREATED_BLOCK_NUMBER) / - (currentBlock - CAULDRON_CREATED_BLOCK_NUMBER) - ); - const [vaultsBuilt, vaultsReceived] = await Promise.all([ - cauldron.queryFilter(vaultsBuiltFilter, start, end), - cauldron.queryFilter(vaultsReceivedFilter, start, end), - ]); - console.log(vaultsBuilt, vaultsReceived); + if ((await provider.getNetwork()).chainId === 1337) { + // Ganache log fetching is so slow as to be unusable for us. Use hardcoded pools instead. + seriesDiscovered({ + poolAddress: '0x80142add3A597b1eD1DE392A56B2cef3d8302797', + seriesId: '0x303230350000' + }); + seriesDiscovered({ + poolAddress: '0xEf82611C6120185D3BF6e020D1993B49471E7da0', + seriesId: '0x303230360000' + }); + } else { + const currentBlock: number = await provider.getBlockNumber(); + const vaultsBuiltFilter = cauldron.filters.VaultBuilt(null, account, null); + const vaultsReceivedFilter = cauldron.filters.VaultGiven(null, account); + const seriesAddedFilter = cauldron.filters.SeriesAdded(null, ILK_ID, null); + + const cauldronWithFilter = cauldron as Cauldron & { + queryFilter(a: typeof vaultsBuiltFilter, b: number, c: number): Promise; + queryFilter(a: typeof vaultsReceivedFilter, b: number, c: number): Promise; + queryFilter(a: typeof seriesAddedFilter, b: number, c: number): Promise; + }; + + let end = currentBlock; + while (end > CAULDRON_CREATED_BLOCK_NUMBER) { + const start = Math.max(end - BLOCK_STEPS, CAULDRON_CREATED_BLOCK_NUMBER); + const [vaultsBuilt, vaultsReceived] = await Promise.all([ + cauldronWithFilter.queryFilter(vaultsBuiltFilter, start, end), + cauldronWithFilter.queryFilter(vaultsReceivedFilter, start, end), + ]); + const series = await cauldronWithFilter.queryFilter(seriesAddedFilter, start, end); + if (vaultsBuilt.length !== 0 || vaultsReceived.length !== 0) + // TODO: Call callback + console.log(vaultsBuilt, vaultsReceived); + if (series.length !== 0) + console.log(series); + console.log(start); + end = start; + } } } diff --git a/package-lock.json b/package-lock.json index f1e7514..fd07ef1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "ethers": "^5.6.1", - "ganache": "^7.0.4" + "ganache": "^7.0.5" }, "devDependencies": { "ethereum-abi-types-generator": "^1.3.2" @@ -1134,9 +1134,9 @@ } }, "node_modules/ganache": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ganache/-/ganache-7.0.4.tgz", - "integrity": "sha512-/ur2WNQoCwmobZb/TcpMgJlJy4TLtCUMtnCIldChAn5LleeAETaHB80knGqOgGc3ZS+ksokSqmQHuHqMAmlRkg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ganache/-/ganache-7.0.5.tgz", + "integrity": "sha512-CMRj6dwl1WtE3/4uLHhn/TcZA6QB0SM9YFxSp0ToeGJQ+9HYyfPlzuDE7CzniN+PklEHaCcpgoRYAq9Py9m6WA==", "hasShrinkwrap": true, "dependencies": { "@trufflesuite/bigint-buffer": "1.1.9", @@ -1158,7 +1158,6 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/@trufflesuite/bigint-buffer/-/bigint-buffer-1.1.9.tgz", "integrity": "sha512-bdM5cEGCOhDSwminryHJbRmXc1x7dPKg6Pqns3qyTwFlxsqUgxE29lsERS3PlIW1HTjoIGMUqsk1zQQwST1Yxw==", - "hasInstallScript": true, "dependencies": { "node-gyp-build": "4.3.0" } @@ -1191,7 +1190,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.5.tgz", "integrity": "sha512-HTm14iMQKK2FjFLRTM5lAVcyaUzOnqbPtesFIvREgXpJHdQm8bWS+GkQgIkfaBYRHuCnea7w8UVNfwiAQhlr9A==", - "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -1262,7 +1260,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", - "hasInstallScript": true, "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0" @@ -1272,7 +1269,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-6.1.0.tgz", "integrity": "sha512-8C7oJDT44JXxh04aSSsfcMI8YiaGRhOFI9/pMEL7nWJLVsWajDPTRxsSHTM2WcTVY5nXM+SuRHzPPi0GbnDX+w==", - "hasInstallScript": true, "dependencies": { "abstract-leveldown": "^7.2.0", "napi-macros": "~2.0.0", @@ -1344,7 +1340,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz", "integrity": "sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==", - "hasInstallScript": true, "dependencies": { "elliptic": "^6.5.2", "node-addon-api": "^2.0.0", @@ -1355,7 +1350,6 @@ "version": "5.0.7", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.7.tgz", "integrity": "sha512-vLt1O5Pp+flcArHGIyKEQq883nBt8nN8tVBcoL0qUXj2XT1n7p70yGIq2VK98I5FdZ1YHc0wk/koOnHjnXWk1Q==", - "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -2402,9 +2396,9 @@ } }, "ganache": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ganache/-/ganache-7.0.4.tgz", - "integrity": "sha512-/ur2WNQoCwmobZb/TcpMgJlJy4TLtCUMtnCIldChAn5LleeAETaHB80knGqOgGc3ZS+ksokSqmQHuHqMAmlRkg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ganache/-/ganache-7.0.5.tgz", + "integrity": "sha512-CMRj6dwl1WtE3/4uLHhn/TcZA6QB0SM9YFxSp0ToeGJQ+9HYyfPlzuDE7CzniN+PklEHaCcpgoRYAq9Py9m6WA==", "requires": { "@trufflesuite/bigint-buffer": "1.1.9", "bufferutil": "4.0.5", diff --git a/package.json b/package.json index 7999400..85e23b9 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "license": "ISC", "dependencies": { "ethers": "^5.6.1", - "ganache": "^7.0.4" + "ganache": "^7.0.5" }, "devDependencies": { "ethereum-abi-types-generator": "^1.3.2"