Skip to content

Commit

Permalink
Merge pull request #130 from 1inch/feature/stata-token-wrapper
Browse files Browse the repository at this point in the history
[SC-1126] AaveV3 StataTokenWrapper
  • Loading branch information
zZoMROT authored Apr 8, 2024
2 parents cb59e07 + 6bb2945 commit 1cceb81
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 0 deletions.
16 changes: 16 additions & 0 deletions contracts/interfaces/IStaticATokenLM.sol
Original file line number Diff line number Diff line change
@@ -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);
}
51 changes: 51 additions & 0 deletions contracts/wrappers/StataTokenWrapper.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
22 changes: 22 additions & 0 deletions deploy/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -52,6 +73,7 @@ const getContract = async (deployments, contractName, deploymentName = contractN
module.exports = {
addAaveTokens,
getAllAave3ReservesTokens,
getAllAaveV3UnderlyingTokensForStataTokens,
deployCompoundTokenWrapper,
getContract,
};
6 changes: 6 additions & 0 deletions test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ const tokens = {
wstETH: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0',
BEAN: '0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab',
'3CRV': '0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490',
stataUSDC: '0x73edDFa87C71ADdC275c2b9890f5c3a8480bC9E6',
stataWETH: '0x252231882FB38481497f3C767469106297c8d93b',
stataDAI: '0xaf270C38fF895EA3f95Ed488CEACe2386F038249',
base: {
WETH: '0x4200000000000000000000000000000000000006',
DAI: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb',
Expand Down Expand Up @@ -88,6 +91,9 @@ const deployParams = {
AaveWrapperV3: {
lendingPool: '0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3',
},
StataTokenWrapper: {
staticATokenFactory: '0x411D79b8cC43384FDE66CaBf9b6a17180c842511',
},
UniswapV3: {
factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
initcodeHash: '0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54',
Expand Down
114 changes: 114 additions & 0 deletions test/wrappers/StataTokenWrapper.js
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 1cceb81

Please sign in to comment.