Tested and Fixed contract

This commit is contained in:
Hadrien Charlanes 2021-03-24 11:37:43 +01:00
parent a3e11800a4
commit 314aa37de2
7 changed files with 928 additions and 82 deletions

View File

@ -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);
}

View File

@ -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
);
}
}

View File

@ -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(

View File

@ -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}`,

View File

@ -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

View File

@ -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",

View File

@ -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: <ERC20>staticAToken,
staticAToken: <ERC20>staticAWeth,
underlying: <ERC20>(<unknown>weth),
aToken: <ERC20>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: <ERC20>staticAWeth,
underlying: <ERC20>(<unknown>weth),
aToken: <ERC20>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: <ERC20>staticAWeth,
underlying: <ERC20>(<unknown>weth),
aToken: <ERC20>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: <ERC20>staticAWeth,
underlying: <ERC20>(<unknown>weth),
aToken: <ERC20>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: <ERC20>staticAWeth,
underlying: <ERC20>(<unknown>weth),
aToken: <ERC20>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: <ERC20>staticAWeth,
underlying: <ERC20>(<unknown>weth),
aToken: <ERC20>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()
);
});
});