Skip to content

Commit

Permalink
Add aave test harness (#10)
Browse files Browse the repository at this point in the history
* forge install aave/protocol-v2

* forge install https://github.com/aave/aave-v3-core

* Bump forge-std to get new vm cheatcodes

* Downgrade solc so that we can import aave's OZ dependencies

* Ignore .env file

* Fix foundry.toml profile warning

* Add a test that builds

* Add our aToken to Aave on Optimism

* Pull the real AToken address out of Aave storage

* Fix computation of atoken address storage slot

* Confirm we can withdraw from Aave

* Improve Aave test comments

* Add link to aave mainnet networks

* Improve atokenAddress computation notes

* Add .env.example

* Update foundry URL

* Rename token -> govToken

* Test that GOV can be borrowed/liquidated

* Modify based on PR feedback
  • Loading branch information
davidlaprade authored Oct 28, 2022
1 parent e10859b commit 7a9d4ac
Show file tree
Hide file tree
Showing 13 changed files with 316 additions and 16 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export MAINNET_RPC_URL="https://your.rpc.url/here"
export OPTIMISM_RPC_URL="https://your.rpc.url/here"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
cache/
out/
.env
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
[submodule "lib/aave-v3-core"]
path = lib/aave-v3-core
url = https://github.com/aave/aave-v3-core
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ To learn more about Flexible Voting, and the use cases it enables, read the intr
This repo is built using [Foundry](https://github.com/foundry-rs/foundry)

1. [Install Foundry](https://github.com/foundry-rs/foundry#installation)
2. Install dependencies with `forge install`
3. Build the contracts with `forge build`
4. Run the test suite with `forge test`
1. Install dependencies with `forge install`
1. Build the contracts with `forge build`
1. `cp .env.example .env` and edit `.env` with your keys
1. Run the test suite with `forge test`

## Contribute

Expand Down
7 changes: 5 additions & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
[default]
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']

# See more config options https://github.com/gakonst/foundry/tree/master/config
[rpc_endpoints]
optimism = "${OPTIMISM_RPC_URL}"

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
1 change: 1 addition & 0 deletions lib/aave-v3-core
Submodule aave-v3-core added at f3e037
2 changes: 1 addition & 1 deletion lib/forge-std
289 changes: 289 additions & 0 deletions test/AaveAtokenFork.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
// SPDX-License-Identifier: Unlicensed
pragma solidity >=0.8.10;

import { Test } from "forge-std/Test.sol";
import { Vm } from "forge-std/Vm.sol";
import { ERC20 } from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

import { IAToken } from "aave-v3-core/contracts/interfaces/IAToken.sol";
import { AToken } from "aave-v3-core/contracts/protocol/tokenization/AToken.sol";
import { IPool } from 'aave-v3-core/contracts/interfaces/IPool.sol';
import { ConfiguratorInputTypes } from 'aave-v3-core/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol';
import { PoolConfigurator } from 'aave-v3-core/contracts/protocol/pool/PoolConfigurator.sol';
import { DataTypes } from 'aave-v3-core/contracts/protocol/libraries/types/DataTypes.sol';
import { AaveOracle } from 'aave-v3-core/contracts/misc/AaveOracle.sol';

import { GovToken } from "./GovToken.sol";

import { Pool } from 'aave-v3-core/contracts/protocol/pool/Pool.sol';
import "forge-std/console2.sol";

contract AaveAtokenForkTest is Test {
uint256 forkId;

IAToken aToken;
GovToken govToken;
IPool pool;

address dai = 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1;
address weth = 0x4200000000000000000000000000000000000006;

function setUp() public {
// We need to use optimism for Aave V3 because it's not (yet?) on mainnet.
// https://docs.aave.com/developers/deployed-contracts/v3-mainnet
uint256 optimismForkBlock = 26332308; // The optimism block number at the time this test was written.
forkId = vm.createSelectFork(vm.rpcUrl("optimism"), optimismForkBlock);

// deploy the GOV token
govToken = new GovToken();
pool = IPool(0x794a61358D6845594F94dc1DB02A252b5b4814aD); // pool from https://dune.com/queries/1329814

// Uncomment this line to temporarily etch local code onto the fork address
// so that we can do things like add console.log statements during
// debugging:
// vm.etch(address(pool), address(new Pool(pool.ADDRESSES_PROVIDER())).code);

PoolConfigurator _poolConfigurator = PoolConfigurator(0x8145eddDf43f50276641b55bd3AD95944510021E); // pool.ADDRESSES_PROVIDER().getPoolConfigurator()

// deploy the aGOV token
AToken _aTokenImplementation = new AToken(pool);

// This is the stableDebtToken implementation that all of the Optimism
// aTokens use. You can see this here: https://dune.com/queries/1332820.
// Each token uses a different stableDebtToken, but those are just proxies.
// They each proxy to this address for their implementation. We will do the
// same.
address _stableDebtTokenImpl = 0x52A1CeB68Ee6b7B5D13E0376A1E0E4423A8cE26e;
string memory _stableDebtTokenName = "Aave Optimism Stable Debt GOV";
string memory _stableDebtTokenSymbol = "stableDebtOptGOV";

// This is the variableDebtToken implementation that all of the Optimism
// aTokens use. You can see this here: https://dune.com/queries/1332820.
// Each token uses a different variableDebtToken, but those are just proxies.
// They each proxy to this address for their implementation. We will do the
// same.
address _variableDebtTokenImpl = 0x81387c40EB75acB02757C1Ae55D5936E78c9dEd3;
string memory _variableDebtTokenName = "Aave Optimism Variable Debt GOV";
string memory _variableDebtTokenSymbol = "variableDebtOptGOV";

ConfiguratorInputTypes.InitReserveInput[] memory _initReservesInput = new ConfiguratorInputTypes.InitReserveInput[](1);
_initReservesInput[0] = ConfiguratorInputTypes.InitReserveInput(
address(_aTokenImplementation), // aTokenImpl
_stableDebtTokenImpl, // stableDebtTokenImpl
_variableDebtTokenImpl, // variableDebtTokenImpl
govToken.decimals(), // underlyingAssetDecimals
0x4aa694e6c06D6162d95BE98a2Df6a521d5A7b521, // interestRateStrategyAddress, taken from https://dune.com/queries/1332820
address(govToken), // underlyingAsset
// treasury + incentives data from https://dune.com/queries/1329814
0xB2289E329D2F85F1eD31Adbb30eA345278F21bcf, // treasury
0x0aadeE9418641b5749e872eDEF9844200143865D, // incentivesController
"Aave V3 Optimism GOV", // aTokenName
"aOptGOV", // aTokenSymbol
_variableDebtTokenName,
_variableDebtTokenSymbol,
_stableDebtTokenName,
_stableDebtTokenSymbol,
bytes("10") // chainID??
);

// Add our AToken to Aave.
address _aaveAdmin = 0xE50c8C619d05ff98b22Adf991F17602C774F785c;
vm.prank(_aaveAdmin);
vm.recordLogs();
_poolConfigurator.initReserves(_initReservesInput);

// Retrieve the address of the aToken contract just deployed.
Vm.Log[] memory _emittedEvents = vm.getRecordedLogs();
Vm.Log memory _event;
bytes32 _eventSig = keccak256("ReserveInitialized(address,address,address,address,address)");
for (uint256 _i; _i < _emittedEvents.length; _i++) {
_event = _emittedEvents[_i];
if (_event.topics[0] == _eventSig) {
// event ReserveInitialized(
// address indexed asset,
// address indexed aToken, <-- The topic we want.
// address stableDebtToken,
// address variableDebtToken,
// address interestRateStrategyAddress
// );
aToken = AToken(address(uint160(uint256(_event.topics[2]))));
}
}

// Configure GOV to serve as collateral.
//
// We are copying the DAI configs here. Configs were obtained by inserting
// console.log statements and printing out
// _reserves[daiAddr].configuration.getParams() from within
// GenericLogic.calculateUserAccountData.
// tok ltv liqThr liqBon
// ------------------------
// DAI 7500 8000 10500
// wETH 8000 8250 10500
// wBTC 7000 7500 11000
// USDC 8000 8500 10500
vm.prank(_aaveAdmin);
_poolConfigurator.configureReserveAsCollateral(
address(govToken), // underlyingAsset
7500, // ltv, i.e. loan-to-value
8000, // liquidationThreshold, i.e. threshold at which positions will be liquidated
10500 // liquidationBonus
);

// Configure GOV to be borrowed.
vm.prank(_aaveAdmin);
_poolConfigurator.setReserveBorrowing(address(govToken), true);

// Allow GOV to be borrowed with stablecoins as collateral.
vm.prank(_aaveAdmin);
_poolConfigurator.setReserveStableRateBorrowing(address(govToken), true);

// Sometimes Aave uses oracles to get price information, e.g. when
// determining the value of collateral relative to loan value. Since GOV
// isn't a real thing and doesn't have a real price, we need to mock these
// calls. When borrowing, the oracle interaction happens in
// GenericLogic.calculateUserAccountData L130
address _priceOracle = pool.ADDRESSES_PROVIDER().getPriceOracle();
vm.mockCall(
_priceOracle,
abi.encodeWithSelector(
AaveOracle.getAssetPrice.selector,
address(govToken)
),
// Aave only seems to use USD-based oracles, so we will do the same.
abi.encode(1e8) // 1 GOV == $1 USD
);

}

function testFork_SetupCanSupplyGovToAave() public {
assertEq(ERC20(address(aToken)).symbol(), "aOptGOV");
assertEq(ERC20(address(aToken)).name(), "Aave V3 Optimism GOV");

// Confirm that the atoken._underlyingAsset == govToken
//
// $ forge inspect lib/aave-v3-core/contracts/protocol/tokenization/AToken.sol:AToken storage
// ...
// "label": "_underlyingAsset",
// "offset": 0,
// "slot": "61",
// "type": "t_address"
assertEq(
address(uint160(uint256(
vm.load(address(aToken), bytes32(uint256(61)))
))),
address(govToken)
);

// Mint GOV and deposit into aave.
// Confirm that we can supply GOV to the aToken.
assertEq(aToken.balanceOf(address(this)), 0);
govToken.exposed_mint(address(this), 42 ether);
govToken.approve(address(pool), type(uint256).max);
pool.supply(
address(govToken),
2 ether,
address(this),
0 // referral code
);
assertEq(govToken.balanceOf(address(this)), 40 ether);
assertEq(aToken.balanceOf(address(this)), 2 ether);

// We can withdraw our GOV when we want to.
pool.withdraw(
address(govToken),
2 ether,
address(this)
);
assertEq(govToken.balanceOf(address(this)), 42 ether);
assertEq(aToken.balanceOf(address(this)), 0 ether);
}

function testFork_SetupCanBorrowAgainstGovCollateral() public {
// supply GOV
govToken.exposed_mint(address(this), 42 ether);
govToken.approve(address(pool), type(uint256).max);
pool.supply(
address(govToken),
2 ether,
address(this),
0 // referral code
);
assertEq(govToken.balanceOf(address(this)), 40 ether);
assertEq(aToken.balanceOf(address(this)), 2 ether);

assertEq(ERC20(dai).balanceOf(address(this)), 0);

// borrow DAI against GOV
pool.borrow(
dai,
42, // amount of DAI to borrow
uint256(DataTypes.InterestRateMode.STABLE), // interestRateMode
0, // referralCode
address(this) // onBehalfOf
);

assertEq(ERC20(dai).balanceOf(address(this)), 42);
}

function testFork_SetupCanBorrowGovAndBeLiquidated() public {
// Someone else supplies GOV -- necessary so we can borrow it
address _bob = address(0xBEEF);
govToken.exposed_mint(_bob, 1100e18);
vm.startPrank(_bob);
govToken.approve(address(pool), type(uint256).max);
// Don't supply all of the GOV, some will be needed to liquidate.
pool.supply(address(govToken), 1000e18, _bob, 0);
vm.stopPrank();

// We suppy WETH.
deal(weth, address(this), 100 ether);
ERC20(weth).approve(address(pool), type(uint256).max);
pool.supply(weth, 100 ether, address(this), 0);
ERC20 _awethToken = ERC20(0xe50fA9b3c56FfB159cB0FCA61F5c9D750e8128c8);
uint256 _thisATokenBalance = _awethToken.balanceOf(address(this));
assertEq(_thisATokenBalance, 100 ether);

// Borrow GOV against WETH
uint256 _initGovBalance = govToken.balanceOf(address(this));
pool.borrow(
address(govToken),
42e18, // amount of GOV to borrow
uint256(DataTypes.InterestRateMode.STABLE), // interestRateMode
0, // referralCode
address(this) // onBehalfOf
);
uint256 _currentGovBalance = govToken.balanceOf(address(this));
assertEq(_initGovBalance, 0);
assertEq(_currentGovBalance, 42e18);

// Oh no, WETH goes to ~zero!
address _priceOracle = pool.ADDRESSES_PROVIDER().getPriceOracle();
vm.mockCall(
_priceOracle,
abi.encodeWithSelector(
AaveOracle.getAssetPrice.selector,
weth
),
abi.encode(1) // 1 bip
);

// Liquidate GOV position
uint256 _bobInitAtokenBalance = _awethToken.balanceOf(_bob);
vm.prank(_bob);
pool.liquidationCall(
weth, // collateralAsset
address(govToken), // borrow asset
address(this), // borrower
42e18, // amount borrowed
true // don't receive atoken, receive underlying
);
uint256 _bobCurrentAtokenBalance = _awethToken.balanceOf(_bob);
assertEq(_bobInitAtokenBalance, 0);
assertApproxEqRel(
_bobCurrentAtokenBalance,
_thisATokenBalance,
0.01e18
);
}
}
2 changes: 1 addition & 1 deletion test/FractionalGovernor.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
pragma solidity >=0.8.10;

import "../src/GovernorCountingFractional.sol";
import "openzeppelin-contracts/contracts/governance/extensions/GovernorVotes.sol";
Expand Down
2 changes: 1 addition & 1 deletion test/FractionalPool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ contract FractionalPoolTest is DSTestPlus {

function _mintGovAndApprovePool(address _holder, uint256 _amount) public {
vm.assume(_holder != address(0));
token.THIS_IS_JUST_A_TEST_HOOK_mint(_holder, _amount);
token.exposed_mint(_holder, _amount);
vm.prank(_holder);
token.approve(address(pool), type(uint256).max);
}
Expand Down
6 changes: 3 additions & 3 deletions test/GovToken.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
pragma solidity >=0.8.10;

import { ERC20Votes } from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Votes.sol";
import { ERC20Permit } from "openzeppelin-contracts/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
Expand All @@ -9,11 +9,11 @@ contract GovToken is ERC20Votes {

constructor() ERC20("Governance Token", "GOV") ERC20Permit("GOV") { }

function THIS_IS_JUST_A_TEST_HOOK_mint(address to, uint256 amount) public {
function exposed_mint(address to, uint256 amount) public {
_mint(to, amount);
}

function THIS_IS_JUST_A_TEST_HOOK_maxSupply() external view returns (uint256) {
function exposed_maxSupply() external view returns (uint256) {
return uint256(_maxSupply());
}
}
Loading

0 comments on commit 7a9d4ac

Please sign in to comment.