diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a22b39..0f2aa71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: env: FOUNDRY_PROFILE: ci + FOUNDRY_DISABLE_NIGHTLY_WARNING: 1 MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} OPTIMISM_RPC_URL: ${{ secrets.OPTIMISM_RPC_URL }} diff --git a/src/FlexVotingBase.sol b/src/FlexVotingBase.sol new file mode 100644 index 0000000..e76b4be --- /dev/null +++ b/src/FlexVotingBase.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import {IFractionalGovernor} from "src/interfaces/IFractionalGovernor.sol"; +import {IVotingToken} from "src/interfaces/IVotingToken.sol"; + +/// @notice This is an abstract contract designed to make it easy to build +/// clients for governance systems that inherit from GovernorCountingFractional, +/// a.k.a. Flexible Voting governors. +/// +/// A "client" in this sense is a contract that: + +/// - (a) receives deposits of governance tokens from its users, +/// - (b) gives said depositors the ability to express their voting preferences +/// on governance proposals, and +/// - (c) casts votes on said proposals to flexible voting governors according +/// to the expressed preferences of its depositors. +/// +/// This contract assumes that a child contract will implement a mechanism for +/// receiving and storing deposit balances, part (a). With that in place, this +/// contract supplies features (b) and (c). +/// +/// A key concept here is that of a user's "raw balance". The raw balance is the +/// system's internal representation of a user's claim on the governance tokens +/// that it custodies. Since different systems might represent such claims in +/// different ways, this contract leaves the implementation of the `_rawBalance` +/// function to the child contract. +/// +/// The simplest such representation would be to directly store the cumulative +/// balance of the governance token that the user has deposited. In such a +/// system, the amount that the user deposits is the amount that the user has +/// claim to. If the user has claim to 1e18 governance tokens, the internal +/// representation is just 1e18. +/// +/// In many systems, however, the raw balance will not be equivalent to the +/// amount of governance tokens the user has claim to. In Aave, for example, +/// deposit amounts are scaled down by an ever-increasing index that represents +/// the cumulative amount of interest earned over the lifetime of deposits. The +/// "raw balance" of a user in Aave's case is this scaled down amount, since it +/// is the value that represents the user's claim on deposits. Thus for Aave, a +/// users's raw balance will always be less than the actual amount they have +/// claim to. +/// +/// If the raw balance can be identified and defined for a system, and +/// `_rawBalance` can be implemented for it, then this contract will take care +/// of the rest. +abstract contract FlexVotingBase { + using SafeCast for uint256; + + // @dev Trace208 is used instead of Trace224 because the former allocates 48 + // bits to its _key. We need at least 48 bits because the _key is going to be + // a timepoint. Timepoints in the context of ERC20Votes and ERC721Votes + // conform to the EIP-6372 standard, which specifies they be uint48s. + using Checkpoints for Checkpoints.Trace208; + + /// @notice The governor contract associated with this governance token. It + /// must be one that supports fractional voting, e.g. GovernorCountingFractional. + IFractionalGovernor public immutable GOVERNOR; + + /// @dev Mapping from address to the checkpoint history of internal voting + /// weight for that address, i.e. how much weight they can call `expressVote` + /// with at a given time. + mapping(address => Checkpoints.Trace208) internal voteWeightCheckpoints; + + /// @dev History of the sum total of voting weight in the system. May or may + /// not be equivalent to this contract's balance of `GOVERNOR`s token at a + /// given time. + Checkpoints.Trace208 internal totalVoteWeightCheckpoints; + + /// @param _governor The address of the flex-voting-compatible governance contract. + constructor(address _governor) { + GOVERNOR = IFractionalGovernor(_governor); + } + + /// @dev Returns a representation of the current amount of `GOVERNOR`s + /// token that `_user` has claim to in this system. It may or may not be + /// equivalent to the withdrawable balance of `GOVERNOR`s token for `user`, + /// e.g. if the internal representation of balance has been scaled down. + function _rawBalanceOf(address _user) internal view virtual returns (uint208); + + // TODO Should we rename this function to avoid collision with FlexVotingDelegable? + // https://github.com/ScopeLift/flexible-voting/issues/88 + /// @dev Delegates the present contract's voting rights with `GOVERNOR` to itself. + function _selfDelegate() internal { + IVotingToken(GOVERNOR.token()).delegate(address(this)); + } + + function _applyDeltaToCheckpoint(Checkpoints.Trace208 storage _checkpoint, int256 _delta) + internal + returns (uint208 _prevTotal, uint208 _newTotal) + { + // The casting in this function is safe since: + // - if oldTotal + delta > int256.max it will panic and revert. + // - if |delta| <= oldTotal there is no risk of wrapping + // - if |delta| > oldTotal + // * uint256(oldTotal + delta) will wrap but the wrapped value will + // necessarily be greater than uint208.max, so SafeCast will revert. + // * the lowest that oldTotal + delta can be is int256.min (when + // oldTotal is 0 and delta is int256.min). The wrapped value of a + // negative signed integer is: + // wrapped(integer) = uint256.max + integer + // Substituting: + // wrapped(int256.min) = uint256.max + int256.min + // But: + // uint256.max + int256.min > uint208.max + // Substituting again: + // wrapped(int256.min) > uint208.max, which will revert when safecast. + _prevTotal = _checkpoint.latest(); + int256 _castTotal = int256(uint256(_prevTotal)); + _newTotal = SafeCast.toUint208(uint256(_castTotal + _delta)); + + uint48 _timepoint = IVotingToken(GOVERNOR.token()).clock(); + _checkpoint.push(_timepoint, _newTotal); + } + + /// @dev Checkpoints internal voting weight of `user` after applying `_delta`. + function _checkpointVoteWeightOf(address _user, int256 _delta) internal virtual { + _applyDeltaToCheckpoint(voteWeightCheckpoints[_user], _delta); + } + + /// @dev Checkpoints the total vote weight after applying `_delta`. + function _checkpointTotalVoteWeight(int256 _delta) internal virtual { + _applyDeltaToCheckpoint(totalVoteWeightCheckpoints, _delta); + } +} diff --git a/src/FlexVotingClient.sol b/src/FlexVotingClient.sol index 707ccbb..1d95e2a 100644 --- a/src/FlexVotingClient.sol +++ b/src/FlexVotingClient.sol @@ -5,57 +5,22 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; import {IFractionalGovernor} from "src/interfaces/IFractionalGovernor.sol"; import {IVotingToken} from "src/interfaces/IVotingToken.sol"; +import {FlexVotingBase} from "src/FlexVotingBase.sol"; /// @notice This is an abstract contract designed to make it easy to build clients /// for governance systems that inherit from GovernorCountingFractional, a.k.a. /// Flexible Voting governors. /// -/// A "client" in this sense is a contract that: - -/// - (a) receives deposits of governance tokens from its users, -/// - (b) gives said depositors the ability to express their voting preferences -/// on governance proposals, and -/// - (c) casts votes on said proposals to flexible voting governors according -/// to the expressed preferences of its depositors. -/// -/// This contract assumes that a child contract will implement a mechanism for -/// receiving and storing deposit balances, part (a). With that in place, this -/// contract supplies features (b) and (c). -/// -/// A key concept here is that of a user's "raw balance". The raw balance is the -/// system's internal representation of a user's claim on the governance tokens -/// that it custodies. Since different systems might represent such claims in -/// different ways, this contract leaves the implementation of the `_rawBalance` -/// function to the child contract. -/// -/// The simplest such representation would be to directly store the cumulative -/// balance of the governance token that the user has deposited. In such a -/// system, the amount that the user deposits is the amount that the user has -/// claim to. If the user has claim to 1e18 governance tokens, the internal -/// representation is just 1e18. -/// -/// In many systems, however, the raw balance will not be equivalent to the -/// amount of governance tokens the user has claim to. In Aave, for example, -/// deposit amounts are scaled down by an ever-increasing index that represents -/// the cumulative amount of interest earned over the lifetime of deposits. The -/// "raw balance" of a user in Aave's case is this scaled down amount, since it -/// is the value that represents the user's claim on deposits. Thus for Aave, a -/// users's raw balance will always be less than the actual amount they have -/// claim to. -/// -/// If the raw balance can be identified and defined for a system, and -/// `_rawBalance` can be implemented for it, then this contract will take care -/// of the rest. -abstract contract FlexVotingClient { - using SafeCast for uint256; - - // @dev Trace208 is used instead of Trace224 because the former allocates 48 - // bits to its _key. We need at least 48 bits because the _key is going to be - // a timepoint. Timepoints in the context of ERC20Votes and ERC721Votes - // conform to the EIP-6372 standard, which specifies they be uint48s. +/// This contract extends FlexVotingBase, adding two features: +/// (a) the ability for depositors to express voting preferences on +/// {Governor}'s proposals, and +/// (b) the ability to cast fractional, rolled up votes on behalf of depositors. +abstract contract FlexVotingClient is FlexVotingBase { using Checkpoints for Checkpoints.Trace208; + using SafeCast for uint256; - /// @notice The voting options corresponding to those used in the Governor. + /// @notice The voting options. The order of options should match that of the + /// voting options in the corresponding {Governor} contract. enum VoteType { Against, For, @@ -70,25 +35,14 @@ abstract contract FlexVotingClient { } /// @dev Map proposalId to an address to whether they have voted on this proposal. - mapping(uint256 => mapping(address => bool)) private proposalVotersHasVoted; + mapping(uint256 => mapping(address => bool)) private proposalVoterHasVoted; /// @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; - - /// @dev Mapping from address to the checkpoint history of raw balances - /// of that address. - mapping(address => Checkpoints.Trace208) private balanceCheckpoints; - - /// @dev History of the sum total of raw balances in the system. May or may - /// not be equivalent to this contract's balance of `GOVERNOR`s token at a - /// given time. - Checkpoints.Trace208 internal totalBalanceCheckpoints; - - // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7b74442c5e87ea51dde41c7f18a209fa5154f1a4/contracts/governance/extensions/GovernorCountingFractional.sol#L37 + /// Constant used by OZ's implementation of {GovernorCountingFractional} to + /// signal fractional voting. + /// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7b74442c5e87ea51dde41c7f18a209fa5154f1a4/contracts/governance/extensions/GovernorCountingFractional.sol#L37 uint8 internal constant VOTE_TYPE_FRACTIONAL = 255; error FlexVotingClient__NoVotingWeight(); @@ -96,38 +50,23 @@ abstract contract FlexVotingClient { error FlexVotingClient__InvalidSupportValue(); error FlexVotingClient__NoVotesExpressed(); - /// @param _governor The address of the flex-voting-compatible governance contract. - constructor(address _governor) { - GOVERNOR = IFractionalGovernor(_governor); - } - - /// @dev Returns a representation of the current amount of `GOVERNOR`s - /// token that `_user` has claim to in this system. It may or may not be - /// equivalent to the withdrawable balance of `GOVERNOR`s token for `user`, - /// e.g. if the internal representation of balance has been scaled down. - function _rawBalanceOf(address _user) internal view virtual returns (uint208); - /// @dev Used as the `reason` param when submitting a vote to `GOVERNOR`. function _castVoteReasonString() internal virtual returns (string memory) { return "rolled-up vote from governance token holders"; } - /// @dev Delegates the present contract's voting rights with `GOVERNOR` to itself. - function _selfDelegate() internal { - IVotingToken(GOVERNOR.token()).delegate(address(this)); - } - /// @notice Allow the caller 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 = getPastRawBalance(msg.sender, GOVERNOR.proposalSnapshot(proposalId)); + function expressVote(uint256 proposalId, uint8 support) external virtual { + address voter = msg.sender; + uint256 weight = getPastVoteWeight(voter, GOVERNOR.proposalSnapshot(proposalId)); if (weight == 0) revert FlexVotingClient__NoVotingWeight(); - if (proposalVotersHasVoted[proposalId][msg.sender]) revert FlexVotingClient__AlreadyVoted(); - proposalVotersHasVoted[proposalId][msg.sender] = true; + if (proposalVoterHasVoted[proposalId][voter]) revert FlexVotingClient__AlreadyVoted(); + proposalVoterHasVoted[proposalId][voter] = true; if (support == uint8(VoteType.Against)) { proposalVotes[proposalId].againstVotes += SafeCast.toUint128(weight); @@ -142,10 +81,12 @@ abstract contract FlexVotingClient { } /// @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 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. + /// accumulated votes expressed by users. Uses the total internal vote weight + /// to proportionally split weight among expressed votes. Can be called by + /// anyone. It is idempotent and can be called multiple times during the + /// lifecycle of a given proposal. + /// @param proposalId The ID of the proposal which the FlexVotingClient will + /// now vote on. function castVote(uint256 proposalId) external { ProposalVote storage _proposalVote = proposalVotes[proposalId]; if (_proposalVote.forVotes + _proposalVote.againstVotes + _proposalVote.abstainVotes == 0) { @@ -154,7 +95,7 @@ abstract contract FlexVotingClient { uint256 _proposalSnapshot = GOVERNOR.proposalSnapshot(proposalId); - // We use the snapshot of total raw balances to determine the weight with + // We use the snapshot of total vote weight 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 @@ -167,29 +108,26 @@ abstract contract FlexVotingClient { // 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 + // Using the total vote weight 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(_proposalSnapshot); + uint256 _totalVotesInternal = getPastTotalVoteWeight(_proposalSnapshot); // We need 256 bits because of the multiplication we're about to do. - uint256 _votingWeightAtSnapshot = + uint256 _totalTokenWeight = IVotingToken(address(GOVERNOR.token())).getPastVotes(address(this), _proposalSnapshot); - // forVotesRaw forVoteWeight - // --------------------- = ------------------ - // totalRawBalance totalVoteWeight + // userVotesInternal userVoteWeight + // ------------------------- = -------------------- + // totalVotesInternal totalTokenWeight // - // 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 - ); + // userVoteWeight = userVotesInternal * totalTokenWeight / totalVotesInternal + uint128 _forVotesToCast = + SafeCast.toUint128((_totalTokenWeight * _proposalVote.forVotes) / _totalVotesInternal); + uint128 _againstVotesToCast = + SafeCast.toUint128((_totalTokenWeight * _proposalVote.againstVotes) / _totalVotesInternal); + uint128 _abstainVotesToCast = + SafeCast.toUint128((_totalTokenWeight * _proposalVote.abstainVotes) / _totalVotesInternal); // Clear the stored votes so that we don't double-cast them. delete proposalVotes[proposalId]; @@ -201,54 +139,22 @@ abstract contract FlexVotingClient { ); } - /// @dev Checkpoints the _user's current raw balance. - function _checkpointRawBalanceOf(address _user) internal { - balanceCheckpoints[_user].push(IVotingToken(GOVERNOR.token()).clock(), _rawBalanceOf(_user)); - } - - /// @dev Checkpoints the total balance after applying `_delta`. - function _checkpointTotalBalance(int256 _delta) internal { - // The casting in this function is safe since: - // - if oldTotal + delta > int256.max it will panic and revert. - // - if |delta| <= oldTotal - // * there is no risk of wrapping - // - if |delta| > oldTotal - // * uint256(oldTotal + delta) will wrap but the wrapped value will - // necessarily be greater than uint208.max, so SafeCast will revert. - // * the lowest that oldTotal + delta can be is int256.min (when - // oldTotal is 0 and delta is int256.min). The wrapped value of a - // negative signed integer is: - // wrapped(integer) = uint256.max + integer - // Substituting: - // wrapped(int256.min) = uint256.max + int256.min - // But: - // uint256.max + int256.min > uint208.max - // Substituting again: - // wrapped(int256.min) > uint208.max, which will revert when safecast. - uint256 _oldTotal = uint256(totalBalanceCheckpoints.latest()); - uint256 _newTotal = uint256(int256(_oldTotal) + _delta); - - totalBalanceCheckpoints.push( - IVotingToken(GOVERNOR.token()).clock(), SafeCast.toUint208(_newTotal) - ); - } - - /// @notice Returns the `_user`'s raw balance at `_timepoint`. - /// @param _user The account that's historical raw balance will be looked up. - /// @param _timepoint The timepoint at which to lookup the _user's raw - /// balance, either a block number or a timestamp as determined by + /// @notice Returns the `_user`'s internal voting weight at `_timepoint`. + /// @param _user The account that's historical vote weight will be looked up. + /// @param _timepoint The timepoint at which to lookup the _user's internal + /// voting weight, either a block number or a timestamp as determined by /// {GOVERNOR.token().clock()}. - function getPastRawBalance(address _user, uint256 _timepoint) public view returns (uint256) { + function getPastVoteWeight(address _user, uint256 _timepoint) public view returns (uint256) { uint48 key = SafeCast.toUint48(_timepoint); - return balanceCheckpoints[_user].upperLookup(key); + return voteWeightCheckpoints[_user].upperLookup(key); } - /// @notice Returns the sum total of raw balances of all users at `_timepoint`. - /// @param _timepoint The timepoint at which to lookup the total balance, + /// @notice Returns the total internal voting weight of all users at `_timepoint`. + /// @param _timepoint The timepoint at which to lookup the total weight, /// either a block number or a timestamp as determined by /// {GOVERNOR.token().clock()}. - function getPastTotalBalance(uint256 _timepoint) public view returns (uint256) { + function getPastTotalVoteWeight(uint256 _timepoint) public view returns (uint256) { uint48 key = SafeCast.toUint48(_timepoint); - return totalBalanceCheckpoints.upperLookup(key); + return totalVoteWeightCheckpoints.upperLookup(key); } } diff --git a/src/FlexVotingDelegable.sol b/src/FlexVotingDelegable.sol new file mode 100644 index 0000000..6ddcbb0 --- /dev/null +++ b/src/FlexVotingDelegable.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; + +import {FlexVotingBase} from "src/FlexVotingBase.sol"; + +/// @notice This is an abstract contract designed to make it easy to build +/// clients for governance systems that inherit from GovernorCountingFractional, +/// a.k.a. Flexible Voting governors. +/// +/// This contract extends FlexVotingBase, adding the ability to subdelegate one's +/// internal voting weight. It is meant to be inherited from in conjunction with +/// FlexVotingClient. Doing so makes the following usecase possible: +/// - user A deposits 100 governance tokens in a FlexVotingClient +/// - user B deposits 50 governance tokens into the same client +/// - user A delegates voting weight to user B +/// - a proposal is created in the Governor contract +/// - user B expresses a voting preference P on the proposal to the client +/// - the client casts its votes on the proposal to the Governor contract +/// - user B's voting weight is combined with user A's voting weight so that +/// 150 tokens are effectively cast with voting preference P on behalf of +/// users A and B. +abstract contract FlexVotingDelegable is Context, FlexVotingBase { + using Checkpoints for Checkpoints.Trace208; + + // @dev Emitted when an account changes its delegate. + event DelegateChanged( + address indexed delegator, address indexed fromDelegate, address indexed toDelegate + ); + + // @dev Emitted when a delegate change results in changes to a delegate's + // number of voting weight. + event DelegateWeightChanged(address indexed delegate, uint256 previousVotes, uint256 newVotes); + + mapping(address account => address) private _delegatee; + + // @dev Delegates votes from the sender to `delegatee`. + function delegate(address delegatee) public virtual { + address account = _msgSender(); + _delegate(account, delegatee); + } + + // @dev Returns the delegate that `account` has chosen. Assumes + // self-delegation if no delegate has been chosen. + function delegates(address _account) public view virtual returns (address) { + address _proxy = _delegatee[_account]; + if (_proxy == address(0)) return _account; + return _proxy; + } + + // @dev Delegate all of `account`'s voting units to `delegatee`. + // + // Emits events {DelegateChanged} and {DelegateWeightChanged}. + function _delegate(address account, address delegatee) internal virtual { + address oldDelegate = delegates(account); + _delegatee[account] = delegatee; + + int256 _delta = int256(uint256(_rawBalanceOf(account))); + emit DelegateChanged(account, oldDelegate, delegatee); + _updateDelegateBalance(oldDelegate, delegatee, _delta); + } + + function _checkpointVoteWeightOf(address _user, int256 _delta) internal virtual override { + address _proxy = delegates(_user); + _applyDeltaToCheckpoint(voteWeightCheckpoints[_proxy], _delta); + } + + // @dev Moves delegated votes from one delegate to another. + function _updateDelegateBalance(address from, address to, int256 _delta) internal virtual { + if (from == to || _delta == 0) return; + + // Decrement old delegate's weight. + (uint208 _oldFrom, uint208 _newFrom) = + _applyDeltaToCheckpoint(voteWeightCheckpoints[from], -_delta); + emit DelegateWeightChanged(from, _oldFrom, _newFrom); + + // Increment new delegate's weight. + (uint208 _oldTo, uint208 _newTo) = _applyDeltaToCheckpoint(voteWeightCheckpoints[to], _delta); + emit DelegateWeightChanged(to, _oldTo, _newTo); + } +} diff --git a/test/FlexVotingClient.invariants.t.sol b/test/FlexVotingClient.invariants.t.sol index 332c5fc..50e47c3 100644 --- a/test/FlexVotingClient.invariants.t.sol +++ b/test/FlexVotingClient.invariants.t.sol @@ -107,7 +107,7 @@ contract FlexVotingInvariantTest is FlexVotingInvariantSetup { uint256 _checkpoint = block.number; vm.roll(_checkpoint + 1); assertEq( - flexClient.getPastTotalBalance(_checkpoint), + flexClient.getPastTotalVoteWeight(_checkpoint), handler.ghost_depositSum() - handler.ghost_withdrawSum() ); @@ -115,9 +115,9 @@ contract FlexVotingInvariantTest is FlexVotingInvariantSetup { address[] memory _depositors = handler.getActors(); for (uint256 d; d < _depositors.length; d++) { address _depositor = _depositors[d]; - _sum += flexClient.getPastRawBalance(_depositor, _checkpoint); + _sum += flexClient.getPastVoteWeight(_depositor, _checkpoint); } - assertEq(flexClient.getPastTotalBalance(_checkpoint), _sum); + assertEq(flexClient.getPastTotalVoteWeight(_checkpoint), _sum); } function invariant_SumOfDepositsIsGTEProposalVotes() public view { diff --git a/test/FlexVotingClient.t.sol b/test/FlexVotingClient.t.sol index c491c09..fc41291 100644 --- a/test/FlexVotingClient.t.sol +++ b/test/FlexVotingClient.t.sol @@ -1,1334 +1,102 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {Test} from "forge-std/Test.sol"; -import {Vm} from "forge-std/Vm.sol"; -import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol"; -import {IGovernor} from "@openzeppelin/contracts/governance/Governor.sol"; -import {GovernorCountingSimple as GCS} from - "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; -import {SignedMath} from "@openzeppelin/contracts/utils/math/SignedMath.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {IVotingToken} from "src/interfaces/IVotingToken.sol"; -import {IFractionalGovernor} from "src/interfaces/IFractionalGovernor.sol"; -import {FlexVotingClient as FVC} from "src/FlexVotingClient.sol"; import {MockFlexVotingClient} from "test/MockFlexVotingClient.sol"; -import {GovToken, TimestampGovToken} from "test/GovToken.sol"; -import {FractionalGovernor} from "test/FractionalGovernor.sol"; -import {ProposalReceiverMock} from "test/ProposalReceiverMock.sol"; - -abstract contract FlexVotingClientTest is Test { - MockFlexVotingClient flexClient; - GovToken token; - FractionalGovernor governor; - ProposalReceiverMock receiver; - - // This max is a limitation of GovernorCountingFractional's vote storage size. - // See GovernorCountingFractional.ProposalVote struct. - uint256 MAX_VOTES = type(uint128).max; - - // The highest valid vote type, represented as a uint256. - uint256 MAX_VOTE_TYPE = uint256(type(GCS.VoteType).max); - - function setUp() public { - if (_timestampClock()) token = new TimestampGovToken(); - else token = new GovToken(); - vm.label(address(token), "token"); - - governor = new FractionalGovernor("Governor", IVotes(token)); - vm.label(address(governor), "governor"); - - flexClient = new MockFlexVotingClient(address(governor)); - vm.label(address(flexClient), "flexClient"); - - receiver = new ProposalReceiverMock(); - vm.label(address(receiver), "receiver"); - } - - function _timestampClock() internal pure virtual returns (bool); - - function _now() internal view returns (uint48) { - return token.clock(); - } - - function _advanceTimeBy(uint256 _timeUnits) internal { - if (_timestampClock()) vm.warp(block.timestamp + _timeUnits); - else vm.roll(block.number + _timeUnits); - } - - function _advanceTimeTo(uint256 _timepoint) internal { - if (_timestampClock()) vm.warp(_timepoint); - else vm.roll(_timepoint); - } - - function _mintGovAndApproveFlexClient(address _user, uint208 _amount) public { - vm.assume(_user != address(0)); - token.exposed_mint(_user, _amount); - vm.prank(_user); - token.approve(address(flexClient), type(uint256).max); - } - - function _mintGovAndDepositIntoFlexClient(address _address, uint208 _amount) internal { - _mintGovAndApproveFlexClient(_address, _amount); - vm.prank(_address); - flexClient.deposit(_amount); - } - - function _createAndSubmitProposal() internal returns (uint256 proposalId) { - // Proposal will underflow if we're on the zero block - if (_now() == 0) _advanceTimeBy(1); - - // Create a proposal - bytes memory receiverCallData = abi.encodeWithSignature("mockReceiverFunction()"); - address[] memory targets = new address[](1); - uint256[] memory values = new uint256[](1); - bytes[] memory calldatas = new bytes[](1); - targets[0] = address(receiver); - values[0] = 0; // No ETH will be sent. - calldatas[0] = receiverCallData; - - // Submit the proposal. - proposalId = governor.propose(targets, values, calldatas, "A great proposal"); - assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Pending)); - - // Advance proposal to active state. - _advanceTimeTo(governor.proposalSnapshot(proposalId) + 1); - assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active)); - } - - function _assumeSafeUser(address _user) internal view { - vm.assume(_user != address(flexClient)); - vm.assume(_user != address(0)); - } - - function _randVoteType(uint8 _seed) public view returns (GCS.VoteType) { - return - GCS.VoteType(uint8(bound(uint256(_seed), uint256(type(GCS.VoteType).min), MAX_VOTE_TYPE))); - } - - function _assumeSafeVoteParams(address _account, uint208 _voteWeight) - public - view - returns (uint208 _boundedWeight) - { - _assumeSafeUser(_account); - _boundedWeight = uint208(bound(_voteWeight, 1, MAX_VOTES)); - } - - function _assumeSafeVoteParams(address _account, uint208 _voteWeight, uint8 _supportType) - public - view - returns (uint208 _boundedWeight, GCS.VoteType _boundedSupport) - { - _assumeSafeUser(_account); - _boundedSupport = _randVoteType(_supportType); - _boundedWeight = uint208(bound(_voteWeight, 1, MAX_VOTES)); - } -} - -abstract contract Deployment is FlexVotingClientTest { - function test_FlexVotingClientDeployment() public view { - assertEq(token.name(), "Governance Token"); - assertEq(token.symbol(), "GOV"); - - assertEq(address(flexClient.GOVERNOR()), address(governor)); - assertEq(token.delegates(address(flexClient)), address(flexClient)); - - assertEq(governor.name(), "Governor"); - assertEq(address(governor.token()), address(token)); - } -} - -abstract contract Constructor is FlexVotingClientTest { - function test_SetsGovernor() public view { - assertEq(address(flexClient.GOVERNOR()), address(governor)); - } - - function test_SelfDelegates() public view { - assertEq(token.delegates(address(flexClient)), address(flexClient)); - } -} - -// Contract name has a leading underscore for scopelint spec support. -abstract contract _RawBalanceOf is FlexVotingClientTest { - function testFuzz_ReturnsZeroForNonDepositors(address _user) public view { - _assumeSafeUser(_user); - assertEq(flexClient.exposed_rawBalanceOf(_user), 0); - } - - function testFuzz_IncreasesOnDeposit(address _user, uint208 _amount) public { - _assumeSafeUser(_user); - _amount = uint208(bound(_amount, 1, MAX_VOTES)); - - // Deposit some gov. - _mintGovAndDepositIntoFlexClient(_user, _amount); - - assertEq(flexClient.exposed_rawBalanceOf(_user), _amount); - } - - function testFuzz_DecreasesOnWithdrawal(address _user, uint208 _amount) public { - _assumeSafeUser(_user); - _amount = uint208(bound(_amount, 1, MAX_VOTES)); - - // Deposit some gov. - _mintGovAndDepositIntoFlexClient(_user, _amount); - - assertEq(flexClient.exposed_rawBalanceOf(_user), _amount); - - vm.prank(_user); - flexClient.withdraw(_amount); - assertEq(flexClient.exposed_rawBalanceOf(_user), 0); - } - - function testFuzz_UnaffectedByBorrow(address _user, uint208 _deposit, uint208 _borrow) public { - _assumeSafeUser(_user); - _deposit = uint208(bound(_deposit, 1, MAX_VOTES)); - _borrow = uint208(bound(_borrow, 1, _deposit)); - - // Deposit some gov. - _mintGovAndDepositIntoFlexClient(_user, _deposit); - - assertEq(flexClient.exposed_rawBalanceOf(_user), _deposit); - - vm.prank(_user); - flexClient.borrow(_borrow); - - // Raw balance is unchanged. - assertEq(flexClient.exposed_rawBalanceOf(_user), _deposit); - } -} - -// Contract name has a leading underscore for scopelint spec support. -abstract contract _CastVoteReasonString is FlexVotingClientTest { - function test_ReturnsDescriptiveString() public { - assertEq( - flexClient.exposed_castVoteReasonString(), "rolled-up vote from governance token holders" - ); - } -} - -// Contract name has a leading underscore for scopelint spec support. -abstract contract _SelfDelegate is FlexVotingClientTest { - function testFuzz_SetsClientAsTheDelegate(address _delegatee) public { - vm.assume(_delegatee != address(0)); - vm.assume(_delegatee != address(flexClient)); - - // We self-delegate in the constructor, so we need to first un-delegate for - // this test to be meaningful. - vm.prank(address(flexClient)); - token.delegate(_delegatee); - assertEq(token.delegates(address(flexClient)), _delegatee); - - flexClient.exposed_selfDelegate(); - assertEq(token.delegates(address(flexClient)), address(flexClient)); - } -} - -// Contract name has a leading underscore for scopelint spec support. -abstract contract _CheckpointRawBalanceOf is FlexVotingClientTest { - function testFuzz_StoresTheRawBalanceWithTheTimepoint( - address _user, - uint208 _amount, - uint48 _timepoint - ) public { - vm.assume(_user != address(flexClient)); - _timepoint = uint48(bound(_timepoint, _now() + 1, type(uint48).max)); - _amount = uint208(bound(_amount, 1, MAX_VOTES)); - - flexClient.exposed_setDeposits(_user, _amount); - assertEq(flexClient.getPastRawBalance(_user, _timepoint), 0); - - _advanceTimeTo(_timepoint); - - flexClient.exposed_checkpointRawBalanceOf(_user); - assertEq(flexClient.getPastRawBalance(_user, _timepoint), _amount); - } -} - -abstract contract _CheckpointTotalBalance is FlexVotingClientTest { - int256 MAX_UINT208 = int256(uint256(type(uint208).max)); - - function testFuzz_writesACheckpointAtClockTime(int256 _value, uint48 _timepoint) public { - _timepoint = uint48(bound(_timepoint, 1, type(uint48).max - 1)); - _value = bound(_value, 1, MAX_UINT208); - assertEq(flexClient.exposed_latestTotalBalance(), 0); - - _advanceTimeTo(_timepoint); - flexClient.exposed_checkpointTotalBalance(_value); - _advanceTimeBy(1); - - assertEq(flexClient.getPastTotalBalance(_timepoint), uint256(_value)); - assertEq(flexClient.exposed_latestTotalBalance(), uint256(_value)); - } - - function testFuzz_checkpointsTheTotalBalanceDeltaAtClockTime( - int256 _initBalance, - int256 _delta, - uint48 _timepoint - ) public { - _timepoint = uint48(bound(_timepoint, 1, type(uint48).max - 1)); - _initBalance = bound(_initBalance, 1, MAX_UINT208 - 1); - _delta = bound(_delta, -_initBalance, MAX_UINT208 - _initBalance); - flexClient.exposed_checkpointTotalBalance(_initBalance); - - _advanceTimeTo(_timepoint); - flexClient.exposed_checkpointTotalBalance(_delta); - _advanceTimeBy(1); - - assertEq(flexClient.getPastTotalBalance(_timepoint), uint256(_initBalance + _delta)); - } - - function testFuzz_RevertIf_negativeDeltaWraps(int256 delta, uint208 balance) public { - // Math.abs(delta) must be > balance for the concerning scenario to arise. - delta = bound(delta, type(int256).min, -int256(uint256(balance)) - 1); - assertTrue(SignedMath.abs(delta) > balance); - - // Effectively this function has 5 steps. - // - // Step 1: Cast balance up from a uint208 to a uint256. - // Safe, since uint256 is bigger. - uint256 balanceUint256 = uint256(balance); - - // Step 2: Cast balance down to int256. - // Safe, since uint208.max < int256.max. - int256 balanceInt256 = int256(balanceUint256); - - // Step 3: Add the delta. The result might be negative. - int256 netBalanceInt256 = balanceInt256 + delta; - - // Step 4: Cast back to uint256. - // - // This is where things get a little scary. - // uint256(int256) = 2^256 + int256, for int256 < 0. - // If |delta| > balance, then netBalance will be a negative int256 and when - // we cast to uint256 it will wrap to a very large positive number. - uint256 netBalanceUint256 = uint256(netBalanceInt256); - - // Step 5: Cast back to uint208. - // We need to ensure that when |delta| > balance: - // uint256(balance + delta) > uint208.max - // As this will cause the safecast to fail. - assert(netBalanceUint256 > type(uint208).max); - vm.expectRevert(); - SafeCast.toUint208(netBalanceUint256); - } +import { + Deployment, + Constructor, + _RawBalanceOf, + _CastVoteReasonString, + _SelfDelegate, + _CheckpointVoteWeightOf, + _CheckpointTotalVoteWeight, + GetPastRawBalance, + GetPastTotalBalance, + Withdraw, + Deposit, + ExpressVote, + CastVote, + Borrow +} from "test/SharedFlexVoting.t.sol"; - function testFuzz_RevertIf_withdrawalFromZero(int256 _withdraw) public { - _withdraw = bound(_withdraw, type(int208).min, -1); - vm.expectRevert(); - flexClient.exposed_checkpointTotalBalance(_withdraw); - } - - function testFuzz_RevertIf_withdrawalExceedsDeposit(int256 _deposit, int256 _withdraw) public { - _deposit = bound(_deposit, 1, type(int208).max - 1); - _withdraw = bound(_withdraw, type(int208).min, (-1 * _deposit) - 1); - - flexClient.exposed_checkpointTotalBalance(_deposit); - vm.expectRevert(); - flexClient.exposed_checkpointTotalBalance(_withdraw); - } - - function testFuzz_RevertIf_depositsOverflow(int256 _deposit1, int256 _deposit2) public { - int256 _max = int256(uint256(type(uint208).max)); - _deposit1 = bound(_deposit1, 1, _max); - _deposit2 = bound(_deposit2, 1 + _max - _deposit1, _max); - - flexClient.exposed_checkpointTotalBalance(_deposit1); - vm.expectRevert(); - flexClient.exposed_checkpointTotalBalance(_deposit2); - } -} - -abstract contract GetPastRawBalance is FlexVotingClientTest { - function testFuzz_ReturnsZeroForUsersWithoutDeposits( - address _depositor, - address _nonDepositor, - uint208 _amount - ) public { - vm.assume(_depositor != address(flexClient)); - vm.assume(_nonDepositor != address(flexClient)); - vm.assume(_nonDepositor != _depositor); - _amount = uint208(bound(_amount, 1, MAX_VOTES)); - - _advanceTimeBy(1); - assertEq(flexClient.getPastRawBalance(_depositor, 0), 0); - assertEq(flexClient.getPastRawBalance(_nonDepositor, 0), 0); - - _mintGovAndDepositIntoFlexClient(_depositor, _amount); - _advanceTimeBy(1); - - assertEq(flexClient.getPastRawBalance(_depositor, _now() - 1), _amount); - assertEq(flexClient.getPastRawBalance(_nonDepositor, _now() - 1), 0); - } - - function testFuzz_ReturnsCurrentValueForFutureTimepoints( - address _user, - uint208 _amount, - uint48 _timepoint - ) public { - vm.assume(_user != address(flexClient)); - _timepoint = uint48(bound(_timepoint, _now() + 1, type(uint48).max)); - _amount = uint208(bound(_amount, 1, MAX_VOTES)); - - _mintGovAndDepositIntoFlexClient(_user, _amount); - - assertEq(flexClient.getPastRawBalance(_user, _now()), _amount); - assertEq(flexClient.getPastRawBalance(_user, _timepoint), _amount); - - _advanceTimeTo(_timepoint); - - assertEq(flexClient.getPastRawBalance(_user, _now()), _amount); - } - - function testFuzz_ReturnsUserBalanceAtAGivenTimepoint( - address _user, - uint208 _amountA, - uint208 _amountB, - uint48 _timepoint - ) public { - vm.assume(_user != address(flexClient)); - _timepoint = uint48(bound(_timepoint, _now() + 1, type(uint48).max)); - _amountA = uint208(bound(_amountA, 1, MAX_VOTES)); - _amountB = uint208(bound(_amountB, 0, MAX_VOTES - _amountA)); - - uint48 _initTimepoint = _now(); - _mintGovAndDepositIntoFlexClient(_user, _amountA); - - _advanceTimeTo(_timepoint); - - _mintGovAndDepositIntoFlexClient(_user, _amountB); - _advanceTimeBy(1); - - uint48 _zeroTimepoint = 0; - assertEq(flexClient.getPastRawBalance(_user, _zeroTimepoint), 0); - assertEq(flexClient.getPastRawBalance(_user, _initTimepoint), _amountA); - assertEq(flexClient.getPastRawBalance(_user, _timepoint), _amountA + _amountB); - } -} - -abstract contract GetPastTotalBalance is FlexVotingClientTest { - function testFuzz_ReturnsZeroWithoutDeposits(uint48 _future) public view { - uint48 _zeroTimepoint = 0; - assertEq(flexClient.getPastTotalBalance(_zeroTimepoint), 0); - assertEq(flexClient.getPastTotalBalance(_future), 0); - } - - function testFuzz_ReturnsCurrentValueForFutureTimepoints( - address _user, - uint208 _amount, - uint48 _future - ) public { - vm.assume(_user != address(flexClient)); - _future = uint48(bound(_future, _now() + 1, type(uint48).max)); - _amount = uint208(bound(_amount, 1, MAX_VOTES)); - - _mintGovAndDepositIntoFlexClient(_user, _amount); - - assertEq(flexClient.getPastTotalBalance(_now()), _amount); - assertEq(flexClient.getPastTotalBalance(_future), _amount); - - _advanceTimeTo(_future); - - assertEq(flexClient.getPastTotalBalance(_now()), _amount); - } - - function testFuzz_SumsAllUserDeposits( - address _userA, - uint208 _amountA, - address _userB, - uint208 _amountB - ) public { - vm.assume(_userA != address(flexClient)); - vm.assume(_userB != address(flexClient)); - vm.assume(_userA != _userB); - - _amountA = uint208(bound(_amountA, 1, MAX_VOTES)); - _amountB = uint208(bound(_amountB, 0, MAX_VOTES - _amountA)); - - _mintGovAndDepositIntoFlexClient(_userA, _amountA); - _mintGovAndDepositIntoFlexClient(_userB, _amountB); - - _advanceTimeBy(1); - - assertEq(flexClient.getPastTotalBalance(_now()), _amountA + _amountB); - } - - function testFuzz_ReturnsTotalDepositsAtAGivenTimepoint( - address _userA, - uint208 _amountA, - address _userB, - uint208 _amountB, - uint48 _future - ) public { - vm.assume(_userA != address(flexClient)); - vm.assume(_userB != address(flexClient)); - vm.assume(_userA != _userB); - _future = uint48(bound(_future, _now() + 1, type(uint48).max)); - - _amountA = uint208(bound(_amountA, 1, MAX_VOTES)); - _amountB = uint208(bound(_amountB, 0, MAX_VOTES - _amountA)); - - assertEq(flexClient.getPastTotalBalance(_now()), 0); - - _mintGovAndDepositIntoFlexClient(_userA, _amountA); - _advanceTimeTo(_future); - _mintGovAndDepositIntoFlexClient(_userB, _amountB); - - assertEq(flexClient.getPastTotalBalance(_now() - _future + 1), _amountA); - assertEq(flexClient.getPastTotalBalance(_now()), _amountA + _amountB); +// Block number tests. +contract BlockNumberClock_Deployment is Deployment { + function _timestampClock() internal pure override returns (bool) { + return false; } -} - -abstract contract Withdraw is FlexVotingClientTest { - function testFuzz_UserCanWithdrawGovTokens(address _lender, address _borrower, uint208 _amount) - public - { - _amount = uint208(bound(_amount, 0, type(uint208).max)); - vm.assume(_lender != address(flexClient)); - vm.assume(_borrower != address(flexClient)); - vm.assume(_borrower != address(0)); - vm.assume(_lender != _borrower); - - uint256 _initBalance = token.balanceOf(_borrower); - assertEq(flexClient.deposits(_borrower), 0); - assertEq(flexClient.borrowTotal(_borrower), 0); - _mintGovAndDepositIntoFlexClient(_lender, _amount); - assertEq(flexClient.deposits(_lender), _amount); - - // Borrow the funds. - vm.prank(_borrower); - flexClient.borrow(_amount); - - assertEq(token.balanceOf(_borrower), _initBalance + _amount); - assertEq(flexClient.borrowTotal(_borrower), _amount); - - // Deposit totals are unaffected. - assertEq(flexClient.deposits(_lender), _amount); - assertEq(flexClient.deposits(_borrower), 0); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } - - // `borrow`s affects on vote weights are tested in Vote contract below. } -abstract contract Deposit is FlexVotingClientTest { - function testFuzz_UserCanDepositGovTokens(address _user, uint208 _amount) public { - _amount = uint208(bound(_amount, 0, type(uint208).max)); - vm.assume(_user != address(flexClient)); - uint256 initialBalance = token.balanceOf(_user); - assertEq(flexClient.deposits(_user), 0); - - _mintGovAndDepositIntoFlexClient(_user, _amount); - - assertEq(token.balanceOf(address(flexClient)), _amount); - assertEq(token.balanceOf(_user), initialBalance); - assertEq(token.getVotes(address(flexClient)), _amount); - - // Confirm internal accounting has updated. - assertEq(flexClient.deposits(_user), _amount); +contract BlockNumberClock_Constructor is Constructor { + function _timestampClock() internal pure override returns (bool) { + return false; } - function testFuzz_DepositsAreCheckpointed( - address _user, - uint208 _amountA, - uint208 _amountB, - uint24 _depositDelay - ) public { - _amountA = uint208(bound(_amountA, 1, MAX_VOTES)); - _amountB = uint208(bound(_amountB, 0, MAX_VOTES - _amountA)); - - // Deposit some gov. - _mintGovAndDepositIntoFlexClient(_user, _amountA); - assertEq(flexClient.deposits(_user), _amountA); - - _advanceTimeBy(1); // Advance so that we can look at checkpoints. - - // We can still retrieve the user's balance at the given time. - uint256 _checkpoint1 = _now() - 1; - assertEq( - flexClient.getPastRawBalance(_user, _checkpoint1), - _amountA, - "user's first deposit was not properly checkpointed" - ); - - uint256 _checkpoint2 = _now() + _depositDelay; - _advanceTimeTo(_checkpoint2); - - // Deposit some more. - _mintGovAndDepositIntoFlexClient(_user, _amountB); - assertEq(flexClient.deposits(_user), _amountA + _amountB); - - _advanceTimeBy(1); // Advance so that we can look at checkpoints. - - assertEq( - flexClient.getPastRawBalance(_user, _checkpoint1), - _amountA, - "user's first deposit was not properly checkpointed" - ); - assertEq( - flexClient.getPastRawBalance(_user, _checkpoint2), - _amountA + _amountB, - "user's second deposit was not properly checkpointed" - ); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } -abstract contract ExpressVote is FlexVotingClientTest { - function testFuzz_IncrementsInternalAccouting( - address _user, - uint208 _voteWeight, - uint8 _supportType - ) public { - GCS.VoteType _voteType; - (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_user, _voteWeight); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // _user should now be able to express his/her vote on the proposal. - vm.prank(_user); - flexClient.expressVote(_proposalId, uint8(_voteType)); - (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient.proposalVotes(_proposalId); - assertEq(_forVotesExpressed, _voteType == GCS.VoteType.For ? _voteWeight : 0); - assertEq(_againstVotesExpressed, _voteType == GCS.VoteType.Against ? _voteWeight : 0); - assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _voteWeight : 0); - - // No votes have been cast yet. - (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = - governor.proposalVotes(_proposalId); - assertEq(_forVotes, 0); - assertEq(_againstVotes, 0); - assertEq(_abstainVotes, 0); - } - - function testFuzz_RevertWhen_DepositingAfterProposal( - address _user, - uint208 _voteWeight, - uint8 _supportType - ) public { - GCS.VoteType _voteType; - (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); - - // Create the proposal *before* the user deposits anything. - uint256 _proposalId = _createAndSubmitProposal(); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_user, _voteWeight); - - // Now try to express a voting preference on the proposal. - assertEq(flexClient.deposits(_user), _voteWeight); - vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); - vm.prank(_user); - flexClient.expressVote(_proposalId, uint8(_voteType)); - } - - function testFuzz_RevertWhen_NoClientWeightButTokenWeight( - address _user, - uint208 _voteWeight, - uint8 _supportType - ) public { - GCS.VoteType _voteType; - (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); - - // Mint gov but do not deposit. - _mintGovAndApproveFlexClient(_user, _voteWeight); - assertEq(token.balanceOf(_user), _voteWeight); - assertEq(flexClient.deposits(_user), 0); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // _user should NOT be able to express his/her vote on the proposal. - vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); - vm.prank(_user); - flexClient.expressVote(_proposalId, uint8(_voteType)); - - // Deposit into the client. - vm.prank(_user); - flexClient.deposit(_voteWeight); - assertEq(flexClient.deposits(_user), _voteWeight); - - // _user should still NOT be able to express his/her vote on the proposal. - // Despite having a deposit balance, he/she didn't have a balance at the - // proposal snapshot. - vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); - vm.prank(_user); - flexClient.expressVote(_proposalId, uint8(_voteType)); - } - - function testFuzz_RevertOn_DoubleVotes(address _user, uint208 _voteWeight, uint8 _supportType) - public - { - GCS.VoteType _voteType; - (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_user, _voteWeight); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // _user should now be able to express his/her vote on the proposal. - vm.prank(_user); - flexClient.expressVote(_proposalId, uint8(_voteType)); - - ( - uint256 _againstVotesExpressedInit, - uint256 _forVotesExpressedInit, - uint256 _abstainVotesExpressedInit - ) = flexClient.proposalVotes(_proposalId); - assertEq(_forVotesExpressedInit, _voteType == GCS.VoteType.For ? _voteWeight : 0); - assertEq(_againstVotesExpressedInit, _voteType == GCS.VoteType.Against ? _voteWeight : 0); - assertEq(_abstainVotesExpressedInit, _voteType == GCS.VoteType.Abstain ? _voteWeight : 0); - - // Vote early and often! - vm.expectRevert(FVC.FlexVotingClient__AlreadyVoted.selector); - vm.prank(_user); - flexClient.expressVote(_proposalId, uint8(_voteType)); - - // No votes changed. - (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient.proposalVotes(_proposalId); - assertEq(_forVotesExpressed, _forVotesExpressedInit); - assertEq(_againstVotesExpressed, _againstVotesExpressedInit); - assertEq(_abstainVotesExpressed, _abstainVotesExpressedInit); +contract BlockNumberClock__RawBalanceOf is _RawBalanceOf { + function _timestampClock() internal pure override returns (bool) { + return false; } - function testFuzz_RevertOn_UnknownVoteType(address _user, uint208 _voteWeight, uint8 _supportType) - public - { - // Force vote type to be unrecognized. - _supportType = uint8(bound(_supportType, MAX_VOTE_TYPE + 1, type(uint8).max)); - - _assumeSafeUser(_user); - _voteWeight = uint208(bound(_voteWeight, 1, MAX_VOTES)); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_user, _voteWeight); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // Now try to express a voting preference with a bogus support type. - vm.expectRevert(FVC.FlexVotingClient__InvalidSupportValue.selector); - vm.prank(_user); - flexClient.expressVote(_proposalId, _supportType); - } - - function testFuzz_RevertOn_UnknownProposal( - address _user, - uint208 _voteWeight, - uint8 _supportType, - uint256 _proposalId - ) public { - _assumeSafeUser(_user); - _voteWeight = uint208(bound(_voteWeight, 1, MAX_VOTES)); - - // Confirm that we've pulled a bogus proposal number. - // This is the condition Governor.state checks for when raising - // GovernorNonexistentProposal. - vm.assume(governor.proposalSnapshot(_proposalId) == 0); - - // Force vote type to be unrecognized. - _supportType = uint8(bound(_supportType, MAX_VOTE_TYPE + 1, type(uint8).max)); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_user, _voteWeight); - - // Create a real proposal to verify the two won't be mixed up when - // expressing. - uint256 _id = _createAndSubmitProposal(); - assert(_proposalId != _id); - - // Now try to express a voting preference on the bogus proposal. - vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); - vm.prank(_user); - flexClient.expressVote(_proposalId, _supportType); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } -abstract contract CastVote is FlexVotingClientTest { - function testFuzz_SubmitsVotesToGovernor(address _user, uint208 _voteWeight, uint8 _supportType) - public - { - GCS.VoteType _voteType; - (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_user, _voteWeight); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // _user should now be able to express his/her vote on the proposal. - vm.prank(_user); - flexClient.expressVote(_proposalId, uint8(_voteType)); - (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient.proposalVotes(_proposalId); - assertEq(_forVotesExpressed, _voteType == GCS.VoteType.For ? _voteWeight : 0); - assertEq(_againstVotesExpressed, _voteType == GCS.VoteType.Against ? _voteWeight : 0); - assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _voteWeight : 0); - - // No votes have been cast yet. - (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = - governor.proposalVotes(_proposalId); - assertEq(_forVotes, 0); - assertEq(_againstVotes, 0); - assertEq(_abstainVotes, 0); - - // Submit votes on behalf of the flexClient. - flexClient.castVote(_proposalId); - - // Governor should now record votes from the flexClient. - (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); - assertEq(_forVotes, _forVotesExpressed); - assertEq(_againstVotes, _againstVotesExpressed); - assertEq(_abstainVotes, _abstainVotesExpressed); - } - - function testFuzz_WeightIsSnapshotDependent( - address _user, - uint208 _voteWeightA, - uint208 _voteWeightB, - uint8 _supportType - ) public { - GCS.VoteType _voteType; - (_voteWeightA, _voteType) = _assumeSafeVoteParams(_user, _voteWeightA, _supportType); - _voteWeightB = _assumeSafeVoteParams(_user, _voteWeightB); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_user, _voteWeightA); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // Sometime later the user deposits some more. - _advanceTimeTo(governor.proposalDeadline(_proposalId) - 1); - _mintGovAndDepositIntoFlexClient(_user, _voteWeightB); - - vm.prank(_user); - flexClient.expressVote(_proposalId, uint8(_voteType)); - - // The internal proposal vote weight should not reflect the new deposit weight. - (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient.proposalVotes(_proposalId); - assertEq(_forVotesExpressed, _voteType == GCS.VoteType.For ? _voteWeightA : 0); - assertEq(_againstVotesExpressed, _voteType == GCS.VoteType.Against ? _voteWeightA : 0); - assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _voteWeightA : 0); - - // Submit votes on behalf of the flexClient. - flexClient.castVote(_proposalId); - - // Votes cast should likewise reflect only the earlier balance. - (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = - governor.proposalVotes(_proposalId); - assertEq(_forVotes, _voteType == GCS.VoteType.For ? _voteWeightA : 0); - assertEq(_againstVotes, _voteType == GCS.VoteType.Against ? _voteWeightA : 0); - assertEq(_abstainVotes, _voteType == GCS.VoteType.Abstain ? _voteWeightA : 0); - } - - function testFuzz_TracksMultipleUsersVotes( - address _userA, - address _userB, - uint208 _voteWeightA, - uint208 _voteWeightB - ) public { - vm.assume(_userA != _userB); - _assumeSafeUser(_userA); - _assumeSafeUser(_userB); - _voteWeightA = uint208(bound(_voteWeightA, 1, MAX_VOTES - 1)); - _voteWeightB = uint208(bound(_voteWeightB, 1, MAX_VOTES - _voteWeightA)); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_userA, _voteWeightA); - _mintGovAndDepositIntoFlexClient(_userB, _voteWeightB); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // users should now be able to express their votes on the proposal. - vm.prank(_userA); - flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Against)); - vm.prank(_userB); - flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Abstain)); - - (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient.proposalVotes(_proposalId); - assertEq(_forVotesExpressed, 0); - assertEq(_againstVotesExpressed, _voteWeightA); - assertEq(_abstainVotesExpressed, _voteWeightB); - - // The governor should have not recieved any votes yet. - (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = - governor.proposalVotes(_proposalId); - assertEq(_forVotes, 0); - assertEq(_againstVotes, 0); - assertEq(_abstainVotes, 0); - - // Submit votes on behalf of the flexClient. - flexClient.castVote(_proposalId); - - // Governor should now record votes for the flexClient. - (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); - assertEq(_forVotes, 0); - assertEq(_againstVotes, _voteWeightA); - assertEq(_abstainVotes, _voteWeightB); - } - - struct VoteWeightIsScaledTestVars { - address userA; - address userB; - address userC; - address userD; - uint208 voteWeightA; - uint8 supportTypeA; - uint208 voteWeightB; - uint8 supportTypeB; - uint208 borrowAmountC; - uint208 borrowAmountD; - } - - function testFuzz_ScalesVoteWeightBasedOnPoolBalance(VoteWeightIsScaledTestVars memory _vars) - public - { - _vars.userA = address(0xbeef); - _vars.userB = address(0xbabe); - _vars.userC = address(0xf005ba11); - _vars.userD = address(0xba5eba11); - - _vars.supportTypeA = uint8(bound(_vars.supportTypeA, 0, MAX_VOTE_TYPE)); - _vars.supportTypeB = uint8(bound(_vars.supportTypeB, 0, MAX_VOTE_TYPE)); - - _vars.voteWeightA = uint208(bound(_vars.voteWeightA, 1e4, MAX_VOTES - 1e4 - 1)); - _vars.voteWeightB = uint208(bound(_vars.voteWeightB, 1e4, MAX_VOTES - _vars.voteWeightA - 1)); - - uint208 _maxBorrowWeight = _vars.voteWeightA + _vars.voteWeightB; - _vars.borrowAmountC = uint208(bound(_vars.borrowAmountC, 1, _maxBorrowWeight - 1)); - _vars.borrowAmountD = - uint208(bound(_vars.borrowAmountD, 1, _maxBorrowWeight - _vars.borrowAmountC)); - - // These are here just as a sanity check that all of the bounding above worked. - vm.assume(_vars.voteWeightA + _vars.voteWeightB < MAX_VOTES); - vm.assume(_vars.voteWeightA + _vars.voteWeightB >= _vars.borrowAmountC + _vars.borrowAmountD); - - // Mint and deposit. - _mintGovAndDepositIntoFlexClient(_vars.userA, _vars.voteWeightA); - _mintGovAndDepositIntoFlexClient(_vars.userB, _vars.voteWeightB); - uint256 _initDepositWeight = token.balanceOf(address(flexClient)); - - // Borrow from the flexClient, decreasing its token balance. - vm.prank(_vars.userC); - flexClient.borrow(_vars.borrowAmountC); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // Jump ahead to the proposal snapshot to lock in the flexClient's balance. - _advanceTimeTo(governor.proposalSnapshot(_proposalId) + 1); - uint256 _expectedVotingWeight = token.balanceOf(address(flexClient)); - assert(_expectedVotingWeight < _initDepositWeight); - - // A+B express votes - vm.prank(_vars.userA); - flexClient.expressVote(_proposalId, _vars.supportTypeA); - vm.prank(_vars.userB); - flexClient.expressVote(_proposalId, _vars.supportTypeB); - - // Borrow more from the flexClient, just to confirm that the vote weight will be based - // on the snapshot blocktime/number. - vm.prank(_vars.userD); - flexClient.borrow(_vars.borrowAmountD); - - // Submit votes on behalf of the flexClient. - flexClient.castVote(_proposalId); - - // Vote should be cast as a percentage of the depositer's expressed types, since - // the actual weight is different from the deposit weight. - (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = - governor.proposalVotes(_proposalId); - - // These can differ because votes are rounded. - assertApproxEqAbs(_againstVotes + _forVotes + _abstainVotes, _expectedVotingWeight, 1); - - if (_vars.supportTypeA == _vars.supportTypeB) { - assertEq(_forVotes, _vars.supportTypeA == uint8(GCS.VoteType.For) ? _expectedVotingWeight : 0); - assertEq( - _againstVotes, _vars.supportTypeA == uint8(GCS.VoteType.Against) ? _expectedVotingWeight : 0 - ); - assertEq( - _abstainVotes, _vars.supportTypeA == uint8(GCS.VoteType.Abstain) ? _expectedVotingWeight : 0 - ); - } else { - uint256 _expectedVotingWeightA = - (_vars.voteWeightA * _expectedVotingWeight) / _initDepositWeight; - uint256 _expectedVotingWeightB = - (_vars.voteWeightB * _expectedVotingWeight) / _initDepositWeight; - - // We assert the weight is within a range of 1 because scaled weights are sometimes floored. - if (_vars.supportTypeA == uint8(GCS.VoteType.For)) { - assertApproxEqAbs(_forVotes, _expectedVotingWeightA, 1); - } - if (_vars.supportTypeB == uint8(GCS.VoteType.For)) { - assertApproxEqAbs(_forVotes, _expectedVotingWeightB, 1); - } - if (_vars.supportTypeA == uint8(GCS.VoteType.Against)) { - assertApproxEqAbs(_againstVotes, _expectedVotingWeightA, 1); - } - if (_vars.supportTypeB == uint8(GCS.VoteType.Against)) { - assertApproxEqAbs(_againstVotes, _expectedVotingWeightB, 1); - } - if (_vars.supportTypeA == uint8(GCS.VoteType.Abstain)) { - assertApproxEqAbs(_abstainVotes, _expectedVotingWeightA, 1); - } - if (_vars.supportTypeB == uint8(GCS.VoteType.Abstain)) { - assertApproxEqAbs(_abstainVotes, _expectedVotingWeightB, 1); - } - } - } - - // This is important because it ensures you can't *gain* voting weight by - // getting other people to not vote. - function testFuzz_AbandonsUnexpressedVotingWeight( - uint208 _voteWeightA, - uint208 _voteWeightB, - uint8 _supportTypeA, - uint208 _borrowAmount - ) public { - // We need to do this to prevent: - // "CompilerError: Stack too deep, try removing local variables." - address[3] memory _users = [ - address(0xbeef), // userA - address(0xbabe), // userB - address(0xf005ba11) // userC - ]; - - // Requirements: - // voteWeights and borrow each >= 1 - // voteWeights and borrow each <= uint128.max - // _voteWeightA + _voteWeightB < MAX_VOTES - // _voteWeightA + _voteWeightB > _borrowAmount - _voteWeightA = uint208(bound(_voteWeightA, 1, MAX_VOTES - 2)); - _voteWeightB = uint208(bound(_voteWeightB, 1, MAX_VOTES - _voteWeightA - 1)); - _borrowAmount = uint208(bound(_borrowAmount, 1, _voteWeightA + _voteWeightB - 1)); - GCS.VoteType _voteTypeA = _randVoteType(_supportTypeA); - - // Mint and deposit. - _mintGovAndDepositIntoFlexClient(_users[0], _voteWeightA); - _mintGovAndDepositIntoFlexClient(_users[1], _voteWeightB); - uint256 _initDepositWeight = token.balanceOf(address(flexClient)); - - // Borrow from the flexClient, decreasing its token balance. - vm.prank(_users[2]); - flexClient.borrow(_borrowAmount); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // Jump ahead to the proposal snapshot to lock in the flexClient's balance. - _advanceTimeTo(governor.proposalSnapshot(_proposalId) + 1); - uint256 _totalPossibleVotingWeight = token.balanceOf(address(flexClient)); - - uint256 _fullVotingWeight = token.balanceOf(address(flexClient)); - assert(_fullVotingWeight < _initDepositWeight); - assertEq(_fullVotingWeight, _voteWeightA + _voteWeightB - _borrowAmount); - - // Only user A expresses a vote. - vm.prank(_users[0]); - flexClient.expressVote(_proposalId, uint8(_voteTypeA)); - - // Submit votes on behalf of the flexClient. - flexClient.castVote(_proposalId); - - // Vote should be cast as a percentage of the depositer's expressed types, since - // the actual weight is different from the deposit weight. - (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = - governor.proposalVotes(_proposalId); - - uint256 _expectedVotingWeightA = (_voteWeightA * _fullVotingWeight) / _initDepositWeight; - uint256 _expectedVotingWeightB = (_voteWeightB * _fullVotingWeight) / _initDepositWeight; - - // The flexClient *could* have voted with this much weight. - assertApproxEqAbs( - _totalPossibleVotingWeight, _expectedVotingWeightA + _expectedVotingWeightB, 1 - ); - - // Actually, though, the flexClient did not vote with all of the weight it could have. - // VoterB's votes were never cast because he/she did not express his/her preference. - assertApproxEqAbs( - _againstVotes + _forVotes + _abstainVotes, // The total actual weight. - _expectedVotingWeightA, // VoterB's weight has been abandoned, only A's is counted. - 1 - ); - - // We assert the weight is within a range of 1 because scaled weights are sometimes floored. - if (_voteTypeA == GCS.VoteType.For) assertApproxEqAbs(_forVotes, _expectedVotingWeightA, 1); - if (_voteTypeA == GCS.VoteType.Against) { - assertApproxEqAbs(_againstVotes, _expectedVotingWeightA, 1); - } - if (_voteTypeA == GCS.VoteType.Abstain) { - assertApproxEqAbs(_abstainVotes, _expectedVotingWeightA, 1); - } - } - - function testFuzz_VotingWeightIsUnaffectedByDepositsAfterProposal( - uint208 _voteWeightA, - uint208 _voteWeightB, - uint8 _supportTypeA - ) public { - // We need to do this to prevent: - // "CompilerError: Stack too deep, try removing local variables." - address[3] memory _users = [ - address(0xbeef), // userA - address(0xbabe), // userB - address(0xf005ba11) // userC - ]; - - // We need _voteWeightA + _voteWeightB < MAX_VOTES. - _voteWeightA = uint208(bound(_voteWeightA, 1, MAX_VOTES - 2)); - _voteWeightB = uint208(bound(_voteWeightB, 1, MAX_VOTES - _voteWeightA - 1)); - GCS.VoteType _voteTypeA = _randVoteType(_supportTypeA); - - // Mint and deposit for just userA. - _mintGovAndDepositIntoFlexClient(_users[0], _voteWeightA); - uint256 _initDepositWeight = token.balanceOf(address(flexClient)); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // Jump ahead to the proposal snapshot to lock in the flexClient's balance. - _advanceTimeTo(governor.proposalSnapshot(_proposalId) + 1); - - // Now mint and deposit for userB. - _mintGovAndDepositIntoFlexClient(_users[1], _voteWeightB); - - uint256 _fullVotingWeight = token.balanceOf(address(flexClient)); - assert(_fullVotingWeight > _initDepositWeight); - assertEq(_fullVotingWeight, _voteWeightA + _voteWeightB); - - // Only user A expresses a vote. - vm.prank(_users[0]); - flexClient.expressVote(_proposalId, uint8(_voteTypeA)); - - // Submit votes on behalf of the flexClient. - flexClient.castVote(_proposalId); - - (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = - governor.proposalVotes(_proposalId); - - // We assert the weight is within a range of 1 because scaled weights are sometimes floored. - if (_voteTypeA == GCS.VoteType.For) assertEq(_forVotes, _voteWeightA); - if (_voteTypeA == GCS.VoteType.Against) assertEq(_againstVotes, _voteWeightA); - if (_voteTypeA == GCS.VoteType.Abstain) assertEq(_abstainVotes, _voteWeightA); - } - - function testFuzz_CanCallMultipleTimesForTheSameProposal( - address _userA, - address _userB, - uint208 _voteWeightA, - uint208 _voteWeightB - ) public { - _voteWeightA = uint208(bound(_voteWeightA, 1, type(uint120).max)); - _voteWeightB = uint208(bound(_voteWeightB, 1, type(uint120).max)); - - vm.assume(_userA != address(flexClient)); - vm.assume(_userB != address(flexClient)); - vm.assume(_userA != _userB); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_userA, _voteWeightA); - _mintGovAndDepositIntoFlexClient(_userB, _voteWeightB); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // users should now be able to express their votes on the proposal. - vm.prank(_userA); - flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Against)); - - (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient.proposalVotes(_proposalId); - assertEq(_forVotesExpressed, 0); - assertEq(_againstVotesExpressed, _voteWeightA); - assertEq(_abstainVotesExpressed, 0); - - // The governor should have not recieved any votes yet. - (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = - governor.proposalVotes(_proposalId); - assertEq(_forVotes, 0); - assertEq(_againstVotes, 0); - assertEq(_abstainVotes, 0); - - // Submit votes on behalf of the flexClient. - flexClient.castVote(_proposalId); - - // Governor should now record votes for the flexClient. - (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); - assertEq(_forVotes, 0); - assertEq(_againstVotes, _voteWeightA); - assertEq(_abstainVotes, 0); - - // The second user now decides to express and cast. - vm.prank(_userB); - flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Abstain)); - flexClient.castVote(_proposalId); - - // Governor should now record votes for both users. - (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); - assertEq(_forVotes, 0); - assertEq(_againstVotes, _voteWeightA); // This should be unchanged! - assertEq(_abstainVotes, _voteWeightB); // Second user's votes are now in. - } - - function testFuzz_RevertWhen_NoVotesToCast(address _user, uint208 _voteWeight, uint8 _supportType) - public - { - GCS.VoteType _voteType; - (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_user, _voteWeight); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // No one has expressed, there are no votes to cast. - vm.expectRevert(FVC.FlexVotingClient__NoVotesExpressed.selector); - flexClient.castVote(_proposalId); - - // _user expresses his/her vote on the proposal. - vm.prank(_user); - flexClient.expressVote(_proposalId, uint8(_voteType)); - - // Submit votes on behalf of the flexClient. - flexClient.castVote(_proposalId); - - // All votes have been cast, there's nothing new to send to the governor. - vm.expectRevert(FVC.FlexVotingClient__NoVotesExpressed.selector); - flexClient.castVote(_proposalId); - } - - function testFuzz_RevertWhen_AfterVotingPeriod( - address _user, - uint208 _voteWeight, - uint8 _supportType - ) public { - GCS.VoteType _voteType; - (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_user, _voteWeight); - - // Create the proposal. - uint256 _proposalId = _createAndSubmitProposal(); - - // Express vote preference. - vm.prank(_user); - flexClient.expressVote(_proposalId, uint8(_voteType)); - - // Jump ahead so that we're outside of the proposal's voting period. - _advanceTimeTo(governor.proposalDeadline(_proposalId) + 1); - IGovernor.ProposalState status = IGovernor.ProposalState(uint32(governor.state(_proposalId))); - - // We should not be able to castVote at this point. - vm.expectRevert( - abi.encodeWithSelector( - IGovernor.GovernorUnexpectedProposalState.selector, - _proposalId, - status, - bytes32(1 << uint8(IGovernor.ProposalState.Active)) - ) - ); - flexClient.castVote(_proposalId); +contract BlockNumberClock__CastVoteReasonString is _CastVoteReasonString { + function _timestampClock() internal pure override returns (bool) { + return false; } -} - -abstract contract Borrow is FlexVotingClientTest { - function testFuzz_UsersCanBorrowTokens( - address _depositer, - uint208 _depositAmount, - address _borrower, - uint208 _borrowAmount - ) public { - _depositAmount = _assumeSafeVoteParams(_depositer, _depositAmount); - _borrowAmount = _assumeSafeVoteParams(_borrower, _borrowAmount); - vm.assume(_depositAmount > _borrowAmount); - - // Deposit some funds. - _mintGovAndDepositIntoFlexClient(_depositer, _depositAmount); - - // Borrow some funds. - uint256 _initBalance = token.balanceOf(_borrower); - vm.prank(_borrower); - flexClient.borrow(_borrowAmount); - - // Tokens should have been transferred. - assertEq(token.balanceOf(_borrower), _initBalance + _borrowAmount); - assertEq(token.balanceOf(address(flexClient)), _depositAmount - _borrowAmount); - - // Borrow total has been tracked. - assertEq(flexClient.borrowTotal(_borrower), _borrowAmount); - // The deposit balance of the depositer should not have changed. - assertEq(flexClient.deposits(_depositer), _depositAmount); - - _advanceTimeBy(1); // Advance so we can check the snapshot. - - // The total deposit snapshot should not have changed. - assertEq(flexClient.getPastTotalBalance(_now() - 1), _depositAmount); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } -// Block number tests. -contract BlockNumberClock_Deployment is Deployment { +contract BlockNumberClock__SelfDelegate is _SelfDelegate { function _timestampClock() internal pure override returns (bool) { return false; } -} -contract BlockNumberClock_Constructor is Constructor { - function _timestampClock() internal pure override returns (bool) { - return false; + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } -contract BlockNumberClock__RawBalanceOf is _RawBalanceOf { +contract BlockNumberClock__CheckpointVoteWeightOf is _CheckpointVoteWeightOf { function _timestampClock() internal pure override returns (bool) { return false; } -} -contract BlockNumberClock__CastVoteReasonString is _CastVoteReasonString { - function _timestampClock() internal pure override returns (bool) { - return false; + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } -contract BlockNumberClock__SelfDelegate is _SelfDelegate { +contract BlockNumberClock_GetPastRawBalance is GetPastRawBalance { function _timestampClock() internal pure override returns (bool) { return false; } -} -contract BlockNumberClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { - function _timestampClock() internal pure override returns (bool) { - return false; + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } -contract BlockNumberClock_GetPastRawBalance is GetPastRawBalance { +contract BlockNumber__CheckpointTotalVoteWeight is _CheckpointTotalVoteWeight { function _timestampClock() internal pure override returns (bool) { return false; } -} -contract BlockNumber__CheckpointTotalBalance is _CheckpointTotalBalance { - function _timestampClock() internal pure override returns (bool) { - return false; + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -1336,36 +104,60 @@ contract BlockNumberClock_GetPastTotalBalance is GetPastTotalBalance { function _timestampClock() internal pure override returns (bool) { return false; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract BlockNumberClock_Withdraw is Withdraw { function _timestampClock() internal pure override returns (bool) { return false; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract BlockNumberClock_Deposit is Deposit { function _timestampClock() internal pure override returns (bool) { return false; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract BlockNumberClock_ExpressVote is ExpressVote { function _timestampClock() internal pure override returns (bool) { return false; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract BlockNumberClock_CastVote is CastVote { function _timestampClock() internal pure override returns (bool) { return false; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract BlockNumberClock_Borrow is Borrow { function _timestampClock() internal pure override returns (bool) { return false; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } // Timestamp tests. @@ -1373,82 +165,138 @@ contract TimestampClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract TimestampClock_Constructor is Constructor { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract TimestampClock__RawBalanceOf is _RawBalanceOf { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract TimestampClock__CastVoteReasonString is _CastVoteReasonString { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract TimestampClock__SelfDelegate is _SelfDelegate { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } -contract TimestampClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { +contract TimestampClock__CheckpointVoteWeightOf is _CheckpointVoteWeightOf { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract TimestampClock_GetPastRawBalance is GetPastRawBalance { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } -contract TimestampClock__CheckpointTotalBalance is _CheckpointTotalBalance { +contract TimestampClock__CheckpointTotalVoteWeight is _CheckpointTotalVoteWeight { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract TimestampClock_GetPastTotalBalance is GetPastTotalBalance { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract TimestampClock_Withdraw is Withdraw { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract TimestampClock_Deposit is Deposit { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract TimestampClock_ExpressVote is ExpressVote { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract TimestampClock_CastVote is CastVote { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } contract TimestampClock_Borrow is Borrow { function _timestampClock() internal pure override returns (bool) { return true; } + + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); + } } diff --git a/test/FlexVotingDelegable.t.sol b/test/FlexVotingDelegable.t.sol new file mode 100644 index 0000000..24bd027 --- /dev/null +++ b/test/FlexVotingDelegable.t.sol @@ -0,0 +1,700 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {FlexVotingDelegable} from "src/FlexVotingDelegable.sol"; +import {MockFlexVotingClient as MFVC} from "test/MockFlexVotingClient.sol"; +import {MockFlexVotingDelegableClient} from "test/MockFlexVotingDelegableClient.sol"; +import {GovernorCountingSimple as GCS} from + "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; + +import {FlexVotingClient as FVC} from "src/FlexVotingClient.sol"; + +import { + FlexVotingClientTest, + Deployment, + Constructor, + _RawBalanceOf, + _CastVoteReasonString, + _SelfDelegate, + _CheckpointVoteWeightOf, + _CheckpointTotalVoteWeight, + GetPastRawBalance, + GetPastTotalBalance, + Withdraw, + Deposit, + ExpressVote, + CastVote, + Borrow +} from "test/SharedFlexVoting.t.sol"; + +abstract contract Delegation is FlexVotingClientTest { + struct Delegator { + address addr; + uint208 weight; + } + + // We cast the flexClient to the delegatable client to access the delegate + // function. + function client() internal view returns (MockFlexVotingDelegableClient) { + return MockFlexVotingDelegableClient(address(flexClient)); + } + + function testFuzz_selfDelegationByDefault(address _delegator) public { + _assumeSafeUser(_delegator); + + // By default, the delegator should delegate to themselves. + assertEq(client().delegates(_delegator), _delegator); + + // The delegator can still explicitly delegate to himself. + vm.prank(_delegator); + client().delegate(_delegator); + assertEq(client().delegates(_delegator), _delegator); + } + + function testFuzz_delegateEmitsEvents(address _delegator, address _delegate, uint208 _weight) + public + { + _assumeSafeUser(_delegator); + _assumeSafeUser(_delegate); + vm.assume(_delegator != _delegate); + _weight = uint208(bound(_weight, 1, MAX_VOTES)); + + _mintGovAndDepositIntoFlexClient(_delegator, _weight); + + vm.expectEmit(); + emit FlexVotingDelegable.DelegateChanged(_delegator, _delegator, _delegate); + vm.expectEmit(); + emit FlexVotingDelegable.DelegateWeightChanged(_delegate, 0, _weight); + vm.prank(_delegator); + client().delegate(_delegate); + } + + function testFuzz_delegationAddsToDelegateWeight( + address _delegator, + uint208 _delegatorWeight, + address _delegate, + uint208 _delegateWeight, + uint8 _supportType + ) public { + vm.assume(_delegator != _delegate); + _assumeSafeUser(_delegator); + _assumeSafeUser(_delegate); + _delegateWeight = uint208(bound(_delegateWeight, 1, MAX_VOTES - 1)); + _delegatorWeight = uint208(bound(_delegatorWeight, 1, MAX_VOTES - _delegateWeight)); + GCS.VoteType _voteType = _randVoteType(_supportType); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_delegator, _delegatorWeight); + _mintGovAndDepositIntoFlexClient(_delegate, _delegateWeight); + + _advanceTimeBy(1); // Make past balances retrievable. + assertEq(client().getPastVoteWeight(_delegate, _now() - 1), _delegateWeight); + assertEq(client().getPastVoteWeight(_delegator, _now() - 1), _delegatorWeight); + + // Delegate. + vm.expectEmit(); + emit FlexVotingDelegable.DelegateWeightChanged( + _delegate, _delegateWeight, _delegateWeight + _delegatorWeight + ); + vm.prank(_delegator); + client().delegate(_delegate); + + uint256 _combined = _delegatorWeight + _delegateWeight; + _advanceTimeBy(1); // Make past balances retrievable. + assertEq(client().getPastVoteWeight(_delegator, _now() - 1), 0); + assertEq(client().getPastVoteWeight(_delegate, _now() - 1), _combined); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // The delegate expresses a vote. + vm.prank(_delegate); + client().expressVote(_proposalId, uint8(_voteType)); + + (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = + client().proposalVotes(_proposalId); + assertEq(_forVotesExpressed, _voteType == GCS.VoteType.For ? _combined : 0); + assertEq(_againstVotesExpressed, _voteType == GCS.VoteType.Against ? _combined : 0); + assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _combined : 0); + } + + function testFuzz_multipleAddressesDelegate( + Delegator memory _delegatorA, + Delegator memory _delegatorB, + Delegator memory _delegatorC, + Delegator memory _delegatorD, + Delegator memory _delegate, + uint8 _supportType + ) public { + Delegator[] memory _users = new Delegator[](5); + _users[0] = _delegatorA; + _users[1] = _delegatorB; + _users[2] = _delegatorC; + _users[3] = _delegatorD; + _users[4] = _delegate; + + for (uint256 i = 0; i < _users.length; i++) { + _assumeSafeUser(_users[i].addr); + } + + vm.assume(_delegatorA.addr != _delegatorB.addr); + vm.assume(_delegatorA.addr != _delegatorC.addr); + vm.assume(_delegatorA.addr != _delegatorD.addr); + vm.assume(_delegatorA.addr != _delegate.addr); + vm.assume(_delegatorB.addr != _delegatorC.addr); + vm.assume(_delegatorB.addr != _delegatorD.addr); + vm.assume(_delegatorB.addr != _delegate.addr); + vm.assume(_delegatorC.addr != _delegatorD.addr); + vm.assume(_delegatorC.addr != _delegate.addr); + vm.assume(_delegatorD.addr != _delegate.addr); + + vm.label(_delegatorA.addr, "delegatorA"); + vm.label(_delegatorB.addr, "delegatorB"); + vm.label(_delegatorC.addr, "delegatorC"); + vm.label(_delegatorD.addr, "delegatorD"); + vm.label(_delegate.addr, "delegate"); + + uint256 _remaining = uint256(MAX_VOTES) - 4; + _delegatorA.weight = uint208(bound(_delegatorA.weight, 1, _remaining)); + _remaining -= _delegatorA.weight - 1; + _delegatorB.weight = uint208(bound(_delegatorB.weight, 1, _remaining)); + _remaining -= _delegatorB.weight - 1; + _delegatorC.weight = uint208(bound(_delegatorC.weight, 1, _remaining)); + _remaining -= _delegatorC.weight - 1; + _delegatorD.weight = uint208(bound(_delegatorD.weight, 1, _remaining)); + _remaining -= _delegatorD.weight - 1; + _delegate.weight = uint208(bound(_delegate.weight, 1, _remaining)); + + GCS.VoteType _voteType = _randVoteType(_supportType); + + // Deposit some funds. + for (uint256 i = 0; i < _users.length; i++) { + _mintGovAndDepositIntoFlexClient(_users[i].addr, _users[i].weight); + } + + _advanceTimeBy(1); + + // Delegate. + for (uint256 i = 0; i < _users.length - 1; i++) { + vm.prank(_users[i].addr); + client().delegate(_delegate.addr); + } + + _advanceTimeBy(1); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // The delegate expresses a vote. + vm.prank(_delegate.addr); + client().expressVote(_proposalId, uint8(_voteType)); + + uint256 _combined; + for (uint256 i = 0; i < _users.length; i++) { + _combined += _users[i].weight; + } + + (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = + client().proposalVotes(_proposalId); + assertEq(_forVotesExpressed, _voteType == GCS.VoteType.For ? _combined : 0); + assertEq(_againstVotesExpressed, _voteType == GCS.VoteType.Against ? _combined : 0); + assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _combined : 0); + } + + function testFuzz_delegateCanExpressVoteAfterWithdrawal( + address _delegator, + address _delegate, + uint208 _weight, + uint8 _supportType + ) public { + GCS.VoteType _voteType; + (_weight, _voteType) = _assumeSafeVoteParams(_delegator, _weight, _supportType); + _assumeSafeUser(_delegate); + vm.assume(_delegator != _delegate); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_delegator, _weight); + + // Delegate. + vm.prank(_delegator); + client().delegate(_delegate); + assertEq(client().delegates(_delegator), _delegate); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // The delegator withdraws their funds without voting. + vm.prank(_delegator); + client().withdraw(_weight); + assertEq(client().deposits(_delegator), 0); + + // The delegate can still vote on the proposal. + vm.prank(_delegate); + client().expressVote(_proposalId, uint8(_voteType)); + + (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = + client().proposalVotes(_proposalId); + assertEq(_forVotesExpressed, _voteType == GCS.VoteType.For ? _weight : 0); + assertEq(_againstVotesExpressed, _voteType == GCS.VoteType.Against ? _weight : 0); + assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _weight : 0); + } + + function testFuzz_RevertIf_delegateDoubleVotes( + address _delegator, + address _delegate, + uint208 _weight, + uint8 _supportType + ) public { + GCS.VoteType _voteType; + (_weight, _voteType) = _assumeSafeVoteParams(_delegator, _weight, _supportType); + _assumeSafeUser(_delegate); + vm.assume(_delegator != _delegate); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_delegator, _weight); + + // Delegate. + vm.prank(_delegator); + client().delegate(_delegate); + assertEq(client().delegates(_delegator), _delegate); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // The delegate expresses a voting preference. + vm.prank(_delegate); + client().expressVote(_proposalId, uint8(_voteType)); + + // Even if you're voting for multiple people, you can't double vote. + vm.expectRevert(FVC.FlexVotingClient__AlreadyVoted.selector); + vm.prank(_delegate); + client().expressVote(_proposalId, uint8(_voteType)); + } + + function testFuzz_delegatorCanChangeDelegates( + address _delegator, + address _delegateA, + address _delegateB, + uint208 _weight, + uint8 _supportType + ) public { + _assumeSafeUser(_delegator); + _assumeSafeUser(_delegateA); + _assumeSafeUser(_delegateB); + + vm.assume(_delegator != _delegateA); + vm.assume(_delegator != _delegateB); + vm.assume(_delegateA != _delegateB); + + vm.label(_delegator, "delegator"); + vm.label(_delegateA, "delegateA"); + vm.label(_delegateB, "delegateB"); + + GCS.VoteType _voteType = _randVoteType(_supportType); + _weight = uint208(bound(_weight, 1, MAX_VOTES)); + _mintGovAndDepositIntoFlexClient(_delegator, _weight); + + _advanceTimeBy(1); + + // Delegate to first account. + vm.prank(_delegator); + client().delegate(_delegateA); + + _advanceTimeBy(1); + + // Create the first proposal. + uint256 _proposalA = _createAndSubmitProposal(); + + _advanceTimeBy(1); + + // Change delegate to second account. + vm.prank(_delegator); + client().delegate(_delegateB); + + // Create the second proposal. + uint256 _proposalB = _createAndSubmitProposal("anotherReceiverFunction()"); + + // The delegator and delegateB should not be able to vote on proposalA. + vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); + vm.prank(_delegator); + client().expressVote(_proposalA, uint8(_voteType)); + vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); + vm.prank(_delegateB); + client().expressVote(_proposalA, uint8(_voteType)); + + // The delegator and delegateA should not be able to vote on proposalB. + vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); + vm.prank(_delegator); + client().expressVote(_proposalB, uint8(_voteType)); + vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); + vm.prank(_delegateA); + client().expressVote(_proposalB, uint8(_voteType)); + + // Delegate A should be able to express a vote on the first proposal. + vm.prank(_delegateA); + client().expressVote(_proposalA, uint8(_voteType)); + + // Delegate B should be able to express a vote on the second proposal. + vm.prank(_delegateB); + client().expressVote(_proposalB, uint8(_voteType)); + + (uint256 _againstA, uint256 _forA, uint256 _abstainA) = client().proposalVotes(_proposalA); + assertEq(_forA, _voteType == GCS.VoteType.For ? _weight : 0); + assertEq(_againstA, _voteType == GCS.VoteType.Against ? _weight : 0); + assertEq(_abstainA, _voteType == GCS.VoteType.Abstain ? _weight : 0); + + (uint256 _againstB, uint256 _forB, uint256 _abstainB) = client().proposalVotes(_proposalB); + assertEq(_forB, _voteType == GCS.VoteType.For ? _weight : 0); + assertEq(_againstB, _voteType == GCS.VoteType.Against ? _weight : 0); + assertEq(_abstainB, _voteType == GCS.VoteType.Abstain ? _weight : 0); + } + + function testFuzz_delegateCanExpressVoteWithoutDepositing( + address _delegator, + address _delegate, + uint208 _weight, + uint8 _supportType + ) public { + GCS.VoteType _voteType; + (_weight, _voteType) = _assumeSafeVoteParams(_delegator, _weight, _supportType); + _assumeSafeUser(_delegate); + vm.assume(_delegator != _delegate); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_delegator, _weight); + + // Delegate. + vm.prank(_delegator); + client().delegate(_delegate); + assertEq(client().delegates(_delegator), _delegate); + assertEq(client().delegates(_delegate), _delegate); + + // The delegator has not delegated *token* weight to the delegate. + assertEq(token.delegates(_delegator), address(0)); + assertEq(token.balanceOf(_delegator), 0); + assertEq(token.balanceOf(_delegate), 0); + + // Create the proposal. + uint48 _proposalTimepoint = _now(); + uint256 _proposalId = _createAndSubmitProposal(); + + // The delegator has no weight to vote with, despite having a deposit balance. + assertEq(client().deposits(_delegator), _weight); + assertEq(client().getPastVoteWeight(_delegator, _proposalTimepoint), 0); + vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); + vm.prank(_delegator); + client().expressVote(_proposalId, uint8(_voteType)); + + // The delegate *has* weight to vote with, despite having no deposit balance. + assertEq(client().deposits(_delegate), 0); + assertEq(client().getPastVoteWeight(_delegate, _proposalTimepoint), _weight); + vm.prank(_delegate); + client().expressVote(_proposalId, uint8(_voteType)); + + (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = + client().proposalVotes(_proposalId); + assertEq(_forVotesExpressed, _voteType == GCS.VoteType.For ? _weight : 0); + assertEq(_againstVotesExpressed, _voteType == GCS.VoteType.Against ? _weight : 0); + assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _weight : 0); + } +} + +contract BlockNumberClock_Deployment is Deployment { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber_Constructor is Constructor { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber__RawBalanceOf is _RawBalanceOf { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber__CastVoteReasonString is _CastVoteReasonString { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber__SelfDelegate is _SelfDelegate { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber__CheckpointVoteWeightOf is _CheckpointVoteWeightOf { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber__CheckpointTotalVoteWeight is _CheckpointTotalVoteWeight { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber_GetPastRawBalance is GetPastRawBalance { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber_GetPastTotalBalance is GetPastTotalBalance { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber_Withdraw is Withdraw { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber_Deposit is Deposit { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber_ExpressVote is ExpressVote { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber_CastVote is CastVote { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumber_Borrow is Borrow { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract BlockNumberClock_Delegation is Delegation { + function _timestampClock() internal pure override returns (bool) { + return false; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClockClock_Deployment is Deployment { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock_Constructor is Constructor { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock__RawBalanceOf is _RawBalanceOf { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock__CastVoteReasonString is _CastVoteReasonString { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock__SelfDelegate is _SelfDelegate { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock__CheckpointVoteWeightOf is _CheckpointVoteWeightOf { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock__CheckpointTotalVoteWeight is _CheckpointTotalVoteWeight { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock_GetPastRawBalance is GetPastRawBalance { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock_GetPastTotalBalance is GetPastTotalBalance { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock_Withdraw is Withdraw { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock_Deposit is Deposit { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock_ExpressVote is ExpressVote { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock_CastVote is CastVote { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClock_Borrow is Borrow { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} + +contract TimestampClockClock_Delegation is Delegation { + function _timestampClock() internal pure override returns (bool) { + return true; + } + + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); + } +} diff --git a/test/MockFlexVotingClient.sol b/test/MockFlexVotingClient.sol index de55db4..986b747 100644 --- a/test/MockFlexVotingClient.sol +++ b/test/MockFlexVotingClient.sol @@ -6,6 +6,7 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; import {IVotingToken} from "src/interfaces/IVotingToken.sol"; +import {FlexVotingBase} from "src/FlexVotingBase.sol"; import {FlexVotingClient} from "src/FlexVotingClient.sol"; contract MockFlexVotingClient is FlexVotingClient { @@ -20,7 +21,7 @@ contract MockFlexVotingClient is FlexVotingClient { /// @notice Map borrower to total amount borrowed. mapping(address => uint256) public borrowTotal; - constructor(address _governor) FlexVotingClient(_governor) { + constructor(address _governor) FlexVotingBase(_governor) { TOKEN = ERC20Votes(GOVERNOR.token()); _selfDelegate(); } @@ -29,16 +30,18 @@ contract MockFlexVotingClient is FlexVotingClient { return deposits[_user]; } + // Test hooks + // --------------------------------------------------------------------------- function exposed_rawBalanceOf(address _user) external view returns (uint208) { return _rawBalanceOf(_user); } - function exposed_latestTotalBalance() external view returns (uint208) { - return totalBalanceCheckpoints.latest(); + function exposed_latestTotalWeight() external view returns (uint208) { + return totalVoteWeightCheckpoints.latest(); } - function exposed_checkpointTotalBalance(int256 _delta) external { - return _checkpointTotalBalance(_delta); + function exposed_checkpointTotalVoteWeight(int256 _delta) external { + return _checkpointTotalVoteWeight(_delta); } function exposed_castVoteReasonString() external returns (string memory) { @@ -53,17 +56,20 @@ contract MockFlexVotingClient is FlexVotingClient { deposits[_user] = _amount; } - function exposed_checkpointRawBalanceOf(address _user) external { - return _checkpointRawBalanceOf(_user); + function exposed_checkpointVoteWeightOf(address _user, int256 _delta) external { + _checkpointVoteWeightOf(_user, _delta); } + // End test hooks + // --------------------------------------------------------------------------- /// @notice Allow a holder of the governance token to deposit it into the pool. /// @param _amount The amount to be deposited. function deposit(uint208 _amount) public { deposits[msg.sender] += _amount; - FlexVotingClient._checkpointRawBalanceOf(msg.sender); - FlexVotingClient._checkpointTotalBalance(int256(uint256(_amount))); + int256 _delta = int256(uint256(_amount)); + _checkpointVoteWeightOf(msg.sender, _delta); + _checkpointTotalVoteWeight(_delta); // Assumes revert on failure. TOKEN.transferFrom(msg.sender, address(this), _amount); @@ -75,8 +81,9 @@ contract MockFlexVotingClient is FlexVotingClient { // Overflows & reverts if user does not have sufficient deposits. deposits[msg.sender] -= _amount; - FlexVotingClient._checkpointRawBalanceOf(msg.sender); - FlexVotingClient._checkpointTotalBalance(-1 * int256(uint256(_amount))); + int256 _delta = -1 * int256(uint256(_amount)); + _checkpointVoteWeightOf(msg.sender, _delta); + _checkpointTotalVoteWeight(_delta); TOKEN.transfer(msg.sender, _amount); // Assumes revert on failure. } diff --git a/test/MockFlexVotingDelegableClient.sol b/test/MockFlexVotingDelegableClient.sol new file mode 100644 index 0000000..62cb03c --- /dev/null +++ b/test/MockFlexVotingDelegableClient.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.10; + +import {FlexVotingClient} from "src/FlexVotingClient.sol"; +import {FlexVotingDelegable} from "src/FlexVotingDelegable.sol"; +import {MockFlexVotingClient} from "test/MockFlexVotingClient.sol"; +import {FlexVotingBase} from "src/FlexVotingBase.sol"; + +contract MockFlexVotingDelegableClient is MockFlexVotingClient, FlexVotingDelegable { + constructor(address _governor) MockFlexVotingClient(_governor) {} + + function _checkpointVoteWeightOf(address _user, int256 _delta) + internal + override(FlexVotingBase, FlexVotingDelegable) + { + return FlexVotingDelegable._checkpointVoteWeightOf(_user, _delta); + } +} diff --git a/test/SharedFlexVoting.t.sol b/test/SharedFlexVoting.t.sol new file mode 100644 index 0000000..7a60eed --- /dev/null +++ b/test/SharedFlexVoting.t.sol @@ -0,0 +1,1305 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol"; +import {IGovernor} from "@openzeppelin/contracts/governance/Governor.sol"; +import {GovernorCountingSimple as GCS} from + "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; +import {SignedMath} from "@openzeppelin/contracts/utils/math/SignedMath.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import {IVotingToken} from "src/interfaces/IVotingToken.sol"; +import {IFractionalGovernor} from "src/interfaces/IFractionalGovernor.sol"; +import {FlexVotingClient as FVC} from "src/FlexVotingClient.sol"; +import {MockFlexVotingClient} from "test/MockFlexVotingClient.sol"; +import {GovToken, TimestampGovToken} from "test/GovToken.sol"; +import {FractionalGovernor} from "test/FractionalGovernor.sol"; +import {ProposalReceiverMock} from "test/ProposalReceiverMock.sol"; + +contract SafeCaster { + using SafeCast for uint256; + + function toUint208(uint256 _value) public pure returns (uint208) { + return _value.toUint208(); + } +} + +abstract contract FlexVotingClientTest is Test { + MockFlexVotingClient flexClient; + GovToken token; + FractionalGovernor governor; + ProposalReceiverMock receiver; + + // This max is a limitation of GovernorCountingFractional's vote storage size. + // See GovernorCountingFractional.ProposalVote struct. + uint256 MAX_VOTES = type(uint128).max; + + // The highest valid vote type, represented as a uint256. + uint256 MAX_VOTE_TYPE = uint256(type(GCS.VoteType).max); + + function setUp() public { + if (_timestampClock()) token = new TimestampGovToken(); + else token = new GovToken(); + vm.label(address(token), "token"); + + governor = new FractionalGovernor("Governor", IVotes(token)); + vm.label(address(governor), "governor"); + + _deployFlexClient(address(governor)); + vm.label(address(flexClient), "flexclient"); + + receiver = new ProposalReceiverMock(); + vm.label(address(receiver), "receiver"); + } + + function _timestampClock() internal pure virtual returns (bool); + + // Function to deploy FlexVotingClient and write to `flexClient` storage var. + function _deployFlexClient(address _governor) internal virtual; + + function _now() internal view returns (uint48) { + return token.clock(); + } + + function _advanceTimeBy(uint256 _timeUnits) internal { + if (_timestampClock()) vm.warp(block.timestamp + _timeUnits); + else vm.roll(block.number + _timeUnits); + } + + function _advanceTimeTo(uint256 _timepoint) internal { + if (_timestampClock()) vm.warp(_timepoint); + else vm.roll(_timepoint); + } + + function _mintGovAndApproveFlexClient(address _user, uint208 _amount) public { + vm.assume(_user != address(0)); + token.exposed_mint(_user, _amount); + vm.prank(_user); + token.approve(address(flexClient), type(uint256).max); + } + + function _mintGovAndDepositIntoFlexClient(address _address, uint208 _amount) internal { + _mintGovAndApproveFlexClient(_address, _amount); + vm.prank(_address); + flexClient.deposit(_amount); + } + + function _createAndSubmitProposal() internal returns (uint256 proposalId) { + return _createAndSubmitProposal("mockReceiverFunction()"); + } + + function _createAndSubmitProposal(string memory _sig) internal returns (uint256 proposalId) { + // Proposal will underflow if we're on the zero block + if (_now() == 0) _advanceTimeBy(1); + + // Create a proposal + bytes memory receiverCallData = abi.encodeWithSignature(_sig); + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](1); + bytes[] memory calldatas = new bytes[](1); + targets[0] = address(receiver); + values[0] = 0; // No ETH will be sent. + calldatas[0] = receiverCallData; + + // Submit the proposal. + proposalId = governor.propose(targets, values, calldatas, "A great proposal"); + assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Pending)); + + // Advance proposal to active state. + _advanceTimeTo(governor.proposalSnapshot(proposalId) + 1); + assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active)); + } + + function _assumeSafeUser(address _user) internal view { + vm.assume(_user != address(flexClient)); + vm.assume(_user != address(0)); + } + + function _randVoteType(uint8 _seed) public view returns (GCS.VoteType) { + return + GCS.VoteType(uint8(bound(uint256(_seed), uint256(type(GCS.VoteType).min), MAX_VOTE_TYPE))); + } + + function _assumeSafeVoteParams(address _account, uint208 _voteWeight) + public + view + returns (uint208 _boundedWeight) + { + _assumeSafeUser(_account); + _boundedWeight = uint208(bound(_voteWeight, 1, MAX_VOTES)); + } + + function _assumeSafeVoteParams(address _account, uint208 _voteWeight, uint8 _supportType) + public + view + returns (uint208 _boundedWeight, GCS.VoteType _boundedSupport) + { + _assumeSafeUser(_account); + _boundedSupport = _randVoteType(_supportType); + _boundedWeight = uint208(bound(_voteWeight, 1, MAX_VOTES)); + } +} + +abstract contract Deployment is FlexVotingClientTest { + function test_FlexVotingClientDeployment() public view { + assertEq(token.name(), "Governance Token"); + assertEq(token.symbol(), "GOV"); + + assertEq(address(flexClient.GOVERNOR()), address(governor)); + assertEq(token.delegates(address(flexClient)), address(flexClient)); + + assertEq(governor.name(), "Governor"); + assertEq(address(governor.token()), address(token)); + } +} + +abstract contract Constructor is FlexVotingClientTest { + function test_SetsGovernor() public view { + assertEq(address(flexClient.GOVERNOR()), address(governor)); + } + + function test_SelfDelegates() public view { + assertEq(token.delegates(address(flexClient)), address(flexClient)); + } +} + +// Contract name has a leading underscore for scopelint spec support. +abstract contract _RawBalanceOf is FlexVotingClientTest { + function testFuzz_ReturnsZeroForNonDepositors(address _user) public view { + _assumeSafeUser(_user); + assertEq(flexClient.exposed_rawBalanceOf(_user), 0); + } + + function testFuzz_IncreasesOnDeposit(address _user, uint208 _amount) public { + _assumeSafeUser(_user); + _amount = uint208(bound(_amount, 1, MAX_VOTES)); + + // Deposit some gov. + _mintGovAndDepositIntoFlexClient(_user, _amount); + + assertEq(flexClient.exposed_rawBalanceOf(_user), _amount); + } + + function testFuzz_DecreasesOnWithdrawal(address _user, uint208 _amount) public { + _assumeSafeUser(_user); + _amount = uint208(bound(_amount, 1, MAX_VOTES)); + + // Deposit some gov. + _mintGovAndDepositIntoFlexClient(_user, _amount); + + assertEq(flexClient.exposed_rawBalanceOf(_user), _amount); + + vm.prank(_user); + flexClient.withdraw(_amount); + assertEq(flexClient.exposed_rawBalanceOf(_user), 0); + } + + function testFuzz_UnaffectedByBorrow(address _user, uint208 _deposit, uint208 _borrow) public { + _assumeSafeUser(_user); + _deposit = uint208(bound(_deposit, 1, MAX_VOTES)); + _borrow = uint208(bound(_borrow, 1, _deposit)); + + // Deposit some gov. + _mintGovAndDepositIntoFlexClient(_user, _deposit); + + assertEq(flexClient.exposed_rawBalanceOf(_user), _deposit); + + vm.prank(_user); + flexClient.borrow(_borrow); + + // Raw balance is unchanged. + assertEq(flexClient.exposed_rawBalanceOf(_user), _deposit); + } +} + +// Contract name has a leading underscore for scopelint spec support. +abstract contract _CastVoteReasonString is FlexVotingClientTest { + function test_ReturnsDescriptiveString() public { + assertEq( + flexClient.exposed_castVoteReasonString(), "rolled-up vote from governance token holders" + ); + } +} + +// Contract name has a leading underscore for scopelint spec support. +abstract contract _SelfDelegate is FlexVotingClientTest { + function testFuzz_SetsClientAsTheDelegate(address _delegatee) public { + vm.assume(_delegatee != address(0)); + vm.assume(_delegatee != address(flexClient)); + + // We self-delegate in the constructor, so we need to first un-delegate for + // this test to be meaningful. + vm.prank(address(flexClient)); + token.delegate(_delegatee); + assertEq(token.delegates(address(flexClient)), _delegatee); + + flexClient.exposed_selfDelegate(); + assertEq(token.delegates(address(flexClient)), address(flexClient)); + } +} + +// Contract name has a leading underscore for scopelint spec support. +abstract contract _CheckpointVoteWeightOf is FlexVotingClientTest { + function testFuzz_StoresTheRawBalanceWithTheTimepoint( + address _user, + uint208 _amount, + uint48 _future + ) public { + vm.assume(_user != address(flexClient)); + _future = uint48(bound(_future, _now() + 1, type(uint48).max)); + _amount = uint208(bound(_amount, 1, MAX_VOTES)); + uint48 _past = _now(); + + _advanceTimeTo(_future); + flexClient.exposed_setDeposits(_user, _amount); + int256 _delta = int256(uint256(_amount)); + flexClient.exposed_checkpointVoteWeightOf(_user, _delta); + + assertEq(flexClient.getPastVoteWeight(_user, _past), 0); + assertEq(flexClient.getPastVoteWeight(_user, _future), _amount); + } +} + +abstract contract _CheckpointTotalVoteWeight is FlexVotingClientTest { + int256 MAX_UINT208 = int256(uint256(type(uint208).max)); + + function testFuzz_writesACheckpointAtClockTime(int256 _value, uint48 _timepoint) public { + _timepoint = uint48(bound(_timepoint, 1, type(uint48).max - 1)); + _value = bound(_value, 1, MAX_UINT208); + assertEq(flexClient.exposed_latestTotalWeight(), 0); + + _advanceTimeTo(_timepoint); + flexClient.exposed_checkpointTotalVoteWeight(_value); + _advanceTimeBy(1); + + assertEq(flexClient.getPastTotalVoteWeight(_timepoint), uint256(_value)); + assertEq(flexClient.exposed_latestTotalWeight(), uint256(_value)); + } + + function testFuzz_checkpointsTheTotalBalanceDeltaAtClockTime( + int256 _initBalance, + int256 _delta, + uint48 _timepoint + ) public { + _timepoint = uint48(bound(_timepoint, 1, type(uint48).max - 1)); + _initBalance = bound(_initBalance, 1, MAX_UINT208 - 1); + _delta = bound(_delta, -_initBalance, MAX_UINT208 - _initBalance); + flexClient.exposed_checkpointTotalVoteWeight(_initBalance); + + _advanceTimeTo(_timepoint); + flexClient.exposed_checkpointTotalVoteWeight(_delta); + _advanceTimeBy(1); + + assertEq(flexClient.getPastTotalVoteWeight(_timepoint), uint256(_initBalance + _delta)); + } + + function testFuzz_RevertIf_negativeDeltaWraps(int256 delta, uint208 balance) public { + // Math.abs(delta) must be > balance for the concerning scenario to arise. + delta = bound(delta, type(int256).min, -int256(uint256(balance)) - 1); + assertTrue(SignedMath.abs(delta) > balance); + + // Effectively this function has 5 steps. + // + // Step 1: Cast balance up from a uint208 to a uint256. + // Safe, since uint256 is bigger. + uint256 balanceUint256 = uint256(balance); + + // Step 2: Cast balance down to int256. + // Safe, since uint208.max < int256.max. + int256 balanceInt256 = int256(balanceUint256); + + // Step 3: Add the delta. The result might be negative. + int256 netBalanceInt256 = balanceInt256 + delta; + + // Step 4: Cast back to uint256. + // + // This is where things get a little scary. + // uint256(int256) = 2^256 + int256, for int256 < 0. + // If |delta| > balance, then netBalance will be a negative int256 and when + // we cast to uint256 it will wrap to a very large positive number. + uint256 netBalanceUint256 = uint256(netBalanceInt256); + + // Step 5: Cast back to uint208. + // We need to ensure that when |delta| > balance: + // uint256(balance + delta) > uint208.max + // As this will cause the safecast to fail. + assert(netBalanceUint256 > type(uint208).max); + + // We create a wrapper contract so that we can expect SafeCast reverts. + // `expectRevert` only works if the revert happens at a different level in + // the callstack. + SafeCaster _safeCast = new SafeCaster(); + vm.expectRevert(); + _safeCast.toUint208(netBalanceUint256); + } + + function testFuzz_RevertIf_withdrawalFromZero(int256 _withdraw) public { + _withdraw = bound(_withdraw, type(int208).min, -1); + vm.expectRevert(); + flexClient.exposed_checkpointTotalVoteWeight(_withdraw); + } + + function testFuzz_RevertIf_withdrawalExceedsDeposit(int256 _deposit, int256 _withdraw) public { + _deposit = bound(_deposit, 1, type(int208).max - 1); + _withdraw = bound(_withdraw, type(int208).min, (-1 * _deposit) - 1); + + flexClient.exposed_checkpointTotalVoteWeight(_deposit); + vm.expectRevert(); + flexClient.exposed_checkpointTotalVoteWeight(_withdraw); + } + + function testFuzz_RevertIf_depositsOverflow(int256 _deposit1, int256 _deposit2) public { + int256 _max = int256(uint256(type(uint208).max)); + _deposit1 = bound(_deposit1, 1, _max); + _deposit2 = bound(_deposit2, 1 + _max - _deposit1, _max); + + flexClient.exposed_checkpointTotalVoteWeight(_deposit1); + vm.expectRevert(); + flexClient.exposed_checkpointTotalVoteWeight(_deposit2); + } +} + +abstract contract GetPastRawBalance is FlexVotingClientTest { + function testFuzz_ReturnsZeroForUsersWithoutDeposits( + address _depositor, + address _nonDepositor, + uint208 _amount + ) public { + vm.assume(_depositor != address(flexClient)); + vm.assume(_nonDepositor != address(flexClient)); + vm.assume(_nonDepositor != _depositor); + _amount = uint208(bound(_amount, 1, MAX_VOTES)); + + _advanceTimeBy(1); + assertEq(flexClient.getPastVoteWeight(_depositor, 0), 0); + assertEq(flexClient.getPastVoteWeight(_nonDepositor, 0), 0); + + _mintGovAndDepositIntoFlexClient(_depositor, _amount); + _advanceTimeBy(1); + + assertEq(flexClient.getPastVoteWeight(_depositor, _now() - 1), _amount); + assertEq(flexClient.getPastVoteWeight(_nonDepositor, _now() - 1), 0); + } + + function testFuzz_ReturnsCurrentValueForFutureTimepoints( + address _user, + uint208 _amount, + uint48 _timepoint + ) public { + vm.assume(_user != address(flexClient)); + _timepoint = uint48(bound(_timepoint, _now() + 1, type(uint48).max)); + _amount = uint208(bound(_amount, 1, MAX_VOTES)); + + _mintGovAndDepositIntoFlexClient(_user, _amount); + + assertEq(flexClient.getPastVoteWeight(_user, _now()), _amount); + assertEq(flexClient.getPastVoteWeight(_user, _timepoint), _amount); + + _advanceTimeTo(_timepoint); + + assertEq(flexClient.getPastVoteWeight(_user, _now()), _amount); + } + + function testFuzz_ReturnsUserBalanceAtAGivenTimepoint( + address _user, + uint208 _amountA, + uint208 _amountB, + uint48 _timepoint + ) public { + vm.assume(_user != address(flexClient)); + _timepoint = uint48(bound(_timepoint, _now() + 1, type(uint48).max)); + _amountA = uint208(bound(_amountA, 1, MAX_VOTES)); + _amountB = uint208(bound(_amountB, 0, MAX_VOTES - _amountA)); + + uint48 _initTimepoint = _now(); + _mintGovAndDepositIntoFlexClient(_user, _amountA); + + _advanceTimeTo(_timepoint); + + _mintGovAndDepositIntoFlexClient(_user, _amountB); + _advanceTimeBy(1); + + uint48 _zeroTimepoint = 0; + assertEq(flexClient.getPastVoteWeight(_user, _zeroTimepoint), 0); + assertEq(flexClient.getPastVoteWeight(_user, _initTimepoint), _amountA); + assertEq(flexClient.getPastVoteWeight(_user, _timepoint), _amountA + _amountB); + } +} + +abstract contract GetPastTotalBalance is FlexVotingClientTest { + function testFuzz_ReturnsZeroWithoutDeposits(uint48 _future) public view { + uint48 _zeroTimepoint = 0; + assertEq(flexClient.getPastTotalVoteWeight(_zeroTimepoint), 0); + assertEq(flexClient.getPastTotalVoteWeight(_future), 0); + } + + function testFuzz_ReturnsCurrentValueForFutureTimepoints( + address _user, + uint208 _amount, + uint48 _future + ) public { + vm.assume(_user != address(flexClient)); + _future = uint48(bound(_future, _now() + 1, type(uint48).max)); + _amount = uint208(bound(_amount, 1, MAX_VOTES)); + + _mintGovAndDepositIntoFlexClient(_user, _amount); + + assertEq(flexClient.getPastTotalVoteWeight(_now()), _amount); + assertEq(flexClient.getPastTotalVoteWeight(_future), _amount); + + _advanceTimeTo(_future); + + assertEq(flexClient.getPastTotalVoteWeight(_now()), _amount); + } + + function testFuzz_SumsAllUserDeposits( + address _userA, + uint208 _amountA, + address _userB, + uint208 _amountB + ) public { + vm.assume(_userA != address(flexClient)); + vm.assume(_userB != address(flexClient)); + vm.assume(_userA != _userB); + + _amountA = uint208(bound(_amountA, 1, MAX_VOTES)); + _amountB = uint208(bound(_amountB, 0, MAX_VOTES - _amountA)); + + _mintGovAndDepositIntoFlexClient(_userA, _amountA); + _mintGovAndDepositIntoFlexClient(_userB, _amountB); + + _advanceTimeBy(1); + + assertEq(flexClient.getPastTotalVoteWeight(_now()), _amountA + _amountB); + } + + function testFuzz_ReturnsTotalDepositsAtAGivenTimepoint( + address _userA, + uint208 _amountA, + address _userB, + uint208 _amountB, + uint48 _future + ) public { + vm.assume(_userA != address(flexClient)); + vm.assume(_userB != address(flexClient)); + vm.assume(_userA != _userB); + _future = uint48(bound(_future, _now() + 1, type(uint48).max)); + + _amountA = uint208(bound(_amountA, 1, MAX_VOTES)); + _amountB = uint208(bound(_amountB, 0, MAX_VOTES - _amountA)); + + assertEq(flexClient.getPastTotalVoteWeight(_now()), 0); + + _mintGovAndDepositIntoFlexClient(_userA, _amountA); + _advanceTimeTo(_future); + _mintGovAndDepositIntoFlexClient(_userB, _amountB); + + assertEq(flexClient.getPastTotalVoteWeight(_now() - _future + 1), _amountA); + assertEq(flexClient.getPastTotalVoteWeight(_now()), _amountA + _amountB); + } +} + +abstract contract Withdraw is FlexVotingClientTest { + function testFuzz_UserCanWithdrawGovTokens(address _lender, address _borrower, uint208 _amount) + public + { + _amount = uint208(bound(_amount, 0, type(uint208).max)); + vm.assume(_lender != address(flexClient)); + vm.assume(_borrower != address(flexClient)); + vm.assume(_borrower != address(0)); + vm.assume(_lender != _borrower); + + uint256 _initBalance = token.balanceOf(_borrower); + assertEq(flexClient.deposits(_borrower), 0); + assertEq(flexClient.borrowTotal(_borrower), 0); + + _mintGovAndDepositIntoFlexClient(_lender, _amount); + assertEq(flexClient.deposits(_lender), _amount); + + // Borrow the funds. + vm.prank(_borrower); + flexClient.borrow(_amount); + + assertEq(token.balanceOf(_borrower), _initBalance + _amount); + assertEq(flexClient.borrowTotal(_borrower), _amount); + + // Deposit totals are unaffected. + assertEq(flexClient.deposits(_lender), _amount); + assertEq(flexClient.deposits(_borrower), 0); + } + + // `borrow`s affects on vote weights are tested in Vote contract below. +} + +abstract contract Deposit is FlexVotingClientTest { + function testFuzz_UserCanDepositGovTokens(address _user, uint208 _amount) public { + _amount = uint208(bound(_amount, 0, type(uint208).max)); + vm.assume(_user != address(flexClient)); + uint256 initialBalance = token.balanceOf(_user); + assertEq(flexClient.deposits(_user), 0); + + _mintGovAndDepositIntoFlexClient(_user, _amount); + + assertEq(token.balanceOf(address(flexClient)), _amount); + assertEq(token.balanceOf(_user), initialBalance); + assertEq(token.getVotes(address(flexClient)), _amount); + + // Confirm internal accounting has updated. + assertEq(flexClient.deposits(_user), _amount); + } + + function testFuzz_DepositsAreCheckpointed( + address _user, + uint208 _amountA, + uint208 _amountB, + uint24 _depositDelay + ) public { + _amountA = uint208(bound(_amountA, 1, MAX_VOTES)); + _amountB = uint208(bound(_amountB, 0, MAX_VOTES - _amountA)); + + // Deposit some gov. + _mintGovAndDepositIntoFlexClient(_user, _amountA); + assertEq(flexClient.deposits(_user), _amountA); + + _advanceTimeBy(1); // Advance so that we can look at checkpoints. + + // We can still retrieve the user's balance at the given time. + uint256 _checkpoint1 = _now() - 1; + assertEq( + flexClient.getPastVoteWeight(_user, _checkpoint1), + _amountA, + "user's first deposit was not properly checkpointed" + ); + + uint256 _checkpoint2 = _now() + _depositDelay; + _advanceTimeTo(_checkpoint2); + + // Deposit some more. + _mintGovAndDepositIntoFlexClient(_user, _amountB); + assertEq(flexClient.deposits(_user), _amountA + _amountB); + + _advanceTimeBy(1); // Advance so that we can look at checkpoints. + + assertEq( + flexClient.getPastVoteWeight(_user, _checkpoint1), + _amountA, + "user's first deposit was not properly checkpointed" + ); + assertEq( + flexClient.getPastVoteWeight(_user, _checkpoint2), + _amountA + _amountB, + "user's second deposit was not properly checkpointed" + ); + } +} + +abstract contract ExpressVote is FlexVotingClientTest { + function testFuzz_IncrementsInternalAccouting( + address _user, + uint208 _voteWeight, + uint8 _supportType + ) public { + GCS.VoteType _voteType; + (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_user, _voteWeight); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // _user should now be able to express his/her vote on the proposal. + vm.prank(_user); + flexClient.expressVote(_proposalId, uint8(_voteType)); + (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = + flexClient.proposalVotes(_proposalId); + assertEq(_forVotesExpressed, _voteType == GCS.VoteType.For ? _voteWeight : 0); + assertEq(_againstVotesExpressed, _voteType == GCS.VoteType.Against ? _voteWeight : 0); + assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _voteWeight : 0); + + // No votes have been cast yet. + (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = + governor.proposalVotes(_proposalId); + assertEq(_forVotes, 0); + assertEq(_againstVotes, 0); + assertEq(_abstainVotes, 0); + } + + function testFuzz_RevertWhen_DepositingAfterProposal( + address _user, + uint208 _voteWeight, + uint8 _supportType + ) public { + GCS.VoteType _voteType; + (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); + + // Create the proposal *before* the user deposits anything. + uint256 _proposalId = _createAndSubmitProposal(); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_user, _voteWeight); + + // Now try to express a voting preference on the proposal. + assertEq(flexClient.deposits(_user), _voteWeight); + vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); + vm.prank(_user); + flexClient.expressVote(_proposalId, uint8(_voteType)); + } + + function testFuzz_RevertWhen_NoClientWeightButTokenWeight( + address _user, + uint208 _voteWeight, + uint8 _supportType + ) public { + GCS.VoteType _voteType; + (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); + + // Mint gov but do not deposit. + _mintGovAndApproveFlexClient(_user, _voteWeight); + assertEq(token.balanceOf(_user), _voteWeight); + assertEq(flexClient.deposits(_user), 0); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // _user should NOT be able to express his/her vote on the proposal. + vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); + vm.prank(_user); + flexClient.expressVote(_proposalId, uint8(_voteType)); + + // Deposit into the client. + vm.prank(_user); + flexClient.deposit(_voteWeight); + assertEq(flexClient.deposits(_user), _voteWeight); + + // _user should still NOT be able to express his/her vote on the proposal. + // Despite having a deposit balance, he/she didn't have a balance at the + // proposal snapshot. + vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); + vm.prank(_user); + flexClient.expressVote(_proposalId, uint8(_voteType)); + } + + function testFuzz_RevertOn_DoubleVotes(address _user, uint208 _voteWeight, uint8 _supportType) + public + { + GCS.VoteType _voteType; + (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_user, _voteWeight); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // _user should now be able to express his/her vote on the proposal. + vm.prank(_user); + flexClient.expressVote(_proposalId, uint8(_voteType)); + + ( + uint256 _againstVotesExpressedInit, + uint256 _forVotesExpressedInit, + uint256 _abstainVotesExpressedInit + ) = flexClient.proposalVotes(_proposalId); + assertEq(_forVotesExpressedInit, _voteType == GCS.VoteType.For ? _voteWeight : 0); + assertEq(_againstVotesExpressedInit, _voteType == GCS.VoteType.Against ? _voteWeight : 0); + assertEq(_abstainVotesExpressedInit, _voteType == GCS.VoteType.Abstain ? _voteWeight : 0); + + // Vote early and often! + vm.expectRevert(FVC.FlexVotingClient__AlreadyVoted.selector); + vm.prank(_user); + flexClient.expressVote(_proposalId, uint8(_voteType)); + + // No votes changed. + (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = + flexClient.proposalVotes(_proposalId); + assertEq(_forVotesExpressed, _forVotesExpressedInit); + assertEq(_againstVotesExpressed, _againstVotesExpressedInit); + assertEq(_abstainVotesExpressed, _abstainVotesExpressedInit); + } + + function testFuzz_RevertOn_UnknownVoteType(address _user, uint208 _voteWeight, uint8 _supportType) + public + { + // Force vote type to be unrecognized. + _supportType = uint8(bound(_supportType, MAX_VOTE_TYPE + 1, type(uint8).max)); + + _assumeSafeUser(_user); + _voteWeight = uint208(bound(_voteWeight, 1, MAX_VOTES)); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_user, _voteWeight); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // Now try to express a voting preference with a bogus support type. + vm.expectRevert(FVC.FlexVotingClient__InvalidSupportValue.selector); + vm.prank(_user); + flexClient.expressVote(_proposalId, _supportType); + } + + function testFuzz_RevertOn_UnknownProposal( + address _user, + uint208 _voteWeight, + uint8 _supportType, + uint256 _proposalId + ) public { + _assumeSafeUser(_user); + _voteWeight = uint208(bound(_voteWeight, 1, MAX_VOTES)); + + // Confirm that we've pulled a bogus proposal number. + // This is the condition Governor.state checks for when raising + // GovernorNonexistentProposal. + vm.assume(governor.proposalSnapshot(_proposalId) == 0); + + // Force vote type to be unrecognized. + _supportType = uint8(bound(_supportType, MAX_VOTE_TYPE + 1, type(uint8).max)); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_user, _voteWeight); + + // Create a real proposal to verify the two won't be mixed up when + // expressing. + uint256 _id = _createAndSubmitProposal(); + assert(_proposalId != _id); + + // Now try to express a voting preference on the bogus proposal. + vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); + vm.prank(_user); + flexClient.expressVote(_proposalId, _supportType); + } +} + +abstract contract CastVote is FlexVotingClientTest { + function testFuzz_SubmitsVotesToGovernor(address _user, uint208 _voteWeight, uint8 _supportType) + public + { + GCS.VoteType _voteType; + (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_user, _voteWeight); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // _user should now be able to express his/her vote on the proposal. + vm.prank(_user); + flexClient.expressVote(_proposalId, uint8(_voteType)); + (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = + flexClient.proposalVotes(_proposalId); + assertEq(_forVotesExpressed, _voteType == GCS.VoteType.For ? _voteWeight : 0); + assertEq(_againstVotesExpressed, _voteType == GCS.VoteType.Against ? _voteWeight : 0); + assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _voteWeight : 0); + + // No votes have been cast yet. + (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = + governor.proposalVotes(_proposalId); + assertEq(_forVotes, 0); + assertEq(_againstVotes, 0); + assertEq(_abstainVotes, 0); + + // Submit votes on behalf of the flexClient. + flexClient.castVote(_proposalId); + + // Governor should now record votes from the flexClient. + (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); + assertEq(_forVotes, _forVotesExpressed); + assertEq(_againstVotes, _againstVotesExpressed); + assertEq(_abstainVotes, _abstainVotesExpressed); + } + + function testFuzz_WeightIsSnapshotDependent( + address _user, + uint208 _voteWeightA, + uint208 _voteWeightB, + uint8 _supportType + ) public { + GCS.VoteType _voteType; + (_voteWeightA, _voteType) = _assumeSafeVoteParams(_user, _voteWeightA, _supportType); + _voteWeightB = _assumeSafeVoteParams(_user, _voteWeightB); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_user, _voteWeightA); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // Sometime later the user deposits some more. + _advanceTimeTo(governor.proposalDeadline(_proposalId) - 1); + _mintGovAndDepositIntoFlexClient(_user, _voteWeightB); + + vm.prank(_user); + flexClient.expressVote(_proposalId, uint8(_voteType)); + + // The internal proposal vote weight should not reflect the new deposit weight. + (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = + flexClient.proposalVotes(_proposalId); + assertEq(_forVotesExpressed, _voteType == GCS.VoteType.For ? _voteWeightA : 0); + assertEq(_againstVotesExpressed, _voteType == GCS.VoteType.Against ? _voteWeightA : 0); + assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _voteWeightA : 0); + + // Submit votes on behalf of the flexClient. + flexClient.castVote(_proposalId); + + // Votes cast should likewise reflect only the earlier balance. + (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = + governor.proposalVotes(_proposalId); + assertEq(_forVotes, _voteType == GCS.VoteType.For ? _voteWeightA : 0); + assertEq(_againstVotes, _voteType == GCS.VoteType.Against ? _voteWeightA : 0); + assertEq(_abstainVotes, _voteType == GCS.VoteType.Abstain ? _voteWeightA : 0); + } + + function testFuzz_TracksMultipleUsersVotes( + address _userA, + address _userB, + uint208 _voteWeightA, + uint208 _voteWeightB + ) public { + vm.assume(_userA != _userB); + _assumeSafeUser(_userA); + _assumeSafeUser(_userB); + _voteWeightA = uint208(bound(_voteWeightA, 1, MAX_VOTES - 1)); + _voteWeightB = uint208(bound(_voteWeightB, 1, MAX_VOTES - _voteWeightA)); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_userA, _voteWeightA); + _mintGovAndDepositIntoFlexClient(_userB, _voteWeightB); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // users should now be able to express their votes on the proposal. + vm.prank(_userA); + flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Against)); + vm.prank(_userB); + flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Abstain)); + + (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = + flexClient.proposalVotes(_proposalId); + assertEq(_forVotesExpressed, 0); + assertEq(_againstVotesExpressed, _voteWeightA); + assertEq(_abstainVotesExpressed, _voteWeightB); + + // The governor should have not recieved any votes yet. + (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = + governor.proposalVotes(_proposalId); + assertEq(_forVotes, 0); + assertEq(_againstVotes, 0); + assertEq(_abstainVotes, 0); + + // Submit votes on behalf of the flexClient. + flexClient.castVote(_proposalId); + + // Governor should now record votes for the flexClient. + (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); + assertEq(_forVotes, 0); + assertEq(_againstVotes, _voteWeightA); + assertEq(_abstainVotes, _voteWeightB); + } + + struct VoteWeightIsScaledTestVars { + address userA; + address userB; + address userC; + address userD; + uint208 voteWeightA; + uint8 supportTypeA; + uint208 voteWeightB; + uint8 supportTypeB; + uint208 borrowAmountC; + uint208 borrowAmountD; + } + + function testFuzz_ScalesVoteWeightBasedOnPoolBalance(VoteWeightIsScaledTestVars memory _vars) + public + { + _vars.userA = address(0xbeef); + _vars.userB = address(0xbabe); + _vars.userC = address(0xf005ba11); + _vars.userD = address(0xba5eba11); + + _vars.supportTypeA = uint8(bound(_vars.supportTypeA, 0, MAX_VOTE_TYPE)); + _vars.supportTypeB = uint8(bound(_vars.supportTypeB, 0, MAX_VOTE_TYPE)); + + _vars.voteWeightA = uint208(bound(_vars.voteWeightA, 1e4, MAX_VOTES - 1e4 - 1)); + _vars.voteWeightB = uint208(bound(_vars.voteWeightB, 1e4, MAX_VOTES - _vars.voteWeightA - 1)); + + uint208 _maxBorrowWeight = _vars.voteWeightA + _vars.voteWeightB; + _vars.borrowAmountC = uint208(bound(_vars.borrowAmountC, 1, _maxBorrowWeight - 1)); + _vars.borrowAmountD = + uint208(bound(_vars.borrowAmountD, 1, _maxBorrowWeight - _vars.borrowAmountC)); + + // These are here just as a sanity check that all of the bounding above worked. + vm.assume(_vars.voteWeightA + _vars.voteWeightB < MAX_VOTES); + vm.assume(_vars.voteWeightA + _vars.voteWeightB >= _vars.borrowAmountC + _vars.borrowAmountD); + + // Mint and deposit. + _mintGovAndDepositIntoFlexClient(_vars.userA, _vars.voteWeightA); + _mintGovAndDepositIntoFlexClient(_vars.userB, _vars.voteWeightB); + uint256 _initDepositWeight = token.balanceOf(address(flexClient)); + + // Borrow from the flexClient, decreasing its token balance. + vm.prank(_vars.userC); + flexClient.borrow(_vars.borrowAmountC); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // Jump ahead to the proposal snapshot to lock in the flexClient's balance. + _advanceTimeTo(governor.proposalSnapshot(_proposalId) + 1); + uint256 _expectedVotingWeight = token.balanceOf(address(flexClient)); + assert(_expectedVotingWeight < _initDepositWeight); + + // A+B express votes + vm.prank(_vars.userA); + flexClient.expressVote(_proposalId, _vars.supportTypeA); + vm.prank(_vars.userB); + flexClient.expressVote(_proposalId, _vars.supportTypeB); + + // Borrow more from the flexClient, just to confirm that the vote weight will be based + // on the snapshot blocktime/number. + vm.prank(_vars.userD); + flexClient.borrow(_vars.borrowAmountD); + + // Submit votes on behalf of the flexClient. + flexClient.castVote(_proposalId); + + // Vote should be cast as a percentage of the depositer's expressed types, since + // the actual weight is different from the deposit weight. + (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = + governor.proposalVotes(_proposalId); + + // These can differ because votes are rounded. + assertApproxEqAbs(_againstVotes + _forVotes + _abstainVotes, _expectedVotingWeight, 1); + + if (_vars.supportTypeA == _vars.supportTypeB) { + assertEq(_forVotes, _vars.supportTypeA == uint8(GCS.VoteType.For) ? _expectedVotingWeight : 0); + assertEq( + _againstVotes, _vars.supportTypeA == uint8(GCS.VoteType.Against) ? _expectedVotingWeight : 0 + ); + assertEq( + _abstainVotes, _vars.supportTypeA == uint8(GCS.VoteType.Abstain) ? _expectedVotingWeight : 0 + ); + } else { + uint256 _expectedVotingWeightA = + (_vars.voteWeightA * _expectedVotingWeight) / _initDepositWeight; + uint256 _expectedVotingWeightB = + (_vars.voteWeightB * _expectedVotingWeight) / _initDepositWeight; + + // We assert the weight is within a range of 1 because scaled weights are sometimes floored. + if (_vars.supportTypeA == uint8(GCS.VoteType.For)) { + assertApproxEqAbs(_forVotes, _expectedVotingWeightA, 1); + } + if (_vars.supportTypeB == uint8(GCS.VoteType.For)) { + assertApproxEqAbs(_forVotes, _expectedVotingWeightB, 1); + } + if (_vars.supportTypeA == uint8(GCS.VoteType.Against)) { + assertApproxEqAbs(_againstVotes, _expectedVotingWeightA, 1); + } + if (_vars.supportTypeB == uint8(GCS.VoteType.Against)) { + assertApproxEqAbs(_againstVotes, _expectedVotingWeightB, 1); + } + if (_vars.supportTypeA == uint8(GCS.VoteType.Abstain)) { + assertApproxEqAbs(_abstainVotes, _expectedVotingWeightA, 1); + } + if (_vars.supportTypeB == uint8(GCS.VoteType.Abstain)) { + assertApproxEqAbs(_abstainVotes, _expectedVotingWeightB, 1); + } + } + } + + // This is important because it ensures you can't *gain* voting weight by + // getting other people to not vote. + function testFuzz_AbandonsUnexpressedVotingWeight( + uint208 _voteWeightA, + uint208 _voteWeightB, + uint8 _supportTypeA, + uint208 _borrowAmount + ) public { + // We need to do this to prevent: + // "CompilerError: Stack too deep, try removing local variables." + address[3] memory _users = [ + address(0xbeef), // userA + address(0xbabe), // userB + address(0xf005ba11) // userC + ]; + + // Requirements: + // voteWeights and borrow each >= 1 + // voteWeights and borrow each <= uint128.max + // _voteWeightA + _voteWeightB < MAX_VOTES + // _voteWeightA + _voteWeightB > _borrowAmount + _voteWeightA = uint208(bound(_voteWeightA, 1, MAX_VOTES - 2)); + _voteWeightB = uint208(bound(_voteWeightB, 1, MAX_VOTES - _voteWeightA - 1)); + _borrowAmount = uint208(bound(_borrowAmount, 1, _voteWeightA + _voteWeightB - 1)); + GCS.VoteType _voteTypeA = _randVoteType(_supportTypeA); + + // Mint and deposit. + _mintGovAndDepositIntoFlexClient(_users[0], _voteWeightA); + _mintGovAndDepositIntoFlexClient(_users[1], _voteWeightB); + uint256 _initDepositWeight = token.balanceOf(address(flexClient)); + + // Borrow from the flexClient, decreasing its token balance. + vm.prank(_users[2]); + flexClient.borrow(_borrowAmount); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // Jump ahead to the proposal snapshot to lock in the flexClient's balance. + _advanceTimeTo(governor.proposalSnapshot(_proposalId) + 1); + uint256 _totalPossibleVotingWeight = token.balanceOf(address(flexClient)); + + uint256 _fullVotingWeight = token.balanceOf(address(flexClient)); + assert(_fullVotingWeight < _initDepositWeight); + assertEq(_fullVotingWeight, _voteWeightA + _voteWeightB - _borrowAmount); + + // Only user A expresses a vote. + vm.prank(_users[0]); + flexClient.expressVote(_proposalId, uint8(_voteTypeA)); + + // Submit votes on behalf of the flexClient. + flexClient.castVote(_proposalId); + + // Vote should be cast as a percentage of the depositer's expressed types, since + // the actual weight is different from the deposit weight. + (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = + governor.proposalVotes(_proposalId); + + uint256 _expectedVotingWeightA = (_voteWeightA * _fullVotingWeight) / _initDepositWeight; + uint256 _expectedVotingWeightB = (_voteWeightB * _fullVotingWeight) / _initDepositWeight; + + // The flexClient *could* have voted with this much weight. + assertApproxEqAbs( + _totalPossibleVotingWeight, _expectedVotingWeightA + _expectedVotingWeightB, 1 + ); + + // Actually, though, the flexClient did not vote with all of the weight it could have. + // VoterB's votes were never cast because he/she did not express his/her preference. + assertApproxEqAbs( + _againstVotes + _forVotes + _abstainVotes, // The total actual weight. + _expectedVotingWeightA, // VoterB's weight has been abandoned, only A's is counted. + 1 + ); + + // We assert the weight is within a range of 1 because scaled weights are sometimes floored. + if (_voteTypeA == GCS.VoteType.For) assertApproxEqAbs(_forVotes, _expectedVotingWeightA, 1); + if (_voteTypeA == GCS.VoteType.Against) { + assertApproxEqAbs(_againstVotes, _expectedVotingWeightA, 1); + } + if (_voteTypeA == GCS.VoteType.Abstain) { + assertApproxEqAbs(_abstainVotes, _expectedVotingWeightA, 1); + } + } + + function testFuzz_VotingWeightIsUnaffectedByDepositsAfterProposal( + uint208 _voteWeightA, + uint208 _voteWeightB, + uint8 _supportTypeA + ) public { + // We need to do this to prevent: + // "CompilerError: Stack too deep, try removing local variables." + address[3] memory _users = [ + address(0xbeef), // userA + address(0xbabe), // userB + address(0xf005ba11) // userC + ]; + + // We need _voteWeightA + _voteWeightB < MAX_VOTES. + _voteWeightA = uint208(bound(_voteWeightA, 1, MAX_VOTES - 2)); + _voteWeightB = uint208(bound(_voteWeightB, 1, MAX_VOTES - _voteWeightA - 1)); + GCS.VoteType _voteTypeA = _randVoteType(_supportTypeA); + + // Mint and deposit for just userA. + _mintGovAndDepositIntoFlexClient(_users[0], _voteWeightA); + uint256 _initDepositWeight = token.balanceOf(address(flexClient)); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // Jump ahead to the proposal snapshot to lock in the flexClient's balance. + _advanceTimeTo(governor.proposalSnapshot(_proposalId) + 1); + + // Now mint and deposit for userB. + _mintGovAndDepositIntoFlexClient(_users[1], _voteWeightB); + + uint256 _fullVotingWeight = token.balanceOf(address(flexClient)); + assert(_fullVotingWeight > _initDepositWeight); + assertEq(_fullVotingWeight, _voteWeightA + _voteWeightB); + + // Only user A expresses a vote. + vm.prank(_users[0]); + flexClient.expressVote(_proposalId, uint8(_voteTypeA)); + + // Submit votes on behalf of the flexClient. + flexClient.castVote(_proposalId); + + (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = + governor.proposalVotes(_proposalId); + + // We assert the weight is within a range of 1 because scaled weights are sometimes floored. + if (_voteTypeA == GCS.VoteType.For) assertEq(_forVotes, _voteWeightA); + if (_voteTypeA == GCS.VoteType.Against) assertEq(_againstVotes, _voteWeightA); + if (_voteTypeA == GCS.VoteType.Abstain) assertEq(_abstainVotes, _voteWeightA); + } + + function testFuzz_CanCallMultipleTimesForTheSameProposal( + address _userA, + address _userB, + uint208 _voteWeightA, + uint208 _voteWeightB + ) public { + _voteWeightA = uint208(bound(_voteWeightA, 1, type(uint120).max)); + _voteWeightB = uint208(bound(_voteWeightB, 1, type(uint120).max)); + + vm.assume(_userA != address(flexClient)); + vm.assume(_userB != address(flexClient)); + vm.assume(_userA != _userB); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_userA, _voteWeightA); + _mintGovAndDepositIntoFlexClient(_userB, _voteWeightB); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // users should now be able to express their votes on the proposal. + vm.prank(_userA); + flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Against)); + + (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = + flexClient.proposalVotes(_proposalId); + assertEq(_forVotesExpressed, 0); + assertEq(_againstVotesExpressed, _voteWeightA); + assertEq(_abstainVotesExpressed, 0); + + // The governor should have not recieved any votes yet. + (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = + governor.proposalVotes(_proposalId); + assertEq(_forVotes, 0); + assertEq(_againstVotes, 0); + assertEq(_abstainVotes, 0); + + // Submit votes on behalf of the flexClient. + flexClient.castVote(_proposalId); + + // Governor should now record votes for the flexClient. + (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); + assertEq(_forVotes, 0); + assertEq(_againstVotes, _voteWeightA); + assertEq(_abstainVotes, 0); + + // The second user now decides to express and cast. + vm.prank(_userB); + flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Abstain)); + flexClient.castVote(_proposalId); + + // Governor should now record votes for both users. + (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); + assertEq(_forVotes, 0); + assertEq(_againstVotes, _voteWeightA); // This should be unchanged! + assertEq(_abstainVotes, _voteWeightB); // Second user's votes are now in. + } + + function testFuzz_RevertWhen_NoVotesToCast(address _user, uint208 _voteWeight, uint8 _supportType) + public + { + GCS.VoteType _voteType; + (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_user, _voteWeight); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // No one has expressed, there are no votes to cast. + vm.expectRevert(FVC.FlexVotingClient__NoVotesExpressed.selector); + flexClient.castVote(_proposalId); + + // _user expresses his/her vote on the proposal. + vm.prank(_user); + flexClient.expressVote(_proposalId, uint8(_voteType)); + + // Submit votes on behalf of the flexClient. + flexClient.castVote(_proposalId); + + // All votes have been cast, there's nothing new to send to the governor. + vm.expectRevert(FVC.FlexVotingClient__NoVotesExpressed.selector); + flexClient.castVote(_proposalId); + } + + function testFuzz_RevertWhen_AfterVotingPeriod( + address _user, + uint208 _voteWeight, + uint8 _supportType + ) public { + GCS.VoteType _voteType; + (_voteWeight, _voteType) = _assumeSafeVoteParams(_user, _voteWeight, _supportType); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_user, _voteWeight); + + // Create the proposal. + uint256 _proposalId = _createAndSubmitProposal(); + + // Express vote preference. + vm.prank(_user); + flexClient.expressVote(_proposalId, uint8(_voteType)); + + // Jump ahead so that we're outside of the proposal's voting period. + _advanceTimeTo(governor.proposalDeadline(_proposalId) + 1); + IGovernor.ProposalState status = IGovernor.ProposalState(uint32(governor.state(_proposalId))); + + // We should not be able to castVote at this point. + vm.expectRevert( + abi.encodeWithSelector( + IGovernor.GovernorUnexpectedProposalState.selector, + _proposalId, + status, + bytes32(1 << uint8(IGovernor.ProposalState.Active)) + ) + ); + flexClient.castVote(_proposalId); + } +} + +abstract contract Borrow is FlexVotingClientTest { + function testFuzz_UsersCanBorrowTokens( + address _depositer, + uint208 _depositAmount, + address _borrower, + uint208 _borrowAmount + ) public { + _depositAmount = _assumeSafeVoteParams(_depositer, _depositAmount); + _borrowAmount = _assumeSafeVoteParams(_borrower, _borrowAmount); + vm.assume(_depositAmount > _borrowAmount); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_depositer, _depositAmount); + + // Borrow some funds. + uint256 _initBalance = token.balanceOf(_borrower); + vm.prank(_borrower); + flexClient.borrow(_borrowAmount); + + // Tokens should have been transferred. + assertEq(token.balanceOf(_borrower), _initBalance + _borrowAmount); + assertEq(token.balanceOf(address(flexClient)), _depositAmount - _borrowAmount); + + // Borrow total has been tracked. + assertEq(flexClient.borrowTotal(_borrower), _borrowAmount); + + // The deposit balance of the depositer should not have changed. + assertEq(flexClient.deposits(_depositer), _depositAmount); + + _advanceTimeBy(1); // Advance so we can check the snapshot. + + // The total deposit snapshot should not have changed. + assertEq(flexClient.getPastTotalVoteWeight(_now() - 1), _depositAmount); + } +} diff --git a/test/handlers/FlexVotingClientHandler.sol b/test/handlers/FlexVotingClientHandler.sol index fa77df6..df97db8 100644 --- a/test/handlers/FlexVotingClientHandler.sol +++ b/test/handlers/FlexVotingClientHandler.sol @@ -251,8 +251,9 @@ contract FlexVotingClientHandler is Test { calldatas[0] = receiverCallData; // Submit the proposal. - vm.prank(msg.sender); + vm.startPrank(msg.sender); _proposalId = governor.propose(targets, values, calldatas, _proposalName); + vm.stopPrank(); proposals.add(_proposalId); // Roll the clock to get voting started. @@ -326,7 +327,7 @@ contract FlexVotingClientHandler is Test { address _voter = _voters.at(i); // We need deposits less withdrawals for the user AT proposal time. _vars.aggDepositWeight += - flexClient.getPastRawBalance(_voter, governor.proposalSnapshot(_proposalId)); + flexClient.getPastVoteWeight(_voter, governor.proposalSnapshot(_proposalId)); } ghost_depositsCast[_proposalId] += _vars.aggDepositWeight;