Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Compound V3 client contract #46

Merged
merged 30 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
59cbe9b
Install Comet (aka Compound v3)
davidlaprade Mar 28, 2023
0b42108
Test initial setup/config
davidlaprade Mar 29, 2023
0f8863d
Test that we can supply + withdraw
davidlaprade May 1, 2023
aa7eea2
Test that supplied gov earns yield and can be borrowed
davidlaprade May 2, 2023
7435540
Expand yield claiming test
davidlaprade May 2, 2023
edc30df
Begin testing vote functionality
davidlaprade May 2, 2023
0d7277c
Abstract re-usable flex voting client behavior into separate contract
davidlaprade May 2, 2023
8eab9ee
Have CometFlexVoting use the FlexVotingClient
davidlaprade May 4, 2023
08a7316
Checkpoint changes to principal balance
davidlaprade May 11, 2023
dc09ee9
Update comment
davidlaprade May 11, 2023
2fe4e57
Add some tests
davidlaprade May 11, 2023
4edcf50
Fix total deposit checkpointing in Comet
davidlaprade May 15, 2023
984a8d3
Implement more tests, add testing TODOs
davidlaprade May 15, 2023
f9307fe
Use Comet fork so that updateBasePrincipal can be overridden
davidlaprade May 15, 2023
2d2908c
scopelint
davidlaprade May 15, 2023
95ab0c5
Temporarily remove size check and formatting on CI
davidlaprade May 16, 2023
671d697
Why are the tests failing?
davidlaprade May 16, 2023
39b0d8c
Expose MAINNET_RPC_URL
davidlaprade May 16, 2023
abb876d
Apply suggestions from code review
davidlaprade May 17, 2023
306eee7
Remove need for comments in struct instances
davidlaprade May 17, 2023
951fcdf
Re-enable formatting with just the forge formatter
davidlaprade May 17, 2023
82c2ba0
Miscellaneous PR review changes
davidlaprade May 17, 2023
375b73b
Flesh out natspec for FlexVotingClient
davidlaprade May 17, 2023
1ac8bc2
Fix natspec comment levels in FlexVotingClient
davidlaprade May 18, 2023
56bf3cf
Miscellaneous test file revisions for PR review
davidlaprade May 18, 2023
3f1a7ae
More PR tweaks
davidlaprade May 19, 2023
c3b2fe3
Add FlexVotingComet description
davidlaprade May 25, 2023
9228936
Allow client contracts to customize the voting reason param
davidlaprade May 26, 2023
c51734f
Remove TODOs
davidlaprade May 26, 2023
c6cc147
Update src/CometFlexVoting.sol
davidlaprade May 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 16 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:

env:
FOUNDRY_PROFILE: ci
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
OPTIMISM_RPC_URL: ${{ secrets.OPTIMISM_RPC_URL }}

jobs:
Expand All @@ -25,7 +26,7 @@ jobs:
- name: Build contracts
run: |
forge --version
forge build --sizes
forge build
davidlaprade marked this conversation as resolved.
Show resolved Hide resolved
test:
runs-on: ubuntu-latest
steps:
Expand All @@ -49,21 +50,22 @@ jobs:
with:
version: nightly

- name: Install scopelint
uses: engineerd/configurator@v0.0.8
with:
name: scopelint
repo: ScopeLift/scopelint
fromGitHubReleases: true
version: latest
pathInArchive: scopelint-x86_64-linux/scopelint
urlTemplate: https://github.com/ScopeLift/scopelint/releases/download/{{version}}/scopelint-x86_64-linux.tar.xz
token: ${{ secrets.GITHUB_TOKEN }}
# - name: Install scopelint
# uses: engineerd/configurator@v0.0.8
# with:
# name: scopelint
# repo: ScopeLift/scopelint
# fromGitHubReleases: true
# version: latest
# pathInArchive: scopelint-x86_64-linux/scopelint
# urlTemplate: https://github.com/ScopeLift/scopelint/releases/download/{{version}}/scopelint-x86_64-linux.tar.xz
# token: ${{ secrets.GITHUB_TOKEN }}

- name: Check formatting
run: |
scopelint --version
scopelint check
run: forge fmt --check
# run: |
# scopelint --version
# scopelint check

