diff --git a/contracts/interfaces/IStaticAToken.sol b/contracts/interfaces/IStaticAToken.sol new file mode 100644 index 00000000..d478a30d --- /dev/null +++ b/contracts/interfaces/IStaticAToken.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {ILendingPool} from './ILendingPool.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; + +/** + * @title IStaticAToken + * @dev Wrapper token that allows to deposit tokens on the Aave protocol and receive + * a token which balance doesn't increase automatically, but uses an ever-increasing exchange rate + * - Only supporting deposits and withdrawals + * @author Aave + **/ +interface IStaticAToken { + struct SignatureParams { + uint8 v; + bytes32 r; + bytes32 s; + } + + /** + * @dev Deposits `ASSET` in the Aave protocol and mints static aTokens to msg.sender + * @param recipient The address that will receive the static aTokens + * @param amount The amount of underlying `ASSET` to deposit (e.g. deposit of 100 USDC) + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + * @param fromUnderlying bool + * - `true` if the msg.sender comes with underlying tokens (e.g. USDC) + * - `false` if the msg.sender comes already with aTokens (e.g. aUSDC) + * @return uint256 The amount of StaticAToken minted, static balance + **/ + function deposit( + address recipient, + uint256 amount, + uint16 referralCode, + bool fromUnderlying + ) external returns (uint256); + + /** + * @dev Burns `amount` of static aToken, with recipient receiving the corresponding amount of `ASSET` + * @param recipient The address that will receive the amount of `ASSET` withdrawn from the Aave protocol + * @param amount The amount to withdraw, in static balance of StaticAToken + * @param toUnderlying bool + * - `true` for the recipient to get underlying tokens (e.g. USDC) + * - `false` for the recipient to get aTokens (e.g. aUSDC) + * @return amountToBurn: StaticATokens burnt, static balance + * @return amountToWithdraw: underlying/aToken send to `recipient`, dynamic balance + **/ + function withdraw( + address recipient, + uint256 amount, + bool toUnderlying + ) external returns (uint256, uint256); + + /** + * @dev Burns `amount` of static aToken, with recipient receiving the corresponding amount of `ASSET` + * @param recipient The address that will receive the amount of `ASSET` withdrawn from the Aave protocol + * @param amount The amount to withdraw, in dynamic balance of aToken/underlying asset + * @param toUnderlying bool + * - `true` for the recipient to get underlying tokens (e.g. USDC) + * - `false` for the recipient to get aTokens (e.g. aUSDC) + * @return amountToBurn: StaticATokens burnt, static balance + * @return amountToWithdraw: underlying/aToken send to `recipient`, dynamic balance + **/ + function withdrawDynamicAmount( + address recipient, + uint256 amount, + bool toUnderlying + ) external returns (uint256, uint256); + + /** + * @dev Implements the permit function as for + * https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md + * @param owner The owner of the funds + * @param spender The spender + * @param value The amount + * @param deadline The deadline timestamp, type(uint256).max for max deadline + * @param v Signature param + * @param s Signature param + * @param r Signature param + * @param chainId Passing the chainId in order to be fork-compatible + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s, + uint256 chainId + ) external; + + /** + * @dev Allows to deposit on Aave via meta-transaction + * https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md + * @param depositor Address from which the funds to deposit are going to be pulled + * @param recipient Address that will receive the staticATokens, in the average case, same as the `depositor` + * @param value The amount to deposit + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + * @param fromUnderlying bool + * - `true` if the msg.sender comes with underlying tokens (e.g. USDC) + * - `false` if the msg.sender comes already with aTokens (e.g. aUSDC) + * @param deadline The deadline timestamp, type(uint256).max for max deadline + * @param sigParams Signature params: v,r,s + * @param chainId Passing the chainId in order to be fork-compatible + * @return uint256 The amount of StaticAToken minted, static balance + */ + function metaDeposit( + address depositor, + address recipient, + uint256 value, + uint16 referralCode, + bool fromUnderlying, + uint256 deadline, + SignatureParams calldata sigParams, + uint256 chainId + ) external returns (uint256); + + /** + * @dev Allows to withdraw from Aave via meta-transaction + * https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md + * @param owner Address owning the staticATokens + * @param recipient Address that will receive the underlying withdrawn from Aave + * @param staticAmount The amount of staticAToken to withdraw. If > 0, `dynamicAmount` needs to be 0 + * @param dynamicAmount The amount of underlying/aToken to withdraw. If > 0, `staticAmount` needs to be 0 + * @param toUnderlying bool + * - `true` for the recipient to get underlying tokens (e.g. USDC) + * - `false` for the recipient to get aTokens (e.g. aUSDC) + * @param deadline The deadline timestamp, type(uint256).max for max deadline + * @param sigParams Signature params: v,r,s + * @param chainId Passing the chainId in order to be fork-compatible + * @return amountToBurn: StaticATokens burnt, static balance + * @return amountToWithdraw: underlying/aToken send to `recipient`, dynamic balance + */ + function metaWithdraw( + address owner, + address recipient, + uint256 staticAmount, + uint256 dynamicAmount, + bool toUnderlying, + uint256 deadline, + SignatureParams calldata sigParams, + uint256 chainId + ) external returns (uint256, uint256); + + /** + * @dev Utility method to get the current aToken balance of an user, from his staticAToken balance + * @param account The address of the user + * @return uint256 The aToken balance + **/ + function dynamicBalanceOf(address account) external view returns (uint256); + + /** + * @dev Converts a static amount (scaled balance on aToken) to the aToken/underlying value, + * using the current liquidity index on Aave + * @param amount The amount to convert from + * @return uint256 The dynamic amount + **/ + function staticToDynamicAmount(uint256 amount) external view returns (uint256); + + /** + * @dev Converts an aToken or underlying amount to the what it is denominated on the aToken as + * scaled balance, function of the principal and the liquidity index + * @param amount The amount to convert from + * @return uint256 The static (scaled) amount + **/ + function dynamicToStaticAmount(uint256 amount) external view returns (uint256); + + /** + * @dev Returns the Aave liquidity index of the underlying aToken, denominated rate here + * as it can be considered as an ever-increasing exchange rate + * @return bytes32 The domain separator + **/ + function rate() external view returns (uint256); + + /** + * @dev Function to return a dynamic domain separator, in order to be compatible with forks changing chainId + * @param chainId The chain id + * @return bytes32 The domain separator + **/ + function getDomainSeparator(uint256 chainId) external view returns (bytes32); + + function LENDING_POOL() external pure returns (ILendingPool); + + function ATOKEN() external pure returns (IERC20); + + function ASSET() external pure returns (IERC20); + + function EIP712_REVISION() external view returns (bytes memory); + + function EIP712_DOMAIN() external view returns (bytes32); + + function PERMIT_TYPEHASH() external view returns (bytes32); + + function METADEPOSIT_TYPEHASH() external view returns (bytes32); + + function METAWITHDRAWAL_TYPEHASH() external view returns (bytes32); +} diff --git a/contracts/mocks/tokens/StaticATokenMetaTransactionMock.sol b/contracts/mocks/tokens/StaticATokenMetaTransactionMock.sol new file mode 100644 index 00000000..a59475e2 --- /dev/null +++ b/contracts/mocks/tokens/StaticATokenMetaTransactionMock.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {IAToken} from '../../interfaces/IAToken.sol'; +import {IERC20WithPermit} from '../../interfaces/IERC20WithPermit.sol'; +import {IStaticAToken} from '../../interfaces/IStaticAToken.sol'; + +contract StaticATokenMetaTransactionMock { + function permitAndDeposit( + IStaticAToken staticToken, + address recipient, + uint256 value, + uint16 referralCode, + bool fromUnderlying, + uint256 deadline, + IStaticAToken.SignatureParams calldata sigParamsPermit, + IStaticAToken.SignatureParams calldata sigParamsDeposit, + uint256 chainId + ) external returns (uint256) { + // will throw if not permit underlying token + try + IERC20WithPermit( + fromUnderlying ? address(staticToken.ASSET()) : address(staticToken.ATOKEN()) + ) + .permit( + msg.sender, + address(staticToken), + value, + deadline, + sigParamsPermit.v, + sigParamsPermit.r, + sigParamsPermit.s + ) + {} catch { + require(false, 'UNDERLYING_TOKEN_NO_PERMIT'); + } + staticToken.metaDeposit( + msg.sender, + recipient, + value, + referralCode, + fromUnderlying, + deadline, + sigParamsDeposit, + chainId + ); + } +} diff --git a/contracts/protocol/tokenization/StaticAToken.sol b/contracts/protocol/tokenization/StaticAToken.sol index fe3df2d7..39d825bb 100644 --- a/contracts/protocol/tokenization/StaticAToken.sol +++ b/contracts/protocol/tokenization/StaticAToken.sol @@ -3,6 +3,7 @@ pragma solidity 0.6.12; pragma experimental ABIEncoderV2; import {ILendingPool} from '../../interfaces/ILendingPool.sol'; +import {IStaticAToken} from '../../interfaces/IStaticAToken.sol'; import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; import {IAToken} from '../../interfaces/IAToken.sol'; import {ERC20} from '../../dependencies/openzeppelin/contracts/ERC20.sol'; @@ -16,33 +17,27 @@ import {WadRayMath} from '../../protocol/libraries/math/WadRayMath.sol'; * - Only supporting deposits and withdrawals * @author Aave **/ -contract StaticAToken is ERC20 { +contract StaticAToken is IStaticAToken, ERC20 { using SafeERC20 for IERC20; using WadRayMath for uint256; - struct SignatureParams { - uint8 v; - bytes32 r; - bytes32 s; - } - - bytes public constant EIP712_REVISION = bytes('1'); - bytes32 internal constant EIP712_DOMAIN = + bytes public constant override EIP712_REVISION = bytes('1'); + bytes32 public constant override EIP712_DOMAIN = keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'); - bytes32 public constant PERMIT_TYPEHASH = + bytes32 public constant override PERMIT_TYPEHASH = keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); - bytes32 public constant METADEPOSIT_TYPEHASH = + bytes32 public constant override METADEPOSIT_TYPEHASH = keccak256( 'Deposit(address depositor,address recipient,uint256 value,uint16 referralCode,bool fromUnderlying,uint256 nonce,uint256 deadline)' ); - bytes32 public constant METAWITHDRAWAL_TYPEHASH = + bytes32 public constant override METAWITHDRAWAL_TYPEHASH = keccak256( - 'Withdraw(address owner,address recipient,uint256 staticAmount, uint256 dynamicAmount, bool toUnderlying, uint256 nonce,uint256 deadline)' + 'Withdraw(address owner,address recipient,uint256 staticAmount,uint256 dynamicAmount,bool toUnderlying,uint256 nonce,uint256 deadline)' ); - ILendingPool public immutable LENDING_POOL; - IERC20 public immutable ATOKEN; - IERC20 public immutable ASSET; + ILendingPool public immutable override LENDING_POOL; + IERC20 public immutable override ATOKEN; + IERC20 public immutable override ASSET; /// @dev owner => next valid nonce to submit with permit(), metaDeposit() and metaWithdraw() /// We choose to have sequentiality on them for each user to avoid potentially dangerous/bad UX cases @@ -78,7 +73,7 @@ contract StaticAToken is ERC20 { uint256 amount, uint16 referralCode, bool fromUnderlying - ) external returns (uint256) { + ) external override returns (uint256) { return _deposit(msg.sender, recipient, amount, referralCode, fromUnderlying); } @@ -96,7 +91,7 @@ contract StaticAToken is ERC20 { address recipient, uint256 amount, bool toUnderlying - ) external returns (uint256, uint256) { + ) external override returns (uint256, uint256) { return _withdraw(msg.sender, recipient, amount, 0, toUnderlying); } @@ -114,7 +109,7 @@ contract StaticAToken is ERC20 { address recipient, uint256 amount, bool toUnderlying - ) external returns (uint256, uint256) { + ) external override returns (uint256, uint256) { return _withdraw(msg.sender, recipient, 0, amount, toUnderlying); } @@ -139,7 +134,7 @@ contract StaticAToken is ERC20 { bytes32 r, bytes32 s, uint256 chainId - ) external { + ) external override { require(owner != address(0), 'INVALID_OWNER'); //solium-disable-next-line require(block.timestamp <= deadline, 'INVALID_EXPIRATION'); @@ -182,7 +177,7 @@ contract StaticAToken is ERC20 { uint256 deadline, SignatureParams calldata sigParams, uint256 chainId - ) external returns (uint256) { + ) external override returns (uint256) { require(depositor != address(0), 'INVALID_DEPOSITOR'); //solium-disable-next-line require(block.timestamp <= deadline, 'INVALID_EXPIRATION'); @@ -239,7 +234,7 @@ contract StaticAToken is ERC20 { uint256 deadline, SignatureParams calldata sigParams, uint256 chainId - ) external returns (uint256, uint256) { + ) external override returns (uint256, uint256) { require(owner != address(0), 'INVALID_DEPOSITOR'); //solium-disable-next-line require(block.timestamp <= deadline, 'INVALID_EXPIRATION'); @@ -273,7 +268,7 @@ contract StaticAToken is ERC20 { * @param account The address of the user * @return uint256 The aToken balance **/ - function dynamicBalanceOf(address account) external view returns (uint256) { + function dynamicBalanceOf(address account) external view override returns (uint256) { return staticToDynamicAmount(balanceOf(account)); } @@ -283,7 +278,7 @@ contract StaticAToken is ERC20 { * @param amount The amount to convert from * @return uint256 The dynamic amount **/ - function staticToDynamicAmount(uint256 amount) public view returns (uint256) { + function staticToDynamicAmount(uint256 amount) public view override returns (uint256) { return amount.rayMul(rate()); } @@ -293,7 +288,7 @@ contract StaticAToken is ERC20 { * @param amount The amount to convert from * @return uint256 The static (scaled) amount **/ - function dynamicToStaticAmount(uint256 amount) public view returns (uint256) { + function dynamicToStaticAmount(uint256 amount) public view override returns (uint256) { return amount.rayDiv(rate()); } @@ -302,7 +297,7 @@ contract StaticAToken is ERC20 { * as it can be considered as an ever-increasing exchange rate * @return bytes32 The domain separator **/ - function rate() public view returns (uint256) { + function rate() public view override returns (uint256) { return LENDING_POOL.getReserveNormalizedIncome(address(ASSET)); } @@ -311,7 +306,7 @@ contract StaticAToken is ERC20 { * @param chainId The chain id * @return bytes32 The domain separator **/ - function getDomainSeparator(uint256 chainId) public view returns (bytes32) { + function getDomainSeparator(uint256 chainId) public view override returns (bytes32) { return keccak256( abi.encode( diff --git a/hardhat.config.ts b/hardhat.config.ts index a6d411ea..ab6c5e94 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -26,7 +26,7 @@ const ETHERSCAN_KEY = process.env.ETHERSCAN_KEY || ''; const MNEMONIC_PATH = "m/44'/60'/0'/0"; const MNEMONIC = process.env.MNEMONIC || ''; const MAINNET_FORK = process.env.MAINNET_FORK === 'true'; - +const FORKING_BLOCK = Number(process.env.FORKING_BLOCK || '11608298'); // Prevent to load scripts before compilation and typechain if (!SKIP_LOAD) { ['misc', 'migrations', 'dev', 'full', 'verifications', 'deployments', 'helpers'].forEach( @@ -65,7 +65,7 @@ const getCommonNetworkConfig = (networkName: eEthereumNetwork, networkId: number const mainnetFork = MAINNET_FORK ? { - blockNumber: 11608298, + blockNumber: FORKING_BLOCK, url: ALCHEMY_KEY ? `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}` : `https://mainnet.infura.io/v3/${INFURA_KEY}`, diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index 6fce99b6..ca9c3843 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -225,6 +225,102 @@ export const buildPermitParams = ( }, }); +export const buildMetaDepositParams = ( + chainId: number, + token: tEthereumAddress, + revision: string, + tokenName: string, + depositor: tEthereumAddress, + recipient: tEthereumAddress, + nonce: number, + deadline: string, + fromUnderlying: boolean, + referralCode: number, + value: tStringTokenSmallUnits +) => ({ + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Deposit: [ + { name: 'depositor', type: 'address' }, + { name: 'recipient', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'referralCode', type: 'uint16' }, + { name: 'fromUnderlying', type: 'bool' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + primaryType: 'Deposit' as const, + domain: { + name: tokenName, + version: revision, + chainId: chainId, + verifyingContract: token, + }, + message: { + depositor, + fromUnderlying, + recipient, + value, + nonce, + deadline, + referralCode, + }, +}); + +export const buildMetaWithdrawParams = ( + chainId: number, + token: tEthereumAddress, + revision: string, + tokenName: string, + owner: tEthereumAddress, + recipient: tEthereumAddress, + nonce: number, + deadline: string, + toUnderlying: boolean, + staticAmount: tStringTokenSmallUnits, + dynamicAmount: tStringTokenSmallUnits +) => ({ + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Withdraw: [ + { name: 'owner', type: 'address' }, + { name: 'recipient', type: 'address' }, + { name: 'staticAmount', type: 'uint256' }, + { name: 'dynamicAmount', type: 'uint256' }, + { name: 'toUnderlying', type: 'bool' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + primaryType: 'Withdraw' as const, + domain: { + name: tokenName, + version: revision, + chainId: chainId, + verifyingContract: token, + }, + message: { + owner, + toUnderlying, + recipient, + staticAmount, + dynamicAmount, + nonce, + deadline, + }, +}); + export const getSignatureFromTypedData = ( privateKey: string, typedData: any // TODO: should be TypedData, from eth-sig-utils, but TS doesn't accept it diff --git a/package.json b/package.json index 107cdb73..b857b89a 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "test-weth": "hardhat test test/__setup.spec.ts test/weth-gateway.spec.ts", "test-uniswap": "hardhat test test/__setup.spec.ts test/uniswapAdapters*.spec.ts", "test:main:check-list": "MAINNET_FORK=true TS_NODE_TRANSPILE_ONLY=1 hardhat test test/__setup.spec.ts test/mainnet/check-list.spec.ts", - "test:main:staticAToken": "MAINNET_FORK=true TS_NODE_TRANSPILE_ONLY=1 hardhat test test/mainnet/static-atoken.spec.ts", + "test:main:staticAToken": "MAINNET_FORK=true FORKING_BLOCK=12063148 TS_NODE_TRANSPILE_ONLY=1 hardhat test test/mainnet/static-atoken.spec.ts", "dev:coverage": "buidler compile --force && buidler coverage --network coverage", "aave:evm:dev:migration": "npm run compile && hardhat aave:dev", "aave:docker:full:migration": "npm run compile && npm run hardhat:docker -- aave:mainnet", diff --git a/test/mainnet/static-atoken.spec.ts b/test/mainnet/static-atoken.spec.ts index 776bb4c2..198abbff 100644 --- a/test/mainnet/static-atoken.spec.ts +++ b/test/mainnet/static-atoken.spec.ts @@ -1,5 +1,8 @@ import rawDRE from 'hardhat'; import BigNumber from 'bignumber.js'; +import chai, { expect } from 'chai'; +import bignumberChai from 'chai-bignumber'; +import { solidity } from 'ethereum-waffle'; import { LendingPoolFactory, WETH9Factory, @@ -7,14 +10,30 @@ import { ATokenFactory, ERC20, LendingPool, + StaticATokenMetaTransactionMock, + StaticATokenMetaTransactionMockFactory, + WETH9, + AToken, + StaticAToken, } from '../../types'; +import { + buildPermitParams, + buildMetaDepositParams, + buildMetaWithdrawParams, + getSignatureFromTypedData, +} from '../../helpers/contracts-helpers'; import { impersonateAccountsHardhat, DRE, waitForTx } from '../../helpers/misc-utils'; import { utils } from 'ethers'; -import { rayMul } from '../../helpers/ray-math'; +import { rayDiv, rayMul } from '../../helpers/ray-math'; import { MAX_UINT_AMOUNT } from '../../helpers/constants'; import { tEthereumAddress } from '../../helpers/types'; +import { JsonRpcSigner } from '@ethersproject/providers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; +import { parseEther } from '@ethersproject/units'; +import { parse } from 'path'; -const { expect } = require('chai'); +chai.use(bignumberChai()); +chai.use(solidity); const DEFAULT_GAS_LIMIT = 10000000; const DEFAULT_GAS_PRICE = utils.parseUnits('100', 'gwei'); @@ -30,6 +49,8 @@ const AWETH = '0x030bA81f1c18d280636F32af80b9AAd02Cf0854e'; const TEST_USERS = ['0x0F4ee9631f4be0a63756515141281A3E2B293Bbe']; +const AWETH_HOLDER = '0x928477dabc0eD2a6CE6c33966a52eA58CbDEA212'; + type tBalancesInvolved = { aTokenBalanceStaticAToken: BigNumber; aTokenBalanceUser: BigNumber; @@ -75,62 +96,74 @@ const getContext = async ({ staticATokenSupply: new BigNumber((await staticAToken.totalSupply()).toString()), }); -before(async () => { - await rawDRE.run('set-DRE'); +const getInterestAccrued = (ctxBefore: tBalancesInvolved, ctxAfter: tBalancesInvolved) => + rayMul( + rayDiv(ctxBefore.aTokenBalanceStaticAToken.toString(), ctxBefore.currentRate.toString()), + ctxAfter.currentRate.toString() + ).minus(ctxBefore.aTokenBalanceStaticAToken.toString()); - // 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'); -}); +const getUserInterestAccrued = (ctxBefore: tBalancesInvolved, ctxAfter: tBalancesInvolved) => + rayMul( + rayDiv(ctxBefore.aTokenBalanceUser.toString(), ctxBefore.currentRate.toString()), + ctxAfter.currentRate.toString() + ).minus(ctxBefore.aTokenBalanceUser.toString()); 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]); + let weth: WETH9; + let lendingPool: LendingPool; + let aweth: AToken; + let user1Signer: JsonRpcSigner; + let controlledPkSigner: SignerWithAddress; + let staticAWeth: StaticAToken; + before(async () => { + await rawDRE.run('set-DRE'); + // Impersonations + await impersonateAccountsHardhat([ETHER_BANK, ...TEST_USERS, AWETH_HOLDER]); + const ethHolderSigner = rawDRE.ethers.provider.getSigner(ETHER_BANK); + const awethHolderSigner = rawDRE.ethers.provider.getSigner(AWETH_HOLDER); + user1Signer = DRE.ethers.provider.getSigner(TEST_USERS[0]); + controlledPkSigner = (await rawDRE.ethers.getSigners())[0]; - const lendingPool = LendingPoolFactory.connect(LENDING_POOL, userSigner); + lendingPool = LendingPoolFactory.connect(LENDING_POOL, user1Signer); + weth = WETH9Factory.connect(WETH, user1Signer); + aweth = ATokenFactory.connect(AWETH, user1Signer); - const weth = WETH9Factory.connect(WETH, userSigner); + await aweth.connect(awethHolderSigner).transfer(controlledPkSigner.address, parseEther('5.0')); - const aweth = ATokenFactory.connect(AWETH, userSigner); + for (const recipientOfEth of [...TEST_USERS]) { + await ethHolderSigner.sendTransaction({ + from: ethHolderSigner._address, + to: recipientOfEth, + value: utils.parseEther('100'), + ...defaultTxParams, + }); + } + await waitForTx(await weth.deposit({ value: parseEther('5') })); - const amountToDeposit = utils.parseEther('5'); - - await waitForTx(await weth.deposit({ value: amountToDeposit })); - - const staticAToken = await new StaticATokenFactory(userSigner).deploy( + staticAWeth = await new StaticATokenFactory(user1Signer).deploy( LENDING_POOL, AWETH, 'Static Aave Interest Bearing WETH', 'stataAAVE' ); + }); + it('Deposit WETH on stataWETH, then withdraw of the whole balance in underlying', async () => { + const amountToDeposit = utils.parseEther('5'); const ctxtParams: tContextParams = { - staticAToken: staticAToken, + staticAToken: staticAWeth, underlying: (weth), aToken: aweth, - user: userSigner._address, + user: user1Signer._address, lendingPool, }; const ctxtBeforeDeposit = await getContext(ctxtParams); - await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + await waitForTx(await weth.approve(staticAWeth.address, amountToDeposit, defaultTxParams)); await waitForTx( - await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + await staticAWeth.deposit(user1Signer._address, amountToDeposit, 0, true, defaultTxParams) ); const ctxtAfterDeposit = await getContext(ctxtParams); @@ -166,7 +199,7 @@ describe('StaticAToken: aToken wrapper with static balances', () => { const amountToWithdraw = MAX_UINT_AMOUNT; await waitForTx( - await staticAToken.withdraw(userSigner._address, amountToWithdraw, true, defaultTxParams) + await staticAWeth.withdraw(user1Signer._address, amountToWithdraw, true, defaultTxParams) ); const ctxtAfterWithdrawal = await getContext(ctxtParams); @@ -176,13 +209,9 @@ describe('StaticAToken: aToken wrapper with static balances', () => { 'INVALID_ATOKEN_BALANCE_ON_STATICATOKEN_AFTER_WITHDRAW' ).to.be.equal( rayMul( - ctxtAfterWithdrawal.staticATokenSupply.plus(ctxtBeforeWithdrawal.userStaticATokenBalance), + ctxtBeforeWithdrawal.staticATokenSupply.minus(ctxtBeforeWithdrawal.userStaticATokenBalance), ctxtAfterWithdrawal.currentRate - ) - .minus( - rayMul(ctxtBeforeWithdrawal.userStaticATokenBalance, ctxtAfterWithdrawal.currentRate) - ) - .toString() + ).toString() ); expect( @@ -200,25 +229,501 @@ describe('StaticAToken: aToken wrapper with static balances', () => { ).to.be.equal('0'); expect( - ctxtAfterDeposit.underlyingBalanceStaticAToken.toString(), + ctxtAfterWithdrawal.underlyingBalanceStaticAToken.toString(), 'INVALID_UNDERLYNG_BALANCE_OF_STATICATOKEN_AFTER_WITHDRAWAL' - ).to.be.equal(ctxtBeforeDeposit.underlyingBalanceStaticAToken.toString()); + ).to.be.equal(ctxtBeforeWithdrawal.underlyingBalanceStaticAToken.toString()); expect( - ctxtBeforeDeposit.aTokenBalanceUser.toString(), + ctxtAfterWithdrawal.aTokenBalanceUser.toString(), 'INVALID_ATOKEN_BALANCE_OF_USER_AFTER_WITHDRAWAL' - ).to.be.equal(ctxtAfterDeposit.aTokenBalanceUser.toString()); + ).to.be.equal(ctxtBeforeWithdrawal.aTokenBalanceUser.toString()); }); - it('Deposit WETH on stataWETH and then withdraw some balance in underlying', async () => {}); + it('Deposit WETH on stataWETH and then withdraw to some balance in underlying', async () => { + const amountToDeposit = utils.parseEther('5'); - it('Deposit WETH on stataWETH and then withdraw all the balance in aToken', async () => {}); + const ctxtParams: tContextParams = { + staticAToken: staticAWeth, + underlying: (weth), + aToken: aweth, + user: user1Signer._address, + lendingPool, + }; - it('Deposit aWETH on stataWETH and then withdraw some balance in aToken', async () => {}); + const ctxtBeforeDeposit = await getContext(ctxtParams); - it('Deposit using metaDeposit()', async () => {}); + await waitForTx(await weth.approve(staticAWeth.address, amountToDeposit, defaultTxParams)); - it('Withdraw using withdrawDynamicAmount()', async () => {}); + await waitForTx( + await staticAWeth.deposit(user1Signer._address, amountToDeposit, 0, true, defaultTxParams) + ); - it('Withdraw using metaWithdraw()', async () => {}); + 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 = parseEther('2.0'); + + await waitForTx( + await staticAWeth.withdraw(user1Signer._address, amountToWithdraw, true, defaultTxParams) + ); + + const ctxtAfterWithdrawal = await getContext(ctxtParams); + + expect( + ctxtAfterWithdrawal.aTokenBalanceStaticAToken.toString(), + 'INVALID_ATOKEN_BALANCE_ON_STATICATOKEN_AFTER_WITHDRAW' + ).to.be.equal( + rayMul( + ctxtBeforeWithdrawal.staticATokenSupply.minus(amountToWithdraw.toString()), + ctxtAfterWithdrawal.currentRate + ).toString() + ); + + expect( + ctxtAfterWithdrawal.underlyingBalanceUser.toString(), + 'INVALID_UNDERLYING_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal( + ctxtBeforeWithdrawal.underlyingBalanceUser + .plus(rayMul(amountToWithdraw.toString(), ctxtAfterWithdrawal.currentRate)) + .toString() + ); + + expect( + ctxtAfterWithdrawal.userStaticATokenBalance.toString(), + 'INVALID_STATICATOKEN_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal( + ctxtBeforeWithdrawal.userStaticATokenBalance.minus(amountToWithdraw.toString()).toString() + ); + + expect( + ctxtAfterWithdrawal.underlyingBalanceStaticAToken.toString(), + 'INVALID_UNDERLYNG_BALANCE_OF_STATICATOKEN_AFTER_WITHDRAWAL' + ).to.be.equal(ctxtBeforeWithdrawal.underlyingBalanceStaticAToken.toString()); + + expect( + ctxtAfterWithdrawal.aTokenBalanceUser.toString(), + 'INVALID_ATOKEN_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal(ctxtBeforeWithdrawal.aTokenBalanceUser.toString()); + }); + + it('Deposit WETH on stataWETH and then withdrawDynamic some balance in aToken', async () => { + const amountToDeposit = utils.parseEther('4'); + + const ctxtParams: tContextParams = { + staticAToken: staticAWeth, + underlying: (weth), + aToken: aweth, + user: user1Signer._address, + lendingPool, + }; + + const ctxtBeforeDeposit = await getContext(ctxtParams); + + await waitForTx(await weth.approve(staticAWeth.address, amountToDeposit, defaultTxParams)); + + await waitForTx( + await staticAWeth.deposit(user1Signer._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const ctxtAfterDeposit = await getContext(ctxtParams); + const interestAccrued = getInterestAccrued(ctxtBeforeDeposit, ctxtAfterDeposit); + expect(ctxtAfterDeposit.aTokenBalanceStaticAToken.toString()).to.be.equal( + ctxtBeforeDeposit.aTokenBalanceStaticAToken + .plus(new BigNumber(amountToDeposit.toString())) + .plus(interestAccrued) + .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())) + .plus(interestAccrued) + .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 = parseEther('2.0'); + + await waitForTx( + await staticAWeth.withdrawDynamicAmount( + user1Signer._address, + amountToWithdraw, + false, + defaultTxParams + ) + ); + + const ctxtAfterWithdrawal = await getContext(ctxtParams); + const interestAccruedWithdrawal = getInterestAccrued(ctxtBeforeWithdrawal, ctxtAfterWithdrawal); + expect( + ctxtAfterWithdrawal.aTokenBalanceStaticAToken.toString(), + 'INVALID_ATOKEN_BALANCE_ON_STATICATOKEN_AFTER_WITHDRAW' + ).to.be.equal( + ctxtBeforeWithdrawal.aTokenBalanceStaticAToken + .minus(amountToWithdraw.toString()) + .plus(interestAccruedWithdrawal) + .plus('1') // rounding issue + .toString() + ); + expect( + ctxtAfterWithdrawal.underlyingBalanceUser.toString(), + 'INVALID_UNDERLYING_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal(ctxtBeforeWithdrawal.underlyingBalanceUser.toString()); + expect( + ctxtAfterWithdrawal.userStaticATokenBalance.toString(), + 'INVALID_STATICATOKEN_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal( + ctxtBeforeWithdrawal.userStaticATokenBalance + .minus(rayDiv(amountToWithdraw.toString(), ctxtAfterWithdrawal.currentRate)) + .toString() + ); + expect( + ctxtAfterWithdrawal.underlyingBalanceStaticAToken.toString(), + 'INVALID_UNDERLYNG_BALANCE_OF_STATICATOKEN_AFTER_WITHDRAWAL' + ).to.be.equal(ctxtBeforeWithdrawal.underlyingBalanceStaticAToken.toString()); + expect( + ctxtAfterWithdrawal.aTokenBalanceUser.toString(), + 'INVALID_ATOKEN_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal( + ctxtBeforeWithdrawal.aTokenBalanceUser.plus(amountToWithdraw.toString()).toString() + ); + }); + + // tslint:disable-next-line:max-line-length + it('User 2 Deposits aWETH for user 1 and then withdraw some balance to second user in aToken', async () => { + const amountToDeposit = utils.parseEther('4'); + + const ctxtParams: tContextParams = { + staticAToken: staticAWeth, + underlying: (weth), + aToken: aweth, + user: user1Signer._address, + lendingPool, + }; + + const ctxtBeforeDeposit = await getContext(ctxtParams); + + await waitForTx( + await aweth.connect(controlledPkSigner).approve(staticAWeth.address, MAX_UINT_AMOUNT) + ); + + await waitForTx( + await staticAWeth + .connect(controlledPkSigner) + .deposit(user1Signer._address, amountToDeposit, 0, false, defaultTxParams) + ); + + const ctxtAfterDeposit = await getContext(ctxtParams); + const interestAccrued = getInterestAccrued(ctxtBeforeDeposit, ctxtAfterDeposit); + const userInterestAccrued = getUserInterestAccrued(ctxtBeforeDeposit, ctxtAfterDeposit); + expect(ctxtAfterDeposit.aTokenBalanceStaticAToken.toString()).to.be.equal( + ctxtBeforeDeposit.aTokenBalanceStaticAToken + .plus(new BigNumber(amountToDeposit.toString())) + .plus(interestAccrued) + .plus(1) + .toString() + ); + expect(ctxtAfterDeposit.underlyingBalanceUser.toString()).to.be.equal( + ctxtBeforeDeposit.underlyingBalanceUser.toString() + ); + expect(ctxtAfterDeposit.userDynamicStaticATokenBalance.toString()).to.be.equal( + ctxtBeforeDeposit.userDynamicStaticATokenBalance + .plus(new BigNumber(amountToDeposit.toString())) + .plus(interestAccrued) + .plus(1) + .toString() + ); + expect(ctxtAfterDeposit.underlyingBalanceStaticAToken.toString()).to.be.equal( + ctxtBeforeDeposit.underlyingBalanceStaticAToken.toString() + ); + expect(ctxtAfterDeposit.aTokenBalanceUser.toString()).to.be.equal( + ctxtBeforeDeposit.aTokenBalanceUser.plus(userInterestAccrued).toString() + ); + const ctxtBeforeWithdrawal = await getContext(ctxtParams); + + const amountToWithdraw = parseEther('2.0'); + + await waitForTx( + await staticAWeth.withdrawDynamicAmount( + user1Signer._address, + amountToWithdraw, + false, + defaultTxParams + ) + ); + + const ctxtAfterWithdrawal = await getContext(ctxtParams); + const interestAccruedWithdrawal = getInterestAccrued(ctxtBeforeWithdrawal, ctxtAfterWithdrawal); + const userInterestAccruedWithdrawal = getUserInterestAccrued( + ctxtBeforeWithdrawal, + ctxtAfterWithdrawal + ); + expect( + ctxtAfterWithdrawal.aTokenBalanceStaticAToken.toString(), + 'INVALID_ATOKEN_BALANCE_ON_STATICATOKEN_AFTER_WITHDRAW' + ).to.be.equal( + ctxtBeforeWithdrawal.aTokenBalanceStaticAToken + .minus(amountToWithdraw.toString()) + .plus(interestAccruedWithdrawal) + .plus('1') // rounding issue + .toString() + ); + expect( + ctxtAfterWithdrawal.underlyingBalanceUser.toString(), + 'INVALID_UNDERLYING_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal(ctxtBeforeWithdrawal.underlyingBalanceUser.toString()); + expect( + ctxtAfterWithdrawal.userStaticATokenBalance.toString(), + 'INVALID_STATICATOKEN_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal( + ctxtBeforeWithdrawal.userStaticATokenBalance + .minus(rayDiv(amountToWithdraw.toString(), ctxtAfterWithdrawal.currentRate)) + .toString() + ); + expect( + ctxtAfterWithdrawal.underlyingBalanceStaticAToken.toString(), + 'INVALID_UNDERLYNG_BALANCE_OF_STATICATOKEN_AFTER_WITHDRAWAL' + ).to.be.equal(ctxtBeforeWithdrawal.underlyingBalanceStaticAToken.toString()); + expect( + ctxtAfterWithdrawal.aTokenBalanceUser.toString(), + 'INVALID_ATOKEN_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal( + ctxtBeforeWithdrawal.aTokenBalanceUser + .plus(userInterestAccruedWithdrawal) + .plus(amountToWithdraw.toString()) + .toString() + ); + }); + + it('Deposit using permit + metaDeposit()', async () => { + const mockFactory = new StaticATokenMetaTransactionMockFactory(controlledPkSigner); + const metaTransactionMock = await mockFactory.deploy(); + const chainId = DRE.network.config.chainId || 1; + const userBalance = await aweth.balanceOf(controlledPkSigner.address); + const amountToDeposit = new BigNumber(userBalance.div(2).toString()); + + const ctxtParams: tContextParams = { + staticAToken: staticAWeth, + underlying: (weth), + aToken: aweth, + user: controlledPkSigner.address, + lendingPool, + }; + + const ctxtBeforeDeposit = await getContext(ctxtParams); + + const permitParams = buildPermitParams( + 1, // mainnet fork + aweth.address, + '1', + await aweth.name(), + controlledPkSigner.address, + staticAWeth.address, + (await aweth._nonces(controlledPkSigner.address)).toNumber(), + MAX_UINT_AMOUNT, + userBalance.div(2).toString() + ); + const depositParams = buildMetaDepositParams( + 1, // mainnet fork + staticAWeth.address, + '1', + await staticAWeth.name(), + controlledPkSigner.address, + controlledPkSigner.address, + (await staticAWeth._nonces(controlledPkSigner.address)).toNumber(), + MAX_UINT_AMOUNT, + false, + 0, + userBalance.div(2).toString() + ); + + const ownerPrivateKey = require('../../test-wallets.js').accounts[0].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + expect( + (await aweth.allowance(controlledPkSigner.address, metaTransactionMock.address)).toString() + ).to.be.equal('0', 'INVALID_ALLOWANCE_BEFORE_PERMIT'); + const { v: permitV, r: permitR, s: permitS } = getSignatureFromTypedData( + ownerPrivateKey, + permitParams + ); + const { v: depositV, r: depositR, s: depositS } = getSignatureFromTypedData( + ownerPrivateKey, + depositParams + ); + await metaTransactionMock.permitAndDeposit( + staticAWeth.address, + controlledPkSigner.address, + userBalance.div(2), + 0, + false, + MAX_UINT_AMOUNT, + { + v: permitV, + r: permitR, + s: permitS, + }, + { + v: depositV, + r: depositR, + s: depositS, + }, + 1 + ); + const ctxtAfterDeposit = await getContext(ctxtParams); + const interestAccrued = getInterestAccrued(ctxtBeforeDeposit, ctxtAfterDeposit); + const userInterestAccrued = getUserInterestAccrued(ctxtBeforeDeposit, ctxtAfterDeposit); + expect(ctxtAfterDeposit.aTokenBalanceStaticAToken.toString()).to.be.equal( + ctxtBeforeDeposit.aTokenBalanceStaticAToken + .plus(new BigNumber(amountToDeposit.toString())) + .plus(interestAccrued) + .minus(1) + .toString() + ); + expect(ctxtAfterDeposit.underlyingBalanceUser.toString()).to.be.equal( + ctxtBeforeDeposit.underlyingBalanceUser.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(ctxtAfterDeposit.aTokenBalanceUser.toString()).to.be.equal( + ctxtBeforeDeposit.aTokenBalanceUser + .plus(userInterestAccrued) + .minus(amountToDeposit) + .toString() + ); + }); + + it('Withdraw using metaWithdraw()', async () => { + const ctxtParams: tContextParams = { + staticAToken: staticAWeth, + underlying: (weth), + aToken: aweth, + user: controlledPkSigner.address, + lendingPool, + }; + const ctxtBeforeWithdrawal = await getContext(ctxtParams); + const amountToWithdraw = parseEther('0.1'); + + const withdrawParams = buildMetaWithdrawParams( + 1, // mainnet fork + staticAWeth.address, + '1', + await staticAWeth.name(), + controlledPkSigner.address, + controlledPkSigner.address, + (await staticAWeth._nonces(controlledPkSigner.address)).toNumber(), + MAX_UINT_AMOUNT, + false, + '0', + amountToWithdraw.toString() + ); + const ownerPrivateKey = require('../../test-wallets.js').accounts[0].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, withdrawParams); + + await waitForTx( + await staticAWeth.metaWithdraw( + controlledPkSigner.address, + controlledPkSigner.address, + 0, + amountToWithdraw, + false, + MAX_UINT_AMOUNT, + { + v, + r, + s, + }, + '1' + ) + ); + + const ctxtAfterWithdrawal = await getContext(ctxtParams); + const interestAccruedWithdrawal = getInterestAccrued(ctxtBeforeWithdrawal, ctxtAfterWithdrawal); + const userInterestAccruedWithdrawal = getUserInterestAccrued( + ctxtBeforeWithdrawal, + ctxtAfterWithdrawal + ); + expect( + ctxtAfterWithdrawal.aTokenBalanceStaticAToken.toString(), + 'INVALID_ATOKEN_BALANCE_ON_STATICATOKEN_AFTER_WITHDRAW' + ).to.be.equal( + ctxtBeforeWithdrawal.aTokenBalanceStaticAToken + .minus(amountToWithdraw.toString()) + .plus(interestAccruedWithdrawal) + .toString() + ); + expect( + ctxtAfterWithdrawal.underlyingBalanceUser.toString(), + 'INVALID_UNDERLYING_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal(ctxtBeforeWithdrawal.underlyingBalanceUser.toString()); + expect( + ctxtAfterWithdrawal.userStaticATokenBalance.toString(), + 'INVALID_STATICATOKEN_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal( + ctxtBeforeWithdrawal.userStaticATokenBalance + .minus(rayDiv(amountToWithdraw.toString(), ctxtAfterWithdrawal.currentRate)) + .toString() + ); + expect( + ctxtAfterWithdrawal.underlyingBalanceStaticAToken.toString(), + 'INVALID_UNDERLYNG_BALANCE_OF_STATICATOKEN_AFTER_WITHDRAWAL' + ).to.be.equal(ctxtBeforeWithdrawal.underlyingBalanceStaticAToken.toString()); + expect( + ctxtAfterWithdrawal.aTokenBalanceUser.toString(), + 'INVALID_ATOKEN_BALANCE_OF_USER_AFTER_WITHDRAWAL' + ).to.be.equal( + ctxtBeforeWithdrawal.aTokenBalanceUser + .plus(userInterestAccruedWithdrawal) + .plus(amountToWithdraw.toString()) + .toString() + ); + }); });