diff --git a/scripts/fetch-event-blocknumber.ts b/scripts/fetch-event-blocknumber.ts index f7ae6d25d..6cec9e4f0 100644 --- a/scripts/fetch-event-blocknumber.ts +++ b/scripts/fetch-event-blocknumber.ts @@ -7,7 +7,7 @@ import { Network } from '../src/constants'; import { Address } from '../src/types'; import { generateConfig } from '../src/config'; // TODO: Import correct ABI -import ABI from '../src/abi/erc20.json'; +import ABI from '../src/abi/angle/stagToken.json'; // This is a helper script to fetch blockNumbers where a certain // event was released by a certain contract @@ -40,12 +40,18 @@ async function getBlockNumbersForEvents( } // TODO: Set your values here -const network = Network.AVALANCHE; -const eventNames = ['Transfer']; -const address = '0xc0253c3cc6aa5ab407b5795a04c28fb063273894'; +const network = Network.MAINNET; +const eventNames = [ + 'Accrued', + 'Deposit', + 'Withdraw', + 'ToggledPause', + 'RateUpdated', +]; +const address = '0x004626a008b1acdc4c74ab51644093b155e59a23'; const provider = new StaticJsonRpcProvider( generateConfig(network).privateHttpProvider, network, ); -getBlockNumbersForEvents(address, ABI, eventNames, 0, 2000, provider); +getBlockNumbersForEvents(address, ABI, eventNames, 0, 200000000, provider); diff --git a/src/abi/angle/stagToken.json b/src/abi/angle/stagToken.json new file mode 100644 index 000000000..1b9bf59ef --- /dev/null +++ b/src/abi/angle/stagToken.json @@ -0,0 +1,963 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "InvalidRate", + "type": "error" + }, + { + "inputs": [], + "name": "NotGovernor", + "type": "error" + }, + { + "inputs": [], + "name": "NotGovernorOrGuardian", + "type": "error" + }, + { + "inputs": [], + "name": "Paused", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "interest", + "type": "uint256" + } + ], + "name": "Accrued", + "type": "event" + }, + { + "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": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newMaxRate", + "type": "uint256" + } + ], + "name": "MaxRateUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newRate", + "type": "uint256" + } + ], + "name": "RateUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint128", + "name": "pauseStatus", + "type": "uint128" + } + ], + "name": "ToggledPause", + "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": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "contract IAccessControlManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "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": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "asset", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_totalAssets", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "exp", + "type": "uint256" + } + ], + "name": "computeUpdatedAssets", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "convertToAssets", + "outputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "name": "convertToShares", + "outputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "deposit", + "outputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "estimatedAPR", + "outputs": [ + { + "internalType": "uint256", + "name": "apr", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IAccessControlManager", + "name": "_accessControlManager", + "type": "address" + }, + { + "internalType": "contract IERC20MetadataUpgradeable", + "name": "asset_", + "type": "address" + }, + { + "internalType": "string", + "name": "name_", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol_", + "type": "string" + }, + { + "internalType": "uint256", + "name": "divizer", + "type": "uint256" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "isGovernor", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "isGovernorOrGuardian", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastUpdate", + "outputs": [ + { + "internalType": "uint40", + "name": "", + "type": "uint40" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "maxDeposit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "maxMint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "maxRedeem", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "maxWithdraw", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "name": "previewDeposit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "previewMint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "previewRedeem", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "name": "previewWithdraw", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rate", + "outputs": [ + { + "internalType": "uint208", + "name": "", + "type": "uint208" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "redeem", + "outputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newMaxRate", + "type": "uint256" + } + ], + "name": "setMaxRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint208", + "name": "newRate", + "type": "uint208" + } + ], + "name": "setRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "togglePause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalAssets", + "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": "amount", + "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": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "withdraw", + "outputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/dex/angle-staked-stable/angle-staked-stable-e2e.test.ts b/src/dex/angle-staked-stable/angle-staked-stable-e2e.test.ts new file mode 100644 index 000000000..4adc93c9a --- /dev/null +++ b/src/dex/angle-staked-stable/angle-staked-stable-e2e.test.ts @@ -0,0 +1,146 @@ +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { testE2E } from '../../../tests/utils-e2e'; +import { + Tokens, + Holders, + NativeTokenSymbols, +} from '../../../tests/constants-e2e'; +import { Network, ContractMethod, SwapSide } from '../../constants'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { generateConfig } from '../../config'; + +/* + README + ====== + + This test script should add e2e tests for AngleStakedStable. The tests + should cover as many cases as possible. Most of the DEXes follow + the following test structure: + - DexName + - ForkName + Network + - ContractMethod + - ETH -> Token swap + - Token -> ETH swap + - Token -> Token swap + + The template already enumerates the basic structure which involves + testing simpleSwap, multiSwap, megaSwap contract methods for + ETH <> TOKEN and TOKEN <> TOKEN swaps. You should replace tokenA and + tokenB with any two highly liquid tokens on AngleStakedStable for the tests + to work. If the tokens that you would like to use are not defined in + Tokens or Holders map, you can update the './tests/constants-e2e' + + Other than the standard cases that are already added by the template + it is highly recommended to add test cases which could be specific + to testing AngleStakedStable (Eg. Tests based on poolType, special tokens, + etc). + + You can run this individual test script by running: + `npx jest src/dex//-e2e.test.ts` + + e2e tests use the Tenderly fork api. Please add the following to your + .env file: + TENDERLY_TOKEN=Find this under Account>Settings>Authorization. + TENDERLY_ACCOUNT_ID=Your Tenderly account name. + TENDERLY_PROJECT=Name of a Tenderly project you have created in your + dashboard. + + (This comment should be removed from the final implementation) +*/ + +function testForNetwork( + network: Network, + dexKey: string, + tokenASymbol: string, + tokenBSymbol: string, + tokenAAmount: string, + tokenBAmount: string, + nativeTokenAmount: string, +) { + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + const tokens = Tokens[network]; + const holders = Holders[network]; + const nativeTokenSymbol = NativeTokenSymbols[network]; + + const sideToContractMethods = new Map([ + [SwapSide.SELL, [ContractMethod.simpleSwap]], + [SwapSide.BUY, [ContractMethod.simpleBuy]], + ]); + + describe(`${network}`, () => { + sideToContractMethods.forEach((contractMethods, side) => + describe(`${side}`, () => { + contractMethods.forEach((contractMethod: ContractMethod) => { + describe(`${contractMethod}`, () => { + it(`${tokenASymbol} -> ${tokenBSymbol}`, async () => { + await testE2E( + tokens[tokenASymbol], + tokens[tokenBSymbol], + holders[tokenASymbol], + side === SwapSide.SELL ? tokenAAmount : tokenBAmount, + side, + dexKey, + contractMethod, + network, + provider, + ); + }); + }); + }); + }), + ); + }); +} + +describe('AngleStakedStable E2E', () => { + const dexKey = 'AngleStakedStable'; + + describe('Mainnet - agEUR -> stEUR', () => { + const network = Network.MAINNET; + + const tokenASymbol: string = 'agEUR'; + const tokenBSymbol: string = 'stEUR'; + + const tokenAAmount: string = '1000000000000000000'; + const tokenBAmount: string = '1000000000000000000'; + const nativeTokenAmount = '1000000000000000000'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + nativeTokenAmount, + ); + }); + describe('Mainnet - stEUR -> agEUR', () => { + const network = Network.MAINNET; + + const tokenASymbol: string = 'stEUR'; + const tokenBSymbol: string = 'agEUR'; + + const tokenAAmount: string = '1000000000000000000'; + const tokenBAmount: string = '1000000000000000000'; + const nativeTokenAmount = '1000000000000000000'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + nativeTokenAmount, + ); + + // TODO: Add any additional test cases required to test AngleStakedStable + }); +}); diff --git a/src/dex/angle-staked-stable/angle-staked-stable-events.test.ts b/src/dex/angle-staked-stable/angle-staked-stable-events.test.ts new file mode 100644 index 000000000..50427749f --- /dev/null +++ b/src/dex/angle-staked-stable/angle-staked-stable-events.test.ts @@ -0,0 +1,117 @@ +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { AngleStakedStableEventPool } from './angle-staked-stable-pool'; +import { Network } from '../../constants'; +import { Address } from '../../types'; +import { DummyDexHelper } from '../../dex-helper/index'; +import { testEventSubscriber } from '../../../tests/utils-events'; +import { PoolState } from './types'; +import { AngleStakedStableConfig } from './config'; + +/* + README + ====== + + This test script adds unit tests for AngleStakedStable event based + system. This is done by fetching the state on-chain before the + event block, manually pushing the block logs to the event-subscriber, + comparing the local state with on-chain state. + + Most of the logic for testing is abstracted by `testEventSubscriber`. + You need to do two things to make the tests work: + + 1. Fetch the block numbers where certain events were released. You + can modify the `./scripts/fetch-event-blocknumber.ts` to get the + block numbers for different events. Make sure to get sufficient + number of blockNumbers to cover all possible cases for the event + mutations. + + 2. Complete the implementation for fetchPoolState function. The + function should fetch the on-chain state of the event subscriber + using just the blocknumber. + + The template tests only include the test for a single event + subscriber. There can be cases where multiple event subscribers + exist for a single DEX. In such cases additional tests should be + added. + + You can run this individual test script by running: + `npx jest src/dex//-events.test.ts` + + (This comment should be removed from the final implementation) +*/ + +jest.setTimeout(50 * 1000); + +async function fetchPoolState( + angleStakedStablePools: AngleStakedStableEventPool, + blockNumber: number, + poolAddress: string, +): Promise { + // TODO: complete me! + return angleStakedStablePools.generateState(blockNumber); +} + +// eventName -> blockNumbers +type EventMappings = Record; + +describe('AngleStakedStable EventPool Mainnet', function () { + const dexKey = 'AngleStakedStable'; + const network = Network.MAINNET; + const dexHelper = new DummyDexHelper(network); + const logger = dexHelper.getLogger(dexKey); + let angleStakedStablePool: AngleStakedStableEventPool; + + // poolAddress -> EventMappings + const eventsToTest: Record = { + '0x004626a008b1acdc4c74ab51644093b155e59a23': { + Accrued: [], + Deposit: [18134921, 18135441], + Withdraw: [18135030], + ToggledPause: [], + RateUpdated: [18176668], + }, + }; + + beforeEach(async () => { + angleStakedStablePool = new AngleStakedStableEventPool( + dexKey, + network, + dexHelper, + logger, + AngleStakedStableConfig[dexKey][network], + ); + }); + + Object.entries(eventsToTest).forEach( + ([poolAddress, events]: [string, EventMappings]) => { + describe(`Events for ${poolAddress}`, () => { + Object.entries(events).forEach( + ([eventName, blockNumbers]: [string, number[]]) => { + describe(`${eventName}`, () => { + blockNumbers.forEach((blockNumber: number) => { + it(`State after ${blockNumber}`, async function () { + await testEventSubscriber( + angleStakedStablePool, + angleStakedStablePool.addressesSubscribed, + (_blockNumber: number) => + fetchPoolState( + angleStakedStablePool, + _blockNumber, + poolAddress, + ), + blockNumber, + `${dexKey}_${poolAddress}`, + dexHelper.provider, + ); + }); + }); + }); + }, + ); + }); + }, + ); +}); diff --git a/src/dex/angle-staked-stable/angle-staked-stable-integration.test.ts b/src/dex/angle-staked-stable/angle-staked-stable-integration.test.ts new file mode 100644 index 000000000..4d8bdbc7d --- /dev/null +++ b/src/dex/angle-staked-stable/angle-staked-stable-integration.test.ts @@ -0,0 +1,266 @@ +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { Interface, Result } from '@ethersproject/abi'; +import { DummyDexHelper } from '../../dex-helper/index'; +import { Network, SwapSide } from '../../constants'; +import { BI_POWS } from '../../bigint-constants'; +import { AngleStakedStable } from './angle-staked-stable'; +import { + checkPoolPrices, + checkPoolsLiquidity, + checkConstantPoolPrices, +} from '../../../tests/utils'; +import { Tokens } from '../../../tests/constants-e2e'; +import { AngleStakedStableEventPool } from './angle-staked-stable-pool'; + +/* + README + ====== + + This test script adds tests for AngleStakedStable general integration + with the DEX interface. The test cases below are example tests. + It is recommended to add tests which cover AngleStakedStable specific + logic. + + You can run this individual test script by running: + `npx jest src/dex//-integration.test.ts` + + (This comment should be removed from the final implementation) +*/ + +function getReaderCalldata( + exchangeAddress: string, + readerIface: Interface, + amounts: bigint[], + funcName: string, + // TODO: Put here additional arguments you need +) { + return amounts.map(amount => ({ + target: exchangeAddress, + callData: readerIface.encodeFunctionData(funcName, [amount]), + })); +} + +function decodeReaderResult( + results: Result, + readerIface: Interface, + funcName: string, +) { + // TODO: Adapt this function for your needs + return results.map(result => { + const parsed = readerIface.decodeFunctionResult(funcName, result); + return BigInt(parsed[0]._hex); + }); +} + +async function checkOnChainPricing( + angleStakedStable: AngleStakedStable, + funcName: string, + blockNumber: number, + prices: bigint[], + amounts: bigint[], +) { + const exchangeAddress = '0x004626a008b1acdc4c74ab51644093b155e59a23'; // TODO: Put here the real exchange address + + // TODO: Replace dummy interface with the real one + // Normally you can get it from angleStakedStable.Iface or from eventPool. + // It depends on your implementation + const readerIface = AngleStakedStableEventPool.angleStakedStableIface; + + const readerCallData = getReaderCalldata( + exchangeAddress, + readerIface, + amounts.slice(1), + funcName, + ); + const readerResult = ( + await angleStakedStable.dexHelper.multiContract.methods + .aggregate(readerCallData) + .call({}, blockNumber) + ).returnData; + + const expectedPrices = [0n].concat( + decodeReaderResult(readerResult, readerIface, funcName), + ); + + // No exact computation because of the bigInt approx + // for (let i = 0; i < expectedPrices.length; ++i) { + // expect(prices[i]).toBeGreaterThanOrEqual(expectedPrices[i] - 1n); + // expect(prices[i]).toBeLessThanOrEqual(expectedPrices[i] + 1n); + // } + + expect(prices).toEqual(expectedPrices); +} + +async function testPricingOnNetwork( + angleStakedStable: AngleStakedStable, + network: Network, + dexKey: string, + blockNumber: number, + srcTokenSymbol: string, + destTokenSymbol: string, + side: SwapSide, + amounts: bigint[], + funcNameToCheck: string, +) { + const networkTokens = Tokens[network]; + + const pools = await angleStakedStable.getPoolIdentifiers( + networkTokens[srcTokenSymbol], + networkTokens[destTokenSymbol], + side, + blockNumber, + ); + console.log( + `${srcTokenSymbol} <> ${destTokenSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await angleStakedStable.getPricesVolume( + networkTokens[srcTokenSymbol], + networkTokens[destTokenSymbol], + amounts, + side, + blockNumber, + pools, + ); + console.log( + `${srcTokenSymbol} <> ${destTokenSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + if (angleStakedStable.hasConstantPriceLargeAmounts) { + checkConstantPoolPrices(poolPrices!, amounts, dexKey); + } else { + checkPoolPrices(poolPrices!, amounts, side, dexKey); + } + + // Check if onchain pricing equals to calculated ones + await checkOnChainPricing( + angleStakedStable, + funcNameToCheck, + blockNumber, + poolPrices![0].prices, + amounts, + ); +} + +describe('AngleStakedStable', function () { + const dexKey = 'AngleStakedStable'; + let blockNumber: number; + let angleStakedStable: AngleStakedStable; + + describe('Mainnet', () => { + const network = Network.MAINNET; + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + + // TODO: Put here token Symbol to check against + // Don't forget to update relevant tokens in constant-e2e.ts + const srcTokenSymbol = 'agEUR'; + const destTokenSymbol = 'stEUR'; + const funcNameSell = 'previewDeposit'; + const funcNameBuy = 'previewMint'; + + // const srcTokenSymbol = 'stEUR'; + // const destTokenSymbol = 'agEUR'; + // const funcNameSell = 'previewRedeem'; + // const funcNameBuy = 'previewWithdraw'; + + const amountsForSell = [ + 0n, + 1n * BI_POWS[tokens[srcTokenSymbol].decimals], + 2n * BI_POWS[tokens[srcTokenSymbol].decimals], + 3n * BI_POWS[tokens[srcTokenSymbol].decimals], + 4n * BI_POWS[tokens[srcTokenSymbol].decimals], + 5n * BI_POWS[tokens[srcTokenSymbol].decimals], + 6n * BI_POWS[tokens[srcTokenSymbol].decimals], + 7n * BI_POWS[tokens[srcTokenSymbol].decimals], + 8n * BI_POWS[tokens[srcTokenSymbol].decimals], + 9n * BI_POWS[tokens[srcTokenSymbol].decimals], + 10n * BI_POWS[tokens[srcTokenSymbol].decimals], + ]; + + const amountsForBuy = [ + 0n, + 1n * BI_POWS[tokens[destTokenSymbol].decimals], + 2n * BI_POWS[tokens[destTokenSymbol].decimals], + 3n * BI_POWS[tokens[destTokenSymbol].decimals], + 4n * BI_POWS[tokens[destTokenSymbol].decimals], + 5n * BI_POWS[tokens[destTokenSymbol].decimals], + 6n * BI_POWS[tokens[destTokenSymbol].decimals], + 7n * BI_POWS[tokens[destTokenSymbol].decimals], + 8n * BI_POWS[tokens[destTokenSymbol].decimals], + 9n * BI_POWS[tokens[destTokenSymbol].decimals], + 10n * BI_POWS[tokens[destTokenSymbol].decimals], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + angleStakedStable = new AngleStakedStable(network, dexKey, dexHelper); + if (angleStakedStable.initializePricing) { + await angleStakedStable.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + angleStakedStable, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.SELL, + amountsForSell, + funcNameSell, // TODO: Put here proper function name to check pricing + ); + }); + + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + await testPricingOnNetwork( + angleStakedStable, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.BUY, + amountsForBuy, + funcNameBuy, // TODO: Put here proper function name to check pricing + ); + }); + + it('getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const newAngleStakedStable = new AngleStakedStable( + network, + dexKey, + dexHelper, + ); + if (newAngleStakedStable.updatePoolState) { + await newAngleStakedStable.updatePoolState(); + } + const poolLiquidity = await newAngleStakedStable.getTopPoolsForToken( + tokens[srcTokenSymbol].address, + 10, + ); + console.log(`${srcTokenSymbol} Top Pools:`, poolLiquidity); + + if (!newAngleStakedStable.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][srcTokenSymbol].address, + dexKey, + ); + } + }); + }); +}); diff --git a/src/dex/angle-staked-stable/angle-staked-stable-pool.ts b/src/dex/angle-staked-stable/angle-staked-stable-pool.ts new file mode 100644 index 000000000..b6265e78e --- /dev/null +++ b/src/dex/angle-staked-stable/angle-staked-stable-pool.ts @@ -0,0 +1,287 @@ +import { Interface } from '@ethersproject/abi'; +import { DeepReadonly } from 'ts-essentials'; +import { Address, BlockHeader, DexConfigMap, Log, Logger } from '../../types'; +import { + bigIntify, + catchParseLogError, + currentBigIntTimestampInS, +} from '../../utils'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { DexParams, PoolState } from './types'; +import StakedStableABI from '../../abi/angle/stagToken.json'; + +export class AngleStakedStableEventPool extends StatefulEventSubscriber { + handlers: { + [event: string]: ( + event: any, + state: DeepReadonly, + log: Readonly, + blockHeader: Readonly, + ) => DeepReadonly | null; + } = {}; + + static angleStakedStableIface = new Interface(StakedStableABI); + + logDecoder: (log: Log) => any; + + addressesSubscribed: string[]; + + BASE_27 = BigInt(1e27); + HALF_BASE_27 = BigInt(1e27 / 2); + ZERO = BigInt(0); + + constructor( + readonly parentName: string, + protected network: number, + protected dexHelper: IDexHelper, + logger: Logger, + protected config: DexParams, // TODO: add any additional params required for event subscriber + ) { + // TODO: Add pool name + super(parentName, 'Staked_Stable', dexHelper, logger); + + // TODO: make logDecoder decode logs that + this.logDecoder = (log: Log) => + AngleStakedStableEventPool.angleStakedStableIface.parseLog(log); + this.addressesSubscribed = [config.stEUR]; + + // Add handlers + this.handlers['Accrued'] = this.handleAccrued.bind(this); + this.handlers['Deposit'] = this.handleDeposit.bind(this); + this.handlers['Withdraw'] = this.handleWithdraw.bind(this); + this.handlers['ToggledPause'] = this.handleToggledPause.bind(this); + this.handlers['RateUpdated'] = this.handleRateUpdated.bind(this); + } + + /** + * The function is called every time any of the subscribed + * addresses release log. The function accepts the current + * state, updates the state according to the log, and returns + * the updated state. + * @param state - Current state of event subscriber + * @param log - Log released by one of the subscribed addresses + * @returns Updates state of the event subscriber after the log + */ + protected processLog( + state: DeepReadonly, + log: Readonly, + blockHeader: Readonly, + ): DeepReadonly | null { + try { + const event = this.logDecoder(log); + if (event.name in this.handlers) { + return this.handlers[event.name](event, state, log, blockHeader); + } + } catch (e) { + catchParseLogError(e, this.logger); + } + + return null; + } + + /** + * The function generates state using on-chain calls. This + * function is called to regenerate state if the event based + * system fails to fetch events and the local state is no + * more correct. + * @param blockNumber - Blocknumber for which the state should + * should be generated + * @returns state of the event subscriber at blocknumber + */ + async generateState(blockNumber: number): Promise> { + // TODO: complete me! + let poolState = { + totalAssets: 0n, + totalSupply: 0n, + lastUpdate: 0n, + paused: false, + rate: 0n, + } as PoolState; + + const multicall = [ + { + target: this.config.stEUR, + callData: + AngleStakedStableEventPool.angleStakedStableIface.encodeFunctionData( + 'totalAssets', + ), + }, + { + target: this.config.stEUR, + callData: + AngleStakedStableEventPool.angleStakedStableIface.encodeFunctionData( + 'totalSupply', + ), + }, + { + target: this.config.stEUR, + callData: + AngleStakedStableEventPool.angleStakedStableIface.encodeFunctionData( + 'lastUpdate', + ), + }, + { + target: this.config.stEUR, + callData: + AngleStakedStableEventPool.angleStakedStableIface.encodeFunctionData( + 'paused', + ), + }, + ]; + + // on chain call + const returnData = ( + await this.dexHelper.multiContract.methods + .aggregate(multicall) + .call({}, blockNumber) + ).returnData; + + // Decode + poolState.totalAssets = bigIntify( + AngleStakedStableEventPool.angleStakedStableIface.decodeFunctionResult( + 'totalAssets', + returnData[0], + )[0], + ); + poolState.totalSupply = bigIntify( + AngleStakedStableEventPool.angleStakedStableIface.decodeFunctionResult( + 'totalSupply', + returnData[1], + )[0], + ); + poolState.lastUpdate = bigIntify( + AngleStakedStableEventPool.angleStakedStableIface.decodeFunctionResult( + 'lastUpdate', + returnData[2], + )[0], + ); + poolState.paused = + AngleStakedStableEventPool.angleStakedStableIface.decodeFunctionResult( + 'paused', + returnData[3], + )[0] as boolean; + + return poolState; + } + + getRateDeposit(amount: bigint, state: PoolState): bigint { + // TODO should not be needed if events are taken in the order triggered + const newTotalAssets = this._accrue(state); + return amount == 0n || state.totalSupply == 0n + ? amount + : (amount * state.totalSupply) / newTotalAssets; + } + + getRateMint(shares: bigint, state: PoolState): bigint { + // TODO should not be needed + const newTotalAssets = this._accrue(state); + const roundUp = + (shares * newTotalAssets) % state.totalSupply > 0n ? 1n : 0n; + return state.totalSupply == 0n + ? shares + : (shares * newTotalAssets) / state.totalSupply + roundUp; + } + + getRateRedeem(shares: bigint, state: PoolState): bigint { + // TODO should not be needed + const newTotalAssets = this._accrue(state); + return state.totalSupply == 0n + ? shares + : (shares * newTotalAssets) / state.totalSupply; + } + + getRateWithdraw(amount: bigint, state: PoolState): bigint { + // TODO should not be needed + const newTotalAssets = this._accrue(state); + const roundUp = + (amount * state.totalSupply) % newTotalAssets > 0n ? 1n : 0n; + return amount == 0n || state.totalSupply == 0n + ? amount + : (amount * state.totalSupply) / newTotalAssets + roundUp; + } + + _accrue(state: PoolState): bigint { + const newTotalAssets = this._computeUpdatedAssets( + state.totalAssets, + state.rate, + currentBigIntTimestampInS() - state.lastUpdate, + ); + return newTotalAssets; + } + + _computeUpdatedAssets(amount: bigint, rate: bigint, exp: bigint): bigint { + if (exp == 0n || rate > 0) return amount; + const expMinusOne = exp - 1n; + const expMinusTwo = exp > 2n ? exp - 2n : 0n; + const basePowerTwo = (rate * rate + this.HALF_BASE_27) / this.BASE_27; + const basePowerThree = + (basePowerTwo * rate + this.HALF_BASE_27) / this.BASE_27; + const secondTerm = (exp * expMinusOne * basePowerTwo) / 2n; + const thirdTerm = (exp * expMinusOne * expMinusTwo * basePowerThree) / 6n; + return ( + (amount * (this.BASE_27 + rate * exp + secondTerm + thirdTerm)) / + this.BASE_27 + ); + } + + handleAccrued( + event: any, + state: PoolState, + log: Readonly, + blockHeader: BlockHeader, + ): DeepReadonly | null { + const interest = bigIntify(event.args.interest); + state.totalAssets += interest; + state.lastUpdate = bigIntify(blockHeader.timestamp); + return state; + } + + handleRateUpdated( + event: any, + state: PoolState, + log: Readonly, + blockHeader: BlockHeader, + ): DeepReadonly | null { + state.lastUpdate = bigIntify(blockHeader.timestamp); + return state; + } + + handleDeposit( + event: any, + state: PoolState, + log: Readonly, + blockHeader: BlockHeader, + ): DeepReadonly | null { + const assets = bigIntify(event.args.assets); + const shares = bigIntify(event.args.shares); + state.totalAssets += assets; + state.totalSupply += shares; + state.lastUpdate = bigIntify(blockHeader.timestamp); + return state; + } + + handleWithdraw( + event: any, + state: PoolState, + log: Readonly, + blockHeader: BlockHeader, + ): DeepReadonly | null { + const assets = bigIntify(event.args.assets); + const shares = bigIntify(event.args.shares); + state.totalAssets -= assets; + state.totalSupply -= shares; + state.lastUpdate = bigIntify(blockHeader.timestamp); + return state; + } + + handleToggledPause( + event: any, + state: PoolState, + log: Readonly, + ): DeepReadonly | null { + const pauseStatus: boolean = event.args.pauseStatus; + state.paused = pauseStatus; + return state; + } +} diff --git a/src/dex/angle-staked-stable/angle-staked-stable.ts b/src/dex/angle-staked-stable/angle-staked-stable.ts new file mode 100644 index 000000000..d8a2d9285 --- /dev/null +++ b/src/dex/angle-staked-stable/angle-staked-stable.ts @@ -0,0 +1,323 @@ +import { Interface } from '@ethersproject/abi'; +import { AsyncOrSync } from 'ts-essentials'; +import { + Token, + Address, + ExchangePrices, + PoolPrices, + AdapterExchangeParam, + SimpleExchangeParam, + PoolLiquidity, + Logger, +} from '../../types'; +import { SwapSide, Network } from '../../constants'; +import * as CALLDATA_GAS_COST from '../../calldata-gas-cost'; +import { getDexKeysWithNetwork } from '../../utils'; +import { IDex } from '../../dex/idex'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { AngleStakedStableData, DexParams, PoolState } from './types'; +import { SimpleExchange } from '../simple-exchange'; +import { AngleStakedStableConfig, Adapters } from './config'; +import { AngleStakedStableEventPool } from './angle-staked-stable-pool'; +import StakedStableABI from '../../abi/angle/stagToken.json'; + +const AngleStakedGasCost = 0; + +export class AngleStakedStable + extends SimpleExchange + implements IDex +{ + static readonly wstETHIface = new Interface(StakedStableABI); + protected config: DexParams; + + readonly hasConstantPriceLargeAmounts = false; + readonly needWrapNative = true; + + readonly isFeeOnTransferSupported = false; + + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = + getDexKeysWithNetwork(AngleStakedStableConfig); + + logger: Logger; + + protected eventPools: AngleStakedStableEventPool | null = null; + protected isPaused: boolean = false; + + constructor( + readonly network: Network, + readonly dexKey: string, + readonly dexHelper: IDexHelper, + protected adapters = Adapters[network] || {}, // TODO: add any additional optional params to support other fork DEXes + ) { + super(dexHelper, dexKey); + const config = AngleStakedStableConfig[dexKey][network]; + this.logger = dexHelper.getLogger(dexKey); + this.config = { + stEUR: config.stEUR.toLowerCase(), + agEUR: config.agEUR.toLowerCase(), + }; + } + + // Initialize pricing is called once in the start of + // pricing service. It is intended to setup the integration + // for pricing requests. It is optional for a DEX to + // implement this function + async initializePricing(blockNumber: number) { + // TODO: complete me! + this.eventPools = new AngleStakedStableEventPool( + this.dexKey, + this.network, + this.dexHelper, + this.logger, + this.config, + ); + await this.eventPools.initialize(blockNumber); + } + + // Returns the list of contract adapters (name and index) + // for a buy/sell. Return null if there are no adapters. + getAdapters(side: SwapSide): { name: string; index: number }[] | null { + return this.adapters[side] ? this.adapters[side] : null; + } + + // Returns list of pool identifiers that can be used + // for a given swap. poolIdentifiers must be unique + // across DEXes. It is recommended to use + // ${dexKey}_${poolAddress} as a poolIdentifier + async getPoolIdentifiers( + srcToken: Token, + destToken: Token, + side: SwapSide, + blockNumber: number, + ): Promise { + // TODO: complete me! + if (!this._knownAddress(srcToken, destToken)) return []; + else return [this.dexKey]; + } + + // Returns pool prices for amounts. + // If limitPools is defined only pools in limitPools + // should be used. If limitPools is undefined then + // any pools can be used. + async getPricesVolume( + srcToken: Token, + destToken: Token, + amounts: bigint[], + side: SwapSide, + blockNumber: number, + limitPools?: string[], + ): Promise> { + // TODO: complete me! + const srcTokenAddress = srcToken.address.toLowerCase(); + const destTokenAddress = destToken.address.toLowerCase(); + if (!this._knownAddress(srcToken, destToken)) return null; + + const state = this.eventPools?.getState(blockNumber); + if (this.eventPools === null || state === undefined || state === null) + return null; + if (srcTokenAddress === this.config.agEUR && side === SwapSide.SELL) + return [ + { + prices: amounts.map(amount => + this.eventPools!.getRateDeposit(amount, state), + ), + unit: this.eventPools.getRateDeposit(1n * BigInt(10 ** 18), state), + gasCost: AngleStakedGasCost, + exchange: this.dexKey, + data: { exchange: `${this.config.stEUR}` }, + poolAddresses: [`${this.config.stEUR}_${this.config.agEUR}`], + }, + ]; + else if (destTokenAddress === this.config.agEUR && side === SwapSide.SELL) + return [ + { + prices: amounts.map(share => + this.eventPools!.getRateRedeem(share, state), + ), + unit: this.eventPools.getRateRedeem(1n * BigInt(10 ** 18), state), + gasCost: AngleStakedGasCost, + exchange: this.dexKey, + data: { exchange: `${this.config.stEUR}` }, + poolAddresses: [`${this.config.stEUR}_${this.config.agEUR}`], + }, + ]; + else if (srcTokenAddress === this.config.agEUR && side === SwapSide.BUY) + return [ + { + prices: amounts.map(share => + this.eventPools!.getRateMint(share, state), + ), + unit: this.eventPools.getRateMint(1n * BigInt(10 ** 18), state), + gasCost: AngleStakedGasCost, + exchange: this.dexKey, + data: { exchange: `${this.config.stEUR}` }, + poolAddresses: [`${this.config.stEUR}_${this.config.agEUR}`], + }, + ]; + else + return [ + { + prices: amounts.map(amount => + this.eventPools!.getRateWithdraw(amount, state), + ), + unit: this.eventPools.getRateWithdraw(1n * BigInt(10 ** 18), state), + gasCost: AngleStakedGasCost, + exchange: this.dexKey, + data: { exchange: `${this.config.stEUR}` }, + poolAddresses: [`${this.config.stEUR}_${this.config.agEUR}`], + }, + ]; + } + + // Returns estimated gas cost of calldata for this DEX in multiSwap + getCalldataGasCost( + poolPrices: PoolPrices, + ): number | number[] { + // TODO: update if there is any payload in getAdapterParam + return CALLDATA_GAS_COST.DEX_NO_PAYLOAD; + } + + // Encode params required by the exchange adapter + // Used for multiSwap, buy & megaSwap + // Hint: abiCoder.encodeParameter() could be useful + getAdapterParam( + srcToken: string, + destToken: string, + srcAmount: string, + destAmount: string, + data: AngleStakedStableData, + side: SwapSide, + ): AdapterExchangeParam { + // TODO: complete me! + const { exchange } = data; + + // Encode here the payload for adapter + const payload = ''; + + return { + targetExchange: exchange, + payload, + networkFee: '0', + }; + } + + // Encode call data used by simpleSwap like routers + // Used for simpleSwap & simpleBuy + // Hint: this.buildSimpleParamWithoutWETHConversion + // could be useful + async getSimpleParam( + srcToken: string, + destToken: string, + srcAmount: string, + destAmount: string, + data: AngleStakedStableData, + side: SwapSide, + ): Promise { + const { exchange } = data; + + // Encode here the transaction arguments + const swapData = + srcToken.toLowerCase() === this.config.agEUR + ? AngleStakedStableEventPool.angleStakedStableIface.encodeFunctionData( + side === SwapSide.SELL ? 'deposit' : 'mint', + [ + side === SwapSide.SELL ? srcAmount : destAmount, + this.augustusAddress, + ], + ) + : AngleStakedStableEventPool.angleStakedStableIface.encodeFunctionData( + side === SwapSide.SELL ? 'redeem' : 'withdraw', + [ + side === SwapSide.SELL ? srcAmount : destAmount, + this.augustusAddress, + this.augustusAddress, + ], + ); + + return this.buildSimpleParamWithoutWETHConversion( + srcToken, + srcAmount, + destToken, + destAmount, + swapData, + exchange, + ); + } + + // This is called once before getTopPoolsForToken is + // called for multiple tokens. This can be helpful to + // update common state required for calculating + // getTopPoolsForToken. It is optional for a DEX + // to implement this + async updatePoolState(): Promise { + // TODO: complete me! + const tokenBalanceMultiCall = [ + { + target: this.config.stEUR, + callData: + AngleStakedStableEventPool.angleStakedStableIface.encodeFunctionData( + 'paused', + ), + }, + ]; + const returnData = ( + await this.dexHelper.multiContract.methods + .aggregate(tokenBalanceMultiCall) + .call() + ).returnData; + + this.isPaused = + AngleStakedStableEventPool.angleStakedStableIface.decodeFunctionResult( + 'paused', + returnData[0], + )[0] as boolean; + } + + // Returns list of top pools based on liquidity. Max + // limit number pools should be returned. + async getTopPoolsForToken( + tokenAddress: Address, + limit: number, + ): Promise { + if ( + this.isPaused || + (tokenAddress.toLowerCase() != this.config.agEUR && + tokenAddress.toLowerCase() != this.config.stEUR) + ) + return []; + else + return [ + { + exchange: this.dexKey, + address: this.config.stEUR, + connectorTokens: [ + tokenAddress.toLowerCase() == this.config.agEUR + ? ({ address: this.config.stEUR, decimals: 18 } as Token) + : ({ address: this.config.agEUR, decimals: 18 } as Token), + ], + // liquidity is infinite as to have been able to mint stEUR, you must have deposited agEUR + liquidityUSD: 1e12, + }, + ]; + } + + // This is optional function in case if your implementation has acquired any resources + // you need to release for graceful shutdown. For example, it may be any interval timer + releaseResources(): AsyncOrSync {} + + _knownAddress(srcToken: Token, destToken: Token): boolean { + const srcTokenAddress = srcToken.address.toLowerCase(); + const destTokenAddress = destToken.address.toLowerCase(); + if ( + !( + (srcTokenAddress === this.config.agEUR && + destTokenAddress === this.config.stEUR) || + (srcTokenAddress === this.config.stEUR && + destTokenAddress === this.config.agEUR) + ) + ) { + return false; + } + return true; + } +} diff --git a/src/dex/angle-staked-stable/config.ts b/src/dex/angle-staked-stable/config.ts new file mode 100644 index 000000000..63a7a1f84 --- /dev/null +++ b/src/dex/angle-staked-stable/config.ts @@ -0,0 +1,26 @@ +import { DexParams } from './types'; +import { DexConfigMap, AdapterMappings } from '../../types'; +import { Network, SwapSide } from '../../constants'; + +export const AngleStakedStableConfig: DexConfigMap = { + AngleStakedStable: { + [Network.ARBITRUM]: { + agEUR: '0x', + stEUR: '0x004626A008B1aCdC4c74ab51644093b155e59A23', + }, + [Network.MAINNET]: { + agEUR: '0x1a7e4e63778B4f12a199C062f3eFdD288afCBce8', + stEUR: '0x004626A008B1aCdC4c74ab51644093b155e59A23', + }, + [Network.OPTIMISM]: { + agEUR: '0x', + stEUR: '0x004626A008B1aCdC4c74ab51644093b155e59A23', + }, + [Network.POLYGON]: { + agEUR: '0x', + stEUR: '0x004626A008B1aCdC4c74ab51644093b155e59A23', + }, + }, +}; + +export const Adapters: Record = {}; diff --git a/src/dex/angle-staked-stable/types.ts b/src/dex/angle-staked-stable/types.ts new file mode 100644 index 000000000..ccc69d5d3 --- /dev/null +++ b/src/dex/angle-staked-stable/types.ts @@ -0,0 +1,16 @@ +import { Address } from '../../types'; + +export type PoolState = { + totalAssets: bigint; + totalSupply: bigint; + lastUpdate: bigint; + rate: bigint; + paused: boolean; +}; + +export type AngleStakedStableData = { exchange: Address }; + +export type DexParams = { + stEUR: Address; + agEUR: Address; +}; diff --git a/src/dex/index.ts b/src/dex/index.ts index 7484ca450..f8e60cea1 100644 --- a/src/dex/index.ts +++ b/src/dex/index.ts @@ -75,6 +75,7 @@ import { PancakeswapV3 } from './pancakeswap-v3/pancakeswap-v3'; import { Algebra } from './algebra/algebra'; import { QuickPerps } from './quick-perps/quick-perps'; import { NomiswapV2 } from './uniswap-v2/nomiswap-v2'; +import { AngleStakedStable } from './angle-staked-stable/angle-staked-stable'; const LegacyDexes = [ CurveV2, @@ -147,6 +148,7 @@ const Dexes = [ SwaapV2, QuickPerps, NomiswapV2, + AngleStakedStable, ]; export type LegacyDexConstructor = new (dexHelper: IDexHelper) => IDexTxBuilder< diff --git a/tests/constants-e2e.ts b/tests/constants-e2e.ts index 069890872..20dae50b9 100644 --- a/tests/constants-e2e.ts +++ b/tests/constants-e2e.ts @@ -311,6 +311,14 @@ export const Tokens: { address: '0x8751d4196027d4e6da63716fa7786b5174f04c15', decimals: 18, }, + agEUR: { + address: '0x1a7e4e63778b4f12a199c062f3efdd288afcbce8', + decimals: 18, + }, + stEUR: { + address: '0x004626a008b1acdc4c74ab51644093b155e59a23', + decimals: 18, + }, }, [Network.ROPSTEN]: { DAI: { @@ -441,6 +449,14 @@ export const Tokens: { address: '0xa3fa99a148fa48d14ed51d610c367c61876997f1', decimals: 18, }, + agEUR: { + address: '0xe0b52e49357fd4daf2c15e02058dce6bc0057db4', + decimals: 18, + }, + stEUR: { + address: '0x004626a008b1acdc4c74ab51644093b155e59a23', + decimals: 18, + }, }, [Network.FANTOM]: { FTM: { address: ETHER_ADDRESS, decimals: 18 }, @@ -809,6 +825,14 @@ export const Tokens: { address: '0x3d9907f9a368ad0a51be60f7da3b97cf940982d8', decimals: 18, }, + agEUR: { + address: '0xfa5ed56a203466cbbc2430a43c66b9d8723528e7', + decimals: 18, + }, + stEUR: { + address: '0x004626a008b1acdc4c74ab51644093b155e59a23', + decimals: 18, + }, }, [Network.OPTIMISM]: { DAI: { @@ -860,6 +884,14 @@ export const Tokens: { address: '0x68f180fcCe6836688e9084f035309E29Bf0A2095', decimals: 8, }, + agEUR: { + address: '0x9485aca5bbbe1667ad97c7fe7c4531a624c8b1ed', + decimals: 18, + }, + stEUR: { + address: '0x004626a008b1acdc4c74ab51644093b155e59a23', + decimals: 18, + }, }, [Network.ZKEVM]: { ETH: { @@ -960,6 +992,8 @@ export const Holders: { crvUSD: '0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635', GHO: '0x844Dc85EdD8492A56228D293cfEbb823EF3E10EC', wibBTC: '0xFbdCA68601f835b27790D98bbb8eC7f05FDEaA9B', + agEUR: '0xa116f421ff82a9704428259fd8cc63347127b777', + stEUR: '0xfda462548ce04282f4b6d6619823a7c64fdc0185', }, [Network.ROPSTEN]: { ETH: '0x43262A12d8610AA70C15DbaeAC321d51613c9071',