diff --git a/contracts/dependencies/openzeppelin/contracts/ERC20.sol b/contracts/dependencies/openzeppelin/contracts/ERC20.sol index 07fea73f..33f6b0d6 100644 --- a/contracts/dependencies/openzeppelin/contracts/ERC20.sol +++ b/contracts/dependencies/openzeppelin/contracts/ERC20.sol @@ -41,8 +41,8 @@ contract ERC20 is Context, IERC20 { uint256 private _totalSupply; - string private _name; - string private _symbol; + string internal _name; + string internal _symbol; uint8 private _decimals; /** diff --git a/contracts/interfaces/IInitializableStaticATokenLM.sol b/contracts/interfaces/IInitializableStaticATokenLM.sol new file mode 100644 index 00000000..883fa6db --- /dev/null +++ b/contracts/interfaces/IInitializableStaticATokenLM.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +import {ILendingPool} from './ILendingPool.sol'; +import {IAaveIncentivesController} from './IAaveIncentivesController.sol'; + +/** + * @title IInitializableStaticATokenLM + * @notice Interface for the initialize function on StaticATokenLM + * @author Aave + **/ +interface IInitializableStaticATokenLM { + /** + * @dev Emitted when a StaticATokenLM is initialized + * @param pool The address of the lending pool where this aToken will be used + * @param aToken The address of the underlying asset of this aToken (aWETH) + * @param staticATokenName The name of the Static aToken + * @param staticATokenSymbol The symbol of the Static aToken + **/ + event Initialized( + address indexed pool, + address aToken, + string staticATokenName, + string staticATokenSymbol + ); + + /** + * @dev Initializes the StaticATokenLM + * @param pool The address of the lending pool where this aToken will be used + * @param aToken The address of the underlying aToken (aWETH) + * @param staticATokenName The name of the stat Static aToken + * @param staticATokenSymbol The symbol of the Static aToken + */ + function initialize( + ILendingPool pool, + address aToken, + string calldata staticATokenName, + string calldata staticATokenSymbol + ) external; +} diff --git a/contracts/interfaces/IStaticATokenLM.sol b/contracts/interfaces/IStaticATokenLM.sol index c1594501..15c24744 100644 --- a/contracts/interfaces/IStaticATokenLM.sol +++ b/contracts/interfaces/IStaticATokenLM.sol @@ -3,8 +3,11 @@ pragma solidity 0.6.12; pragma experimental ABIEncoderV2; import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; +import {ILendingPool} from './ILendingPool.sol'; +import {IAaveIncentivesController} from './IAaveIncentivesController.sol'; +import {IInitializableStaticATokenLM} from './IInitializableStaticATokenLM.sol'; -interface IStaticATokenLM is IERC20 { +interface IStaticATokenLM is IERC20, IInitializableStaticATokenLM { struct SignatureParams { uint8 v; bytes32 r; @@ -226,4 +229,14 @@ interface IStaticATokenLM is IERC20 { function getLifetimeRewards() external view returns (uint256); function getLastRewardBlock() external view returns (uint256); + + function LENDING_POOL() external returns (ILendingPool); + + function INCENTIVES_CONTROLLER() external returns (IAaveIncentivesController); + + function ATOKEN() external returns (IERC20); + + function ASSET() external returns (IERC20); + + function REWARD_TOKEN() external returns (IERC20); } diff --git a/contracts/protocol/tokenization/StaticATokenLM.sol b/contracts/protocol/tokenization/StaticATokenLM.sol index e8fb96eb..db26941a 100644 --- a/contracts/protocol/tokenization/StaticATokenLM.sol +++ b/contracts/protocol/tokenization/StaticATokenLM.sol @@ -4,9 +4,12 @@ pragma experimental ABIEncoderV2; import {ILendingPool} from '../../interfaces/ILendingPool.sol'; import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {IERC20Detailed} from '../../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; import {IAToken} from '../../interfaces/IAToken.sol'; import {IStaticATokenLM} from '../../interfaces/IStaticATokenLM.sol'; import {IAaveIncentivesController} from '../../interfaces/IAaveIncentivesController.sol'; +import {IInitializableStaticATokenLM} from '../../interfaces/IInitializableStaticATokenLM.sol'; +import {VersionedInitializable} from '../libraries/aave-upgradeability/VersionedInitializable.sol'; import {StaticATokenErrors} from '../libraries/helpers/StaticATokenErrors.sol'; @@ -23,7 +26,11 @@ import {SafeMath} from '../../dependencies/openzeppelin/contracts/SafeMath.sol'; * The token support claiming liquidity mining rewards from the Aave system. * @author Aave **/ -contract StaticATokenLM is IStaticATokenLM, ERC20 { +contract StaticATokenLM is + VersionedInitializable, + ERC20('STATIC_ATOKEN_IMPL', 'STATIC_ATOKEN_IMPL'), + IStaticATokenLM +{ using SafeERC20 for IERC20; using SafeMath for uint256; using WadRayMath for uint256; @@ -43,11 +50,13 @@ contract StaticATokenLM is IStaticATokenLM, ERC20 { 'Withdraw(address owner,address recipient,uint256 staticAmount,uint256 dynamicAmount,bool toUnderlying,uint256 nonce,uint256 deadline)' ); - ILendingPool public immutable LENDING_POOL; - IAaveIncentivesController public immutable INCENTIVES_CONTROLLER; - IERC20 public immutable ATOKEN; - IERC20 public immutable ASSET; - IERC20 public immutable REWARD_TOKEN; + uint256 public constant STATIC_ATOKEN_LM_REVISION = 0x1; + + ILendingPool public override LENDING_POOL; + IAaveIncentivesController public override INCENTIVES_CONTROLLER; + IERC20 public override ATOKEN; + IERC20 public override ASSET; + IERC20 public override REWARD_TOKEN; mapping(address => uint256) public _nonces; @@ -61,21 +70,32 @@ contract StaticATokenLM is IStaticATokenLM, ERC20 { // user => unclaimedRewards (in RAYs) mapping(address => uint256) private _unclaimedRewards; - constructor( - ILendingPool lendingPool, + ///@inheritdoc VersionedInitializable + function getRevision() internal pure virtual override returns (uint256) { + return STATIC_ATOKEN_LM_REVISION; + } + + ///@inheritdoc IInitializableStaticATokenLM + function initialize( + ILendingPool pool, address aToken, - string memory wrappedTokenName, - string memory wrappedTokenSymbol - ) public ERC20(wrappedTokenName, wrappedTokenSymbol) { - LENDING_POOL = lendingPool; + string calldata staticATokenName, + string calldata staticATokenSymbol + ) external override initializer { + LENDING_POOL = pool; ATOKEN = IERC20(aToken); - IERC20 underlyingAsset = ASSET = IERC20(IAToken(aToken).UNDERLYING_ASSET_ADDRESS()); - underlyingAsset.safeApprove(address(lendingPool), type(uint256).max); + _name = staticATokenName; + _symbol = staticATokenSymbol; + _setupDecimals(IERC20Detailed(aToken).decimals()); - IAaveIncentivesController incentivesController = - INCENTIVES_CONTROLLER = IAToken(aToken).getIncentivesController(); - REWARD_TOKEN = IERC20(incentivesController.REWARD_TOKEN()); + ASSET = IERC20(IAToken(aToken).UNDERLYING_ASSET_ADDRESS()); + ASSET.safeApprove(address(pool), type(uint256).max); + + INCENTIVES_CONTROLLER = IAToken(aToken).getIncentivesController(); + REWARD_TOKEN = IERC20(INCENTIVES_CONTROLLER.REWARD_TOKEN()); + + emit Initialized(address(pool), aToken, staticATokenName, staticATokenSymbol); } ///@inheritdoc IStaticATokenLM diff --git a/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining-rewards.spec.ts b/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining-rewards.spec.ts index b48fa08a..1a417e75 100644 --- a/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining-rewards.spec.ts +++ b/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining-rewards.spec.ts @@ -30,6 +30,7 @@ import { _TypedDataEncoder } from 'ethers/lib/utils'; import { expect, use } from 'chai'; import { getCurrentBlock } from '../../../../helpers/contracts-helpers'; +import { stat } from 'fs'; //use(solidity); @@ -99,13 +100,18 @@ describe('StaticATokenLM: aToken wrapper with static balances and liquidity mini aweth = ATokenFactory.connect(AWETH, userSigner); stkAave = ERC20Factory.connect(STKAAVE, userSigner); - staticAToken = await new StaticATokenLMFactory(userSigner).deploy( + staticAToken = await new StaticATokenLMFactory(userSigner).deploy(); + await staticAToken.initialize( LENDING_POOL, AWETH, 'Static Aave Interest Bearing WETH', 'stataAAVE' ); + expect(await staticAToken.decimals()).to.be.eq(18); + expect(await staticAToken.name()).to.be.eq('Static Aave Interest Bearing WETH'); + expect(await staticAToken.symbol()).to.be.eq('stataAAVE'); + snap = await evmSnapshot(); }); @@ -188,6 +194,11 @@ describe('StaticATokenLM: aToken wrapper with static balances and liquidity mini it('Check getters', async () => { const amountToDeposit = utils.parseEther('5'); + const accRewardsPerTokenPre = await staticAToken.getAccRewardsPerToken(); + const lifetimeRewardsClaimedPre = await staticAToken.getLifetimeRewardsClaimed(); + const lifetimeRewards = await staticAToken.getLifetimeRewards(); + const lastRewardBlock = await staticAToken.getLastRewardBlock(); + // Just preparation await waitForTx(await weth.deposit({ value: amountToDeposit.mul(2) })); await waitForTx( @@ -207,6 +218,13 @@ describe('StaticATokenLM: aToken wrapper with static balances and liquidity mini expect(staticBalance).to.be.eq(staticBalanceFromDynamic); expect(dynamicBalance).to.be.eq(dynamicBalanceFromStatic); + + await staticAToken.collectAndUpdateRewards(); + + expect(await staticAToken.getAccRewardsPerToken()).to.be.gt(accRewardsPerTokenPre); + expect(await staticAToken.getLifetimeRewardsClaimed()).to.be.gt(lifetimeRewardsClaimedPre); + expect(await staticAToken.getLifetimeRewards()).to.be.gt(lifetimeRewards); + expect(await staticAToken.getLastRewardBlock()).to.be.gt(lastRewardBlock); }); it('Multiple deposits in one block (Breaks if GasReport enabled)', async () => { diff --git a/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining.spec.ts b/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining.spec.ts index c3550bad..3d297b0c 100644 --- a/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining.spec.ts +++ b/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining.spec.ts @@ -162,7 +162,8 @@ describe('StaticATokenLM: aToken wrapper with static balances and liquidity mini aweth = ATokenFactory.connect(AWETH, userSigner); stkAave = ERC20Factory.connect(STKAAVE, userSigner); - staticAToken = await new StaticATokenLMFactory(userSigner).deploy( + staticAToken = await new StaticATokenLMFactory(userSigner).deploy(); + await staticAToken.initialize( LENDING_POOL, AWETH, 'Static Aave Interest Bearing WETH', diff --git a/test-suites/test-aave/mainnet/static-atoken.spec.ts b/test-suites/test-aave/mainnet/static-atoken.spec.ts deleted file mode 100644 index 776bb4c2..00000000 --- a/test-suites/test-aave/mainnet/static-atoken.spec.ts +++ /dev/null @@ -1,224 +0,0 @@ -import rawDRE from 'hardhat'; -import BigNumber from 'bignumber.js'; -import { - LendingPoolFactory, - WETH9Factory, - StaticATokenFactory, - ATokenFactory, - ERC20, - LendingPool, -} from '../../types'; -import { impersonateAccountsHardhat, DRE, waitForTx } from '../../helpers/misc-utils'; -import { utils } from 'ethers'; -import { rayMul } from '../../helpers/ray-math'; -import { MAX_UINT_AMOUNT } from '../../helpers/constants'; -import { tEthereumAddress } from '../../helpers/types'; - -const { expect } = require('chai'); - -const DEFAULT_GAS_LIMIT = 10000000; -const DEFAULT_GAS_PRICE = utils.parseUnits('100', 'gwei'); - -const defaultTxParams = { gasLimit: DEFAULT_GAS_LIMIT, gasPrice: DEFAULT_GAS_PRICE }; - -const ETHER_BANK = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; -const LENDING_POOL = '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9'; - -const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; - -const AWETH = '0x030bA81f1c18d280636F32af80b9AAd02Cf0854e'; - -const TEST_USERS = ['0x0F4ee9631f4be0a63756515141281A3E2B293Bbe']; - -type tBalancesInvolved = { - aTokenBalanceStaticAToken: BigNumber; - aTokenBalanceUser: BigNumber; - underlyingBalanceUser: BigNumber; - underlyingBalanceStaticAToken: BigNumber; - userStaticATokenBalance: BigNumber; - userDynamicStaticATokenBalance: BigNumber; - currentRate: BigNumber; - staticATokenSupply: BigNumber; -}; - -type tContextParams = { - staticAToken: ERC20; - underlying: ERC20; - aToken: ERC20; - user: tEthereumAddress; - lendingPool: LendingPool; -}; - -const getContext = async ({ - staticAToken, - underlying, - aToken, - user, - lendingPool, -}: tContextParams): Promise => ({ - aTokenBalanceStaticAToken: new BigNumber( - (await aToken.balanceOf(staticAToken.address)).toString() - ), - aTokenBalanceUser: new BigNumber((await aToken.balanceOf(user)).toString()), - underlyingBalanceUser: new BigNumber((await underlying.balanceOf(user)).toString()), - underlyingBalanceStaticAToken: new BigNumber( - (await underlying.balanceOf(staticAToken.address)).toString() - ), - userStaticATokenBalance: new BigNumber((await staticAToken.balanceOf(user)).toString()), - userDynamicStaticATokenBalance: new BigNumber( - rayMul( - new BigNumber((await staticAToken.balanceOf(user)).toString()), - new BigNumber((await lendingPool.getReserveNormalizedIncome(WETH)).toString()) - ) - ), - currentRate: new BigNumber((await lendingPool.getReserveNormalizedIncome(WETH)).toString()), - staticATokenSupply: new BigNumber((await staticAToken.totalSupply()).toString()), -}); - -before(async () => { - await rawDRE.run('set-DRE'); - - // Impersonations - await impersonateAccountsHardhat([ETHER_BANK, ...TEST_USERS]); - - const ethHolderSigner = DRE.ethers.provider.getSigner(ETHER_BANK); - for (const recipientOfEth of [...TEST_USERS]) { - await ethHolderSigner.sendTransaction({ - from: ethHolderSigner._address, - to: recipientOfEth, - value: utils.parseEther('100'), - ...defaultTxParams, - }); - } - - console.log('\n***************'); - console.log('Test setup finished'); - console.log('***************\n'); -}); - -describe('StaticAToken: aToken wrapper with static balances', () => { - it('Deposit WETH on stataWETH, then withdraw of the whole balance in underlying', async () => { - const userSigner = DRE.ethers.provider.getSigner(TEST_USERS[0]); - - const lendingPool = LendingPoolFactory.connect(LENDING_POOL, userSigner); - - const weth = WETH9Factory.connect(WETH, userSigner); - - const aweth = ATokenFactory.connect(AWETH, userSigner); - - const amountToDeposit = utils.parseEther('5'); - - await waitForTx(await weth.deposit({ value: amountToDeposit })); - - const staticAToken = await new StaticATokenFactory(userSigner).deploy( - LENDING_POOL, - AWETH, - 'Static Aave Interest Bearing WETH', - 'stataAAVE' - ); - - const ctxtParams: tContextParams = { - staticAToken: staticAToken, - underlying: (weth), - aToken: aweth, - user: userSigner._address, - lendingPool, - }; - - const ctxtBeforeDeposit = await getContext(ctxtParams); - - await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); - - await waitForTx( - await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) - ); - - const ctxtAfterDeposit = await getContext(ctxtParams); - - expect(ctxtAfterDeposit.aTokenBalanceStaticAToken.toString()).to.be.equal( - ctxtBeforeDeposit.aTokenBalanceStaticAToken - .plus(new BigNumber(amountToDeposit.toString())) - .toString() - ); - - expect(ctxtAfterDeposit.underlyingBalanceUser.toString()).to.be.equal( - ctxtBeforeDeposit.underlyingBalanceUser - .minus(new BigNumber(amountToDeposit.toString())) - .toString() - ); - - expect(ctxtAfterDeposit.userDynamicStaticATokenBalance.toString()).to.be.equal( - ctxtBeforeDeposit.userDynamicStaticATokenBalance - .plus(new BigNumber(amountToDeposit.toString())) - .toString() - ); - - expect(ctxtAfterDeposit.underlyingBalanceStaticAToken.toString()).to.be.equal( - ctxtBeforeDeposit.underlyingBalanceStaticAToken.toString() - ); - - expect(ctxtBeforeDeposit.aTokenBalanceUser.toString()).to.be.equal( - ctxtAfterDeposit.aTokenBalanceUser.toString() - ); - - const ctxtBeforeWithdrawal = await getContext(ctxtParams); - - const amountToWithdraw = MAX_UINT_AMOUNT; - - await waitForTx( - await staticAToken.withdraw(userSigner._address, amountToWithdraw, true, defaultTxParams) - ); - - const ctxtAfterWithdrawal = await getContext(ctxtParams); - - expect( - ctxtAfterWithdrawal.aTokenBalanceStaticAToken.toString(), - 'INVALID_ATOKEN_BALANCE_ON_STATICATOKEN_AFTER_WITHDRAW' - ).to.be.equal( - rayMul( - ctxtAfterWithdrawal.staticATokenSupply.plus(ctxtBeforeWithdrawal.userStaticATokenBalance), - ctxtAfterWithdrawal.currentRate - ) - .minus( - rayMul(ctxtBeforeWithdrawal.userStaticATokenBalance, ctxtAfterWithdrawal.currentRate) - ) - .toString() - ); - - expect( - ctxtAfterWithdrawal.underlyingBalanceUser.toString(), - 'INVALID_UNDERLYING_BALANCE_OF_USER_AFTER_WITHDRAWAL' - ).to.be.equal( - ctxtBeforeWithdrawal.underlyingBalanceUser - .plus(rayMul(ctxtBeforeWithdrawal.userStaticATokenBalance, ctxtAfterWithdrawal.currentRate)) - .toString() - ); - - expect( - ctxtAfterWithdrawal.userStaticATokenBalance.toString(), - 'INVALID_STATICATOKEN_BALANCE_OF_USER_AFTER_WITHDRAWAL' - ).to.be.equal('0'); - - expect( - ctxtAfterDeposit.underlyingBalanceStaticAToken.toString(), - 'INVALID_UNDERLYNG_BALANCE_OF_STATICATOKEN_AFTER_WITHDRAWAL' - ).to.be.equal(ctxtBeforeDeposit.underlyingBalanceStaticAToken.toString()); - - expect( - ctxtBeforeDeposit.aTokenBalanceUser.toString(), - 'INVALID_ATOKEN_BALANCE_OF_USER_AFTER_WITHDRAWAL' - ).to.be.equal(ctxtAfterDeposit.aTokenBalanceUser.toString()); - }); - - it('Deposit WETH on stataWETH and then withdraw some balance in underlying', async () => {}); - - it('Deposit WETH on stataWETH and then withdraw all the balance in aToken', async () => {}); - - it('Deposit aWETH on stataWETH and then withdraw some balance in aToken', async () => {}); - - it('Deposit using metaDeposit()', async () => {}); - - it('Withdraw using withdrawDynamicAmount()', async () => {}); - - it('Withdraw using metaWithdraw()', async () => {}); -});