diff --git a/src/services/oracle.ts b/src/services/oracle.ts index a0d87378..24acd5c2 100644 --- a/src/services/oracle.ts +++ b/src/services/oracle.ts @@ -11,6 +11,7 @@ export const OracleAbi = [ "function calculations() external view returns (address[] memory)", "function getPriceUsdcRecommended(address) public view returns (uint256)", "function usdcAddress() public view returns (address)", + "function getNormalizedValueUsdc(address,uint256) view returns (uint256)", // Calculations Curve "function isCurveLpToken(address) public view returns (bool)", "function getCurvePriceUsdc(address) public view returns (uint256)", @@ -51,7 +52,7 @@ export class OracleService extends ContractService { switch (chainId) { case 1: case 1337: - return "0xd3ca98D986Be88b72Ff95fc2eC976a5E6339150d"; + return "0x83d95e0D5f402511dB06817Aff3f9eA88224B030"; case 250: return "0xae813841436fe29b95a14AC701AFb1502C4CB789"; } @@ -76,6 +77,17 @@ export class OracleService extends ContractService { return await this.contract.read.getPriceUsdcRecommended(token, overrides).then(int); } + /** + * Get the normalized Usdc value for the token and corresponding quantity. + * @param token + * @param amount + * @param overrides + * @returns Usdc exchange rate (6 decimals) + */ + async getNormalizedValueUsdc(token: Address, amount: Integer, overrides: CallOverrides = {}): Promise { + return await this.contract.read.getNormalizedValueUsdc(token, amount, overrides).then(int); + } + /** * Get the token address that lens considers Usdc. * @param overrides diff --git a/src/services/simulation.ts b/src/services/simulation.ts index ea90848e..335b4c03 100644 --- a/src/services/simulation.ts +++ b/src/services/simulation.ts @@ -1,7 +1,48 @@ +import { getAddress } from "@ethersproject/address"; +import { Contract } from "@ethersproject/contracts"; +import BigNumber from "bignumber.js"; + +import { ChainId } from "../chain"; import { Service } from "../common"; -import { Address } from "../types"; +import { Context } from "../context"; +import { Address, Integer, SdkError } from "../types"; +import { OracleService } from "./oracle"; +import { ZapperService } from "./zapper"; const baseUrl = "https://simulate.yearn.network"; +const latestBlockKey = -1; +const gasLimit = 8000000; +const VaultAbi = [ + "function deposit(uint256 amount) public", + "function withdraw(uint256 amount) public", + "function token() view returns (address)" +]; + +interface TransactionOutcome { + sourceTokenAddress: Address; + sourceTokenAmount: Integer; + targetTokenAddress: Address; + targetTokenAmount: Integer; + conversionRate: number; + slippage: number; +} + +interface SimulationCallTrace { + output: Integer; + calls: SimulationCallTrace[]; +} + +interface SimulationTransactionInfo { + call_trace: SimulationCallTrace; +} + +interface SimulationTransaction { + transaction_info: SimulationTransactionInfo; +} + +interface SimulationResponse { + transaction: SimulationTransaction; +} /** * [[SimulationService]] allows the simulation of ethereum transactions using Tenderly's api. @@ -28,48 +69,282 @@ export class SimulationService extends Service { network_id: "1" }; - const response: Response = await fetch(`${baseUrl}/fork`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(body) - }).then(res => res.json()); - + const response: Response = await makeRequest(`${baseUrl}/fork`, body); return response.simulation_fork.id; } /** * Simulate a transaction - * @param block the block number to simluate the transaction at * @param from * @param to * @param input the encoded input data as per the ethereum abi specification * @returns data about the simluated transaction */ - async simulateRaw(block: number, from: Address, to: Address, input: String): Promise { + async simulateRaw(from: Address, to: Address, input: String): Promise { const body = { - network_id: "1", - block_number: block, + network_id: this.chainId, + block_number: latestBlockKey, transaction_index: 0, from: from, input: input, to: to, - gas: 800000, + gas: gasLimit, + simulation_type: "quick", + gas_price: "0", + value: "0", + save: true + }; + + return await makeRequest(`${baseUrl}/simulate`, body); + } + + async deposit( + from: Address, + token: Address, + amount: Integer, + vault: Address, + slippage?: number + ): Promise { + const signer = this.ctx.provider.write.getSigner(from); + const vaultContract = new Contract(vault, VaultAbi, signer); + const underlyingToken = await vaultContract.token(); + const isZapping = underlyingToken !== getAddress(token); + + if (isZapping) { + if (slippage === undefined) { + throw new SdkError("slippage needs to be specified for a zap"); + } + return zapIn(from, token, amount, vault, slippage, this.chainId, this.ctx); + } else { + return directDeposit(from, token, amount, vault, vaultContract, this.chainId); + } + } + + async withdraw( + from: Address, + token: Address, + amount: Integer, + vault: Address, + slippage?: number + ): Promise { + const signer = this.ctx.provider.write.getSigner(from); + const vaultContract = new Contract(vault, VaultAbi, signer); + const underlyingToken = await vaultContract.token(); + const isZapping = underlyingToken !== getAddress(token); + + if (isZapping) { + if (slippage === undefined) { + throw new SdkError("slippage needs to be specified for a zap"); + } + return zapOut(from, token, amount, vault, slippage, this.chainId, this.ctx); + } else { + return directWithdraw(from, token, amount, vault, vaultContract, this.chainId); + } + } + + async approve(from: Address, token: Address, amount: Integer, vault: Address) { + const TokenAbi = ["function approve(address spender,uint256 amount) bool"]; + const signer = this.ctx.provider.write.getSigner(from); + const tokenContract = new Contract(token, TokenAbi, signer); + const encodedInputData = tokenContract.interface.encodeFunctionData("approve", [vault, amount]); + + const body = { + network_id: this.chainId.toString(), + block_number: latestBlockKey, + from: from, + input: encodedInputData, + to: token, + gas: gasLimit, simulation_type: "quick", gas_price: "0", value: "0", save: true }; - const response = await fetch(`${baseUrl}/simulate`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(body) - }).then(res => res.json()); + console.log(body); - return response; + // todo } } + +async function directDeposit( + from: Address, + token: Address, + amount: Integer, + vault: Address, + vaultContract: Contract, + chainId: ChainId +): Promise { + const encodedInputData = vaultContract.interface.encodeFunctionData("deposit", [amount]); + + const body = { + network_id: chainId.toString(), + block_number: latestBlockKey, + from: from, + input: encodedInputData, + to: vault, + gas: gasLimit, + simulation_type: "quick", + gas_price: "0", + value: "0", + save: true + }; + + const simulationResponse: SimulationResponse = await makeRequest(`${baseUrl}/simulate`, body); + const tokensReceived = simulationResponse.transaction.transaction_info.call_trace.output; + + const result: TransactionOutcome = { + sourceTokenAddress: token, + sourceTokenAmount: amount, + targetTokenAddress: vault, + targetTokenAmount: tokensReceived, + conversionRate: 1, + slippage: 0 + }; + + return result; +} + +async function zapIn( + from: Address, + token: Address, + amount: Integer, + vault: Address, + slippagePercentage: number, + chainId: ChainId, + ctx: Context +): Promise { + const zapperService = new ZapperService(chainId, ctx); + const zapInParams = await zapperService.zapIn(from, token, amount, vault, "0", slippagePercentage); + + const body = { + network_id: chainId.toString(), + block_number: latestBlockKey, + from: from, + input: zapInParams.data, + to: zapInParams.to, + gas: gasLimit, + simulation_type: "quick", + gas_price: "0", + value: zapInParams.value, + save: true + }; + + const simulationResponse: SimulationResponse = await makeRequest(`${baseUrl}/simulate`, body); + const tokensReceived = simulationResponse.transaction.transaction_info.call_trace.output; + + const oracle = new OracleService(chainId, ctx); + + const zapInAmountUsdc = await oracle.getNormalizedValueUsdc(token, tokensReceived); + const boughtAssetAmountUsdc = await oracle.getNormalizedValueUsdc(vault, amount); + + const conversionRate = new BigNumber(boughtAssetAmountUsdc).div(new BigNumber(zapInAmountUsdc)).toNumber(); + const slippage = 1 - conversionRate; + + const result: TransactionOutcome = { + sourceTokenAddress: token, + sourceTokenAmount: amount, + targetTokenAddress: zapInParams.buyTokenAddress, + targetTokenAmount: tokensReceived, + conversionRate: conversionRate, + slippage: slippage + }; + + return result; +} + +async function directWithdraw( + from: Address, + token: Address, + amount: Integer, + vault: Address, + vaultContract: Contract, + chainId: ChainId +): Promise { + const encodedInputData = vaultContract.interface.encodeFunctionData("withdraw", [amount]); + + const body = { + network_id: chainId.toString(), + block_number: latestBlockKey, + from: from, + input: encodedInputData, + to: vault, + gas: gasLimit, + simulation_type: "quick", + gas_price: "0", + value: "0", + save: true + }; + + const simulationResponse: SimulationResponse = await makeRequest(`${baseUrl}/simulate`, body); + const output = simulationResponse.transaction.transaction_info.call_trace.calls[0].output; + + let result: TransactionOutcome = { + sourceTokenAddress: vault, + sourceTokenAmount: amount, + targetTokenAddress: token, + targetTokenAmount: output, + conversionRate: 1, + slippage: 0 + }; + + return result; +} + +async function zapOut( + from: Address, + token: Address, + amount: Integer, + vault: Address, + slippagePercentage: number, + chainId: ChainId, + ctx: Context +): Promise { + const zapper = new ZapperService(chainId, ctx); + const zapOutParams = await zapper.zapOut(from, token, amount, vault, "0", slippagePercentage); + + const body = { + network_id: chainId.toString(), + block_number: latestBlockKey, + from: zapOutParams.from, + input: zapOutParams.data, + to: zapOutParams.to, + gas: gasLimit, + simulation_type: "quick", + gas_price: "0", + value: "0", + save: true + }; + + const simulationResponse: SimulationResponse = await makeRequest(`${baseUrl}/simulate`, body); + const output = new BigNumber(simulationResponse.transaction.transaction_info.call_trace.output).toFixed(0); + + const oracle = new OracleService(chainId, ctx); + + const zapOutAmountUsdc = await oracle.getNormalizedValueUsdc(token, output); + const soldAssetAmountUsdc = await oracle.getNormalizedValueUsdc(vault, amount); + + const conversionRate = new BigNumber(zapOutAmountUsdc).div(new BigNumber(soldAssetAmountUsdc)).toNumber(); + const slippage = 1 - conversionRate; + + let result: TransactionOutcome = { + sourceTokenAddress: vault, + sourceTokenAmount: amount, + targetTokenAddress: token, + targetTokenAmount: output, + conversionRate: conversionRate, + slippage: slippage + }; + + return result; +} + +async function makeRequest(path: string, body: any): Promise { + return await fetch(path, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(body) + }).then(res => res.json()); +} diff --git a/src/services/zapper.ts b/src/services/zapper.ts index 230c413f..084e671f 100644 --- a/src/services/zapper.ts +++ b/src/services/zapper.ts @@ -133,7 +133,8 @@ export class ZapperService extends Service { poolAddress: vault, gasPrice: gasPrice, slippagePercentage: slippagePercentage.toString(), - api_key: this.ctx.zapper + api_key: this.ctx.zapper, + skipGasEstimate: "true" }); const response: ZapInOutput = await fetch(`${url}?${params}`) .then(handleHttpError) @@ -167,7 +168,8 @@ export class ZapperService extends Service { poolAddress: vault, gasPrice: gasPrice, slippagePercentage: slippagePercentage.toString(), - api_key: this.ctx.zapper + api_key: this.ctx.zapper, + skipGasEstimate: "true" }); const response: ZapOutOutput = await fetch(`${url}?${params}`) .then(handleHttpError)