From 2965748bd073a2b0d07f668e7438f5bfa3e61ba6 Mon Sep 17 00:00:00 2001 From: David Laprade Date: Wed, 4 Jan 2023 14:48:09 -0500 Subject: [PATCH] Write a flex voting Atoken using scaled balance caching (#21) * Copy ATokenNaive into ATokenReserveCache * Write an initial test for checkpointing rebased balances * Write some failing tests * Make stored balance checkpointing test pass * Handle raw balance checkpointing during withdrawals * Compute vote weights based on scaled balances Why this works: https://github.com/ScopeLift/flexible-voting/issues/8#issuecomment-1349793292 * Test that voting weight transfers * Checkpoint on transfer * Test that votes can be cast by recipients of aToken transfers * Rename ATokenReserveCache --> ATokenCheckpointed * Test getPastTotalDepsits * Add another getPastTotalDeposits test * Add more getPastTotalDeposits tests * scopelint fmt * Appease scopelint * Apply suggestions from code review Co-authored-by: Ed Mazurek Co-authored-by: Matt Solomon * Update based on PR review * Update based on PR review * Update based on PR review * Test that trasferFrom transfers voting weight * Remove unnecessary vm.roll in tests * Add more missing natspec * Override aToken._transfer not aToken.transfer * Clean up compiler warnings * Remove one more unnecessary vm.roll * Override MintableIncentivizedERC20._burn not AToken.burn * Delegate during initialization * Replace calls to super with explicit contracts * Override _mint not mint + mintToTreasury This is so much simpler/cleaner * Update self-delegation comment now that `initialize` has been overridden * Appease scopelint * Bump aave v3 to make AToken functions overrideable * Remove unnecessary return values from _checkpointRawBalanceOf * Add handleRepayment/2 to AToken mock for fork test compatibility * Remove naive implementation * Rename ATokenCheckpointed --> ATokenFlexVoting * Test checkpointing on mintToTreasury * Make tweaks for PR review * Remove unnecessary approximate assertions Co-authored-by: Ed Mazurek Co-authored-by: Matt Solomon --- lib/aave-v3-core | 2 +- src/{ATokenNaive.sol => ATokenFlexVoting.sol} | 197 ++-- src/FractionalPool.sol | 24 +- ...nFork.t.sol => ATokenFlexVotingFork.t.sol} | 975 ++++++++++++++---- test/FractionalPool.t.sol | 9 +- test/GovernorCountingFractional.t.sol | 11 +- test/MockATokenFlexVoting.sol | 42 + 7 files changed, 936 insertions(+), 324 deletions(-) rename src/{ATokenNaive.sol => ATokenFlexVoting.sol} (60%) rename test/{AaveAtokenFork.t.sol => ATokenFlexVotingFork.t.sol} (61%) create mode 100644 test/MockATokenFlexVoting.sol diff --git a/lib/aave-v3-core b/lib/aave-v3-core index 02fbbd4..c38c627 160000 --- a/lib/aave-v3-core +++ b/lib/aave-v3-core @@ -1 +1 @@ -Subproject commit 02fbbd4abdd8290d9f7f686d072774713b0ca077 +Subproject commit c38c627683c0db0449b7c9ea2fbd68bde3f8dc01 diff --git a/src/ATokenNaive.sol b/src/ATokenFlexVoting.sol similarity index 60% rename from src/ATokenNaive.sol rename to src/ATokenFlexVoting.sol index 06af3bb..8b083ca 100644 --- a/src/ATokenNaive.sol +++ b/src/ATokenFlexVoting.sol @@ -1,16 +1,20 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.10; +// forgefmt: disable-start import {AToken} from "aave-v3-core/contracts/protocol/tokenization/AToken.sol"; +import {MintableIncentivizedERC20} from "aave-v3-core/contracts/protocol/tokenization/base/MintableIncentivizedERC20.sol"; import {Errors} from "aave-v3-core/contracts/protocol/libraries/helpers/Errors.sol"; import {GPv2SafeERC20} from "aave-v3-core/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol"; import {IAToken} from "aave-v3-core/contracts/interfaces/IAToken.sol"; +import {IAaveIncentivesController} from "aave-v3-core/contracts/interfaces/IAaveIncentivesController.sol"; import {IERC20} from "aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol"; import {IPool} from "aave-v3-core/contracts/interfaces/IPool.sol"; import {WadRayMath} from "aave-v3-core/contracts/protocol/libraries/math/WadRayMath.sol"; import {SafeCast} from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol"; import {Checkpoints} from "openzeppelin-contracts/contracts/utils/Checkpoints.sol"; +// forgefmt: disable-end interface IFractionalGovernor { function token() external returns (address); @@ -31,7 +35,7 @@ interface IVotingToken { function getPastVotes(address account, uint256 blockNumber) external view returns (uint256); } -contract ATokenNaive is AToken { +contract ATokenFlexVoting is AToken { using WadRayMath for uint256; using SafeCast for uint256; using GPv2SafeERC20 for IERC20; @@ -67,12 +71,11 @@ contract ATokenNaive is AToken { mapping(uint256 => ProposalVote) public proposalVotes; /// @notice The governor contract associated with this governance token. It - /// must be one that supports fractional voting, e.g. - /// GovernorCountingFractional. - IFractionalGovernor public immutable governor; + /// must be one that supports fractional voting, e.g. GovernorCountingFractional. + IFractionalGovernor public immutable GOVERNOR; - /// @notice Mapping from address to deposit checkpoint history. - mapping(address => Checkpoints.History) private depositCheckpoints; + /// @notice Mapping from address to stored (not rebased) balance checkpoint history. + mapping(address => Checkpoints.History) private balanceCheckpoints; /// @notice History of total underlying asset balance. Checkpoints.History private totalDepositCheckpoints; @@ -83,15 +86,19 @@ contract ATokenNaive is AToken { /// @param _castVoteWindow The number of blocks that users have to express /// their votes on a proposal before votes can be cast. constructor(IPool _pool, address _governor, uint32 _castVoteWindow) AToken(_pool) { - governor = IFractionalGovernor(_governor); + GOVERNOR = IFractionalGovernor(_governor); CAST_VOTE_WINDOW = _castVoteWindow; } - // TODO Is there a better way to do this? It cannot be done in the constructor - // because the AToken is just used a proxy -- it won't share an address with - // the implementation (i.e. this code). + // Self-delegation cannot be done in the constructor because the aToken is + // just a proxy -- it won't share an address with the implementation (i.e. + // this code). Instead we do it at the end of `initialize`. But even that won't + // handle already-initialized aTokens. For those, we'll need to self-delegate + // during the upgrade process. More details in these issues: + // https://github.com/aave/aave-v3-core/pull/774 + // https://github.com/ScopeLift/flexible-voting/issues/16 function selfDelegate() public { - IVotingToken(governor.token()).delegate(address(this)); + IVotingToken(GOVERNOR.token()).delegate(address(this)); } /// @notice Method which returns the deadline (as a block number) by which @@ -106,7 +113,7 @@ contract ATokenNaive is AToken { view returns (uint256 _lastVotingBlock) { - _lastVotingBlock = governor.proposalDeadline(proposalId) - CAST_VOTE_WINDOW; + _lastVotingBlock = GOVERNOR.proposalDeadline(proposalId) - CAST_VOTE_WINDOW; } /// @notice Allow a depositor to express their voting preference for a given @@ -117,7 +124,7 @@ contract ATokenNaive is AToken { /// @param support The depositor's vote preferences in accordance with the `VoteType` enum. function expressVote(uint256 proposalId, uint8 support) external { require(!hasCastVotesOnProposal[proposalId], "too late to express, votes already cast"); - uint256 weight = getPastDeposits(msg.sender, governor.proposalSnapshot(proposalId)); + uint256 weight = getPastStoredBalance(msg.sender, GOVERNOR.proposalSnapshot(proposalId)); require(weight > 0, "no weight"); require(!proposalVotersHasVoted[proposalId][msg.sender], "already voted"); @@ -153,31 +160,33 @@ contract ATokenNaive is AToken { "no votes expressed" ); - uint256 _proposalSnapshotBlockNumber = governor.proposalSnapshot(proposalId); + uint256 _proposalSnapshotBlockNumber = GOVERNOR.proposalSnapshot(proposalId); - // Use the snapshot of total deposits to determine total voting weight. We cannot - // use the proposalVote numbers alone, since some people with deposits at the - // snapshot might not have expressed votes. - uint256 _totalDepositWeightAtSnapshot = getPastTotalDeposits(_proposalSnapshotBlockNumber); + // Use the snapshot of total raw balances to determine total voting weight. + // We cannot use the proposalVote numbers alone, since some people with + // balances at the snapshot might not have expressed votes. We don't want to + // make it possible for aToken holders to *increase* their voting power when + // other people don't express their votes. That'd be a terrible incentive. + uint256 _totalRawBalanceAtSnapshot = getPastTotalBalances(_proposalSnapshotBlockNumber); // We need 256 bits because of the multiplication we're about to do. uint256 _votingWeightAtSnapshot = IVotingToken(address(_underlyingAsset)).getPastVotes( address(this), _proposalSnapshotBlockNumber ); - // forVotesRaw forVotesScaled - // --------------------- = --------------------- - // totalDeposits deposits (@snapshot) + // forVotesRaw forVoteWeight + // --------------------- = ------------------ + // totalRawBalance totalVoteWeight // - // forVotesScaled = forVotesRaw * deposits@snapshot / totalDeposits + // forVoteWeight = forVotesRaw * totalVoteWeight / totalRawBalance uint128 _forVotesToCast = SafeCast.toUint128( - (_votingWeightAtSnapshot * _proposalVote.forVotes) / _totalDepositWeightAtSnapshot + (_votingWeightAtSnapshot * _proposalVote.forVotes) / _totalRawBalanceAtSnapshot ); uint128 _againstVotesToCast = SafeCast.toUint128( - (_votingWeightAtSnapshot * _proposalVote.againstVotes) / _totalDepositWeightAtSnapshot + (_votingWeightAtSnapshot * _proposalVote.againstVotes) / _totalRawBalanceAtSnapshot ); uint128 _abstainVotesToCast = SafeCast.toUint128( - (_votingWeightAtSnapshot * _proposalVote.abstainVotes) / _totalDepositWeightAtSnapshot + (_votingWeightAtSnapshot * _proposalVote.abstainVotes) / _totalRawBalanceAtSnapshot ); // This param is ignored by the governor when voting with fractional @@ -187,94 +196,102 @@ contract ATokenNaive is AToken { hasCastVotesOnProposal[proposalId] = true; bytes memory fractionalizedVotes = abi.encodePacked(_forVotesToCast, _againstVotesToCast, _abstainVotesToCast); - governor.castVoteWithReasonAndParams( - proposalId, unusedSupportParam, "crowd-sourced vote", fractionalizedVotes + GOVERNOR.castVoteWithReasonAndParams( + proposalId, + unusedSupportParam, + "rolled-up vote from aToken holders", // Reason string. + fractionalizedVotes ); } - /// @notice Implements the basic logic to mint a scaled balance token. - /// @param caller The address performing the mint - /// @param onBehalfOf The address of the user that will receive the scaled tokens - /// @param amount The amount of tokens getting minted - /// @param index The next liquidity index of the reserve - /// @return `true` if the the previous balance of the user was 0 - function _mintScaledWithCheckpoint( - address caller, - address onBehalfOf, - uint256 amount, - uint256 index - ) internal returns (bool) { - // We increment by `amount` instead of any computed/rebased amounts because - // `amount` is what actually gets transferred of the underlying asset. We - // need our checkpoints to still match up with underlying asset transactions. - Checkpoints.History storage _depositHistory = depositCheckpoints[onBehalfOf]; - _depositHistory.push(_depositHistory.latest() + amount); - totalDepositCheckpoints.push(totalDepositCheckpoints.latest() + amount); + /// @notice Returns the _user's current balance in storage. + function _rawBalanceOf(address _user) internal view returns (uint256) { + return _userState[_user].balance; + } - return _mintScaled(caller, onBehalfOf, amount, index); + /// @notice Checkpoints the _user's current raw balance. + function _checkpointRawBalanceOf(address _user) internal { + balanceCheckpoints[_user].push(_rawBalanceOf(_user)); } - function getPastDeposits(address _voter, uint256 _blockNumber) public returns (uint256) { - return depositCheckpoints[_voter].getAtBlock(_blockNumber); + /// @notice Returns the _user's balance in storage at the _blockNumber. + /// @param _user The account that's historical balance will be looked up. + /// @param _blockNumber The block at which to lookup the _user's balance. + function getPastStoredBalance(address _user, uint256 _blockNumber) public view returns (uint256) { + return balanceCheckpoints[_user].getAtProbablyRecentBlock(_blockNumber); } - function getPastTotalDeposits(uint256 _blockNumber) public returns (uint256) { - return totalDepositCheckpoints.getAtBlock(_blockNumber); + /// @notice Returns the total stored balance of all users at _blockNumber. + /// @param _blockNumber The block at which to lookup the total stored balance. + function getPastTotalBalances(uint256 _blockNumber) public view returns (uint256) { + return totalDepositCheckpoints.getAtProbablyRecentBlock(_blockNumber); } // forgefmt: disable-start //=========================================================================== // BEGIN: Aave overrides //=========================================================================== - /// Note: this has been modified from Aave v3's AToken to call our custom - /// mintScaledWithCheckpoint function. + /// Note: this has been modified from Aave v3's AToken to delegate voting + /// power to itself during initialization. /// - /// @inheritdoc IAToken - function mint( - address caller, - address onBehalfOf, - uint256 amount, - uint256 index - ) external virtual override onlyPool returns (bool) { - return _mintScaledWithCheckpoint(caller, onBehalfOf, amount, index); + /// @inheritdoc AToken + function initialize( + IPool initializingPool, + address treasury, + address underlyingAsset, + IAaveIncentivesController incentivesController, + uint8 aTokenDecimals, + string calldata aTokenName, + string calldata aTokenSymbol, + bytes calldata params + ) public override initializer { + AToken.initialize( + initializingPool, + treasury, + underlyingAsset, + incentivesController, + aTokenDecimals, + aTokenName, + aTokenSymbol, + params + ); + + selfDelegate(); } - /// Note: this has been modified from Aave v3's AToken to call our custom - /// mintScaledWithCheckpoint function. + /// Note: this has been modified from Aave v3's MintableIncentivizedERC20 to + /// checkpoint raw balances accordingly. /// - /// @inheritdoc IAToken - function mintToTreasury(uint256 amount, uint256 index) external override onlyPool { - if (amount == 0) { - return; - } - _mintScaledWithCheckpoint(address(POOL), _treasury, amount, index); + /// @inheritdoc MintableIncentivizedERC20 + function _burn(address account, uint128 amount) internal override { + MintableIncentivizedERC20._burn(account, amount); + _checkpointRawBalanceOf(account); + totalDepositCheckpoints.push(totalDepositCheckpoints.latest() - amount); } - /// Note: this has been modified from Aave v3's AToken to update deposit - /// balance accordingly. We cannot just call `super` here because the function - /// is external. + /// Note: this has been modified from Aave v3's MintableIncentivizedERC20 to + /// checkpoint raw balances accordingly. /// - /// @inheritdoc IAToken - function burn( + /// @inheritdoc MintableIncentivizedERC20 + function _mint(address account, uint128 amount) internal override { + MintableIncentivizedERC20._mint(account, amount); + _checkpointRawBalanceOf(account); + totalDepositCheckpoints.push(totalDepositCheckpoints.latest() + amount); + } + + /// Note: this has been modified from Aave v3's AToken contract to + /// checkpoint raw balances accordingly. + /// + /// @inheritdoc AToken + function _transfer( address from, - address receiverOfUnderlying, + address to, uint256 amount, - uint256 index - ) external virtual override onlyPool { - // Begin modifications. - // - // We decrement by `amount` instead of any computed/rebased amounts because - // `amount` is what actually gets transferred of the underlying asset. We - // need our checkpoints to still match up with underlying asset transactions. - Checkpoints.History storage _depositHistory = depositCheckpoints[from]; - _depositHistory.push(_depositHistory.latest() - amount); - totalDepositCheckpoints.push(totalDepositCheckpoints.latest() - amount); - - // End modifications. - _burnScaled(from, receiverOfUnderlying, amount, index); - if (receiverOfUnderlying != address(this)) { - IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount); - } + bool validate + ) internal virtual override { + AToken._transfer(from, to, amount, validate); + _checkpointRawBalanceOf(from); + _checkpointRawBalanceOf(to); } //=========================================================================== // END: Aave overrides diff --git a/src/FractionalPool.sol b/src/FractionalPool.sol index 82235d4..a317552 100644 --- a/src/FractionalPool.sol +++ b/src/FractionalPool.sol @@ -53,10 +53,10 @@ contract FractionalPool { uint32 public constant CAST_VOTE_WINDOW = 1200; // In blocks; 4 hours assuming 12 second blocks. /// @notice The governance token held and lent by this pool. - IVotingToken public immutable token; + IVotingToken public immutable TOKEN; /// @notice The governor contract associated with this governance token. - IFractionalGovernor public immutable governor; + IFractionalGovernor public immutable GOVERNOR; /// @notice Map depositor to deposit amount. mapping(address => uint256) public deposits; @@ -73,8 +73,8 @@ contract FractionalPool { /// @param _token The governance token held and lent by this pool. /// @param _governor The governor contract associated with this governance token. constructor(IVotingToken _token, IFractionalGovernor _governor) { - token = _token; - governor = _governor; + TOKEN = _token; + GOVERNOR = _governor; _token.delegate(address(this)); } @@ -86,7 +86,7 @@ contract FractionalPool { _writeCheckpoint(_checkpoints[msg.sender], _additionFn, _amount); _writeCheckpoint(_totalDepositCheckpoints, _additionFn, _amount); - token.transferFrom(msg.sender, address(this), _amount); // Assumes revert on failure. + TOKEN.transferFrom(msg.sender, address(this), _amount); // Assumes revert on failure. } /// @notice Allow a depositor to withdraw funds previously deposited to the pool. @@ -98,7 +98,7 @@ contract FractionalPool { _writeCheckpoint(_checkpoints[msg.sender], _subtractionFn, _amount); _writeCheckpoint(_totalDepositCheckpoints, _subtractionFn, _amount); - token.transfer(msg.sender, _amount); // Assumes revert on failure. + TOKEN.transfer(msg.sender, _amount); // Assumes revert on failure. } /// @notice Arbitrarily remove tokens from the pool. This is to simulate a borrower, hence the @@ -106,7 +106,7 @@ contract FractionalPool { /// @param _amount The amount to "borrow." function borrow(uint256 _amount) public { borrowTotal[msg.sender] += _amount; - token.transfer(msg.sender, _amount); + TOKEN.transfer(msg.sender, _amount); } /// @notice Allow a depositor to express their voting preference for a given proposal. Their @@ -114,7 +114,7 @@ contract FractionalPool { /// @param proposalId The proposalId in the associated Governor /// @param support The depositor's vote preferences in accordance with the `VoteType` enum. function expressVote(uint256 proposalId, uint8 support) external { - uint256 weight = getPastDeposits(msg.sender, governor.proposalSnapshot(proposalId)); + uint256 weight = getPastDeposits(msg.sender, GOVERNOR.proposalSnapshot(proposalId)); if (weight == 0) revert("no weight"); if (_proposalVotersHasVoted[proposalId][msg.sender]) revert("already voted"); @@ -141,7 +141,7 @@ contract FractionalPool { uint8 unusedSupportParam = uint8(VoteType.Abstain); ProposalVote memory _proposalVote = proposalVotes[proposalId]; - uint256 _proposalSnapshotBlockNumber = governor.proposalSnapshot(proposalId); + uint256 _proposalSnapshotBlockNumber = GOVERNOR.proposalSnapshot(proposalId); // Use the snapshot of total deposits to determine total voting weight. We cannot // use the proposalVote numbers alone, since some people with deposits at the @@ -150,7 +150,7 @@ contract FractionalPool { // We need 256 bits because of the multiplication we're about to do. uint256 _votingWeightAtSnapshot = - token.getPastVotes(address(this), _proposalSnapshotBlockNumber); + TOKEN.getPastVotes(address(this), _proposalSnapshotBlockNumber); // forVotesRaw forVotesScaled // --------------------- = --------------------- @@ -169,7 +169,7 @@ contract FractionalPool { bytes memory fractionalizedVotes = abi.encodePacked(_forVotesToCast, _againstVotesToCast, _abstainVotesToCast); - governor.castVoteWithReasonAndParams( + GOVERNOR.castVoteWithReasonAndParams( proposalId, unusedSupportParam, "crowd-sourced vote", fractionalizedVotes ); } @@ -182,7 +182,7 @@ contract FractionalPool { view returns (uint256 _lastVotingBlock) { - _lastVotingBlock = governor.proposalDeadline(proposalId) - CAST_VOTE_WINDOW; + _lastVotingBlock = GOVERNOR.proposalDeadline(proposalId) - CAST_VOTE_WINDOW; } /// =========================================================================== diff --git a/test/AaveAtokenFork.t.sol b/test/ATokenFlexVotingFork.t.sol similarity index 61% rename from test/AaveAtokenFork.t.sol rename to test/ATokenFlexVotingFork.t.sol index 908e75d..cf83ab4 100644 --- a/test/AaveAtokenFork.t.sol +++ b/test/ATokenFlexVotingFork.t.sol @@ -15,7 +15,7 @@ import { IAToken } from "aave-v3-core/contracts/interfaces/IAToken.sol"; import { IPool } from 'aave-v3-core/contracts/interfaces/IPool.sol'; import { PoolConfigurator } from 'aave-v3-core/contracts/protocol/pool/PoolConfigurator.sol'; -import { ATokenNaive } from "src/ATokenNaive.sol"; +import { MockATokenFlexVoting } from "test/MockATokenFlexVoting.sol"; import { FractionalGovernor } from "test/FractionalGovernor.sol"; import { ProposalReceiverMock } from "test/ProposalReceiverMock.sol"; import { GovToken } from "test/GovToken.sol"; @@ -25,11 +25,11 @@ import { GovToken } from "test/GovToken.sol"; // import { DefaultReserveInterestRateStrategy } from 'aave-v3-core/contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol'; // import { IPoolAddressesProvider } from 'aave-v3-core/contracts/interfaces/IPoolAddressesProvider.sol'; // forgefmt: disable-end - +// contract AaveAtokenForkTest is Test { uint256 forkId; - ATokenNaive aToken; + MockATokenFlexVoting aToken; GovToken govToken; FractionalGovernor governor; ProposalReceiverMock receiver; @@ -39,6 +39,9 @@ contract AaveAtokenForkTest is Test { address dai = 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1; address weth = 0x4200000000000000000000000000000000000006; + uint256 constant INITIAL_REBASING_DEPOSIT = 1000 ether; + address initialSupplier; + enum ProposalState { Pending, Active, @@ -63,6 +66,8 @@ contract AaveAtokenForkTest is Test { uint256 optimismForkBlock = 26_332_308; forkId = vm.createSelectFork(vm.rpcUrl("optimism"), optimismForkBlock); + initialSupplier = makeAddr("InitialSupplier"); + // Deploy the GOV token. govToken = new GovToken(); // Pool from https://dune.com/queries/1329814. @@ -89,7 +94,7 @@ contract AaveAtokenForkTest is Test { // 0x4aa694e6c06D6162d95BE98a2Df6a521d5A7b521, // interestRateStrategyAddress // address( // new DefaultReserveInterestRateStrategy( - // These values were taken from Optimism scan for the etched address. + // // These values were taken from Optimism scan for the etched address. // IPoolAddressesProvider(0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb), // provider // 800000000000000000000000000, // optimalUsageRatio // 0, // baseVariableBorrowRate @@ -109,7 +114,7 @@ contract AaveAtokenForkTest is Test { PoolConfigurator(0x8145eddDf43f50276641b55bd3AD95944510021E); // deploy the aGOV token - AToken _aTokenImplementation = new ATokenNaive(pool, address(governor), 1200); + AToken _aTokenImplementation = new MockATokenFlexVoting(pool, address(governor), 1200); // This is the stableDebtToken implementation that all of the Optimism // aTokens use. You can see this here: https://dune.com/queries/1332820. @@ -171,7 +176,7 @@ contract AaveAtokenForkTest is Test { // address variableDebtToken, // address interestRateStrategyAddress // ); - aToken = ATokenNaive(address(uint160(uint256(_event.topics[2])))); + aToken = MockATokenFlexVoting(address(uint160(uint256(_event.topics[2])))); vm.label(address(aToken), "aToken"); } } @@ -204,6 +209,10 @@ contract AaveAtokenForkTest is Test { vm.prank(_aaveAdmin); _poolConfigurator.setReserveStableRateBorrowing(address(govToken), true); + // Allow the Aave reserve to collect fees on our transactions. + vm.prank(_aaveAdmin); + _poolConfigurator.setReserveFactor(address(govToken), 1000); + // 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 @@ -216,10 +225,6 @@ contract AaveAtokenForkTest is Test { // Aave only seems to use USD-based oracles, so we will do the same. abi.encode(1e8) // 1 GOV == $1 USD ); - - // We need to call this selfDelegate function so that the aToken will give - // its voting power to itself. - aToken.selfDelegate(); } // ------------------ @@ -255,6 +260,36 @@ contract AaveAtokenForkTest is Test { vm.roll(governor.proposalSnapshot(proposalId) + 1); assertEq(uint256(governor.state(proposalId)), uint256(ProposalState.Active)); } + + function _initiateRebasing() internal { + uint256 _initLiquidityRate = pool.getReserveData(address(govToken)).currentLiquidityRate; + + // Have someone mint and deposit some gov. + _mintGovAndSupplyToAave(initialSupplier, INITIAL_REBASING_DEPOSIT); + + // Have someone else borrow some gov. + deal(weth, address(this), 100 ether); + ERC20(weth).approve(address(pool), type(uint256).max); + pool.supply(weth, 100 ether, address(this), 0); + pool.borrow( + address(govToken), + 42 ether, // amount of GOV to borrow + uint256(DataTypes.InterestRateMode.STABLE), // interestRateMode + 0, // referralCode + address(this) // onBehalfOf + ); + + // Advance the clock so that checkpoints become meaningful. + vm.roll(block.number + 42); + vm.warp(block.timestamp + 42 days); + + // We should be rebasing at this point. + assertGt( + pool.getReserveData(address(govToken)).currentLiquidityRate, + _initLiquidityRate, + "If the liquidity rate has not changed, rebasing isn't happening." + ); + } } contract Setup is AaveAtokenForkTest { @@ -330,7 +365,7 @@ contract Setup is AaveAtokenForkTest { function testFork_SetupCanBorrowGovAndBeLiquidated() public { // Someone else supplies GOV -- necessary so we can borrow it - address _bob = address(0xBEEF); + address _bob = makeAddr("testFork_SetupCanBorrowGovAndBeLiquidated address"); govToken.exposed_mint(_bob, 1100e18); vm.startPrank(_bob); govToken.approve(address(pool), type(uint256).max); @@ -383,168 +418,201 @@ contract Setup is AaveAtokenForkTest { } } -contract Supply is AaveAtokenForkTest { - function test_DepositsAreCheckpointed() public { - address _who = address(0xBEEF); - - // TODO randomize? - uint256 _amountA = 42 ether; - uint256 _amountB = 3 ether; - - // There are no initial deposits. - uint256[] memory _checkpoints = new uint256[](3); - _checkpoints[0] = block.number; - - // Advance the clock so that checkpoints become meaningful. - vm.roll(block.number + 42); - vm.warp(block.timestamp + 42 days); - _checkpoints[1] = block.number; - _mintGovAndSupplyToAave(_who, _amountA); - - // Advance the clock and supply again. - vm.roll(block.number + 42); - vm.warp(block.timestamp + 42 days); - _checkpoints[2] = block.number; - _mintGovAndSupplyToAave(_who, _amountB); - - // One more time, so that checkpoint 2 is in the past. - vm.roll(block.number + 1); - - // We can still retrieve the user's balance at the given blocks. - assertEq(aToken.getPastDeposits(_who, _checkpoints[0]), 0); - assertEq(aToken.getPastDeposits(_who, _checkpoints[1]), _amountA); - assertEq(aToken.getPastDeposits(_who, _checkpoints[2]), _amountA + _amountB); - // TODO why isn't this rebasing? - assertEq(aToken.balanceOf(_who), _amountA + _amountB); - } -} - -// TODO Why can't I just do `contract Vote is...` here? -contract VoteTest is AaveAtokenForkTest { +contract CastVote is AaveAtokenForkTest { function test_UserCanCastAgainstVotes() public { - _testUserCanCastVotes(address(0xC0FFEE), 4242 ether, uint8(VoteType.Against)); + _testUserCanCastVotes( + makeAddr("test_UserCanCastAgainstVotes address"), 4242 ether, uint8(VoteType.Against) + ); } function test_UserCanCastForVotes() public { - _testUserCanCastVotes(address(0xC0FFEE), 4242 ether, uint8(VoteType.For)); + _testUserCanCastVotes( + makeAddr("test_UserCanCastForVotes address"), 4242 ether, uint8(VoteType.For) + ); } function test_UserCanCastAbstainVotes() public { - _testUserCanCastVotes(address(0xC0FFEE), 4242 ether, uint8(VoteType.Abstain)); + _testUserCanCastVotes( + makeAddr("test_UserCanCastAbstainVotes address"), 4242 ether, uint8(VoteType.Abstain) + ); } function test_UserCannotExpressAgainstVotesWithoutWeight() public { - _testUserCannotExpressVotesWithoutATokens(address(0xBEEF), 0.42 ether, uint8(VoteType.Against)); + _testUserCannotExpressVotesWithoutATokens( + makeAddr("test_UserCannotExpressAgainstVotesWithoutWeight address"), + 0.42 ether, + uint8(VoteType.Against) + ); } function test_UserCannotExpressForVotesWithoutWeight() public { - _testUserCannotExpressVotesWithoutATokens(address(0xBEEF), 0.42 ether, uint8(VoteType.For)); + _testUserCannotExpressVotesWithoutATokens( + makeAddr("test_UserCannotExpressForVotesWithoutWeight address"), + 0.42 ether, + uint8(VoteType.For) + ); } function test_UserCannotExpressAbstainVotesWithoutWeight() public { - _testUserCannotExpressVotesWithoutATokens(address(0xBEEF), 0.42 ether, uint8(VoteType.Abstain)); + _testUserCannotExpressVotesWithoutATokens( + makeAddr("test_UserCannotExpressAbstainVotesWithoutWeight address"), + 0.42 ether, + uint8(VoteType.Abstain) + ); } function test_UserCannotCastAfterVotingPeriodAgainst() public { - _testUserCannotCastAfterVotingPeriod(address(0xBABE), 4.2 ether, uint8(VoteType.Against)); + _testUserCannotCastAfterVotingPeriod( + makeAddr("test_UserCannotCastAfterVotingPeriodAbstain address"), + 4.2 ether, + uint8(VoteType.Against) + ); } function test_UserCannotCastAfterVotingPeriodFor() public { - _testUserCannotCastAfterVotingPeriod(address(0xBABE), 4.2 ether, uint8(VoteType.For)); + _testUserCannotCastAfterVotingPeriod( + makeAddr("test_UserCannotCastAfterVotingPeriodAbstain address"), + 4.2 ether, + uint8(VoteType.For) + ); } function test_UserCannotCastAfterVotingPeriodAbstain() public { - _testUserCannotCastAfterVotingPeriod(address(0xBABE), 4.2 ether, uint8(VoteType.Abstain)); + _testUserCannotCastAfterVotingPeriod( + makeAddr("test_UserCannotCastAfterVotingPeriodAbstain address"), + 4.2 ether, + uint8(VoteType.Abstain) + ); } function test_UserCannotDoubleVoteAfterVotingAgainst() public { - _tesNoDoubleVoting(address(0xBA5EBA11), 0.042 ether, uint8(VoteType.Against)); + _tesNoDoubleVoting( + makeAddr("test_UserCannotDoubleVoteAfterVoting address"), 0.042 ether, uint8(VoteType.Against) + ); } function test_UserCannotDoubleVoteAfterVotingFor() public { - _tesNoDoubleVoting(address(0xBA5EBA11), 0.042 ether, uint8(VoteType.For)); + _tesNoDoubleVoting( + makeAddr("test_UserCannotDoubleVoteAfterVoting address"), 0.042 ether, uint8(VoteType.For) + ); } function test_UserCannotDoubleVoteAfterVotingAbstain() public { - _tesNoDoubleVoting(address(0xBA5EBA11), 0.042 ether, uint8(VoteType.Abstain)); + _tesNoDoubleVoting( + makeAddr("test_UserCannotDoubleVoteAfterVoting address"), 0.042 ether, uint8(VoteType.Abstain) + ); } function test_UserCannotCastVotesTwiceAfterVotingAgainst() public { - _testUserCannotCastVotesTwice(address(0x0DD), 1.42 ether, uint8(VoteType.Against)); + _testUserCannotCastVotesTwice( + makeAddr("test_UserCannotCastVotesTwiceAfterVoting address"), + 1.42 ether, + uint8(VoteType.Against) + ); } function test_UserCannotCastVotesTwiceAfterVotingFor() public { - _testUserCannotCastVotesTwice(address(0x0DD), 1.42 ether, uint8(VoteType.For)); + _testUserCannotCastVotesTwice( + makeAddr("test_UserCannotCastVotesTwiceAfterVoting address"), 1.42 ether, uint8(VoteType.For) + ); } function test_UserCannotCastVotesTwiceAfterVotingAbstain() public { - _testUserCannotCastVotesTwice(address(0x0DD), 1.42 ether, uint8(VoteType.Abstain)); + _testUserCannotCastVotesTwice( + makeAddr("test_UserCannotCastVotesTwiceAfterVoting address"), + 1.42 ether, + uint8(VoteType.Abstain) + ); } function test_UserCannotExpressAgainstVotesPriorToDepositing() public { _testUserCannotExpressVotesPriorToDepositing( - address(0xC0DE), 4.242 ether, uint8(VoteType.Against) + makeAddr("UserCannotExpressVotesPriorToDepositing address"), + 4.242 ether, + uint8(VoteType.Against) ); } function test_UserCannotExpressForVotesPriorToDepositing() public { - _testUserCannotExpressVotesPriorToDepositing(address(0xC0DE), 4.242 ether, uint8(VoteType.For)); + _testUserCannotExpressVotesPriorToDepositing( + makeAddr("UserCannotExpressVotesPriorToDepositing address"), 4.242 ether, uint8(VoteType.For) + ); } function test_UserCannotExpressAbstainVotesPriorToDepositing() public { _testUserCannotExpressVotesPriorToDepositing( - address(0xC0DE), 4.242 ether, uint8(VoteType.Abstain) + makeAddr("UserCannotExpressVotesPriorToDepositing address"), + 4.242 ether, + uint8(VoteType.Abstain) ); } function test_UserAgainstVotingWeightIsSnapshotDependent() public { _testUserVotingWeightIsSnapshotDependent( - address(0xDAD), 0.00042 ether, 0.042 ether, uint8(VoteType.Against) + makeAddr("UserVotingWeightIsSnapshotDependent address"), + 0.00042 ether, + 0.042 ether, + uint8(VoteType.Against) ); } function test_UserForVotingWeightIsSnapshotDependent() public { _testUserVotingWeightIsSnapshotDependent( - address(0xDAD), 0.00042 ether, 0.042 ether, uint8(VoteType.For) + makeAddr("UserVotingWeightIsSnapshotDependent address"), + 0.00042 ether, + 0.042 ether, + uint8(VoteType.For) ); } function test_UserAbstainVotingWeightIsSnapshotDependent() public { _testUserVotingWeightIsSnapshotDependent( - address(0xDAD), 0.00042 ether, 0.042 ether, uint8(VoteType.Abstain) + makeAddr("UserVotingWeightIsSnapshotDependent address"), + 0.00042 ether, + 0.042 ether, + uint8(VoteType.Abstain) ); } function test_MultipleUsersCanCastVotes() public { _testMultipleUsersCanCastVotes( - address(0xD00D), address(0xF00D), 0.42424242 ether, 0.00000042 ether + makeAddr("MultipleUsersCanCastVotes address 1"), + makeAddr("MultipleUsersCanCastVotes address 2"), + 0.42424242 ether, + 0.00000042 ether ); } function test_UserCannotMakeThePoolCastVotesImmediatelyAfterVotingAgainst() public { _testUserCannotMakeThePoolCastVotesImmediatelyAfterVoting( - address(0xDEAF), 0.000001 ether, uint8(VoteType.Against) + makeAddr("UserCannotMakeThePoolCastVotesImmediatelyAfterVoting address"), + 0.000001 ether, + uint8(VoteType.Against) ); } function test_UserCannotMakeThePoolCastVotesImmediatelyAfterVotingFor() public { _testUserCannotMakeThePoolCastVotesImmediatelyAfterVoting( - address(0xDEAF), 0.000001 ether, uint8(VoteType.For) + makeAddr("UserCannotMakeThePoolCastVotesImmediatelyAfterVoting address"), + 0.000001 ether, + uint8(VoteType.For) ); } function test_UserCannotMakeThePoolCastVotesImmediatelyAfterVotingAbstain() public { _testUserCannotMakeThePoolCastVotesImmediatelyAfterVoting( - address(0xDEAF), 0.000001 ether, uint8(VoteType.Abstain) + makeAddr("UserCannotMakeThePoolCastVotesImmediatelyAfterVoting address"), + 0.000001 ether, + uint8(VoteType.Abstain) ); } function test_VoteWeightIsScaledBasedOnPoolBalanceAgainstFor() public { _testVoteWeightIsScaledBasedOnPoolBalance( VoteWeightIsScaledVars( - address(0xFADE), // voterA - address(0xDEED), // voterB - address(0xB0D), // borrower + makeAddr("VoteWeightIsScaledBasedOnPoolBalance voterA #1"), + makeAddr("VoteWeightIsScaledBasedOnPoolBalance voterB #1"), + makeAddr("VoteWeightIsScaledBasedOnPoolBalance borrower #1"), 12 ether, // voteWeightA 4 ether, // voteWeightB 7 ether, // borrowerAssets @@ -557,9 +625,9 @@ contract VoteTest is AaveAtokenForkTest { function test_VoteWeightIsScaledBasedOnPoolBalanceAgainstAbstain() public { _testVoteWeightIsScaledBasedOnPoolBalance( VoteWeightIsScaledVars( - address(0xFEED), // voterA - address(0xADE), // voterB - address(0xD0E), // borrower + makeAddr("VoteWeightIsScaledBasedOnPoolBalance voterA #2"), + makeAddr("VoteWeightIsScaledBasedOnPoolBalance voterB #2"), + makeAddr("VoteWeightIsScaledBasedOnPoolBalance borrower #2"), 2 ether, // voteWeightA 7 ether, // voteWeightB 4 ether, // borrowerAssets @@ -572,9 +640,9 @@ contract VoteTest is AaveAtokenForkTest { function test_VoteWeightIsScaledBasedOnPoolBalanceForAbstain() public { _testVoteWeightIsScaledBasedOnPoolBalance( VoteWeightIsScaledVars( - address(0xED), // voterA - address(0xABE), // voterB - address(0xBED), // borrower + makeAddr("VoteWeightIsScaledBasedOnPoolBalance voterA #3"), + makeAddr("VoteWeightIsScaledBasedOnPoolBalance voterB #3"), + makeAddr("VoteWeightIsScaledBasedOnPoolBalance borrower #3"), 1 ether, // voteWeightA 1 ether, // voteWeightB 1 ether, // borrowerAssets @@ -587,9 +655,9 @@ contract VoteTest is AaveAtokenForkTest { function test_AgainstVotingWeightIsAbandonedIfSomeoneDoesntExpress() public { _testVotingWeightIsAbandonedIfSomeoneDoesntExpress( VotingWeightIsAbandonedVars( - address(0x111), // voterA - address(0x222), // voterB - address(0x333), // borrower + makeAddr("VotingWeightIsAbandonedIfSomeoneDoesntExpress voterA #1"), + makeAddr("VotingWeightIsAbandonedIfSomeoneDoesntExpress voterB #1"), + makeAddr("VotingWeightIsAbandonedIfSomeoneDoesntExpress borrower #1"), 1 ether, // voteWeightA 1 ether, // voteWeightB 1 ether, // borrowerAssets @@ -601,9 +669,9 @@ contract VoteTest is AaveAtokenForkTest { function test_ForVotingWeightIsAbandonedIfSomeoneDoesntExpress() public { _testVotingWeightIsAbandonedIfSomeoneDoesntExpress( VotingWeightIsAbandonedVars( - address(0xAAA), // voterA - address(0xBBB), // voterB - address(0xCCC), // borrower + makeAddr("VotingWeightIsAbandonedIfSomeoneDoesntExpress voterA #2"), + makeAddr("VotingWeightIsAbandonedIfSomeoneDoesntExpress voterB #2"), + makeAddr("VotingWeightIsAbandonedIfSomeoneDoesntExpress borrower #2"), 42 ether, // voteWeightA 24 ether, // voteWeightB 11 ether, // borrowerAssets @@ -615,9 +683,9 @@ contract VoteTest is AaveAtokenForkTest { function test_AbstainVotingWeightIsAbandonedIfSomeoneDoesntExpress() public { _testVotingWeightIsAbandonedIfSomeoneDoesntExpress( VotingWeightIsAbandonedVars( - address(0x123), // voterA - address(0x456), // voterB - address(0x789), // borrower + makeAddr("VotingWeightIsAbandonedIfSomeoneDoesntExpress voterA #3"), + makeAddr("VotingWeightIsAbandonedIfSomeoneDoesntExpress voterB #3"), + makeAddr("VotingWeightIsAbandonedIfSomeoneDoesntExpress borrower #3"), 24 ether, // voteWeightA 42 ether, // voteWeightB 100 ether, // borrowerAssets @@ -628,8 +696,8 @@ contract VoteTest is AaveAtokenForkTest { function test_AgainstVotingWeightIsUnaffectedByDepositsAfterProposal() public { _testVotingWeightIsUnaffectedByDepositsAfterProposal( - address(0xAAAA), // voterA - address(0xBBBB), // voterB + makeAddr("VotingWeightIsUnaffectedByDepositsAfterProposal voterA #1"), + makeAddr("VotingWeightIsUnaffectedByDepositsAfterProposal voterB #1"), 1 ether, // voteWeightA 2 ether, // voteWeightB uint8(VoteType.Against) // supportTypeA @@ -638,8 +706,8 @@ contract VoteTest is AaveAtokenForkTest { function test_ForVotingWeightIsUnaffectedByDepositsAfterProposal() public { _testVotingWeightIsUnaffectedByDepositsAfterProposal( - address(0xCCCC), // voterA - address(0xDDDD), // voterB + makeAddr("VotingWeightIsUnaffectedByDepositsAfterProposal voterA #2"), + makeAddr("VotingWeightIsUnaffectedByDepositsAfterProposal voterB #2"), 0.42 ether, // voteWeightA 0.042 ether, // voteWeightB uint8(VoteType.For) // supportTypeA @@ -648,8 +716,8 @@ contract VoteTest is AaveAtokenForkTest { function test_AbstainVotingWeightIsUnaffectedByDepositsAfterProposal() public { _testVotingWeightIsUnaffectedByDepositsAfterProposal( - address(0xEEEE), // voterA - address(0xFFFF), // voterB + makeAddr("VotingWeightIsUnaffectedByDepositsAfterProposal voterA #3"), + makeAddr("VotingWeightIsUnaffectedByDepositsAfterProposal voterB #3"), 10 ether, // voteWeightA 20 ether, // voteWeightB uint8(VoteType.Abstain) // supportTypeA @@ -658,7 +726,7 @@ contract VoteTest is AaveAtokenForkTest { function test_AgainstVotingWeightDoesNotGoDownWhenUsersBorrow() public { _testVotingWeightDoesNotGoDownWhenUsersBorrow( - address(0xC0D), + makeAddr("VotingWeightDoesNotGoDownWhenUsersBorrow address 1"), 4.242 ether, // GOV deposit amount 1 ether, // DAI borrow amount uint8(VoteType.Against) // supportType @@ -667,7 +735,7 @@ contract VoteTest is AaveAtokenForkTest { function test_ForVotingWeightDoesNotGoDownWhenUsersBorrow() public { _testVotingWeightDoesNotGoDownWhenUsersBorrow( - address(0xD0C), + makeAddr("VotingWeightDoesNotGoDownWhenUsersBorrow address 2"), 424.2 ether, // GOV deposit amount 4 ether, // DAI borrow amount uint8(VoteType.For) // supportType @@ -676,7 +744,7 @@ contract VoteTest is AaveAtokenForkTest { function test_AbstainVotingWeightDoesNotGoDownWhenUsersBorrow() public { _testVotingWeightDoesNotGoDownWhenUsersBorrow( - address(0xCAD), + makeAddr("VotingWeightDoesNotGoDownWhenUsersBorrow address 3"), 0.4242 ether, // GOV deposit amount 0.0424 ether, // DAI borrow amount uint8(VoteType.Abstain) // supportType @@ -685,7 +753,7 @@ contract VoteTest is AaveAtokenForkTest { function test_AgainstVotingWeightGoesDownWhenUsersFullyWithdraw() public { _testVotingWeightGoesDownWhenUsersWithdraw( - address(0xC0D3), + makeAddr("VotingWeightGoesDownWhenUsersWithdraw address #1"), 42 ether, // supplyAmount type(uint256).max, // withdrawAmount uint8(VoteType.Against) // supportType @@ -694,7 +762,7 @@ contract VoteTest is AaveAtokenForkTest { function test_ForVotingWeightGoesDownWhenUsersFullyWithdraw() public { _testVotingWeightGoesDownWhenUsersWithdraw( - address(0xD0C3), + makeAddr("VotingWeightGoesDownWhenUsersWithdraw address #2"), 42 ether, // supplyAmount type(uint256).max, // withdrawAmount uint8(VoteType.For) // supportType @@ -703,7 +771,7 @@ contract VoteTest is AaveAtokenForkTest { function test_AbstainVotingWeightGoesDownWhenUsersFullyWithdraw() public { _testVotingWeightGoesDownWhenUsersWithdraw( - address(0xCAD3), + makeAddr("VotingWeightGoesDownWhenUsersWithdraw address #3"), 42 ether, // supplyAmount type(uint256).max, // withdrawAmount uint8(VoteType.Abstain) // supportType @@ -712,7 +780,7 @@ contract VoteTest is AaveAtokenForkTest { function test_AgainstVotingWeightGoesDownWhenUsersPartiallyWithdraw() public { _testVotingWeightGoesDownWhenUsersWithdraw( - address(0xC0D4), + makeAddr("VotingWeightGoesDownWhenUsersWithdraw address #4"), 42 ether, // supplyAmount 2 ether, // withdrawAmount uint8(VoteType.Against) // supportType @@ -721,7 +789,7 @@ contract VoteTest is AaveAtokenForkTest { function test_ForVotingWeightGoesDownWhenUsersPartiallyWithdraw() public { _testVotingWeightGoesDownWhenUsersWithdraw( - address(0xD0C4), + makeAddr("VotingWeightGoesDownWhenUsersWithdraw address #5"), 42 ether, // supplyAmount 3 ether, // withdrawAmount uint8(VoteType.For) // supportType @@ -730,7 +798,7 @@ contract VoteTest is AaveAtokenForkTest { function test_AbstainVotingWeightGoesDownWhenUsersPartiallyWithdraw() public { _testVotingWeightGoesDownWhenUsersWithdraw( - address(0xCAD4), + makeAddr("VotingWeightGoesDownWhenUsersWithdraw address #6"), 42 ether, // supplyAmount 10 ether, // withdrawAmount uint8(VoteType.Abstain) // supportType @@ -739,37 +807,122 @@ contract VoteTest is AaveAtokenForkTest { function test_CannotExpressAgainstVoteAfterVotesHaveBeenCast() public { _testCannotExpressVoteAfterVotesHaveBeenCast( - address(0xDAD4242), // userA - address(0xDAD1111), // userB + makeAddr("CannotExpressVoteAfterVotesHaveBeenCast userA #1"), + makeAddr("CannotExpressVoteAfterVotesHaveBeenCast userB #1"), uint8(VoteType.Against) // supportType ); } function test_CannotExpressForVoteAfterVotesHaveBeenCast() public { _testCannotExpressVoteAfterVotesHaveBeenCast( - address(0xDAD424242), // userA - address(0xDAD111111), // userB + makeAddr("CannotExpressVoteAfterVotesHaveBeenCast userA #2"), + makeAddr("CannotExpressVoteAfterVotesHaveBeenCast userB #2"), uint8(VoteType.For) // supportType ); } function test_CannotExpressAbstainVoteAfterVotesHaveBeenCast() public { _testCannotExpressVoteAfterVotesHaveBeenCast( - address(0xDAD42424242), // userA - address(0xDAD11111111), // userB + makeAddr("CannotExpressVoteAfterVotesHaveBeenCast userA #3"), + makeAddr("CannotExpressVoteAfterVotesHaveBeenCast userB #3"), uint8(VoteType.Abstain) // supportType ); } function test_CannotCastVoteWithoutVotesExpressed() public { _testCannotCastVoteWithoutVotesExpressed( - address(0xCA11), // who + makeAddr("CannotCastVoteWithoutVotesExpressed who"), uint8(VoteType.Abstain) // supportType ); } function test_VotingWeightWorksWithRebasing() public { - _testVotingWeightWorksWithRebasing(address(0xABE1), address(0xABE2), 424_242 ether); + _testVotingWeightWorksWithRebasing( + makeAddr("VotingWeightWorksWithRebasing userA"), + makeAddr("VotingWeightWorksWithRebasing userB"), + 424_242 ether + ); + } + + function test_CastForVoteWithFullyTransferredATokens() public { + _testCastVoteWithTransferredATokens( + makeAddr("CastVoteWithTransferredATokens userA #1"), + makeAddr("CastVoteWithTransferredATokens userB #1"), + 1 ether, // weight + 1 ether, // transferAmount + uint8(VoteType.For), // supportTypeA + uint8(VoteType.For) // supportTypeB + ); + } + + function test_CastAgainstVoteWithFullyTransferredATokens() public { + _testCastVoteWithTransferredATokens( + makeAddr("CastVoteWithTransferredATokens userA #2"), + makeAddr("CastVoteWithTransferredATokens userB #2"), + 42 ether, // weight + 42 ether, // transferAmount + uint8(VoteType.For), // supportTypeA + uint8(VoteType.Against) // supportTypeB + ); + } + + function test_CastAbstainVoteWithFullyTransferredATokens() public { + _testCastVoteWithTransferredATokens( + makeAddr("CastVoteWithTransferredATokens userA #3"), + makeAddr("CastVoteWithTransferredATokens userB #3"), + 0.42 ether, // weight + 0.42 ether, // transferAmount + uint8(VoteType.For), // supportTypeA + uint8(VoteType.Abstain) // supportTypeB + ); + } + + function test_CastSameVoteWithBarelyTransferredATokens() public { + _testCastVoteWithTransferredATokens( + makeAddr("CastVoteWithTransferredATokens userA #4"), + makeAddr("CastVoteWithTransferredATokens userB #4"), + // Transfer less than half. + 1 ether, // weight + 0.33 ether, // transferAmount + uint8(VoteType.For), // supportTypeA + uint8(VoteType.For) // supportTypeB + ); + } + + function test_CastDifferentVoteWithBarelyTransferredATokens() public { + _testCastVoteWithTransferredATokens( + makeAddr("CastVoteWithTransferredATokens userA #5"), + makeAddr("CastVoteWithTransferredATokens userB #5"), + // Transfer less than half. + 1 ether, // weight + 0.33 ether, // transferAmount + uint8(VoteType.Abstain), // supportTypeA + uint8(VoteType.Against) // supportTypeB + ); + } + + function test_CastSameVoteWithMostlyTransferredATokens() public { + _testCastVoteWithTransferredATokens( + makeAddr("CastVoteWithTransferredATokens userA #6"), + makeAddr("CastVoteWithTransferredATokens userB #6"), + // Transfer almost all of it. + 42 ether, // weight + 41 ether, // transferAmount + uint8(VoteType.For), // supportTypeA + uint8(VoteType.For) // supportTypeB + ); + } + + function test_CastDifferentVoteWithMostlyTransferredATokens() public { + _testCastVoteWithTransferredATokens( + makeAddr("CastVoteWithTransferredATokens userA #7"), + makeAddr("CastVoteWithTransferredATokens userB #7"), + // Transfer almost all of it. + 42 ether, // weight + 41 ether, // transferAmount + uint8(VoteType.Against), // supportTypeA + uint8(VoteType.For) // supportTypeB + ); } function _testUserCanCastVotes(address _who, uint256 _voteWeight, uint8 _supportType) private { @@ -778,9 +931,6 @@ contract VoteTest is AaveAtokenForkTest { assertEq(aToken.balanceOf(_who), _voteWeight, "aToken balance wrong"); assertEq(govToken.balanceOf(address(aToken)), _voteWeight, "govToken balance wrong"); - // Advance one block so that our votes will be checkpointed by the govToken; - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); assertEq( @@ -834,9 +984,6 @@ contract VoteTest is AaveAtokenForkTest { assertEq(govToken.balanceOf(_who), _voteWeight); - // Advance one block so that our votes will be checkpointed by the govToken; - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -854,9 +1001,6 @@ contract VoteTest is AaveAtokenForkTest { // Deposit some funds. _mintGovAndSupplyToAave(_who, _voteWeight); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -876,9 +1020,6 @@ contract VoteTest is AaveAtokenForkTest { // Deposit some funds. _mintGovAndSupplyToAave(_who, _voteWeight); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -898,9 +1039,6 @@ contract VoteTest is AaveAtokenForkTest { // Deposit some funds. _mintGovAndSupplyToAave(_who, _voteWeight); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -945,9 +1083,6 @@ contract VoteTest is AaveAtokenForkTest { // Deposit some funds. _mintGovAndSupplyToAave(_who, _voteWeightA); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -986,9 +1121,6 @@ contract VoteTest is AaveAtokenForkTest { _mintGovAndSupplyToAave(_userA, _voteWeightA); _mintGovAndSupplyToAave(_userB, _voteWeightB); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -1032,9 +1164,6 @@ contract VoteTest is AaveAtokenForkTest { // Deposit some funds. _mintGovAndSupplyToAave(_who, _voteWeight); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -1070,9 +1199,6 @@ contract VoteTest is AaveAtokenForkTest { _mintGovAndSupplyToAave(_vars.voterB, _vars.voteWeightB); uint256 _initGovBalance = govToken.balanceOf(address(aToken)); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Borrow GOV from the pool, decreasing its token balance. deal(weth, _vars.borrower, _vars.borrowerAssets); vm.startPrank(_vars.borrower); @@ -1089,9 +1215,6 @@ contract VoteTest is AaveAtokenForkTest { assertLt(govToken.balanceOf(address(aToken)), _initGovBalance); vm.stopPrank(); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -1120,38 +1243,24 @@ contract VoteTest is AaveAtokenForkTest { // These can differ because votes are rounded. assertApproxEqAbs(_againstVotes + _forVotes + _abstainVotes, _expectedVotingWeight, 1); + // forgefmt: disable-start if (_vars.supportTypeA == _vars.supportTypeB) { assertEq(_forVotes, _vars.supportTypeA == uint8(VoteType.For) ? _expectedVotingWeight : 0); - assertEq( - _againstVotes, _vars.supportTypeA == uint8(VoteType.Against) ? _expectedVotingWeight : 0 - ); - assertEq( - _abstainVotes, _vars.supportTypeA == uint8(VoteType.Abstain) ? _expectedVotingWeight : 0 - ); + assertEq(_againstVotes, _vars.supportTypeA == uint8(VoteType.Against) ? _expectedVotingWeight : 0); + assertEq(_abstainVotes, _vars.supportTypeA == uint8(VoteType.Abstain) ? _expectedVotingWeight : 0); } else { uint256 _expectedVotingWeightA = (_vars.voteWeightA * _expectedVotingWeight) / _initGovBalance; uint256 _expectedVotingWeightB = (_vars.voteWeightB * _expectedVotingWeight) / _initGovBalance; // We assert the weight is within a range of 1 because scaled weights are sometimes floored. - if (_vars.supportTypeA == uint8(VoteType.For)) { - assertApproxEqAbs(_forVotes, _expectedVotingWeightA, 1); - } - if (_vars.supportTypeB == uint8(VoteType.For)) { - assertApproxEqAbs(_forVotes, _expectedVotingWeightB, 1); - } - if (_vars.supportTypeA == uint8(VoteType.Against)) { - assertApproxEqAbs(_againstVotes, _expectedVotingWeightA, 1); - } - if (_vars.supportTypeB == uint8(VoteType.Against)) { - assertApproxEqAbs(_againstVotes, _expectedVotingWeightB, 1); - } - if (_vars.supportTypeA == uint8(VoteType.Abstain)) { - assertApproxEqAbs(_abstainVotes, _expectedVotingWeightA, 1); - } - if (_vars.supportTypeB == uint8(VoteType.Abstain)) { - assertApproxEqAbs(_abstainVotes, _expectedVotingWeightB, 1); - } + if (_vars.supportTypeA == uint8(VoteType.For)) assertApproxEqAbs(_forVotes, _expectedVotingWeightA, 1); + if (_vars.supportTypeB == uint8(VoteType.For)) assertApproxEqAbs(_forVotes, _expectedVotingWeightB, 1); + if (_vars.supportTypeA == uint8(VoteType.Against)) assertApproxEqAbs(_againstVotes, _expectedVotingWeightA, 1); + if (_vars.supportTypeB == uint8(VoteType.Against)) assertApproxEqAbs(_againstVotes, _expectedVotingWeightB, 1); + if (_vars.supportTypeA == uint8(VoteType.Abstain)) assertApproxEqAbs(_abstainVotes, _expectedVotingWeightA, 1); + if (_vars.supportTypeB == uint8(VoteType.Abstain)) assertApproxEqAbs(_abstainVotes, _expectedVotingWeightB, 1); } + // forgefmt: disable-end } struct VotingWeightIsAbandonedVars { @@ -1175,9 +1284,6 @@ contract VoteTest is AaveAtokenForkTest { _mintGovAndSupplyToAave(_vars.voterB, _vars.voteWeightB); uint256 _initGovBalance = govToken.balanceOf(address(aToken)); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Borrow GOV from the pool, decreasing its token balance. deal(weth, _vars.borrower, _vars.borrowerAssets); vm.startPrank(_vars.borrower); @@ -1194,9 +1300,6 @@ contract VoteTest is AaveAtokenForkTest { assertLt(govToken.balanceOf(address(aToken)), _initGovBalance); vm.stopPrank(); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -1244,16 +1347,12 @@ contract VoteTest is AaveAtokenForkTest { 1 ); + // forgefmt: disable-start // We assert the weight is within a range of 1 because scaled weights are sometimes floored. - if (_vars.supportTypeA == uint8(VoteType.For)) { - assertApproxEqAbs(_forVotes, _expectedVotingWeightA, 1); - } - if (_vars.supportTypeA == uint8(VoteType.Against)) { - assertApproxEqAbs(_againstVotes, _expectedVotingWeightA, 1); - } - if (_vars.supportTypeA == uint8(VoteType.Abstain)) { - assertApproxEqAbs(_abstainVotes, _expectedVotingWeightA, 1); - } + if (_vars.supportTypeA == uint8(VoteType.For)) assertApproxEqAbs(_forVotes, _expectedVotingWeightA, 1); + if (_vars.supportTypeA == uint8(VoteType.Against)) assertApproxEqAbs(_againstVotes, _expectedVotingWeightA, 1); + if (_vars.supportTypeA == uint8(VoteType.Abstain)) assertApproxEqAbs(_abstainVotes, _expectedVotingWeightA, 1); + // forgefmt: disable-end } function _testVotingWeightIsUnaffectedByDepositsAfterProposal( @@ -1270,9 +1369,6 @@ contract VoteTest is AaveAtokenForkTest { _mintGovAndSupplyToAave(_voterA, _voteWeightA); uint256 _initGovBalance = govToken.balanceOf(address(aToken)); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -1323,9 +1419,6 @@ contract VoteTest is AaveAtokenForkTest { _who // onBehalfOf ); - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -1370,9 +1463,6 @@ contract VoteTest is AaveAtokenForkTest { _mintGovAndSupplyToAave(address(this), _withdrawAmount); } - // Advance one block so that our votes will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -1400,22 +1490,12 @@ contract VoteTest is AaveAtokenForkTest { function _testVotingWeightWorksWithRebasing(address _userA, address _userB, uint256 _supplyAmount) private { + _initiateRebasing(); + // Someone supplies GOV to Aave. _mintGovAndSupplyToAave(_userA, _supplyAmount); uint256 _initATokenBalanceA = aToken.balanceOf(_userA); - // Borrow some Gov for aGOV to start rebasing. - deal(weth, address(this), _supplyAmount); - ERC20(weth).approve(address(pool), type(uint256).max); - pool.supply(weth, _supplyAmount, address(this), 0); - pool.borrow( - address(govToken), - _supplyAmount / 10, // amount of GOV to borrow - uint256(DataTypes.InterestRateMode.STABLE), // interestRateMode - 0, // referralCode - address(this) // onBehalfOf - ); - // Let those aGovTokens rebase \o/. vm.roll(block.number + 365 * 24 * 60 * 12); // 12 blocks per min for a year. vm.warp(block.timestamp + 365 days); @@ -1429,9 +1509,6 @@ contract VoteTest is AaveAtokenForkTest { "userA does not have more aTokens than userB" ); - // Advance one block so that weight will be checkpointed by the govToken. - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -1450,8 +1527,8 @@ contract VoteTest is AaveAtokenForkTest { (uint256 _againstVotes, uint256 _forVotes, /*uint256 _abstainVotes */ ) = governor.proposalVotes(_proposalId); - // userA's vote *should* have beaten userB's, but it won't. - assertEq(_forVotes, _againstVotes, "if this fails, you have fixed ATokenNaive!"); + // userA's vote *should* have beaten userB's. + assertGt(_forVotes, _againstVotes, "rebasing isn't reflected in vote weight"); } function _testCannotExpressVoteAfterVotesHaveBeenCast( @@ -1463,9 +1540,6 @@ contract VoteTest is AaveAtokenForkTest { _mintGovAndSupplyToAave(_userA, 1 ether); _mintGovAndSupplyToAave(_userB, 1 ether); - // Advance one block so that our votes will be checkpointed by the govToken; - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -1490,9 +1564,6 @@ contract VoteTest is AaveAtokenForkTest { // Deposit some funds. _mintGovAndSupplyToAave(_who, 1 ether); - // Advance one block so that our votes will be checkpointed by the govToken; - vm.roll(block.number + 1); - // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -1510,4 +1581,476 @@ contract VoteTest is AaveAtokenForkTest { // Now votes should be castable. aToken.castVote(_proposalId); } + + function _testCastVoteWithTransferredATokens( + address _userA, + address _userB, + uint256 _weight, + uint256 _transferAmount, + uint8 _supportTypeA, + uint8 _supportTypeB + ) private { + // Deposit some funds. + _mintGovAndSupplyToAave(_userA, _weight); + assertEq(aToken.balanceOf(_userA), _weight); + assertEq(aToken.balanceOf(_userB), 0); + + // Transfer all aTokens from userA to userB. + vm.prank(_userA); + aToken.transfer(_userB, _transferAmount); + assertEq(aToken.balanceOf(_userA), _weight - _transferAmount); + assertEq(aToken.balanceOf(_userB), _transferAmount); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // Express voting preferences. + if (aToken.balanceOf(_userA) == 0) vm.expectRevert(bytes("no weight")); + vm.prank(_userA); + aToken.expressVote(_proposalId, _supportTypeA); + vm.prank(_userB); + aToken.expressVote(_proposalId, _supportTypeB); + + // Wait until after the pool's voting period closes. + vm.roll(aToken.internalVotingPeriodEnd(_proposalId) + 1); + + // Submit votes on behalf of the pool. + aToken.castVote(_proposalId); + + (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = + governor.proposalVotes(_proposalId); + + if (_supportTypeA == _supportTypeB) { + if (_supportTypeA == uint8(VoteType.For)) assertEq(_forVotes, _weight); + if (_supportTypeA == uint8(VoteType.Against)) assertEq(_againstVotes, _weight); + if (_supportTypeA == uint8(VoteType.Abstain)) assertEq(_abstainVotes, _weight); + } else { + // forgefmt: disable-start + if (_supportTypeA == uint8(VoteType.For)) assertEq(_forVotes, _weight - _transferAmount); + if (_supportTypeA == uint8(VoteType.Against)) assertEq(_againstVotes, _weight - _transferAmount); + if (_supportTypeA == uint8(VoteType.Abstain)) assertEq(_abstainVotes, _weight - _transferAmount); + if (_supportTypeB == uint8(VoteType.For)) assertEq(_forVotes, _transferAmount); + if (_supportTypeB == uint8(VoteType.Against)) assertEq(_againstVotes, _transferAmount); + if (_supportTypeB == uint8(VoteType.Abstain)) assertEq(_abstainVotes, _transferAmount); + // forgefmt: disable-end + } + } +} + +contract GetPastStoredBalanceTest is AaveAtokenForkTest { + function test_GetPastStoredBalanceCorrectlyReadsCheckpoints() public { + _initiateRebasing(); + + address _who = makeAddr("GetPastStoredBalanceCorrectlyReadsCheckpoints _who"); + uint256 _amountA = 42 ether; + uint256 _amountB = 3 ether; + + uint256[] memory _rawBalances = new uint256[](3); + + // Deposit. + _mintGovAndSupplyToAave(_who, _amountA); + _rawBalances[0] = aToken.exposed_RawBalanceOf(_who); + + // It's important that this be greater than a ray, since Aave uses this + // index when determining the raw stored balance. If it were a ray, the + // stored balance would just equal the supplied amount and this test would + // be less meaningful. + assertGt( + pool.getReserveData(address(govToken)).liquidityIndex, + 1e27, + "liquidityIndex has not changed, is rebasing occuring?" + ); + + // The supplied amount should be less than the raw balance, which was + // scaled down by the reserve liquidity index. + assertLt(_rawBalances[0], _amountA, "supply wasn't reduced by liquidityIndex"); + + // Advance the clock. + uint256 _blocksJumped = 42; + vm.roll(block.number + _blocksJumped); + vm.warp(block.timestamp + 42 days); + + // getPastStoredBalance should match the initial raw balance. + assertEq( + aToken.getPastStoredBalance(_who, block.number - _blocksJumped + 1), + _rawBalances[0], + "getPastStoredBalance does not match the initial raw balance" + ); + + // getPastStoredBalance should be able to give us the raw balance at an + // intermediate point. + assertEq( + aToken.getPastStoredBalance( + _who, + block.number - (_blocksJumped / 3) // 1/3 is just an arbitrary point. + ), + _rawBalances[0] + ); + + // Deposit again to make things more complicated. + _mintGovAndSupplyToAave(_who, _amountB); + _rawBalances[1] = aToken.exposed_RawBalanceOf(_who); + + // Advance the clock. + uint256 _blocksJumpedSecondTime = 100; + vm.roll(block.number + _blocksJumpedSecondTime); + vm.warp(block.timestamp + 100 days); + + // Rebasing should not affect the raw balance. + assertGt(_rawBalances[1], _rawBalances[0], "raw balance did not increase"); + + // getPastStoredBalance should match historical balances. + assertEq( + aToken.getPastStoredBalance(_who, block.number - _blocksJumped - _blocksJumpedSecondTime + 1), + _rawBalances[0], + "getPastStoredBalance did not match original raw balance" + ); + assertEq( + aToken.getPastStoredBalance(_who, block.number - _blocksJumpedSecondTime + 1), + _rawBalances[1], + "getPastStoredBalance did not match raw balance after second supply" + ); + // getPastStoredBalance should be able to give us the raw balance at intermediate points. + assertEq( + aToken.getPastStoredBalance(_who, block.number - _blocksJumpedSecondTime / 3), // random point + _rawBalances[1] + ); + assertEq( + aToken.getPastStoredBalance(_who, block.number - _blocksJumpedSecondTime / 3), + aToken.getPastStoredBalance(_who, block.number - 1) + ); + + // Withdrawals should be reflected in getPastStoredBalance. + vm.startPrank(_who); + pool.withdraw( + address(govToken), + aToken.balanceOf(_who) / 3, // Withdraw 1/3rd of balance. + _who + ); + vm.stopPrank(); + + // Advance the clock + uint256 _blocksJumpedThirdTime = 10; + vm.roll(block.number + _blocksJumpedThirdTime); + vm.warp(block.timestamp + 10 days); + + assertEq( + aToken.getPastStoredBalance(_who, block.number - _blocksJumpedThirdTime), + aToken.exposed_RawBalanceOf(_who) + ); + assertEq(aToken.getPastStoredBalance(_who, block.number - 1), aToken.exposed_RawBalanceOf(_who)); + assertGt( + _rawBalances[1], // The raw balance pre-withdrawal. + aToken.getPastStoredBalance(_who, block.number - _blocksJumpedThirdTime) + ); + } + + function test_GetPastStoredBalanceHandlesTransfers() public { + _initiateRebasing(); + + address _userA = makeAddr("GetPastStoredBalanceHandlesTransfers _userA"); + address _userB = makeAddr("GetPastStoredBalanceHandlesTransfers _userB"); + uint256 _amount = 4242 ether; + + // Deposit. + _mintGovAndSupplyToAave(_userA, _amount); + uint256 _initRawBalanceUserA = aToken.exposed_RawBalanceOf(_userA); + + // Advance the clock so that we checkpoint and let some rebasing happen. + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100 days); + + // Get the rebased balances. + uint256 _initBalanceUserA = aToken.balanceOf(_userA); + uint256 _initBalanceUserB = aToken.balanceOf(_userB); + assertGt(_initBalanceUserA, 0); + assertEq(_initBalanceUserB, 0); + + // Transfer aTokens to userB. + vm.prank(_userA); + aToken.transfer(_userB, _initBalanceUserA / 3); + assertEq(aToken.balanceOf(_userA), 2 * _initBalanceUserA / 3); + assertEq(aToken.balanceOf(_userB), 1 * _initBalanceUserA / 3); + + // Advance the clock so that we checkpoint. + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1 days); + + // Confirm voting weight has shifted. + assertEq( + aToken.getPastStoredBalance(_userA, block.number - 1), + 2 * _initRawBalanceUserA / 3 // 2/3rds of A's initial balance + ); + assertEq( + aToken.getPastStoredBalance(_userB, block.number - 1), + 1 * _initRawBalanceUserA / 3 // 1/3rd of A's initial balance + ); + } + + function test_GetPastStoredBalanceHandlesTransferFrom() public { + _initiateRebasing(); + + address _userA = makeAddr("GetPastStoredBalanceHandlesTransfers _userA"); + address _userB = makeAddr("GetPastStoredBalanceHandlesTransfers _userB"); + uint256 _amount = 4242 ether; + + // Deposit. + _mintGovAndSupplyToAave(_userA, _amount); + uint256 _initRawBalanceUserA = aToken.exposed_RawBalanceOf(_userA); + + // Advance the clock so that we checkpoint and let some rebasing happen. + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100 days); + + // Get the rebased balances. + uint256 _initBalanceUserA = aToken.balanceOf(_userA); + uint256 _initBalanceUserB = aToken.balanceOf(_userB); + assertGt(_initBalanceUserA, 0); + assertEq(_initBalanceUserB, 0); + + // Transfer aTokens to userB. + vm.prank(_userA); + aToken.approve(address(this), type(uint256).max); + aToken.transferFrom(_userA, _userB, _initBalanceUserA / 3); + assertEq(aToken.balanceOf(_userA), 2 * _initBalanceUserA / 3); + assertEq(aToken.balanceOf(_userB), _initBalanceUserA / 3); + + // Advance the clock so that we checkpoint. + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1 days); + + // Confirm voting weight has shifted. + assertEq( + aToken.getPastStoredBalance(_userA, block.number - 1), + 2 * _initRawBalanceUserA / 3 // 2/3rds of A's initial balance + ); + assertEq( + aToken.getPastStoredBalance(_userB, block.number - 1), + 1 * _initRawBalanceUserA / 3 // 1/3rd of A's initial balance + ); + } + + function test_MintToTreasuryIsCheckpointed() public { + _initiateRebasing(); + + // Advance the clock so that the treasury earns some interest. + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100 days); + + address _treasury = aToken.exposed_Treasury(); + uint256 _initTreasuryBalance = aToken.balanceOf(_treasury); + + // Repay the borrow and give the treasury more interest. + ERC20(govToken).approve(address(pool), type(uint256).max); + // Give the user some more gov to pay the interest on the borrow. + govToken.exposed_mint(address(this), 10 ether); + pool.repay( + address(govToken), + type(uint256).max, // pay entire debt. + uint256(DataTypes.InterestRateMode.STABLE), // interestRateMode + address(this) + ); + + address[] memory _assetsForMintToTreasury = new address[](1); + _assetsForMintToTreasury[0] = address(govToken); + pool.mintToTreasury(_assetsForMintToTreasury); + + // Advance the block so that we can query checkpoints. + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1 days); + + assertGt(aToken.balanceOf(_treasury), _initTreasuryBalance); + + assertGt(aToken.getPastStoredBalance(_treasury, block.number - 1), 0); + } +} + +contract GetPastTotalBalancesTest is AaveAtokenForkTest { + function test_GetPastTotalBalancesIncreasesOnDeposit() public { + _initiateRebasing(); + assertEq(aToken.getPastTotalBalances(block.number - 1), INITIAL_REBASING_DEPOSIT); + + address _userA = makeAddr("GetPastTotalBalancesIncreasesOnDeposit _userA"); + address _userB = makeAddr("GetPastTotalBalancesIncreasesOnDeposit _userB"); + uint256 _amountA = 4242 ether; + uint256 _amountB = 123 ether; + + // Deposit. + _mintGovAndSupplyToAave(_userA, _amountA); + uint256 _rawBalanceA = aToken.exposed_RawBalanceOf(_userA); + + // Advance the clock so that we checkpoint and let some rebasing happen. + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100 days); + + // forgefmt: disable-start + assertEq(aToken.getPastTotalBalances(block.number - 101), INITIAL_REBASING_DEPOSIT); + assertEq(aToken.getPastTotalBalances(block.number - 100), INITIAL_REBASING_DEPOSIT + _rawBalanceA); + assertEq(aToken.getPastTotalBalances(block.number - 10), INITIAL_REBASING_DEPOSIT + _rawBalanceA); + assertEq(aToken.getPastTotalBalances(block.number - 1), INITIAL_REBASING_DEPOSIT + _rawBalanceA); + // forgefmt: disable-end + + // Another user deposits. + _mintGovAndSupplyToAave(_userB, _amountB); + uint256 _rawBalanceB = aToken.exposed_RawBalanceOf(_userB); + + // Advance the clock to checkpoint + rebase. + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100 days); + + // forgefmt: disable-start + assertEq(aToken.getPastTotalBalances(block.number - 201), INITIAL_REBASING_DEPOSIT); + assertEq(aToken.getPastTotalBalances(block.number - 120), INITIAL_REBASING_DEPOSIT + _rawBalanceA); + assertEq(aToken.getPastTotalBalances(block.number - 20), INITIAL_REBASING_DEPOSIT + _rawBalanceA + _rawBalanceB); + assertEq(aToken.getPastTotalBalances(block.number - 1), INITIAL_REBASING_DEPOSIT + _rawBalanceA + _rawBalanceB); + // forgefmt: disable-end + } + + function test_GetPastTotalBalancesDecreasesOnWithdraw() public { + _initiateRebasing(); + + address _userA = makeAddr("GetPastTotalBalancesDecreasesOnWithdraw _userA"); + uint256 _amountA = 4242 ether; + + // Deposit. + _mintGovAndSupplyToAave(_userA, _amountA); + uint256 _rawBalanceA = aToken.exposed_RawBalanceOf(_userA); + + // Advance the clock so that we checkpoint and let some rebasing happen. + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100 days); + + assertEq(aToken.getPastTotalBalances(block.number - 1), INITIAL_REBASING_DEPOSIT + _rawBalanceA); + + vm.startPrank(_userA); + uint256 _withdrawAmount = aToken.balanceOf(_userA) / 3; + pool.withdraw(address(govToken), _withdrawAmount, _userA); + vm.stopPrank(); + + // Advance the clock so that we checkpoint and let some rebasing happen. + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100 days); + + assertEq( + aToken.getPastTotalBalances(block.number - 1), + INITIAL_REBASING_DEPOSIT + aToken.exposed_RawBalanceOf(_userA) + ); + + uint256 _rawBalanceDelta = _rawBalanceA - aToken.exposed_RawBalanceOf(_userA); + assertEq( + aToken.getPastTotalBalances(block.number - 101) - _rawBalanceDelta, + aToken.getPastTotalBalances(block.number - 1) + ); + } + + function test_GetPastTotalBalancesIsUnaffectedByTransfer() public { + _initiateRebasing(); + + address _userA = makeAddr("GetPastTotalBalancesIsUnaffectedByTransfer _userA"); + address _userB = makeAddr("GetPastTotalBalancesIsUnaffectedByTransfer _userB"); + uint256 _amountA = 4242 ether; + + // Deposit. + _mintGovAndSupplyToAave(_userA, _amountA); + + // Advance the clock so that we checkpoint and let some rebasing happen. + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100 days); + + uint256 _totalDeposits = aToken.getPastTotalBalances(block.number - 1); + + vm.startPrank(_userA); + aToken.transfer(_userB, aToken.balanceOf(_userA) / 2); + vm.stopPrank(); + + // Advance the clock so that we checkpoint and let some rebasing happen. + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100 days); + + assertEq( + aToken.getPastTotalBalances(block.number - 1), + _totalDeposits // No change because of the transfer; + ); + + // Repeat. + vm.startPrank(_userA); + aToken.transfer(_userB, aToken.balanceOf(_userA)); + vm.stopPrank(); + + assertEq(aToken.balanceOf(_userA), 0); + + // Advance the clock so that we checkpoint and let some rebasing happen. + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100 days); + + assertEq( + aToken.getPastTotalBalances(block.number - 1), + _totalDeposits // Still no change caused by transfer. + ); + } + + function test_GetPastTotalBalancesIsUnaffectedByBorrow() public { + _initiateRebasing(); + + address _userA = makeAddr("GetPastTotalBalancesIsUnaffectedByBorrow _userA"); + uint256 _totalDeposits = aToken.getPastTotalBalances(block.number - 1); + + // Borrow gov. + vm.startPrank(_userA); + deal(weth, _userA, 100 ether); + ERC20(weth).approve(address(pool), type(uint256).max); + pool.supply(weth, 100 ether, _userA, 0); + pool.borrow( + address(govToken), + 42 ether, // amount of GOV to borrow + uint256(DataTypes.InterestRateMode.STABLE), // interestRateMode + 0, // referralCode + _userA // onBehalfOf + ); + assertEq(govToken.balanceOf(_userA), 42 ether); + vm.stopPrank(); + + // Advance the clock so that we checkpoint and let some rebasing happen. + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100 days); + + assertEq(aToken.getPastTotalBalances(block.number - 1), _totalDeposits); + } + + function test_GetPastTotalBalancesZerosOutIfAllPositionsAreUnwound() public { + _initiateRebasing(); + + uint256 _totalDeposits = aToken.getPastTotalBalances(block.number - 1); + assertGt(_totalDeposits, 0); + assertGt(govToken.balanceOf(address(aToken)), 0); + + // Repay the borrow that kicked off rebasing. + ERC20(govToken).approve(address(pool), type(uint256).max); + // Give the user some more gov to pay the interest on the borrow. + govToken.exposed_mint(address(this), 10 ether); + pool.repay( + address(govToken), + type(uint256).max, // pay entire debt. + uint256(DataTypes.InterestRateMode.STABLE), // interestRateMode + address(this) + ); + + // Withdraw the only balance. + vm.startPrank(initialSupplier); + pool.withdraw( + address(govToken), + type(uint256).max, // Withdraw it all. + initialSupplier + ); + + // Advance the clock so that we checkpoint. + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1 days); + + assertEq( + aToken.getPastTotalBalances(block.number - 1), + 0, // Total balances should now be zero; any remaining supply belongs to the reserve. + "getPastTotalBalances accounting is wrong" + ); + } } diff --git a/test/FractionalPool.t.sol b/test/FractionalPool.t.sol index 4c6b238..9b10335 100644 --- a/test/FractionalPool.t.sol +++ b/test/FractionalPool.t.sol @@ -80,12 +80,17 @@ contract FractionalPoolTest is Test { assertEq(uint256(governor.state(proposalId)), uint256(ProposalState.Active)); } - function _commonFuzzerAssumptions(address _address, uint256 _voteWeight) public returns (uint256) { + function _commonFuzzerAssumptions(address _address, uint256 _voteWeight) + public + view + returns (uint256) + { return _commonFuzzerAssumptions(_address, _voteWeight, uint8(VoteType.Against)); } function _commonFuzzerAssumptions(address _address, uint256 _voteWeight, uint8 _supportType) public + view returns (uint256) { vm.assume(_address != address(pool)); @@ -100,7 +105,7 @@ contract Deployment is FractionalPoolTest { assertEq(token.name(), "Governance Token"); assertEq(token.symbol(), "GOV"); - assertEq(address(pool.token()), address(token)); + assertEq(address(pool.TOKEN()), address(token)); assertEq(token.delegates(address(pool)), address(pool)); assertEq(governor.name(), "Governor"); diff --git a/test/GovernorCountingFractional.t.sol b/test/GovernorCountingFractional.t.sol index dcaca66..a52ea53 100644 --- a/test/GovernorCountingFractional.t.sol +++ b/test/GovernorCountingFractional.t.sol @@ -151,7 +151,11 @@ contract GovernorCountingFractionalTest is Test { ); } - function _setupNominalVoters(uint256[4] memory weights) internal returns (Voter[4] memory voters) { + function _setupNominalVoters(uint256[4] memory weights) + internal + view + returns (Voter[4] memory voters) + { Voter memory voter; for (uint8 _i; _i < voters.length; _i++) { voter = voters[_i]; @@ -169,12 +173,13 @@ contract GovernorCountingFractionalTest is Test { return address(uint160(uint256(keccak256(abi.encodePacked(salt1, salt2))))); } - function _randomSupportType(uint256 salt) public returns (uint8) { + function _randomSupportType(uint256 salt) public view returns (uint8) { return uint8(bound(salt, 0, uint8(GovernorCompatibilityBravo.VoteType.Abstain))); } function _randomVoteSplit(FractionalVoteSplit memory _voteSplit) public + view returns (FractionalVoteSplit memory) { _voteSplit.percentFor = bound(_voteSplit.percentFor, 0, 1e18); @@ -187,7 +192,7 @@ contract GovernorCountingFractionalTest is Test { function _setupFractionalVoters( uint256[4] memory weights, FractionalVoteSplit[4] memory voteSplits - ) internal returns (Voter[4] memory voters) { + ) internal view returns (Voter[4] memory voters) { voters = _setupNominalVoters(weights); Voter memory voter; diff --git a/test/MockATokenFlexVoting.sol b/test/MockATokenFlexVoting.sol new file mode 100644 index 0000000..3e98b2b --- /dev/null +++ b/test/MockATokenFlexVoting.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity >=0.8.10; + +import {IPool} from "aave-v3-core/contracts/interfaces/IPool.sol"; +import {ATokenFlexVoting} from "src/ATokenFlexVoting.sol"; + +contract MockATokenFlexVoting is ATokenFlexVoting { + constructor(IPool _pool, address _governor, uint32 _castVoteWindow) + ATokenFlexVoting(_pool, _governor, _castVoteWindow) + {} + + function handleRepayment(address user, uint256 amount) external virtual onlyPool { + // We need this because the Aave code we compile is ahead of the Aave code deployed on + // Optimism (where our tests fork from). + // + // Currently on Optimism, AToken.handleRepayment is a 2-argument function, as seen in the + // existing AToken implementation: + // + // https://optimistic.etherscan.io/address/0xa5ba6e5ec19a1bf23c857991c857db62b2aa187b#code + // + // But in the latest Aave v3 code, it is a 3-argument function: + // + // https://github.com/aave/aave-v3-core/blob/c38c627683c0db0449b7c9ea2fbd68bde3f8dc01/contracts/protocol/tokenization/AToken.sol#L166-L170 + // + // The change was made as a result of this issue: + // + // https://github.com/aave/aave-v3-core/issues/742 + // + // We expect that the on-chain AToken implementation will be upgraded to the 3-argument version + // of AToken.handleRepayment at some point in the future. If/when that happens, we should remove + // this. But for now we need our aToken to have this function in our fork tests to maintain + // backwards compatibility. + } + + function exposed_Treasury() public view returns (address) { + return _treasury; + } + + function exposed_RawBalanceOf(address _user) public view returns (uint256) { + return _userState[_user].balance; + } +}