Skip to content

Commit

Permalink
feat: zap in simulation [WEB-484] (#41)
Browse files Browse the repository at this point in the history
* feat: added zap in and out functionality

* fix: linted

* fix: used fast gas price from zapper and slippage passed in

* fix: handled slippage undefined better

* fix: converted gas price to gwei

* fix: renamed to reflect gwei

* fix: removed unused import

* fix: use zero address if eth is used for zapper

* feat: simulated zap in roughly

* fix: utilized ZapperService and refactored request functionality

* fix: added documentation for simulateZapIn

* fix: linted

* fix: removed unused imports

* fix: merged master

* feat: added withdraw simulation

* fix: added todo to approval

* fix: refactored

* fix: fetched prices from oracle

* fix: removed comment

* fix: removed unnecessary bignumber

* chore: format

* chore: revert address change in example

Co-authored-by: nymmrx <[email protected]>
  • Loading branch information
jstashh and nymmrx authored Jul 3, 2021
1 parent 63e1fa5 commit 0366cf5
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 25 deletions.
14 changes: 13 additions & 1 deletion src/services/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down Expand Up @@ -51,7 +52,7 @@ export class OracleService<T extends ChainId> extends ContractService<T> {
switch (chainId) {
case 1:
case 1337:
return "0xd3ca98D986Be88b72Ff95fc2eC976a5E6339150d";
return "0x83d95e0D5f402511dB06817Aff3f9eA88224B030";
case 250:
return "0xae813841436fe29b95a14AC701AFb1502C4CB789";
}
Expand All @@ -76,6 +77,17 @@ export class OracleService<T extends ChainId> extends ContractService<T> {
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<Usdc> {
return await this.contract.read.getNormalizedValueUsdc(token, amount, overrides).then(int);
}

/**
* Get the token address that lens considers Usdc.
* @param overrides
Expand Down
319 changes: 297 additions & 22 deletions src/services/simulation.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<any> {
async simulateRaw(from: Address, to: Address, input: String): Promise<any> {
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<TransactionOutcome> {
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<TransactionOutcome> {
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<TransactionOutcome> {
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<TransactionOutcome> {
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<TransactionOutcome> {
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<TransactionOutcome> {
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<any> {
return await fetch(path, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
}).then(res => res.json());
}
Loading

0 comments on commit 0366cf5

Please sign in to comment.