diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0e02704 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +export MAINNET_RPC_URL="https://your.rpc.url/here" +export OPTIMISM_RPC_URL="https://your.rpc.url/here" diff --git a/.gitignore b/.gitignore index d8a1d07..9f7c0ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ cache/ out/ +.env diff --git a/.gitmodules b/.gitmodules index 5c61095..fc501ed 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/README.md b/README.md index 0a39402..a9d0e31 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/foundry.toml b/foundry.toml index f9d3dfc..672e73c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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 \ No newline at end of file +[rpc_endpoints] +optimism = "${OPTIMISM_RPC_URL}" + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/lib/aave-v3-core b/lib/aave-v3-core new file mode 160000 index 0000000..f3e037b --- /dev/null +++ b/lib/aave-v3-core @@ -0,0 +1 @@ +Subproject commit f3e037b3638e3b7c98f0c09c56c5efde54f7c5d2 diff --git a/lib/forge-std b/lib/forge-std index ced2ef1..cb69e9c 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit ced2ef17510576e3527e5e5dfe72b7b235fd6c00 +Subproject commit cb69e9c07fbd002819c8c6c8db3caeab76b90d6b diff --git a/test/AaveAtokenFork.t.sol b/test/AaveAtokenFork.t.sol new file mode 100644 index 0000000..c772940 --- /dev/null +++ b/test/AaveAtokenFork.t.sol @@ -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 + ); + } +} diff --git a/test/FractionalGovernor.sol b/test/FractionalGovernor.sol index 1199e1b..0cf047a 100644 --- a/test/FractionalGovernor.sol +++ b/test/FractionalGovernor.sol @@ -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"; diff --git a/test/FractionalPool.t.sol b/test/FractionalPool.t.sol index 31efb1d..3b2295b 100644 --- a/test/FractionalPool.t.sol +++ b/test/FractionalPool.t.sol @@ -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); } diff --git a/test/GovToken.sol b/test/GovToken.sol index b07c812..6eb5b59 100644 --- a/test/GovToken.sol +++ b/test/GovToken.sol @@ -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"; @@ -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()); } } diff --git a/test/GovernorCountingFractional.t.sol b/test/GovernorCountingFractional.t.sol index 0d9c7c7..8557dd7 100644 --- a/test/GovernorCountingFractional.t.sol +++ b/test/GovernorCountingFractional.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity >=0.8.10; import { DSTestPlus } from "solmate/test/utils/DSTestPlus.sol"; import { Vm } from "forge-std/Vm.sol"; @@ -198,7 +198,7 @@ contract GovernorCountingFractionalTest is DSTestPlus { function _mintAndDelegateToVoter(Voter memory voter) internal { // Mint tokens for the user. - token.THIS_IS_JUST_A_TEST_HOOK_mint(voter.addr, voter.weight); + token.exposed_mint(voter.addr, voter.weight); // Self-delegate the tokens. vm.prank(voter.addr); @@ -417,7 +417,7 @@ contract GovernorCountingFractionalTest is DSTestPlus { voter.addr = _randomAddress(_weight); // The weight cannot overflow the max supply for the token, but must overflow the // max for the GovernorFractional contract. - voter.weight = bound(_weight, MAX_VOTE_WEIGHT, token.THIS_IS_JUST_A_TEST_HOOK_maxSupply()); + voter.weight = bound(_weight, MAX_VOTE_WEIGHT, token.exposed_maxSupply()); voter.support = _randomSupportType(_weight); _mintAndDelegateToVoter(voter); @@ -437,7 +437,7 @@ contract GovernorCountingFractionalTest is DSTestPlus { voter.addr = _randomAddress(_weight); // The weight cannot overflow the max supply for the token, but must overflow the // max for the GovernorFractional contract. - voter.weight = bound(_weight, MAX_VOTE_WEIGHT, token.THIS_IS_JUST_A_TEST_HOOK_maxSupply()); + voter.weight = bound(_weight, MAX_VOTE_WEIGHT, token.exposed_maxSupply()); _mintAndDelegateToVoter(voter); uint256 _proposalId = _createAndSubmitProposal(); diff --git a/test/ProposalReceiverMock.sol b/test/ProposalReceiverMock.sol index cf651b0..4dc751f 100644 --- a/test/ProposalReceiverMock.sol +++ b/test/ProposalReceiverMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity >=0.8.10; contract ProposalReceiverMock { event MockFunctionCalled();