Skip to content

Commit

Permalink
feat: added pickle jar zap in simulation support [WEB-529] (#60)
Browse files Browse the repository at this point in the history
* feat: added pickle jar zap in simulation support

* fix: renamed pickle jar

* fix: only include yvboost-eth pickle jar

* fix: implemented pricing logic for pickle jars

* fix: linted

* chore: refactor structure

Co-authored-by: nymmrx <[email protected]>
  • Loading branch information
jstashh and nymmrx authored Jul 13, 2021
1 parent 856343f commit 930932b
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 26 deletions.
80 changes: 67 additions & 13 deletions src/interfaces/simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import BigNumber from "bignumber.js";
import { ChainId } from "../chain";
import { ServiceInterface } from "../common";
import { EthAddress, WethAddress, ZeroAddress } from "../helpers";
import { Address, Integer, SdkError, ZapApprovalTransactionOutput } from "../types";
import { PickleJars } from "../services/partners/pickle";
import { Address, Integer, SdkError, ZapApprovalTransactionOutput, ZapProtocol } from "../types";
import { TransactionOutcome } from "../types/custom/simulation";

const baseUrl = "https://simulate.yearn.network";
Expand All @@ -20,6 +21,8 @@ const VaultAbi = [
"function pricePerShare() view returns (uint256)"
];

const PickleJarAbi = ["function token() view returns (address)", "function getRatio() public view returns (uint256)"];

interface SimulationCallTrace {
output: Integer;
calls: SimulationCallTrace[];
Expand Down Expand Up @@ -82,7 +85,17 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
slippage?: number
): Promise<TransactionOutcome> {
const signer = this.ctx.provider.write.getSigner(from);
const vaultContract = new Contract(toVault, VaultAbi, signer);
const zapProtocol = PickleJars.includes(toVault) ? ZapProtocol.PICKLE : ZapProtocol.YEARN;
let abi: string[];
switch (zapProtocol) {
case ZapProtocol.YEARN:
abi = VaultAbi;
break;
case ZapProtocol.PICKLE:
abi = PickleJarAbi;
break;
}
const vaultContract = new Contract(toVault, abi, signer);
const underlyingToken = await vaultContract.token();
const isZapping = underlyingToken !== sellToken;

Expand All @@ -97,12 +110,18 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
needsApproving = false;
} else {
needsApproving = await this.yearn.services.zapper
.zapInApprovalState(from, sellToken)
.zapInApprovalState(from, sellToken, zapProtocol)
.then(state => !state.isApproved);
}

if (needsApproving) {
const approvalTransaction = await this.yearn.services.zapper.zapInApprovalTransaction(from, sellToken, "0");
const approvalTransaction = await this.yearn.services.zapper.zapInApprovalTransaction(
from,
sellToken,
"0",
zapProtocol
);

const forkId = await this.createFork();
const approvalSimulationResponse = await this.simulateZapApprovalTransaction(approvalTransaction, forkId);
return this.zapIn(
Expand All @@ -113,13 +132,14 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
toVault,
vaultContract,
slippage,
zapProtocol,
approvalSimulationResponse.simulation.id,
forkId
).finally(async () => {
await this.deleteFork(forkId);
});
} else {
return this.zapIn(from, sellToken, underlyingToken, amount, toVault, vaultContract, slippage);
return this.zapIn(from, sellToken, underlyingToken, amount, toVault, vaultContract, slippage, zapProtocol);
}
} else {
const needsApproving = await this.depositNeedsApproving(from, sellToken, toVault, amount, signer);
Expand Down Expand Up @@ -282,11 +302,20 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
toVault: Address,
vaultContract: Contract,
slippage: number,
zapProtocol: ZapProtocol,
root?: string,
forkId?: string
): Promise<TransactionOutcome> {
const zapToken = sellToken === EthAddress ? ZeroAddress : sellToken;
const zapInParams = await this.yearn.services.zapper.zapIn(from, zapToken, amount, toVault, "0", slippage);
const zapInParams = await this.yearn.services.zapper.zapIn(
from,
zapToken,
amount,
toVault,
"0",
slippage,
zapProtocol
);
const value = new BigNumber(zapInParams.value).toFixed(0);

const body = {
Expand All @@ -305,19 +334,33 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>

const simulationResponse: SimulationResponse = await this.makeSimulationRequest(body, forkId);
const assetTokensReceived = new BigNumber(simulationResponse.transaction.transaction_info.call_trace.output);
const pricePerShare = await vaultContract.pricePerShare();
const pricePerShare = await this.getPricePerShare(vaultContract, zapProtocol);
const targetUnderlyingTokensReceived = assetTokensReceived
.times(new BigNumber(pricePerShare.toString()))
.div(new BigNumber(10).pow(18))
.multipliedBy(pricePerShare)
.toFixed(0);

const oracleToken = sellToken === EthAddress ? WethAddress : sellToken;
const zapInAmountUsdc = await this.yearn.services.oracle.getNormalizedValueUsdc(oracleToken, amount);

const amountReceived = assetTokensReceived.toFixed(0);
const boughtAssetAmountUsdc = await this.yearn.services.oracle.getNormalizedValueUsdc(toVault, amountReceived);

const conversionRate = new BigNumber(boughtAssetAmountUsdc).div(new BigNumber(zapInAmountUsdc)).toNumber();
let boughtAssetAmountUsdc: BigNumber;

switch (zapProtocol) {
case ZapProtocol.YEARN:
boughtAssetAmountUsdc = await this.yearn.services.oracle
.getNormalizedValueUsdc(toVault, amountReceived)
.then(price => new BigNumber(price));
break;
case ZapProtocol.PICKLE:
boughtAssetAmountUsdc = (await this.yearn.services.pickle.getPriceUsd(toVault))
.dividedBy(new BigNumber(10).pow(18 - 6))
.multipliedBy(new BigNumber(amountReceived));
break;
}

const oracleToken = sellToken === EthAddress ? WethAddress : sellToken;
const zapInAmountUsdc = new BigNumber(await this.yearn.services.oracle.getNormalizedValueUsdc(oracleToken, amount));

const conversionRate = boughtAssetAmountUsdc.div(new BigNumber(zapInAmountUsdc)).toNumber();

const result: TransactionOutcome = {
sourceTokenAddress: sellToken,
Expand Down Expand Up @@ -451,6 +494,17 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
return response;
}

private async getPricePerShare(vaultContract: Contract, zapProtocol: ZapProtocol): Promise<BigNumber> {
switch (zapProtocol) {
case ZapProtocol.YEARN:
const pps = await vaultContract.pricePerShare();
return new BigNumber(pps.toString());
case ZapProtocol.PICKLE:
const ratio = await vaultContract.getRatio();
return new BigNumber(ratio.toString());
}
}

/**
* Create a new fork that can be used to simulate multiple sequential transactions on
* e.g. approval followed by a deposit.
Expand Down
50 changes: 50 additions & 0 deletions src/services/partners/pickle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { getAddress } from "@ethersproject/address";
import { BigNumber } from "bignumber.js";

import { Service } from "../../common";
import { Address } from "../../types/common";

const HourInMilliseconds = 1000 * 60 * 60;
const PickleApiUrl = "https://stkpowy01i.execute-api.us-west-1.amazonaws.com/prod/protocol/pools";

export const PickleJars = [
"0xCeD67a187b923F0E5ebcc77C7f2F7da20099e378" // yvboost-eth
];

export class PickleService extends Service {
private pickleJarUSDPrices: Map<Address, BigNumber> = new Map();
private lastFetchedDate: Date = new Date(0);

/**
* Fetches the USD price of a pickle jar token
* @param jar the address of the jar to fetch
* @returns the price of the jar token in USD
*/
async getPriceUsd(jar: Address): Promise<BigNumber> {
const oneHourAgo = new Date(Date.now() - HourInMilliseconds);
if (this.lastFetchedDate < oneHourAgo) {
await this.fetchPickleJarPrices();
}
return this.pickleJarUSDPrices.get(jar) || new BigNumber(0);
}

private async fetchPickleJarPrices() {
interface JarDatum {
liquidity_locked: number;
jarAddress: Address;
tokens: number;
}

const jarData: JarDatum[] = await fetch(PickleApiUrl).then(res => res.json());

this.pickleJarUSDPrices.clear();
this.lastFetchedDate = new Date();

const relevantJars = jarData.filter(jar => PickleJars.includes(getAddress(jar.jarAddress)));

for (const jarDatum of relevantJars) {
const usdPrice = new BigNumber(jarDatum.liquidity_locked / jarDatum.tokens);
this.pickleJarUSDPrices.set(getAddress(jarDatum.jarAddress), usdPrice);
}
}
}
50 changes: 37 additions & 13 deletions src/services/zapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { Chains } from "../chain";
import { Service } from "../common";
import { EthAddress, handleHttpError, usdc, ZeroAddress } from "../helpers";
import { Address, Balance, BalancesMap, Integer, Token } from "../types";
import { GasPrice, ZapApprovalStateOutput, ZapApprovalTransactionOutput, ZapOutput } from "../types/custom/zapper";
import {
GasPrice,
ZapApprovalStateOutput,
ZapApprovalTransactionOutput,
ZapOutput,
ZapProtocol
} from "../types/custom/zapper";

/**
* [[ZapperService]] interacts with the zapper api to gather more insight for
Expand Down Expand Up @@ -112,9 +118,14 @@ export class ZapperService extends Service {
* Fetches the data needed to check token ZapIn contract approval state
* @param from - the address that is depositing
* @param token - the token to be sold to pay for the deposit
* @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle
*/
async zapInApprovalState(from: Address, token: Address): Promise<ZapApprovalStateOutput> {
const url = "https://api.zapper.fi/v1/zap-in/yearn/approval-state";
async zapInApprovalState(
from: Address,
token: Address,
zapProtocol: ZapProtocol = ZapProtocol.YEARN
): Promise<ZapApprovalStateOutput> {
const url = `https://api.zapper.fi/v1/zap-in/vault/${zapProtocol}/approval-state`;
const params = new URLSearchParams({
ownerAddress: from,
sellTokenAddress: token,
Expand All @@ -132,13 +143,15 @@ export class ZapperService extends Service {
* @param from - the address that is depositing
* @param token - the token to be sold to pay for the deposit
* @param gasPrice
* @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle
*/
async zapInApprovalTransaction(
from: Address,
token: Address,
gasPrice: Integer
gasPrice: Integer,
zapProtocol: ZapProtocol = ZapProtocol.YEARN
): Promise<ZapApprovalTransactionOutput> {
const url = "https://api.zapper.fi/v1/zap-in/yearn/approval-transaction";
const url = `https://api.zapper.fi/v1/zap-in/vault/${zapProtocol}/approval-transaction`;
const params = new URLSearchParams({
gasPrice,
ownerAddress: from,
Expand All @@ -156,9 +169,14 @@ export class ZapperService extends Service {
* Fetches the data needed to check token ZapOut contract approval state
* @param from - the address that is withdrawing
* @param token - the vault token to be withdrawn
* @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle
*/
async zapOutApprovalState(from: Address, token: Address): Promise<ZapApprovalStateOutput> {
const url = "https://api.zapper.fi/v1/zap-out/yearn/approval-state";
async zapOutApprovalState(
from: Address,
token: Address,
zapProtocol: ZapProtocol = ZapProtocol.YEARN
): Promise<ZapApprovalStateOutput> {
const url = `https://api.zapper.fi/v1/zap-out/vault/${zapProtocol}/approval-state`;
const params = new URLSearchParams({
ownerAddress: from,
sellTokenAddress: token,
Expand All @@ -176,13 +194,15 @@ export class ZapperService extends Service {
* @param from - the address that is withdrawing
* @param token - the vault token to be withdrawn
* @param gasPrice
* @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle
*/
async zapOutApprovalTransaction(
from: Address,
token: Address,
gasPrice: Integer
gasPrice: Integer,
zapProtocol: ZapProtocol = ZapProtocol.YEARN
): Promise<ZapApprovalTransactionOutput> {
const url = "https://api.zapper.fi/v1/zap-out/yearn/approval-transaction";
const url = `https://api.zapper.fi/v1/zap-out/vault/${zapProtocol}/approval-transaction`;
const params = new URLSearchParams({
gasPrice,
ownerAddress: from,
Expand All @@ -204,22 +224,24 @@ export class ZapperService extends Service {
* @param vault - the vault to zap into
* @param gasPrice
* @param slippagePercentage - slippage as a decimal
* @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle
*/
async zapIn(
from: Address,
token: Address,
amount: Integer,
vault: Address,
gasPrice: Integer,
slippagePercentage: number
slippagePercentage: number,
zapProtocol: ZapProtocol = ZapProtocol.YEARN
): Promise<ZapOutput> {
let sellToken = token;
if (EthAddress === token) {
// If Ether is being sent, the sellTokenAddress should be the zero address
sellToken = ZeroAddress;
}

const url = "https://api.zapper.fi/v1/zap-in/yearn/transaction";
const url = `https://api.zapper.fi/v1/zap-in/vault/${zapProtocol}/transaction`;
const params = new URLSearchParams({
ownerAddress: from,
sellTokenAddress: sellToken,
Expand All @@ -246,22 +268,24 @@ export class ZapperService extends Service {
* @param vault - the vault to zap out of
* @param gasPrice
* @param slippagePercentage - slippage as a decimal
* @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle
*/
async zapOut(
from: Address,
token: Address,
amount: Integer,
vault: Address,
gasPrice: Integer,
slippagePercentage: number
slippagePercentage: number,
zapProtocol: ZapProtocol = ZapProtocol.YEARN
): Promise<ZapOutput> {
let toToken = token;
if (EthAddress === token) {
// If Ether is being received, the toTokenAddress should be the zero address
toToken = ZeroAddress;
}

const url = "https://api.zapper.fi/v1/zap-out/yearn/transaction";
const url = `https://api.zapper.fi/v1/zap-out/vault/${zapProtocol}/transaction`;
const params = new URLSearchParams({
ownerAddress: from,
toTokenAddress: toToken,
Expand Down
5 changes: 5 additions & 0 deletions src/types/custom/zapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ export interface ZapOutput {
gasPrice: Integer;
gas: Integer;
}

export enum ZapProtocol {
PICKLE = "pickle",
YEARN = "yearn"
}
4 changes: 4 additions & 0 deletions src/yearn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { HelperService } from "./services/helper";
import { IconsService } from "./services/icons";
import { LensService } from "./services/lens";
import { OracleService } from "./services/oracle";
import { PickleService } from "./services/partners/pickle";
import { SubgraphService } from "./services/subgraph";
import { VisionService } from "./services/vision";
import { ZapperService } from "./services/zapper";
Expand All @@ -36,6 +37,8 @@ export class Yearn<T extends ChainId> {
vision: VisionService;
subgraph: SubgraphService;

pickle: PickleService;

helper: HelperService<T>;
};

Expand Down Expand Up @@ -74,6 +77,7 @@ export class Yearn<T extends ChainId> {
icons: new IconsService(chainId, this.context),
vision: new VisionService(chainId, this.context),
subgraph: new SubgraphService(chainId, this.context),
pickle: new PickleService(chainId, this.context),
helper: new HelperService(chainId, this.context)
};

Expand Down

0 comments on commit 930932b

Please sign in to comment.