diff --git a/contracts/interfaces/IStaticATokenLM.sol b/contracts/interfaces/IStaticATokenLM.sol new file mode 100644 index 00000000..2be587a9 --- /dev/null +++ b/contracts/interfaces/IStaticATokenLM.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// STATA_TOKENS in AaveV3 +interface IStaticATokenLM { + function aToken() external view returns (IERC20); + function rate() external view returns (uint256); +} + +interface IStaticATokenFactory { + function getStaticAToken(address underlying) external view returns (address); + function getStaticATokens() external view returns (address[] memory); +} diff --git a/contracts/wrappers/StataTokenWrapper.sol b/contracts/wrappers/StataTokenWrapper.sol new file mode 100644 index 00000000..95d5fb39 --- /dev/null +++ b/contracts/wrappers/StataTokenWrapper.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +import "../interfaces/IStaticATokenLM.sol"; +import "../interfaces/IWrapper.sol"; + +contract StataTokenWrapper is IWrapper { + IStaticATokenFactory public immutable FACTORY; + + mapping(IERC20 => IERC20) public stataTokenToToken; + mapping(IERC20 => IERC20) public tokenToStataToken; + + constructor(IStaticATokenFactory staticATokenFactory) { + FACTORY = staticATokenFactory; + } + + function addMarkets(IERC20[] memory tokens) external { + unchecked { + for (uint256 i = 0; i < tokens.length; i++) { + IERC20 stataToken = IERC20(FACTORY.getStaticAToken(address(tokens[i]))); + if(stataToken == IERC20(address(0))) revert NotAddedMarket(); + stataTokenToToken[stataToken] = tokens[i]; + tokenToStataToken[tokens[i]] = stataToken; + } + } + } + + function removeMarkets(IERC20[] memory tokens) external { + unchecked { + for (uint256 i = 0; i < tokens.length; i++) { + IERC20 stataToken = IERC20(FACTORY.getStaticAToken(address(tokens[i]))); + if(stataToken == IERC20(address(0))) revert NotRemovedMarket(); + delete stataTokenToToken[stataToken]; + delete tokenToStataToken[tokens[i]]; + } + } + } + + function wrap(IERC20 token) external view override returns (IERC20 wrappedToken, uint256 rate) { + IERC20 underlying = stataTokenToToken[token]; + IERC20 stataToken = tokenToStataToken[token]; + if (underlying != IERC20(address(0))) { + return (underlying, IStaticATokenLM(address(token)).rate() / 1e9); + } else if (stataToken != IERC20(address(0))) { + return (stataToken, 1e45 / IStaticATokenLM(address(stataToken)).rate()); + } else { + revert NotSupportedToken(); + } + } +} diff --git a/deploy/utils.js b/deploy/utils.js index 5f917265..57b854a3 100644 --- a/deploy/utils.js +++ b/deploy/utils.js @@ -25,6 +25,27 @@ async function getAllAave3ReservesTokens (lendingPoolV3Address) { return tokens.map(token => token[1]); } +async function getAllAaveV3UnderlyingTokensForStataTokens (staticATokenFactoryAddress) { + const aTokenABI = [ + { + name: 'UNDERLYING_ASSET_ADDRESS', + type: 'function', + inputs: [], + outputs: [{ type: 'address', name: 'value' }], + stateMutability: 'view', + }, + ]; + const staticATokenFactory = await ethers.getContractAt('IStaticATokenFactory', staticATokenFactoryAddress); + const allStataTokens = await staticATokenFactory.getStaticATokens(); + const tokens = []; + for (const token of allStataTokens) { + const stataToken = await ethers.getContractAt('IStaticATokenLM', token); + const aToken = await ethers.getContractAt(aTokenABI, await stataToken.aToken()); + tokens.push(await aToken.UNDERLYING_ASSET_ADDRESS()); + } + return tokens; +} + const deployCompoundTokenWrapper = async (contractInfo, tokenName, deployments, deployer, deploymentName = `CompoundLikeWrapper_${contractInfo.name}`) => { const comptroller = await ethers.getContractAt('IComptroller', contractInfo.address); const cToken = (await comptroller.getAllMarkets()).filter(token => token !== tokenName); @@ -52,6 +73,7 @@ const getContract = async (deployments, contractName, deploymentName = contractN module.exports = { addAaveTokens, getAllAave3ReservesTokens, + getAllAaveV3UnderlyingTokensForStataTokens, deployCompoundTokenWrapper, getContract, }; diff --git a/test/helpers.js b/test/helpers.js index 30266800..1c4207b5 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -54,6 +54,9 @@ const tokens = { wstETH: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', BEAN: '0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab', '3CRV': '0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490', + stataUSDC: '0x73edDFa87C71ADdC275c2b9890f5c3a8480bC9E6', + stataWETH: '0x252231882FB38481497f3C767469106297c8d93b', + stataDAI: '0xaf270C38fF895EA3f95Ed488CEACe2386F038249', base: { WETH: '0x4200000000000000000000000000000000000006', DAI: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb', @@ -88,6 +91,9 @@ const deployParams = { AaveWrapperV3: { lendingPool: '0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3', }, + StataTokenWrapper: { + staticATokenFactory: '0x411D79b8cC43384FDE66CaBf9b6a17180c842511', + }, UniswapV3: { factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984', initcodeHash: '0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54', diff --git a/test/wrappers/StataTokenWrapper.js b/test/wrappers/StataTokenWrapper.js new file mode 100644 index 00000000..db8698ae --- /dev/null +++ b/test/wrappers/StataTokenWrapper.js @@ -0,0 +1,114 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { expect, ether, deployContract, assertRoughlyEqualValues } = require('@1inch/solidity-utils'); +const { tokens, deployParams: { StataTokenWrapper } } = require('../helpers.js'); +const { getAllAaveV3UnderlyingTokensForStataTokens } = require('../../deploy/utils.js'); + +describe('StataTokenWrapper', function () { + const stataTokenABI = [ + { + name: 'previewDeposit', + type: 'function', + inputs: [{ type: 'uint256', name: 'assets' }], + outputs: [{ type: 'uint256', name: 'value' }], + stateMutability: 'view', + }, + { + name: 'previewWithdraw', + type: 'function', + inputs: [{ type: 'uint256', name: 'assets' }], + outputs: [{ type: 'uint256', name: 'value' }], + stateMutability: 'view', + }, + ]; + + async function initContracts () { + const stataTokenWrapper = await deployContract('StataTokenWrapper', [StataTokenWrapper.staticATokenFactory]); + await stataTokenWrapper.addMarkets([tokens.USDC, tokens.WETH]); + const stataTokens = { + USDC: await ethers.getContractAt(stataTokenABI, tokens.stataUSDC), + WETH: await ethers.getContractAt(stataTokenABI, tokens.stataWETH), + DAI: await ethers.getContractAt(stataTokenABI, tokens.stataDAI), + }; + return { stataTokens, stataTokenWrapper }; + } + + it('should retrun underlying tokens for all stata tokens', async function () { + const tokens = await getAllAaveV3UnderlyingTokensForStataTokens(StataTokenWrapper.staticATokenFactory); + const factory = await ethers.getContractAt('IStaticATokenFactory', StataTokenWrapper.staticATokenFactory); + const allStataTokens = await factory.getStaticATokens(); + expect(tokens.length).to.equal(allStataTokens.length); + for (const token of tokens) { + const stataToken = await factory.getStaticAToken(token); + expect(allStataTokens.indexOf(stataToken) !== -1).to.equal(true); + } + }); + + it('should revert with non-supported token', async function () { + const { stataTokenWrapper } = await loadFixture(initContracts); + await expect(stataTokenWrapper.wrap(tokens.CHAI)).to.be.revertedWithCustomError(stataTokenWrapper, 'NotSupportedToken'); + }); + + it('should correct add market', async function () { + const { stataTokens, stataTokenWrapper } = await loadFixture(initContracts); + await stataTokenWrapper.addMarkets([tokens.DAI]); + const response = await stataTokenWrapper.wrap(tokens.DAI); + assertRoughlyEqualValues(await stataTokens.DAI.previewDeposit(ether('1')), response.rate, 1e8); + }); + + it('should correct add already added market', async function () { + const { stataTokens, stataTokenWrapper } = await loadFixture(initContracts); + await stataTokenWrapper.addMarkets([tokens.USDC]); + const response = await stataTokenWrapper.wrap(tokens.USDC); + expect(await stataTokens.USDC.previewDeposit(ether('1'))).to.equal(response.rate); + }); + + it('should revert if added market is incorrect', async function () { + const { stataTokenWrapper } = await loadFixture(initContracts); + await expect(stataTokenWrapper.addMarkets([tokens.CHAI])).to.be.revertedWithCustomError(stataTokenWrapper, 'NotAddedMarket'); + }); + + it('should revert if one of added markets is incorrect', async function () { + const { stataTokenWrapper } = await loadFixture(initContracts); + await expect(stataTokenWrapper.addMarkets([tokens.DAI, tokens.CHAI])).to.be.revertedWithCustomError(stataTokenWrapper, 'NotAddedMarket'); + }); + + it('should correct remove market', async function () { + const { stataTokenWrapper } = await loadFixture(initContracts); + await stataTokenWrapper.removeMarkets([tokens.USDC]); + await expect(stataTokenWrapper.wrap(tokens.USDC)).to.be.revertedWithCustomError(stataTokenWrapper, 'NotSupportedToken'); + }); + + it('should revert if removed market is incorrect', async function () { + const { stataTokenWrapper } = await loadFixture(initContracts); + await expect(stataTokenWrapper.removeMarkets([tokens.CHAI])).to.be.revertedWithCustomError(stataTokenWrapper, 'NotRemovedMarket'); + }); + + it('USDC -> stataUSDC', async function () { + const { stataTokens, stataTokenWrapper } = await loadFixture(initContracts); + const response = await stataTokenWrapper.wrap(tokens.USDC); + expect(response.rate).to.equal(await stataTokens.USDC.previewDeposit(ether('1'))); + expect(response.wrappedToken).to.equal(tokens.stataUSDC); + }); + + it('stataUSDC -> USDC', async function () { + const { stataTokens, stataTokenWrapper } = await loadFixture(initContracts); + const response = await stataTokenWrapper.wrap(tokens.stataUSDC); + assertRoughlyEqualValues(await stataTokens.USDC.previewWithdraw(ether('1')), response.rate, 1e10); + expect(response.wrappedToken).to.equal(tokens.USDC); + }); + + it('WETH -> stataWETH', async function () { + const { stataTokens, stataTokenWrapper } = await loadFixture(initContracts); + const response = await stataTokenWrapper.wrap(tokens.WETH); + expect(response.rate).to.equal(await stataTokens.WETH.previewDeposit(ether('1'))); + expect(response.wrappedToken).to.equal(tokens.stataWETH); + }); + + it('stataWETH -> WETH', async function () { + const { stataTokens, stataTokenWrapper } = await loadFixture(initContracts); + const response = await stataTokenWrapper.wrap(tokens.stataWETH); + assertRoughlyEqualValues(await stataTokens.WETH.previewWithdraw(ether('1')), response.rate, 1e10); + expect(response.wrappedToken).to.equal(tokens.WETH); + }); +});