Skip to content

Commit

Permalink
feat: fantom zaps support (#305)
Browse files Browse the repository at this point in the history
* refactor: generalize networks support

* refactor: network specific code

* feat: support multichain simulations

* test: fix missing chain value

* feat: enable fantom simulations

* fix: native token balance address
  • Loading branch information
xgambitox authored Sep 29, 2022
1 parent e4cbcad commit be2590f
Show file tree
Hide file tree
Showing 12 changed files with 220 additions and 234 deletions.
115 changes: 110 additions & 5 deletions src/chain.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Address } from "./types";

/**
* Supported chains in the yearn ecosystem.
*/
Expand All @@ -9,11 +11,7 @@ export const Chains = {
42161: "arbitrum",
};

export type EthMain = 1;
export type OpMain = 10;
export type FtmMain = 250;
export type EthLocal = 1337;
export type ArbitrumOne = 42161;
export type Network = "ethereum" | "optimism" | "fantom" | "arbitrum";

export type ChainId = keyof typeof Chains;

Expand All @@ -34,3 +32,110 @@ export const isArbitrum = (chainId: ChainId): boolean => {
};

export const allSupportedChains = Object.keys(Chains).map((key) => Number(key));

const ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
const NATIVE_TOKEN_ADDRESS = ZERO_ADDRESS;

export interface NetworkSettings {
[chainId: number]: {
id: Network;
name: string;
chainId: number;
rpcUrl: string;
nativeCurrency: {
name: string;
symbol: string;
decimals: number;
address: string;
};
wrappedTokenAddress?: Address;
simulationsEnabled?: boolean;
zapsEnabled?: boolean;
zapOutTokenSymbols?: string[];
blockExplorerUrl?: string;
};
}

export const NETWORK_SETTINGS: NetworkSettings = {
1: {
id: "ethereum",
name: "Ethereum",
chainId: 1,
rpcUrl: "https://mainnet.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161",
nativeCurrency: {
name: "Ethereum",
symbol: "ETH",
decimals: 18,
address: ETH_ADDRESS,
},
wrappedTokenAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
simulationsEnabled: true,
zapsEnabled: true,
zapOutTokenSymbols: ["ETH", "DAI", "USDC", "USDT", "WBTC"],
blockExplorerUrl: "https://etherscan.io",
},
10: {
id: "optimism",
name: "Optimism",
chainId: 10,
rpcUrl: "https://mainnet.optimism.io",
nativeCurrency: {
name: "Ethereum",
symbol: "ETH",
decimals: 18,
address: NATIVE_TOKEN_ADDRESS,
},
simulationsEnabled: false,
zapsEnabled: false,
blockExplorerUrl: "https://optimistic.etherscan.io",
},
250: {
id: "fantom",
name: "Fantom",
chainId: 250,
rpcUrl: "https://rpc.ftm.tools",
nativeCurrency: {
name: "Fantom",
symbol: "FTM",
decimals: 18,
address: NATIVE_TOKEN_ADDRESS,
},
wrappedTokenAddress: "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83",
simulationsEnabled: true,
zapsEnabled: true,
zapOutTokenSymbols: ["FTM", "DAI", "USDC", "USDT"],
blockExplorerUrl: "https://ftmscan.com",
},
1337: {
id: "ethereum",
name: "Ethereum",
chainId: 1337,
rpcUrl: "http://localhost:8545",
nativeCurrency: {
name: "Ethereum",
symbol: "ETH",
decimals: 18,
address: ETH_ADDRESS,
},
wrappedTokenAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
simulationsEnabled: false,
zapsEnabled: true,
zapOutTokenSymbols: ["ETH", "DAI", "USDC", "USDT", "WBTC"],
},
42161: {
id: "arbitrum",
name: "Arbitrum",
chainId: 42161,
rpcUrl: "https://arb1.arbitrum.io/rpc",
nativeCurrency: {
name: "Ethereum",
symbol: "ETH",
decimals: 18,
address: NATIVE_TOKEN_ADDRESS,
},
simulationsEnabled: false,
zapsEnabled: false,
blockExplorerUrl: "https://arbiscan.io",
},
};
60 changes: 10 additions & 50 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,23 @@
import { BigNumber } from "@ethersproject/bignumber";

import { Address, Integer, SdkError, Token, Usdc } from "./types";
import { ChainId, NETWORK_SETTINGS } from "./chain";
import { Address, Integer, SdkError, Usdc } from "./types";
import { toBN } from "./utils";

export const ZeroAddress = "0x0000000000000000000000000000000000000000";
export const EthAddress = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
export const WethAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
export const WrappedFantomAddress = "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83";

export const SUPPORTED_ZAP_OUT_ADDRESSES_MAINNET = {
ETH: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
DAI: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
WBTC: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
};

export const SUPPORTED_ZAP_OUT_TOKEN_SYMBOLS = ["ETH", "DAI", "USDC", "USDT", "WBTC"];

