From 5cc5ad1952432cf1e291421d8cf79f2b4e437681 Mon Sep 17 00:00:00 2001 From: odiinnn Date: Thu, 29 Aug 2024 11:12:25 +0300 Subject: [PATCH] Add Yieldnest --- src/abi/ynETH.json | 1027 +++++++++++++++++++++++ src/dex/index.ts | 2 + src/dex/yieldnest/config.ts | 28 + src/dex/yieldnest/type.ts | 3 + src/dex/yieldnest/utils.ts | 33 + src/dex/yieldnest/yieldnest-e2e.test.ts | 49 ++ src/dex/yieldnest/yieldnest.ts | 280 ++++++ src/dex/yieldnest/yneth-pool.ts | 50 ++ tests/constants-e2e.ts | 4 + 9 files changed, 1476 insertions(+) create mode 100644 src/abi/ynETH.json create mode 100644 src/dex/yieldnest/config.ts create mode 100644 src/dex/yieldnest/type.ts create mode 100644 src/dex/yieldnest/utils.ts create mode 100644 src/dex/yieldnest/yieldnest-e2e.test.ts create mode 100644 src/dex/yieldnest/yieldnest.ts create mode 100644 src/dex/yieldnest/yneth-pool.ts diff --git a/src/abi/ynETH.json b/src/abi/ynETH.json new file mode 100644 index 000000000..8fd4de7db --- /dev/null +++ b/src/abi/ynETH.json @@ -0,0 +1,1027 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AccessControlBadConfirmation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "neededRole", + "type": "bytes32" + } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "expected", + "type": "address" + }, + { + "internalType": "address", + "name": "provided", + "type": "address" + } + ], + "name": "CallerNotStakingNodeManager", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "MathOverflowedMulDiv", + "type": "error" + }, + { + "inputs": [], + "name": "NoDirectETHDeposit", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "NotRewardsDistributor", + "type": "error" + }, + { + "inputs": [], + "name": "Paused", + "type": "error" + }, + { + "inputs": [], + "name": "TransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "TransfersPaused", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroETH", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalDepositedInPool", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "isPaused", + "type": "bool" + } + ], + "name": "DepositETHPausedUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "ethAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalDepositedInPool", + "type": "uint256" + } + ], + "name": "ETHWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isWhitelisted", + "type": "bool" + } + ], + "name": "PauseWhitelistUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalDepositedInPool", + "type": "uint256" + } + ], + "name": "RewardsReceived", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "TransfersUnpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "ethAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalDepositedInPool", + "type": "uint256" + } + ], + "name": "WithdrawnETHProcessed", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PAUSER_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UNPAUSER_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "whitelistedForTransfers", + "type": "address[]" + } + ], + "name": "addToPauseWhitelist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "depositETH", + "outputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "depositsPaused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "admin", + "type": "address" + }, + { + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "internalType": "address", + "name": "unpauser", + "type": "address" + }, + { + "internalType": "contract IStakingNodesManager", + "name": "stakingNodesManager", + "type": "address" + }, + { + "internalType": "contract IRewardsDistributor", + "name": "rewardsDistributor", + "type": "address" + }, + { + "internalType": "address[]", + "name": "pauseWhitelist", + "type": "address[]" + } + ], + "internalType": "struct ynETH.Init", + "name": "init", + "type": "tuple" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pauseDeposits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "pauseWhiteList", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "name": "previewDeposit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "processWithdrawnETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "receiveRewards", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "unlisted", + "type": "address[]" + } + ], + "name": "removeFromPauseWhitelist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "callerConfirmation", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "rewardsDistributor", + "outputs": [ + { + "internalType": "contract IRewardsDistributor", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "stakingNodesManager", + "outputs": [ + { + "internalType": "contract IStakingNodesManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalAssets", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalDepositedInPool", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpauseDeposits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpauseTransfers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "ethAmount", + "type": "uint256" + } + ], + "name": "withdrawETH", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/src/dex/index.ts b/src/dex/index.ts index b2ba4f66f..69d38656f 100644 --- a/src/dex/index.ts +++ b/src/dex/index.ts @@ -87,6 +87,7 @@ import { OSwap } from './oswap/oswap'; import { ConcentratorArusd } from './concentrator-arusd/concentrator-arusd'; import { FxProtocolRusd } from './fx-protocol-rusd/fx-protocol-rusd'; import { LitePsm } from './lite-psm/lite-psm'; +import { Yieldnest } from './yieldnest/yieldnest'; const LegacyDexes = [ CurveV2, @@ -169,6 +170,7 @@ const Dexes = [ ConcentratorArusd, FxProtocolRusd, LitePsm, + Yieldnest, ]; export type LegacyDexConstructor = new (dexHelper: IDexHelper) => IDexTxBuilder< diff --git a/src/dex/yieldnest/config.ts b/src/dex/yieldnest/config.ts new file mode 100644 index 000000000..89c92180d --- /dev/null +++ b/src/dex/yieldnest/config.ts @@ -0,0 +1,28 @@ +import { Network, SwapSide } from '../../constants'; +import { DexConfigMap } from '../../types'; +import { Yieldnest } from './yieldnest'; + +type DexParams = { + ynETH: `0x${string}`; +}; + +export const YieldnestConfig: DexConfigMap = { + Yieldnest: { + [Network.MAINNET]: { + ynETH: '0x14dc3d915107dca9ed39e29e14fbdfe4358a1346', + }, + }, +}; + +export const Adapters: { + [chainId: number]: { [side: string]: { name: string; index: number }[] }; +} = { + [Network.MAINNET]: { + [SwapSide.SELL]: [ + { + name: '', + index: 0, + }, + ], + }, +}; diff --git a/src/dex/yieldnest/type.ts b/src/dex/yieldnest/type.ts new file mode 100644 index 000000000..b12376c23 --- /dev/null +++ b/src/dex/yieldnest/type.ts @@ -0,0 +1,3 @@ +export type YNETHPoolState = { + ynETHToETHRateFixed: bigint; +}; diff --git a/src/dex/yieldnest/utils.ts b/src/dex/yieldnest/utils.ts new file mode 100644 index 000000000..1158e54a6 --- /dev/null +++ b/src/dex/yieldnest/utils.ts @@ -0,0 +1,33 @@ +import { Contract } from 'web3-eth-contract'; +import { YNETHPoolState } from './type'; +import { Interface, AbiCoder } from '@ethersproject/abi'; +import { BigNumber } from 'ethers'; + +const coder = new AbiCoder(); + +export async function getOnChainStateYnETH( + multiContract: Contract, + poolAddress: string, + poolInterface: Interface, + blockNumber: number | 'latest', +): Promise { + const data: { returnData: any[] } = await multiContract.methods + .aggregate([ + { + target: poolAddress, + callData: poolInterface.encodeFunctionData('previewDeposit', [ + BigNumber.from(10).pow(18), + ]), + }, + ]) + .call({}, blockNumber); + + const decodedData = coder.decode(['uint256'], data.returnData[0]); + + const ETHToynETHRateFixed = BigInt(decodedData[0].toString()); + const ynETHToETHRateFixed = 1n / ETHToynETHRateFixed; + + return { + ynETHToETHRateFixed, + }; +} diff --git a/src/dex/yieldnest/yieldnest-e2e.test.ts b/src/dex/yieldnest/yieldnest-e2e.test.ts new file mode 100644 index 000000000..e7133ddf9 --- /dev/null +++ b/src/dex/yieldnest/yieldnest-e2e.test.ts @@ -0,0 +1,49 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +import { testE2E } from '../../../tests/utils-e2e'; +import { Tokens, Holders } from '../../../tests/constants-e2e'; +import { Network, ContractMethod, SwapSide } from '../../constants'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { generateConfig } from '../../config'; + +describe('Yieldnest', () => { + const network = Network.MAINNET; + const tokens = Tokens[network]; + const holders = Holders[network]; + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + const dexKey = 'Yieldnest'; + + [ContractMethod.swapExactAmountIn].forEach(contractMethod => { + it(`${contractMethod} - ETH -> ynETH`, async () => { + await testE2E( + tokens.ETH, + tokens.ynETH, + holders.ETH, + '1000000000000000000', + SwapSide.SELL, + dexKey, + contractMethod, + network, + provider, + ); + }); + + it(`${contractMethod} - WETH -> ynETH`, async () => { + await testE2E( + tokens.WETH, + tokens.ynETH, + holders.WETH, + '1000000000000000000', + SwapSide.SELL, + dexKey, + contractMethod, + network, + provider, + ); + }); + }); +}); diff --git a/src/dex/yieldnest/yieldnest.ts b/src/dex/yieldnest/yieldnest.ts new file mode 100644 index 000000000..dca6fa74d --- /dev/null +++ b/src/dex/yieldnest/yieldnest.ts @@ -0,0 +1,280 @@ +import { Interface, JsonFragment } from '@ethersproject/abi'; +import { NumberAsString, SwapSide } from '@paraswap/core'; +import { + AdapterExchangeParam, + Address, + DexExchangeParam, + ExchangePrices, + Logger, + PoolLiquidity, + PoolPrices, + SimpleExchangeParam, + Token, + TransferFeeParams, +} from '../../types'; +import { IDex } from '../idex'; +import YNETH_ABI from '../../abi/ynETH.json'; +import { ETHER_ADDRESS, Network, NULL_ADDRESS } from '../../constants'; +import { IDexHelper } from '../../dex-helper'; +import { SimpleExchange } from '../simple-exchange'; +import { BI_POWS } from '../../bigint-constants'; +import { AsyncOrSync } from 'ts-essentials'; +import { YnethPool } from './yneth-pool'; +import { getDexKeysWithNetwork, isETHAddress } from '../../utils'; +import { WethFunctions } from '../weth/types'; +import * as CALLDATA_GAS_COST from '../../calldata-gas-cost'; +import _ from 'lodash'; +import { YieldnestConfig, Adapters } from './config'; +import { BigNumber, ethers } from 'ethers'; + +export enum ynETHFunctions { + deposit = 'depositETH', +} + +export type YieldnestData = {}; +export type YieldnestParams = {}; + +export class Yieldnest + extends SimpleExchange + implements IDex +{ + static dexKeys = ['Yieldnest']; + ynETHInterface: Interface; + needWrapNative = false; + hasConstantPriceLargeAmounts: boolean = true; + ynETHAddress: string; + ynethPool: YnethPool; + logger: Logger; + + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = + getDexKeysWithNetwork(_.pick(YieldnestConfig, ['Yieldnest'])); + + constructor( + protected network: Network, + dexKey: string, + protected dexHelper: IDexHelper, + protected config = YieldnestConfig[dexKey][network], + protected adapters = Adapters[network], + ) { + super(dexHelper, 'Yieldnest'); + + this.network = dexHelper.config.data.network; + this.ynETHInterface = new Interface(YNETH_ABI as JsonFragment[]); + this.ynETHAddress = this.config.ynETH.toLowerCase(); + this.logger = dexHelper.getLogger(this.dexKey); + this.ynethPool = new YnethPool( + this.dexKey, + dexHelper, + this.ynETHAddress, + this.ynETHInterface, + this.logger, + ); + } + + async initializePricing(blockNumber: number) { + const data: { returnData: any[] } = + await this.dexHelper.multiContract.methods + .aggregate([ + { + target: this.ynETHAddress, + callData: this.ynETHInterface.encodeFunctionData('previewDeposit', [ + BigNumber.from(10).pow(18), + ]), + }, + ]) + .call({}, blockNumber); + + const decodedData = ethers.utils.defaultAbiCoder.decode( + ['uint256'], + data.returnData[0], + ); + + const ETHToynETHRateFixed = BigInt(decodedData.toString()) / 10n ** 18n; + + await Promise.all([ + this.ynethPool.initialize(blockNumber, { + state: { ynETHToETHRateFixed: 1n / ETHToynETHRateFixed }, + }), + ]); + } + + isEligibleSwap( + srcToken: Token | string, + destToken: Token | string, + side: SwapSide, + ): boolean { + if (side === SwapSide.BUY) return false; + + const srcTokenAddress = ( + typeof srcToken === 'string' ? srcToken : srcToken.address + ).toLowerCase(); + const destTokenAddress = ( + typeof destToken === 'string' ? destToken : destToken.address + ).toLowerCase(); + + return ( + (isETHAddress(srcTokenAddress) || this.isWETH(srcTokenAddress)) && + destTokenAddress === this.ynETHAddress + ); + } + + assertEligibility( + srcToken: Token | string, + destToken: Token | string, + side: SwapSide, + ) { + if (!this.isEligibleSwap(srcToken, destToken, side)) { + throw new Error('Only eth/weth -> ynETH swaps are supported'); + } + } + + async getPoolIdentifiers( + srcToken: Token, + destToken: Token, + side: SwapSide, + blockNumber: number, + ): Promise { + if (!this.isEligibleSwap(srcToken, destToken, side)) return []; + + return [`${ETHER_ADDRESS}_${destToken.address}`.toLowerCase()]; + } + + async getPricesVolume( + srcToken: Token, + destToken: Token, + amountsIn: bigint[], + side: SwapSide, + blockNumber: number, + limitPools?: string[] | undefined, + transferFees?: TransferFeeParams | undefined, + isFirstSwap?: boolean | undefined, + ): Promise | null> { + if (!this.isEligibleSwap(srcToken, destToken, side)) return null; + + const pool = this.ynethPool; + + if (!pool.getState(blockNumber)) return null; + + const unitIn = BI_POWS[18]; + const unitOut = pool.getPrice(blockNumber, unitIn); + const amountsOut = amountsIn.map(amountIn => + pool.getPrice(blockNumber, amountIn), + ); + + return [ + { + prices: amountsOut, + unit: unitOut, + data: {}, + exchange: this.dexKey, + poolIdentifier: `${ETHER_ADDRESS}_${destToken.address}`.toLowerCase(), + gasCost: 120_000, + poolAddresses: [destToken.address], + }, + ]; + } + + getAdapterParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + destAmount: NumberAsString, + data: YieldnestData, + side: SwapSide, + ): AdapterExchangeParam { + this.assertEligibility(srcToken, destToken, side); + + return { + targetExchange: NULL_ADDRESS, + payload: '0x', + networkFee: '0', + }; + } + + async getSimpleParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + destAmount: NumberAsString, + data: YieldnestData, + side: SwapSide, + ): Promise { + this.assertEligibility(srcToken, destToken, side); + + const callees = []; + const calldata = []; + const values = []; + + if (this.isWETH(srcToken)) { + // note: apparently ERC20 ABI contains wETH fns (deposit() and withdraw()) + const wethUnwrapData = this.erc20Interface.encodeFunctionData( + WethFunctions.withdraw, + [srcAmount], + ); + callees.push(this.dexHelper.config.data.wrappedNativeTokenAddress); + calldata.push(wethUnwrapData); + values.push('0'); + } + + callees.push(destToken); + calldata.push( + this.ynETHInterface.encodeFunctionData(ynETHFunctions.deposit, [ + this.augustusAddress, + ]), + ); + values.push(srcAmount); + + return { + callees, + calldata, + values, + networkFee: '0', + }; + } + + getDexParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + destAmount: NumberAsString, + recipient: Address, + data: YieldnestData, + side: SwapSide, + ): DexExchangeParam { + this.assertEligibility(srcToken, destToken, side); + + const swapData = this.ynETHInterface.encodeFunctionData( + ynETHFunctions.deposit, + [recipient], + ); + + return { + needWrapNative: this.needWrapNative, + dexFuncHasRecipient: true, + exchangeData: swapData, + targetExchange: destToken, + swappedAmountNotPresentInExchangeData: true, + preSwapUnwrapCalldata: this.isWETH(srcToken) + ? this.erc20Interface.encodeFunctionData(WethFunctions.withdraw, [ + srcAmount, + ]) + : undefined, + returnAmountPos: undefined, + }; + } + + getCalldataGasCost(poolPrices: PoolPrices): number | number[] { + return CALLDATA_GAS_COST.DEX_OVERHEAD + CALLDATA_GAS_COST.LENGTH_SMALL; + } + + getAdapters(side: SwapSide): { name: string; index: number }[] | null { + return this.adapters?.[side] || null; + } + + getTopPoolsForToken( + tokenAddress: string, + limit: number, + ): AsyncOrSync { + return []; + } +} diff --git a/src/dex/yieldnest/yneth-pool.ts b/src/dex/yieldnest/yneth-pool.ts new file mode 100644 index 000000000..dc6bd57d1 --- /dev/null +++ b/src/dex/yieldnest/yneth-pool.ts @@ -0,0 +1,50 @@ +import { Interface } from '@ethersproject/abi'; +import { IDexHelper } from '../../dex-helper'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; +import { Address, Log, Logger } from '../../types'; +import { AsyncOrSync, DeepReadonly } from 'ts-essentials'; +import { YNETHPoolState } from './type'; +import { getOnChainStateYnETH } from './utils'; + +export class YnethPool extends StatefulEventSubscriber { + decoder = (log: Log) => this.poolInterface.parseLog(log); + + constructor( + parentName: string, + protected dexHelper: IDexHelper, + private poolAddress: Address, + private poolInterface: Interface, + logger: Logger, + ) { + super(parentName, 'yneth', dexHelper, logger); + this.addressesSubscribed = [poolAddress]; + } + + protected processLog( + state: DeepReadonly, + log: Readonly, + ): AsyncOrSync | null> { + return null; + } + + async generateState( + blockNumber: number | 'latest' = 'latest', + ): Promise> { + const state = await getOnChainStateYnETH( + this.dexHelper.multiContract, + this.poolAddress, + this.poolInterface, + blockNumber, + ); + + return state; + } + + getPrice(blockNumber: number, ethAmount: bigint): bigint { + const state = this.getState(blockNumber); + if (!state) throw new Error('Cannot compute price'); + const { ynETHToETHRateFixed } = state; + + return ethAmount / ynETHToETHRateFixed; + } +} diff --git a/tests/constants-e2e.ts b/tests/constants-e2e.ts index 71e4d9b8d..21f470837 100644 --- a/tests/constants-e2e.ts +++ b/tests/constants-e2e.ts @@ -507,6 +507,10 @@ export const Tokens: { address: '0x07D1718fF05a8C53C8F05aDAEd57C0d672945f9a', decimals: 18, }, + ynETH: { + address: '0x14dc3d915107dca9ed39e29e14fbdfe4358a1346', + decimals: 18, + }, }, [Network.POLYGON]: { jGBP: {