# TODO figure out why this is failing and uncomment
# slither-analyze:
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@
path = lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
branch = v4.8.0
[submodule "lib/comet"]
path = lib/comet
url = https://github.com/davidlaprade/comet
mds1 marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
optimizer = false

[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
optimism = "${OPTIMISM_RPC_URL}"

[fmt]
Expand Down
1 change: 1 addition & 0 deletions lib/comet
Submodule comet added at 292178
185 changes: 16 additions & 169 deletions src/ATokenFlexVoting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,11 @@ 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 {IPool} from "aave-v3-core/contracts/interfaces/IPool.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {Checkpoints} from "@openzeppelin/contracts/utils/Checkpoints.sol";
import {IFractionalGovernor} from "src/interfaces/IFractionalGovernor.sol";
import {IVotingToken} from "src/interfaces/IVotingToken.sol";

davidlaprade marked this conversation as resolved.
Show resolved Hide resolved
import {FlexVotingClient} from "src/FlexVotingClient.sol";
// forgefmt: disable-end

/// @notice This is an extension of Aave V3's AToken contract which makes it possible for AToken
Expand All @@ -39,46 +35,13 @@ import {IVotingToken} from "src/interfaces/IVotingToken.sol";
/// The original AToken that this contract extends is viewable here:
///
/// https://github.com/aave/aave-v3-core/blob/c38c6276/contracts/protocol/tokenization/AToken.sol
contract ATokenFlexVoting is AToken {
using SafeCast for uint256;
contract ATokenFlexVoting is AToken, FlexVotingClient {
using Checkpoints for Checkpoints.History;

/// @notice The voting options corresponding to those used in the Governor.
enum VoteType {
Against,
For,
Abstain
}

/// @notice Data structure to store vote preferences expressed by depositors.
struct ProposalVote {
uint128 againstVotes;
uint128 forVotes;
uint128 abstainVotes;
}

/// @notice Map proposalId to an address to whether they have voted on this proposal.
mapping(uint256 => mapping(address => bool)) private proposalVotersHasVoted;

/// @notice Map proposalId to vote totals expressed on this proposal.
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;

/// @notice Mapping from address to stored (not rebased) balance checkpoint history.
mapping(address => Checkpoints.History) private balanceCheckpoints;

/// @notice History of total stored (not rebased) balances.
Checkpoints.History private totalDepositCheckpoints;

/// @dev Constructor.
/// @param _pool The address of the Pool contract
/// @param _governor The address of the flex-voting-compatible governance contract.
constructor(IPool _pool, address _governor) AToken(_pool) {
GOVERNOR = IFractionalGovernor(_governor);
}
constructor(IPool _pool, address _governor) AToken(_pool) FlexVotingClient(_governor) {}

// forgefmt: disable-start
//===========================================================================
Expand Down Expand Up @@ -109,7 +72,7 @@ contract ATokenFlexVoting is AToken {
params
);

selfDelegate();
FlexVotingClient._selfDelegate();
}

/// Note: this has been modified from Aave v3's MintableIncentivizedERC20 to
Expand All @@ -118,8 +81,10 @@ contract ATokenFlexVoting is AToken {
/// @inheritdoc MintableIncentivizedERC20
function _burn(address account, uint128 amount) internal override {
MintableIncentivizedERC20._burn(account, amount);
_checkpointRawBalanceOf(account);
totalDepositCheckpoints.push(totalDepositCheckpoints.latest() - amount);
FlexVotingClient._checkpointRawBalanceOf(account);
FlexVotingClient.totalBalanceCheckpoints.push(
FlexVotingClient.totalBalanceCheckpoints.latest() - amount
);
}

/// Note: this has been modified from Aave v3's MintableIncentivizedERC20 to
Expand All @@ -128,8 +93,10 @@ contract ATokenFlexVoting is AToken {
/// @inheritdoc MintableIncentivizedERC20
function _mint(address account, uint128 amount) internal override {
MintableIncentivizedERC20._mint(account, amount);
_checkpointRawBalanceOf(account);
totalDepositCheckpoints.push(totalDepositCheckpoints.latest() + amount);
FlexVotingClient._checkpointRawBalanceOf(account);
FlexVotingClient.totalBalanceCheckpoints.push(
FlexVotingClient.totalBalanceCheckpoints.latest() + amount
);
}

/// @dev This has been modified from Aave v3's AToken contract to checkpoint raw balances
Expand All @@ -145,136 +112,16 @@ contract ATokenFlexVoting is AToken {
bool validate
) internal virtual override {
AToken._transfer(from, to, amount, validate);
_checkpointRawBalanceOf(from);
_checkpointRawBalanceOf(to);
FlexVotingClient._checkpointRawBalanceOf(from);
FlexVotingClient._checkpointRawBalanceOf(to);
}
//===========================================================================
// END: Aave overrides
//===========================================================================
// forgefmt: disable-end

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

/// @notice Allow a depositor to express their voting preference for a given
/// proposal. Their preference is recorded internally but not moved to the
/// Governor until `castVote` is called.
/// @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 = getPastStoredBalance(msg.sender, GOVERNOR.proposalSnapshot(proposalId));
require(weight > 0, "no weight");

require(!proposalVotersHasVoted[proposalId][msg.sender], "already voted");
proposalVotersHasVoted[proposalId][msg.sender] = true;

if (support == uint8(VoteType.Against)) {
proposalVotes[proposalId].againstVotes += SafeCast.toUint128(weight);
} else if (support == uint8(VoteType.For)) {
proposalVotes[proposalId].forVotes += SafeCast.toUint128(weight);
} else if (support == uint8(VoteType.Abstain)) {
proposalVotes[proposalId].abstainVotes += SafeCast.toUint128(weight);
} else {
revert("invalid support value, must be included in VoteType enum");
}
}

/// @notice Causes this contract to cast a vote to the Governor for all of the
/// accumulated votes expressed by users. Uses the sum of all raw (unrebased) balances
/// to proportionally split its voting weight. Can be called by anyone. Can be called
/// multiple times during the lifecycle of a given proposal.
/// @param proposalId The ID of the proposal which the Pool will now vote on.
function castVote(uint256 proposalId) external {
ProposalVote storage _proposalVote = proposalVotes[proposalId];
require(
_proposalVote.forVotes + _proposalVote.againstVotes + _proposalVote.abstainVotes > 0,
"no votes expressed"
);
uint256 _proposalSnapshotBlockNumber = GOVERNOR.proposalSnapshot(proposalId);

// We use the snapshot of total raw balances to determine the weight with
// which to vote. We do this for two reasons:
// (1) We cannot use the proposalVote numbers alone, since some people with
// balances at the snapshot might never express their preferences. If a
// large holder never expressed a preference, but this contract nevertheless
// cast votes to the governor with all of its weight, then other users may
// effectively have *increased* their voting weight because someone else
// didn't participate, which creates all kinds of bad incentives.
// (2) Other people might have already expressed their preferences on this
// proposal and had those preferences submitted to the governor by an
// earlier call to this function. The weight of those preferences
// should still be taken into consideration when determining how much
// weight to vote with this time.
// Using the total raw balance to proportion votes in this way means that in
// many circumstances this function will not cast votes with all of its
// weight.
uint256 _totalRawBalanceAtSnapshot = getPastTotalBalance(_proposalSnapshotBlockNumber);

// We need 256 bits because of the multiplication we're about to do.
uint256 _votingWeightAtSnapshot = IVotingToken(address(_underlyingAsset)).getPastVotes(
address(this), _proposalSnapshotBlockNumber
);

// forVotesRaw forVoteWeight
// --------------------- = ------------------
// totalRawBalance totalVoteWeight
//
// forVoteWeight = forVotesRaw * totalVoteWeight / totalRawBalance
uint128 _forVotesToCast = SafeCast.toUint128(
(_votingWeightAtSnapshot * _proposalVote.forVotes) / _totalRawBalanceAtSnapshot
);
uint128 _againstVotesToCast = SafeCast.toUint128(
(_votingWeightAtSnapshot * _proposalVote.againstVotes) / _totalRawBalanceAtSnapshot
);
uint128 _abstainVotesToCast = SafeCast.toUint128(
(_votingWeightAtSnapshot * _proposalVote.abstainVotes) / _totalRawBalanceAtSnapshot
);

// This param is ignored by the governor when voting with fractional
// weights. It makes no difference what vote type this is.
uint8 unusedSupportParam = uint8(VoteType.Abstain);

// Clear the stored votes so that we don't double-cast them.
delete proposalVotes[proposalId];

bytes memory fractionalizedVotes =
abi.encodePacked(_againstVotesToCast, _forVotesToCast, _abstainVotesToCast);
GOVERNOR.castVoteWithReasonAndParams(
proposalId,
unusedSupportParam,
"rolled-up vote from aToken holders", // Reason string.
fractionalizedVotes
);
}

/// @notice Returns the _user's current balance in storage.
function _rawBalanceOf(address _user) internal view returns (uint256) {
function _rawBalanceOf(address _user) internal view override returns (uint256) {
return _userState[_user].balance;
}

/// @notice Checkpoints the _user's current raw balance.
function _checkpointRawBalanceOf(address _user) internal {
balanceCheckpoints[_user].push(_rawBalanceOf(_user));
}

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

/// @notice Returns the total stored balance of all users at _blockNumber.
/// @param _blockNumber The block at which to lookup the total stored balance.
function getPastTotalBalance(uint256 _blockNumber) public view returns (uint256) {
return totalDepositCheckpoints.getAtProbablyRecentBlock(_blockNumber);
}
}
77 changes: 77 additions & 0 deletions src/CometFlexVoting.sol
davidlaprade marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.10;

import {Comet} from "comet/Comet.sol";
import {CometConfiguration} from "comet/CometConfiguration.sol";
import {Checkpoints} from "@openzeppelin/contracts/utils/Checkpoints.sol";

import {FlexVotingClient} from "src/FlexVotingClient.sol";

/// @notice This is an extension of Compound V3's Comet contract which makes it
/// possible for Comet token holders to still vote on governance proposals. This
/// way, holders of governance tokens do not have to choose between earning
/// yield on Compound and voting. They can do both.
///
/// This extension has the following requirements:
/// (a) The base token must be a governance token.
/// (b) The base token's governor contract must support flexible voting (see
/// `GovernorCountingFractional`).
///
/// If these requirements are met, base token depositors can call
/// `Comet.expressVote` to signal their preference on open governance proposals.
/// When they do so, this extension records that preference with weight
/// proportional to the users's Comet balance at the proposal snapshot.
///
/// At any point after voting preferences have been expressed, Comet's public
/// `castVote` function may be called to roll up all internal voting records
/// into a single delegated vote to the Governor contract -- a vote which
/// specifies the exact For/Abstain/Against totals expressed by Comet holders.
/// Votes can be rolled up and cast in this manner multiple times for a given
/// proposal.
///
/// Participating in governance via Comet voting is completely optional. Users
/// otherwise still supply, borrow, and hold tokens with Compound as usual.
///
/// The original Comet that this contract was developed against can be viewed
/// here:
///
/// https://github.com/compound-finance/comet/blob/3780c06b4eaa80a8c78e0ff770a7e8a1518db75e/contracts/Comet.sol
contract CometFlexVoting is Comet, FlexVotingClient {
using Checkpoints for Checkpoints.History;

/// @param _config The configuration struct for this Comet instance.
/// @param _governor The address of the flex-voting-compatible governance contract.
constructor(CometConfiguration.Configuration memory _config, address _governor)
Comet(_config)
FlexVotingClient(_governor)
{
_selfDelegate();
davidlaprade marked this conversation as resolved.
Show resolved Hide resolved
}

/// @notice Returns the current balance in storage for the `account`.
function _rawBalanceOf(address account) internal view override returns (uint256) {
int104 _principal = userBasic[account].principal;
return _principal > 0 ? uint256(int256(_principal)) : 0;
mds1 marked this conversation as resolved.
Show resolved Hide resolved
}

function _castVoteReasonString() internal override returns (string memory) {
return "rolled-up vote from CometFlexVoting token holders";
}

//===========================================================================
// BEGIN: Comet overrides
//===========================================================================
//
// This function is called any time the underlying balance is changed.
apbendi marked this conversation as resolved.
Show resolved Hide resolved
function updateBasePrincipal(address _account, UserBasic memory _userBasic, int104 _principalNew)
internal
override
{
Comet.updateBasePrincipal(_account, _userBasic, _principalNew);
FlexVotingClient._checkpointRawBalanceOf(_account);
FlexVotingClient.totalBalanceCheckpoints.push(uint224(totalSupplyBase));
}
//===========================================================================
// END: Comet overrides
//===========================================================================
}