export const FANTOM_TOKEN: Token = {
address: ZeroAddress,
name: "Fantom",
dataSource: "sdk",
decimals: "18",
priceUsdc: "0",
supported: {
ftmApeZap: true,
},
symbol: "FTM",
};

export const ETH_TOKEN: Token = {
address: EthAddress,
name: "ETH",
dataSource: "sdk",
decimals: "18",
priceUsdc: "0",
supported: {
zapper: true,
},
symbol: "ETH",
};

export const WETH_TOKEN: Token = {
address: WethAddress,
name: "WETH",
dataSource: "sdk",
decimals: "18",
priceUsdc: "0",
supported: {
zapper: true,
vaults: true,
},
symbol: "WETH",
};
export const NativeTokenAddress = ZeroAddress;

// Returns truthy if address is defined as a native token address of a network
export function isNativeToken(address: Address): boolean {
return [EthAddress, ZeroAddress].includes(address);
return [EthAddress, NativeTokenAddress].includes(address);
}

// Returns wrapper token address if it exists and its the native token address of a network
export function getWrapperIfNative(address: Address, chainId: ChainId): Address {
const networkSettings = NETWORK_SETTINGS[chainId];
return isNativeToken(address) ? networkSettings.wrappedTokenAddress ?? address : address;
}

// handle a non-200 `fetch` response.
Expand Down
41 changes: 0 additions & 41 deletions src/interfaces/helpers/mergeZapPropsWithAddressables.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { FANTOM_TOKEN } from "../../helpers";
import { createMockTokenMarketData, createMockVaultMetadata } from "../../test-utils/factories";
import { mergeZapPropsWithAddressables } from "./mergeZapPropsWithAddressables";

Expand Down Expand Up @@ -58,44 +57,4 @@ describe("mergeZapPropsWithAddressables", () => {
])
);
});

it("should set the ftmApeZap properties on an addressable", async () => {
const vaultMetadataMock = {
ftm: createMockVaultMetadata({
displayName: "Zappable",
address: FANTOM_TOKEN.address,
}),
notZappable: createMockVaultMetadata({
displayName: "Not Zappable",
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
}),
};

const actual = mergeZapPropsWithAddressables({
addressables: [vaultMetadataMock.ftm, vaultMetadataMock.notZappable],
supportedVaultAddresses: [FANTOM_TOKEN.address],
zapInType: "ftmApeZap",
zapOutType: "ftmApeZap",
});

expect(actual.length).toEqual(2);
expect(actual).toEqual(
expect.arrayContaining([
{
...vaultMetadataMock.ftm,
allowZapIn: true,
allowZapOut: true,
zapInWith: "ftmApeZap",
zapOutWith: "ftmApeZap",
},
{
...vaultMetadataMock.notZappable,
allowZapIn: false,
allowZapOut: false,
zapInWith: undefined,
zapOutWith: undefined,
},
])
);
});
});
46 changes: 19 additions & 27 deletions src/interfaces/simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import BigNumber from "bignumber.js";

import { ChainId } from "../chain";
import { ServiceInterface } from "../common";
import { EthAddress, isNativeToken, WethAddress, ZeroAddress } from "../helpers";
import { getWrapperIfNative, isNativeToken } from "../helpers";
import { PickleJars } from "../services/partners/pickle";
import { SimulationExecutor, SimulationResponse } from "../simulationExecutor";
import {
Expand Down Expand Up @@ -100,7 +100,7 @@ type ZapOutSimulationArgs = {
* or how many underlying tokens the user will receive upon withdrawing share tokens.
*/
export class SimulationInterface<T extends ChainId> extends ServiceInterface<T> {
private simulationExecutor = new SimulationExecutor(this.yearn.services.telegram, this.ctx);
private simulationExecutor = new SimulationExecutor(this.yearn.services.telegram, this.chainId, this.ctx);

async deposit(
from: Address,
Expand Down Expand Up @@ -212,8 +212,7 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
account: Address,
simulationOutcome: SimulationResponse
): Promise<TransactionOutcome> {
const sourceTokenAddress = token === EthAddress ? WethAddress : token;
const sourceToken = await this.yearn.tokens.findByAddress(sourceTokenAddress);
const sourceToken = await this.yearn.tokens.findByAddress(getWrapperIfNative(token, this.chainId));
const [vaultData] = await this.yearn.vaults.get([vault]);
const targetTokenAmount = await this.parseSimulationTargetTokenAmount(vault, account, simulationOutcome);
const sourceTokenAmountUsdc = toBN(sourceToken?.priceUsdc)
Expand Down Expand Up @@ -378,18 +377,18 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
): Promise<TransactionOutcome> {
const targetTokenAmount = await this.parseSimulationTargetTokenAmount(token, account, simulationOutcome);
const sourceTokenAmountUsdc = await this.yearn.services.oracle.getNormalizedValueUsdc(vault, amount);
const targetTokenAmountUsdc = await this.yearn.services.oracle.getNormalizedValueUsdc(
token === EthAddress ? WethAddress : token,
targetTokenAmount
);
const targetToken = await this.yearn.tokens.findByAddress(getWrapperIfNative(token, this.chainId));
const targetTokenAmountUsdc = toBN(targetToken?.priceUsdc)
.times(toUnit({ amount: targetTokenAmount, decimals: Number(targetToken?.decimals) }))
.toFixed(0);
const conversionRate = toBN(sourceTokenAmountUsdc).eq(0)
? 0
: toBN(targetTokenAmountUsdc).div(sourceTokenAmountUsdc).toNumber();

return {
sourceTokenAddress: token,
sourceTokenAddress: vault,
sourceTokenAmount: amount,
targetTokenAddress: vault,
targetTokenAddress: token,
targetTokenAmount,
targetTokenAmountUsdc,
targetUnderlyingTokenAddress: vault,
Expand Down Expand Up @@ -530,16 +529,14 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
vault: ZappableVault;
skipGasEstimate: boolean;
}): Promise<TransactionOutcome> {
const zapToken = sellToken === EthAddress ? ZeroAddress : sellToken;

if (!options.slippage) {
throw new SdkError("slippage needs to be set", SdkError.NO_SLIPPAGE);
}

const partnerId = this.yearn.services.partner?.partnerId;
const zapProtocol = this.getZapProtocol({ vaultAddress: toVault });
const zapInParams = await this.yearn.services.portals
.zapIn(toVault, zapToken, amount, from, options.slippage, !skipGasEstimate, partnerId)
.zapIn(toVault, sellToken, amount, from, options.slippage, !skipGasEstimate, partnerId)
.catch(() => {
throw new ZapError("zap in", ZapError.ZAP_IN);
});
Expand Down Expand Up @@ -605,11 +602,12 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
break;
}

const oracleToken = sellToken === EthAddress ? WethAddress : sellToken;
const zapInAmountUsdc = toBN(
await this.yearn.services.oracle.getNormalizedValueUsdc(oracleToken, amount).catch(() => {
throw new PriceFetchingError("error fetching price", PriceFetchingError.FETCHING_PRICE_ORACLE);
})
await this.yearn.services.oracle
.getNormalizedValueUsdc(getWrapperIfNative(sellToken, this.chainId), amount)
.catch(() => {
throw new PriceFetchingError("error fetching price", PriceFetchingError.FETCHING_PRICE_ORACLE);
})
);

const conversionRate = amountReceivedUsdc.div(zapInAmountUsdc).toNumber();
Expand Down Expand Up @@ -713,9 +711,8 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
throw new SdkError("slippage needs to be set", SdkError.NO_SLIPPAGE);
}

const zapToken = toToken === EthAddress ? ZeroAddress : toToken;
const zapOutParams = await this.yearn.services.portals
.zapOut(fromVault, zapToken, amount, from, options.slippage, skipGasEstimate)
.zapOut(fromVault, toToken, amount, from, options.slippage, skipGasEstimate)
.catch(() => {
throw new ZapError("error zapping out", ZapError.ZAP_OUT);
});
Expand All @@ -727,7 +724,7 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
const tokensReceived = await (async (): Promise<string> => {
if (!zapOutParams.from || !zapOutParams.to || !zapOutParams.data)
throw new ZapError("error zapping out", ZapError.ZAP_OUT);
if (zapToken === ZeroAddress) {
if (isNativeToken(toToken)) {
const response: SimulationResponse = await this.simulationExecutor.makeSimulationRequest(
from,
zapOutParams.to,
Expand All @@ -748,9 +745,8 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
}
})();

const oracleToken = toToken === EthAddress ? WethAddress : toToken;
const zapOutAmountUsdc = await this.yearn.services.oracle
.getNormalizedValueUsdc(oracleToken, tokensReceived)
.getNormalizedValueUsdc(getWrapperIfNative(toToken, this.chainId), tokensReceived)
.catch(() => {
throw new PriceFetchingError("error fetching price", PriceFetchingError.FETCHING_PRICE_ORACLE);
});
Expand Down Expand Up @@ -784,7 +780,7 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
toVault,
options,
}: DepositArgs): Promise<{ needsApproving: boolean } & ApprovalData> {
if (sellToken === EthAddress) {
if (isNativeToken(sellToken)) {
return { needsApproving: false };
}

Expand Down Expand Up @@ -904,10 +900,6 @@ export class SimulationInterface<T extends ChainId> extends ServiceInterface<T>
return this.simulationExecutor.executeSimulationWithReSimulationOnFailure(simulateFn, forkId);
}

if (zapInWith === "ftmApeZap") {
throw new SdkError("ftmApeZap not implemented yet!");
}

throw new SdkError(`zapInWith "${zapInWith}" not supported yet!`);
}

Expand Down
Loading

0 comments on commit be2590f

Please sign in to comment.