From e33de2414fb1c4805b648fad6d67d540c5d6be2b Mon Sep 17 00:00:00 2001 From: David Laprade Date: Wed, 15 Jan 2025 14:30:22 -0500 Subject: [PATCH 01/21] Initial implementation of sub-delegation --- src/FlexVotingClient.sol | 20 +- src/FlexVotingDelegatable.sol | 94 ++++++++++ test/FlexVotingDelegatable.t.sol | 222 +++++++++++++++++++++++ test/MockFlexVotingDelegatableClient.sol | 68 +++++++ 4 files changed, 399 insertions(+), 5 deletions(-) create mode 100644 src/FlexVotingDelegatable.sol create mode 100644 test/FlexVotingDelegatable.t.sol create mode 100644 test/MockFlexVotingDelegatableClient.sol diff --git a/src/FlexVotingClient.sol b/src/FlexVotingClient.sol index 707ccbb..2bf9412 100644 --- a/src/FlexVotingClient.sol +++ b/src/FlexVotingClient.sol @@ -81,7 +81,7 @@ abstract contract FlexVotingClient { /// @dev Mapping from address to the checkpoint history of raw balances /// of that address. - mapping(address => Checkpoints.Trace208) private balanceCheckpoints; + mapping(address => Checkpoints.Trace208) internal 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 @@ -122,12 +122,22 @@ abstract contract FlexVotingClient { /// 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 = getPastRawBalance(voter, GOVERNOR.proposalSnapshot(proposalId)); + _expressVote(voter, proposalId, support, weight); + } + + function _expressVote( + address voter, + uint256 proposalId, + uint8 support, + uint256 weight + ) internal virtual { if (weight == 0) revert FlexVotingClient__NoVotingWeight(); - if (proposalVotersHasVoted[proposalId][msg.sender]) revert FlexVotingClient__AlreadyVoted(); - proposalVotersHasVoted[proposalId][msg.sender] = true; + if (proposalVotersHasVoted[proposalId][voter]) revert FlexVotingClient__AlreadyVoted(); + proposalVotersHasVoted[proposalId][voter] = true; if (support == uint8(VoteType.Against)) { proposalVotes[proposalId].againstVotes += SafeCast.toUint128(weight); diff --git a/src/FlexVotingDelegatable.sol b/src/FlexVotingDelegatable.sol new file mode 100644 index 0000000..be66d37 --- /dev/null +++ b/src/FlexVotingDelegatable.sol @@ -0,0 +1,94 @@ +// 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 {IVotingToken} from "src/interfaces/IVotingToken.sol"; +import {FlexVotingClient} from "src/FlexVotingClient.sol"; + +abstract contract FlexVotingDelegatable is Context, FlexVotingClient { + 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; + + function expressVote(uint256 proposalId, uint8 support) external override virtual { + address voter = _msgSender(); + uint256 weight = FlexVotingClient.getPastRawBalance(voter, GOVERNOR.proposalSnapshot(proposalId)); + _expressVote(voter, proposalId, support, weight); + } + + // @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; + + emit DelegateChanged(account, oldDelegate, delegatee); + _updateDelegateBalance(oldDelegate, delegatee, _rawBalanceOf(account)); + } + + // @dev Moves delegated votes from one delegate to another. + function _updateDelegateBalance(address from, address to, uint208 amount) internal virtual { + if (from == to || amount == 0) return; + + if (from != address(0)) { + (uint256 oldValue, uint256 newValue) = _push( + FlexVotingClient.balanceCheckpoints[from], + _subtract, + amount + ); + emit DelegateWeightChanged(from, oldValue, newValue); + } + if (to != address(0)) { + (uint256 oldValue, uint256 newValue) = _push( + FlexVotingClient.balanceCheckpoints[to], + _add, + amount + ); + emit DelegateWeightChanged(to, oldValue, newValue); + } + } + + function _push( + Checkpoints.Trace208 storage store, + function(uint208, uint208) view returns (uint208) fn, + uint208 delta + ) private returns (uint208 oldValue, uint208 newValue) { + return store.push( + IVotingToken(GOVERNOR.token()).clock(), + fn(store.latest(), delta) + ); + } + + function _add(uint208 a, uint208 b) private pure returns (uint208) { + return a + b; + } + + function _subtract(uint208 a, uint208 b) private pure returns (uint208) { + return a - b; + } +} diff --git a/test/FlexVotingDelegatable.t.sol b/test/FlexVotingDelegatable.t.sol new file mode 100644 index 0000000..8d94b18 --- /dev/null +++ b/test/FlexVotingDelegatable.t.sol @@ -0,0 +1,222 @@ +// 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 {MockFlexVotingDelegatableClient} from "test/MockFlexVotingDelegatableClient.sol"; +import {GovToken, TimestampGovToken} from "test/GovToken.sol"; +import {FractionalGovernor} from "test/FractionalGovernor.sol"; +import {ProposalReceiverMock} from "test/ProposalReceiverMock.sol"; + +abstract contract FlexVotingDelegatableTest is Test { + MockFlexVotingDelegatableClient 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 MockFlexVotingDelegatableClient(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 FlexVotingDelegatableTest { + 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 Delegation is FlexVotingDelegatableTest { + // TODO + // Users should need to delegate to themselves before they can express? + // We need to checkpoint delegates. + function test_delegation( + address _delegator, + address _delegatee, + uint208 _weight, + uint8 _supportType + ) public { + vm.label(_delegator, "delegator"); + vm.label(_delegatee, "delegatee"); + GCS.VoteType _voteType; + (_weight, _voteType) = _assumeSafeVoteParams(_delegator, _weight, _supportType); + _assumeSafeUser(_delegatee); + vm.assume(_delegator != _delegatee); + + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_delegator, _weight); + + // Delegate. + vm.prank(_delegatee); + flexClient.delegate(_delegatee); + assertEq(flexClient.delegates(_delegatee), _delegatee); + vm.prank(_delegator); + flexClient.delegate(_delegatee); + assertEq(flexClient.delegates(_delegator), _delegatee); + + // The delegator has not delegated *token* weight to the delegatee. + assertEq(token.delegates(_delegator), address(0)); + assertEq(token.balanceOf(_delegator), 0); + assertEq(token.balanceOf(_delegatee), 0); + + // Create the proposal. + uint48 _proposalTimepoint = _now(); + uint256 _proposalId = _createAndSubmitProposal(); + + // The delegator has no weight to vote with, despite having a deposit balance. + assertEq(flexClient.deposits(_delegator), _weight); + assertEq(flexClient.getPastRawBalance(_delegator, _proposalTimepoint), 0); + vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); + vm.prank(_delegator); + flexClient.expressVote(_proposalId, uint8(_voteType)); + + // The delegatee *has* weight to vote with, despite having no deposit balance. + assertEq(flexClient.deposits(_delegatee), 0); + assertEq(flexClient.getPastRawBalance(_delegatee, _proposalTimepoint), _weight); + vm.prank(_delegatee); + flexClient.expressVote(_proposalId, uint8(_voteType)); + + (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = + flexClient.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); + } +} + +// TODO test no double voting + +contract BlockNumberClock_Deployment is Deployment { + function _timestampClock() internal pure override returns (bool) { + return false; + } +} +contract BlockNumberClock_Delegation is Delegation { + function _timestampClock() internal pure override returns (bool) { + return false; + } +} +contract TimestampClock_Deployment is Deployment { + function _timestampClock() internal pure override returns (bool) { + return true; + } +} +contract TimestampClock_Delegation is Delegation { + function _timestampClock() internal pure override returns (bool) { + return true; + } +} diff --git a/test/MockFlexVotingDelegatableClient.sol b/test/MockFlexVotingDelegatableClient.sol new file mode 100644 index 0000000..95d620a --- /dev/null +++ b/test/MockFlexVotingDelegatableClient.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.10; + +import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +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 {FlexVotingClient} from "src/FlexVotingClient.sol"; +import {FlexVotingDelegatable} from "src/FlexVotingDelegatable.sol"; + +contract MockFlexVotingDelegatableClient is FlexVotingDelegatable { + using Checkpoints for Checkpoints.Trace208; + + /// @notice The governance token held and lent by this pool. + ERC20Votes public immutable TOKEN; + + /// @notice Map depositor to deposit amount. + mapping(address => uint208) public deposits; + + /// @notice Map borrower to total amount borrowed. + mapping(address => uint256) public borrowTotal; + + constructor(address _governor) FlexVotingClient(_governor) { + TOKEN = ERC20Votes(GOVERNOR.token()); + _selfDelegate(); + } + + function _rawBalanceOf(address _user) internal view override returns (uint208) { + return deposits[_user]; + } + + /// @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._checkpointTotalBalance(int256(uint256(_amount))); + + address _delegate = delegates(msg.sender); + FlexVotingDelegatable._updateDelegateBalance(address(0), _delegate, _amount); + + // Assumes revert on failure. + TOKEN.transferFrom(msg.sender, address(this), _amount); + } + + /// @notice Allow a depositor to withdraw funds previously deposited to the pool. + /// @param _amount The amount to be withdrawn. + function withdraw(uint208 _amount) public { + // Overflows & reverts if user does not have sufficient deposits. + deposits[msg.sender] -= _amount; + + FlexVotingClient._checkpointTotalBalance(-1 * int256(uint256(_amount))); + + address _delegate = delegates(msg.sender); + FlexVotingDelegatable._updateDelegateBalance(_delegate, address(0), _amount); + + TOKEN.transfer(msg.sender, _amount); // Assumes revert on failure. + } + + /// @notice Arbitrarily remove tokens from the pool. This is to simulate a borrower, hence the + /// method name. Since this is just a proof-of-concept, nothing else is actually done here. + /// @param _amount The amount to "borrow." + function borrow(uint256 _amount) public { + borrowTotal[msg.sender] += _amount; + TOKEN.transfer(msg.sender, _amount); + } +} From 4dc5df6e6648f174858700f7b3207f636b34ecb5 Mon Sep 17 00:00:00 2001 From: David Laprade Date: Thu, 16 Jan 2025 15:38:12 -0500 Subject: [PATCH 02/21] Add more test TODOs --- test/FlexVotingDelegatable.t.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/FlexVotingDelegatable.t.sol b/test/FlexVotingDelegatable.t.sol index 8d94b18..9e1f90f 100644 --- a/test/FlexVotingDelegatable.t.sol +++ b/test/FlexVotingDelegatable.t.sol @@ -142,8 +142,12 @@ abstract contract Deployment is FlexVotingDelegatableTest { abstract contract Delegation is FlexVotingDelegatableTest { // TODO - // Users should need to delegate to themselves before they can express? - // We need to checkpoint delegates. + // - Test nominal case, user voting with his own weight should work normally + // - User can withdraw after delegating to someone, the delegatee can still vote + // after the withdrawal + // - test multiple delegatees to the same delegate + // - test no double voting for delegatee + // - test that delegator can't vote after delegate votes function test_delegation( address _delegator, address _delegatee, @@ -198,8 +202,6 @@ abstract contract Delegation is FlexVotingDelegatableTest { } } -// TODO test no double voting - contract BlockNumberClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return false; From a7a0a2680cba883612ed16861652eaf0e23eb3a4 Mon Sep 17 00:00:00 2001 From: David Laprade Date: Thu, 16 Jan 2025 15:38:46 -0500 Subject: [PATCH 03/21] Am I bad person? --- test/FlexVotingClient.t.sol | 1464 +++++------------------------------ test/SharedFlexVoting.t.sol | 1292 +++++++++++++++++++++++++++++++ 2 files changed, 1476 insertions(+), 1280 deletions(-) create mode 100644 test/SharedFlexVoting.t.sol diff --git a/test/FlexVotingClient.t.sol b/test/FlexVotingClient.t.sol index c491c09..f9f72dd 100644 --- a/test/FlexVotingClient.t.sol +++ b/test/FlexVotingClient.t.sol @@ -1,1371 +1,191 @@ // 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); - } - - 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); - } -} - -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.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" - ); - } -} - -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.getPastTotalBalance(_now() - 1), _depositAmount); - } -} +import { + Deployment, + Constructor, + _RawBalanceOf, + _CastVoteReasonString, + _SelfDelegate, + _CheckpointRawBalanceOf, + _CheckpointTotalBalance, + GetPastRawBalance, + GetPastTotalBalance, + Withdraw, + Deposit, + ExpressVote, + CastVote, + Borrow +} from "test/SharedFlexVoting.t.sol"; // Block number tests. contract BlockNumberClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock_Constructor is Constructor { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock__RawBalanceOf is _RawBalanceOf { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock__CastVoteReasonString is _CastVoteReasonString { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock__SelfDelegate is _SelfDelegate { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock_GetPastRawBalance is GetPastRawBalance { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumber__CheckpointTotalBalance is _CheckpointTotalBalance { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock_GetPastTotalBalance is GetPastTotalBalance { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock_Withdraw is Withdraw { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock_Deposit is Deposit { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock_ExpressVote is ExpressVote { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock_CastVote is CastVote { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract BlockNumberClock_Borrow is Borrow { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } // Timestamp tests. @@ -1373,82 +193,166 @@ contract TimestampClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock_Constructor is Constructor { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock__RawBalanceOf is _RawBalanceOf { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock__CastVoteReasonString is _CastVoteReasonString { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock__SelfDelegate is _SelfDelegate { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock_GetPastRawBalance is GetPastRawBalance { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock__CheckpointTotalBalance is _CheckpointTotalBalance { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock_GetPastTotalBalance is GetPastTotalBalance { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock_Withdraw is Withdraw { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock_Deposit is Deposit { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock_ExpressVote is ExpressVote { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock_CastVote is CastVote { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } contract TimestampClock_Borrow is Borrow { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployClient(address _governor) internal override { + client = address(new MockFlexVotingClient(_governor)); + } + function flexClient() public view override returns (MockFlexVotingClient) { + return MockFlexVotingClient(client); + } } diff --git a/test/SharedFlexVoting.t.sol b/test/SharedFlexVoting.t.sol new file mode 100644 index 0000000..f3e63f8 --- /dev/null +++ b/test/SharedFlexVoting.t.sol @@ -0,0 +1,1292 @@ +// 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 { + address client; + 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"); + + _deployClient(address(governor)); + vm.label(address(client), "client"); + + receiver = new ProposalReceiverMock(); + vm.label(address(receiver), "receiver"); + } + + function _timestampClock() internal pure virtual returns (bool); + + // Function to deploy FlexVotingClient and write to `client` storage var. + function _deployClient(address _governor) internal virtual; + + // Function to cast the stored client address to the contract type under test. + // This is done to facilitate re-use of these tests for FlexVotingClient + // extensions. If you're reading this I'm sorry. Like really sorry. + function flexClient() public view virtual returns (MockFlexVotingClient); + + 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(client, 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 != client); + 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(client), client); + + 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(client), client); + } +} + +// 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 != client); + + // We self-delegate in the constructor, so we need to first un-delegate for + // this test to be meaningful. + vm.prank(client); + token.delegate(_delegatee); + assertEq(token.delegates(client), _delegatee); + + flexClient().exposed_selfDelegate(); + assertEq(token.delegates(client), client); + } +} + +// 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 != client); + _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); + } + + 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 != client); + vm.assume(_nonDepositor != client); + 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 != client); + _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 != client); + _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 != client); + _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 != client); + vm.assume(_userB != client); + 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 != client); + vm.assume(_userB != client); + 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); + } +} + +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 != client); + vm.assume(_borrower != client); + 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 != client); + uint256 initialBalance = token.balanceOf(_user); + assertEq(flexClient().deposits(_user), 0); + + _mintGovAndDepositIntoFlexClient(_user, _amount); + + assertEq(token.balanceOf(client), _amount); + assertEq(token.balanceOf(_user), initialBalance); + assertEq(token.getVotes(client), _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().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" + ); + } +} + +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(client); + + // 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(client); + 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(client); + + // 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(client); + + uint256 _fullVotingWeight = token.balanceOf(client); + 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(client); + + // 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(client); + 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 != client); + vm.assume(_userB != client); + 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(client), _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); + } +} From 6bc6e8958a17186df9080ec086052cb5eda41db2 Mon Sep 17 00:00:00 2001 From: David Laprade Date: Thu, 16 Jan 2025 17:25:40 -0500 Subject: [PATCH 04/21] Add more delegation tests --- test/FlexVotingClient.t.sol | 196 ++++++-------------- test/FlexVotingDelegatable.t.sol | 304 ++++++++++++++++-------------- test/SharedFlexVoting.t.sol | 305 +++++++++++++++---------------- 3 files changed, 370 insertions(+), 435 deletions(-) diff --git a/test/FlexVotingClient.t.sol b/test/FlexVotingClient.t.sol index f9f72dd..272a58f 100644 --- a/test/FlexVotingClient.t.sol +++ b/test/FlexVotingClient.t.sol @@ -24,11 +24,8 @@ contract BlockNumberClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -36,11 +33,8 @@ contract BlockNumberClock_Constructor is Constructor { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -48,11 +42,8 @@ contract BlockNumberClock__RawBalanceOf is _RawBalanceOf { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -60,11 +51,8 @@ contract BlockNumberClock__CastVoteReasonString is _CastVoteReasonString { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -72,11 +60,8 @@ contract BlockNumberClock__SelfDelegate is _SelfDelegate { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -84,11 +69,8 @@ contract BlockNumberClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -96,11 +78,8 @@ contract BlockNumberClock_GetPastRawBalance is GetPastRawBalance { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -108,11 +87,8 @@ contract BlockNumber__CheckpointTotalBalance is _CheckpointTotalBalance { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -120,11 +96,8 @@ contract BlockNumberClock_GetPastTotalBalance is GetPastTotalBalance { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -132,11 +105,8 @@ contract BlockNumberClock_Withdraw is Withdraw { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -144,11 +114,8 @@ contract BlockNumberClock_Deposit is Deposit { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -156,11 +123,8 @@ contract BlockNumberClock_ExpressVote is ExpressVote { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -168,11 +132,8 @@ contract BlockNumberClock_CastVote is CastVote { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -180,11 +141,8 @@ contract BlockNumberClock_Borrow is Borrow { function _timestampClock() internal pure override returns (bool) { return false; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -193,11 +151,8 @@ contract TimestampClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -205,11 +160,8 @@ contract TimestampClock_Constructor is Constructor { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -217,11 +169,8 @@ contract TimestampClock__RawBalanceOf is _RawBalanceOf { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -229,11 +178,8 @@ contract TimestampClock__CastVoteReasonString is _CastVoteReasonString { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -241,11 +187,8 @@ contract TimestampClock__SelfDelegate is _SelfDelegate { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -253,11 +196,8 @@ contract TimestampClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -265,11 +205,8 @@ contract TimestampClock_GetPastRawBalance is GetPastRawBalance { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -277,11 +214,8 @@ contract TimestampClock__CheckpointTotalBalance is _CheckpointTotalBalance { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -289,11 +223,8 @@ contract TimestampClock_GetPastTotalBalance is GetPastTotalBalance { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -301,11 +232,8 @@ contract TimestampClock_Withdraw is Withdraw { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -313,11 +241,8 @@ contract TimestampClock_Deposit is Deposit { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -325,11 +250,8 @@ contract TimestampClock_ExpressVote is ExpressVote { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -337,11 +259,8 @@ contract TimestampClock_CastVote is CastVote { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } @@ -349,10 +268,7 @@ contract TimestampClock_Borrow is Borrow { function _timestampClock() internal pure override returns (bool) { return true; } - function _deployClient(address _governor) internal override { - client = address(new MockFlexVotingClient(_governor)); - } - function flexClient() public view override returns (MockFlexVotingClient) { - return MockFlexVotingClient(client); + function _deployFlexClient(address _governor) internal override { + flexClient = new MockFlexVotingClient(_governor); } } diff --git a/test/FlexVotingDelegatable.t.sol b/test/FlexVotingDelegatable.t.sol index 9e1f90f..98e2691 100644 --- a/test/FlexVotingDelegatable.t.sol +++ b/test/FlexVotingDelegatable.t.sol @@ -1,201 +1,213 @@ // 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 {FlexVotingDelegatable} from "src/FlexVotingDelegatable.sol"; +import {MockFlexVotingClient as MFVC} from "test/MockFlexVotingClient.sol"; +import {MockFlexVotingDelegatableClient} from "test/MockFlexVotingDelegatableClient.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 {MockFlexVotingDelegatableClient} from "test/MockFlexVotingDelegatableClient.sol"; -import {GovToken, TimestampGovToken} from "test/GovToken.sol"; -import {FractionalGovernor} from "test/FractionalGovernor.sol"; -import {ProposalReceiverMock} from "test/ProposalReceiverMock.sol"; -abstract contract FlexVotingDelegatableTest is Test { - MockFlexVotingDelegatableClient flexClient; - GovToken token; - FractionalGovernor governor; - ProposalReceiverMock receiver; +import { + FlexVotingClientTest, + Deployment, + Constructor, + _RawBalanceOf, + _CastVoteReasonString, + _SelfDelegate, + _CheckpointRawBalanceOf, + _CheckpointTotalBalance, + GetPastRawBalance, + GetPastTotalBalance, + Withdraw, + Deposit, + ExpressVote, + CastVote, + Borrow +} from "test/SharedFlexVoting.t.sol"; + +abstract contract Delegation is FlexVotingClientTest { + // We cast the flexClient to the delegatable client to access the delegate + // function. + function client() internal view returns (MockFlexVotingDelegatableClient) { + return MockFlexVotingDelegatableClient(address(flexClient)); + } - // This max is a limitation of GovernorCountingFractional's vote storage size. - // See GovernorCountingFractional.ProposalVote struct. - uint256 MAX_VOTES = type(uint128).max; + // TODO + // - delegating adds to a delegate's own votes + // - test multiple delegatees to the same delegate + // - test no double voting for delegatee + // - test that delegator can't vote after delegate votes + function testFuzz_selfDelegationByDefault( + address _delegator + ) public { + _assumeSafeUser(_delegator); - // The highest valid vote type, represented as a uint256. - uint256 MAX_VOTE_TYPE = uint256(type(GCS.VoteType).max); + // By default, the delegator should delegate to themselves. + assertEq(client().delegates(_delegator), _delegator); - function setUp() public { - if (_timestampClock()) token = new TimestampGovToken(); - else token = new GovToken(); - vm.label(address(token), "token"); + // The delegator can still explicitly delegate to himself. + vm.prank(_delegator); + client().delegate(_delegator); + assertEq(client().delegates(_delegator), _delegator); + } - governor = new FractionalGovernor("Governor", IVotes(token)); - vm.label(address(governor), "governor"); + 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)); - flexClient = new MockFlexVotingDelegatableClient(address(governor)); - vm.label(address(flexClient), "flexClient"); + _mintGovAndDepositIntoFlexClient(_delegator, _weight); - receiver = new ProposalReceiverMock(); - vm.label(address(receiver), "receiver"); + vm.expectEmit(); + emit FlexVotingDelegatable.DelegateChanged(_delegator, _delegator, _delegate); + vm.expectEmit(); + emit FlexVotingDelegatable.DelegateWeightChanged(_delegate, 0, _weight); + vm.prank(_delegator); + client().delegate(_delegate); } - function _timestampClock() internal pure virtual returns (bool); + 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); - function _now() internal view returns (uint48) { - return token.clock(); - } + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_delegator, _delegatorWeight); + _mintGovAndDepositIntoFlexClient(_delegate, _delegateWeight); - function _advanceTimeBy(uint256 _timeUnits) internal { - if (_timestampClock()) vm.warp(block.timestamp + _timeUnits); - else vm.roll(block.number + _timeUnits); - } + _advanceTimeBy(1); // Make past balances retrievable. + assertEq(client().getPastRawBalance(_delegate, _now() - 1), _delegateWeight); + assertEq(client().getPastRawBalance(_delegator, _now() - 1), _delegatorWeight); - function _advanceTimeTo(uint256 _timepoint) internal { - if (_timestampClock()) vm.warp(_timepoint); - else vm.roll(_timepoint); - } + // Delegate. + vm.expectEmit(); + emit FlexVotingDelegatable.DelegateWeightChanged( + _delegate, + _delegateWeight, + _delegateWeight + _delegatorWeight + ); + vm.prank(_delegator); + client().delegate(_delegate); - 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); - } + uint256 _combined = _delegatorWeight + _delegateWeight; + _advanceTimeBy(1); // Make past balances retrievable. + assertEq(client().getPastRawBalance(_delegator, _now() - 1), 0); + assertEq(client().getPastRawBalance(_delegate, _now() - 1), _combined); - function _mintGovAndDepositIntoFlexClient(address _address, uint208 _amount) internal { - _mintGovAndApproveFlexClient(_address, _amount); - vm.prank(_address); - flexClient.deposit(_amount); - } + // Create the proposal. + uint48 _proposalTimepoint = _now(); + uint256 _proposalId = _createAndSubmitProposal(); - 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)); - } + // The delegate expresses a vote. + vm.prank(_delegate); + client().expressVote(_proposalId, uint8(_voteType)); - function _assumeSafeUser(address _user) internal view { - vm.assume(_user != address(flexClient)); - vm.assume(_user != address(0)); + (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 _randVoteType(uint8 _seed) public view returns (GCS.VoteType) { - return - GCS.VoteType(uint8(bound(uint256(_seed), uint256(type(GCS.VoteType).min), MAX_VOTE_TYPE))); - } + 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); - function _assumeSafeVoteParams(address _account, uint208 _voteWeight) - public - view - returns (uint208 _boundedWeight) - { - _assumeSafeUser(_account); - _boundedWeight = uint208(bound(_voteWeight, 1, MAX_VOTES)); - } + // Deposit some funds. + _mintGovAndDepositIntoFlexClient(_delegator, _weight); - 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)); - } -} + // Delegate. + vm.prank(_delegator); + client().delegate(_delegate); + assertEq(client().delegates(_delegator), _delegate); -abstract contract Deployment is FlexVotingDelegatableTest { - function test_FlexVotingClientDeployment() public view { - assertEq(token.name(), "Governance Token"); - assertEq(token.symbol(), "GOV"); + // Create the proposal. + uint48 _proposalTimepoint = _now(); + uint256 _proposalId = _createAndSubmitProposal(); - assertEq(address(flexClient.GOVERNOR()), address(governor)); - assertEq(token.delegates(address(flexClient)), address(flexClient)); + // 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)); - assertEq(governor.name(), "Governor"); - assertEq(address(governor.token()), address(token)); + (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); } -} -abstract contract Delegation is FlexVotingDelegatableTest { - // TODO - // - Test nominal case, user voting with his own weight should work normally - // - User can withdraw after delegating to someone, the delegatee can still vote - // after the withdrawal - // - test multiple delegatees to the same delegate - // - test no double voting for delegatee - // - test that delegator can't vote after delegate votes - function test_delegation( + function testFuzz_delegateCanExpressVoteWithoutDepositing( address _delegator, - address _delegatee, + address _delegate, uint208 _weight, uint8 _supportType ) public { - vm.label(_delegator, "delegator"); - vm.label(_delegatee, "delegatee"); GCS.VoteType _voteType; (_weight, _voteType) = _assumeSafeVoteParams(_delegator, _weight, _supportType); - _assumeSafeUser(_delegatee); - vm.assume(_delegator != _delegatee); + _assumeSafeUser(_delegate); + vm.assume(_delegator != _delegate); // Deposit some funds. _mintGovAndDepositIntoFlexClient(_delegator, _weight); // Delegate. - vm.prank(_delegatee); - flexClient.delegate(_delegatee); - assertEq(flexClient.delegates(_delegatee), _delegatee); vm.prank(_delegator); - flexClient.delegate(_delegatee); - assertEq(flexClient.delegates(_delegator), _delegatee); + client().delegate(_delegate); + assertEq(client().delegates(_delegator), _delegate); + assertEq(client().delegates(_delegate), _delegate); - // The delegator has not delegated *token* weight to the delegatee. + // The delegator has not delegated *token* weight to the delegate. assertEq(token.delegates(_delegator), address(0)); assertEq(token.balanceOf(_delegator), 0); - assertEq(token.balanceOf(_delegatee), 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(flexClient.deposits(_delegator), _weight); - assertEq(flexClient.getPastRawBalance(_delegator, _proposalTimepoint), 0); + assertEq(client().deposits(_delegator), _weight); + assertEq(client().getPastRawBalance(_delegator, _proposalTimepoint), 0); vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); vm.prank(_delegator); - flexClient.expressVote(_proposalId, uint8(_voteType)); + client().expressVote(_proposalId, uint8(_voteType)); - // The delegatee *has* weight to vote with, despite having no deposit balance. - assertEq(flexClient.deposits(_delegatee), 0); - assertEq(flexClient.getPastRawBalance(_delegatee, _proposalTimepoint), _weight); - vm.prank(_delegatee); - flexClient.expressVote(_proposalId, uint8(_voteType)); + // The delegate *has* weight to vote with, despite having no deposit balance. + assertEq(client().deposits(_delegate), 0); + assertEq(client().getPastRawBalance(_delegate, _proposalTimepoint), _weight); + vm.prank(_delegate); + client().expressVote(_proposalId, uint8(_voteType)); (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient.proposalVotes(_proposalId); + 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); @@ -206,19 +218,31 @@ contract BlockNumberClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_governor))); + } } contract TimestampClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + } } contract TimestampClock_Delegation is Delegation { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + } } diff --git a/test/SharedFlexVoting.t.sol b/test/SharedFlexVoting.t.sol index f3e63f8..572d049 100644 --- a/test/SharedFlexVoting.t.sol +++ b/test/SharedFlexVoting.t.sol @@ -19,7 +19,7 @@ import {FractionalGovernor} from "test/FractionalGovernor.sol"; import {ProposalReceiverMock} from "test/ProposalReceiverMock.sol"; abstract contract FlexVotingClientTest is Test { - address client; + MockFlexVotingClient flexClient; GovToken token; FractionalGovernor governor; ProposalReceiverMock receiver; @@ -39,8 +39,8 @@ abstract contract FlexVotingClientTest is Test { governor = new FractionalGovernor("Governor", IVotes(token)); vm.label(address(governor), "governor"); - _deployClient(address(governor)); - vm.label(address(client), "client"); + _deployFlexClient(address(governor)); + vm.label(address(flexClient), "flexclient"); receiver = new ProposalReceiverMock(); vm.label(address(receiver), "receiver"); @@ -48,13 +48,8 @@ abstract contract FlexVotingClientTest is Test { function _timestampClock() internal pure virtual returns (bool); - // Function to deploy FlexVotingClient and write to `client` storage var. - function _deployClient(address _governor) internal virtual; - - // Function to cast the stored client address to the contract type under test. - // This is done to facilitate re-use of these tests for FlexVotingClient - // extensions. If you're reading this I'm sorry. Like really sorry. - function flexClient() public view virtual returns (MockFlexVotingClient); + // 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(); @@ -74,13 +69,13 @@ abstract contract FlexVotingClientTest is Test { vm.assume(_user != address(0)); token.exposed_mint(_user, _amount); vm.prank(_user); - token.approve(client, type(uint256).max); + token.approve(address(flexClient), type(uint256).max); } function _mintGovAndDepositIntoFlexClient(address _address, uint208 _amount) internal { _mintGovAndApproveFlexClient(_address, _amount); vm.prank(_address); - flexClient().deposit(_amount); + flexClient.deposit(_amount); } function _createAndSubmitProposal() internal returns (uint256 proposalId) { @@ -106,7 +101,7 @@ abstract contract FlexVotingClientTest is Test { } function _assumeSafeUser(address _user) internal view { - vm.assume(_user != client); + vm.assume(_user != address(flexClient)); vm.assume(_user != address(0)); } @@ -140,8 +135,8 @@ abstract contract Deployment is FlexVotingClientTest { assertEq(token.name(), "Governance Token"); assertEq(token.symbol(), "GOV"); - assertEq(address(flexClient().GOVERNOR()), address(governor)); - assertEq(token.delegates(client), client); + assertEq(address(flexClient.GOVERNOR()), address(governor)); + assertEq(token.delegates(address(flexClient)), address(flexClient)); assertEq(governor.name(), "Governor"); assertEq(address(governor.token()), address(token)); @@ -150,11 +145,11 @@ abstract contract Deployment is FlexVotingClientTest { abstract contract Constructor is FlexVotingClientTest { function test_SetsGovernor() public view { - assertEq(address(flexClient().GOVERNOR()), address(governor)); + assertEq(address(flexClient.GOVERNOR()), address(governor)); } function test_SelfDelegates() public view { - assertEq(token.delegates(client), client); + assertEq(token.delegates(address(flexClient)), address(flexClient)); } } @@ -162,7 +157,7 @@ abstract contract Constructor is FlexVotingClientTest { abstract contract _RawBalanceOf is FlexVotingClientTest { function testFuzz_ReturnsZeroForNonDepositors(address _user) public view { _assumeSafeUser(_user); - assertEq(flexClient().exposed_rawBalanceOf(_user), 0); + assertEq(flexClient.exposed_rawBalanceOf(_user), 0); } function testFuzz_IncreasesOnDeposit(address _user, uint208 _amount) public { @@ -172,7 +167,7 @@ abstract contract _RawBalanceOf is FlexVotingClientTest { // Deposit some gov. _mintGovAndDepositIntoFlexClient(_user, _amount); - assertEq(flexClient().exposed_rawBalanceOf(_user), _amount); + assertEq(flexClient.exposed_rawBalanceOf(_user), _amount); } function testFuzz_DecreasesOnWithdrawal(address _user, uint208 _amount) public { @@ -182,11 +177,11 @@ abstract contract _RawBalanceOf is FlexVotingClientTest { // Deposit some gov. _mintGovAndDepositIntoFlexClient(_user, _amount); - assertEq(flexClient().exposed_rawBalanceOf(_user), _amount); + assertEq(flexClient.exposed_rawBalanceOf(_user), _amount); vm.prank(_user); - flexClient().withdraw(_amount); - assertEq(flexClient().exposed_rawBalanceOf(_user), 0); + flexClient.withdraw(_amount); + assertEq(flexClient.exposed_rawBalanceOf(_user), 0); } function testFuzz_UnaffectedByBorrow(address _user, uint208 _deposit, uint208 _borrow) public { @@ -197,13 +192,13 @@ abstract contract _RawBalanceOf is FlexVotingClientTest { // Deposit some gov. _mintGovAndDepositIntoFlexClient(_user, _deposit); - assertEq(flexClient().exposed_rawBalanceOf(_user), _deposit); + assertEq(flexClient.exposed_rawBalanceOf(_user), _deposit); vm.prank(_user); - flexClient().borrow(_borrow); + flexClient.borrow(_borrow); // Raw balance is unchanged. - assertEq(flexClient().exposed_rawBalanceOf(_user), _deposit); + assertEq(flexClient.exposed_rawBalanceOf(_user), _deposit); } } @@ -211,7 +206,7 @@ abstract contract _RawBalanceOf is FlexVotingClientTest { abstract contract _CastVoteReasonString is FlexVotingClientTest { function test_ReturnsDescriptiveString() public { assertEq( - flexClient().exposed_castVoteReasonString(), "rolled-up vote from governance token holders" + flexClient.exposed_castVoteReasonString(), "rolled-up vote from governance token holders" ); } } @@ -220,16 +215,16 @@ abstract contract _CastVoteReasonString is FlexVotingClientTest { abstract contract _SelfDelegate is FlexVotingClientTest { function testFuzz_SetsClientAsTheDelegate(address _delegatee) public { vm.assume(_delegatee != address(0)); - vm.assume(_delegatee != client); + 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(client); + vm.prank(address(flexClient)); token.delegate(_delegatee); - assertEq(token.delegates(client), _delegatee); + assertEq(token.delegates(address(flexClient)), _delegatee); - flexClient().exposed_selfDelegate(); - assertEq(token.delegates(client), client); + flexClient.exposed_selfDelegate(); + assertEq(token.delegates(address(flexClient)), address(flexClient)); } } @@ -240,17 +235,17 @@ abstract contract _CheckpointRawBalanceOf is FlexVotingClientTest { uint208 _amount, uint48 _timepoint ) public { - vm.assume(_user != client); + 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); + flexClient.exposed_setDeposits(_user, _amount); + assertEq(flexClient.getPastRawBalance(_user, _timepoint), 0); _advanceTimeTo(_timepoint); - flexClient().exposed_checkpointRawBalanceOf(_user); - assertEq(flexClient().getPastRawBalance(_user, _timepoint), _amount); + flexClient.exposed_checkpointRawBalanceOf(_user); + assertEq(flexClient.getPastRawBalance(_user, _timepoint), _amount); } } @@ -260,14 +255,14 @@ abstract contract _CheckpointTotalBalance is FlexVotingClientTest { 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); + assertEq(flexClient.exposed_latestTotalBalance(), 0); _advanceTimeTo(_timepoint); - flexClient().exposed_checkpointTotalBalance(_value); + flexClient.exposed_checkpointTotalBalance(_value); _advanceTimeBy(1); - assertEq(flexClient().getPastTotalBalance(_timepoint), uint256(_value)); - assertEq(flexClient().exposed_latestTotalBalance(), uint256(_value)); + assertEq(flexClient.getPastTotalBalance(_timepoint), uint256(_value)); + assertEq(flexClient.exposed_latestTotalBalance(), uint256(_value)); } function testFuzz_checkpointsTheTotalBalanceDeltaAtClockTime( @@ -278,13 +273,13 @@ abstract contract _CheckpointTotalBalance is FlexVotingClientTest { _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); + flexClient.exposed_checkpointTotalBalance(_initBalance); _advanceTimeTo(_timepoint); - flexClient().exposed_checkpointTotalBalance(_delta); + flexClient.exposed_checkpointTotalBalance(_delta); _advanceTimeBy(1); - assertEq(flexClient().getPastTotalBalance(_timepoint), uint256(_initBalance + _delta)); + assertEq(flexClient.getPastTotalBalance(_timepoint), uint256(_initBalance + _delta)); } function testFuzz_RevertIf_negativeDeltaWraps(int256 delta, uint208 balance) public { @@ -325,16 +320,16 @@ abstract contract _CheckpointTotalBalance is FlexVotingClientTest { function testFuzz_RevertIf_withdrawalFromZero(int256 _withdraw) public { _withdraw = bound(_withdraw, type(int208).min, -1); vm.expectRevert(); - flexClient().exposed_checkpointTotalBalance(_withdraw); + 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); + flexClient.exposed_checkpointTotalBalance(_deposit); vm.expectRevert(); - flexClient().exposed_checkpointTotalBalance(_withdraw); + flexClient.exposed_checkpointTotalBalance(_withdraw); } function testFuzz_RevertIf_depositsOverflow(int256 _deposit1, int256 _deposit2) public { @@ -342,9 +337,9 @@ abstract contract _CheckpointTotalBalance is FlexVotingClientTest { _deposit1 = bound(_deposit1, 1, _max); _deposit2 = bound(_deposit2, 1 + _max - _deposit1, _max); - flexClient().exposed_checkpointTotalBalance(_deposit1); + flexClient.exposed_checkpointTotalBalance(_deposit1); vm.expectRevert(); - flexClient().exposed_checkpointTotalBalance(_deposit2); + flexClient.exposed_checkpointTotalBalance(_deposit2); } } @@ -354,20 +349,20 @@ abstract contract GetPastRawBalance is FlexVotingClientTest { address _nonDepositor, uint208 _amount ) public { - vm.assume(_depositor != client); - vm.assume(_nonDepositor != client); + 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); + 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); + assertEq(flexClient.getPastRawBalance(_depositor, _now() - 1), _amount); + assertEq(flexClient.getPastRawBalance(_nonDepositor, _now() - 1), 0); } function testFuzz_ReturnsCurrentValueForFutureTimepoints( @@ -375,18 +370,18 @@ abstract contract GetPastRawBalance is FlexVotingClientTest { uint208 _amount, uint48 _timepoint ) public { - vm.assume(_user != client); + 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); + assertEq(flexClient.getPastRawBalance(_user, _now()), _amount); + assertEq(flexClient.getPastRawBalance(_user, _timepoint), _amount); _advanceTimeTo(_timepoint); - assertEq(flexClient().getPastRawBalance(_user, _now()), _amount); + assertEq(flexClient.getPastRawBalance(_user, _now()), _amount); } function testFuzz_ReturnsUserBalanceAtAGivenTimepoint( @@ -395,7 +390,7 @@ abstract contract GetPastRawBalance is FlexVotingClientTest { uint208 _amountB, uint48 _timepoint ) public { - vm.assume(_user != client); + 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)); @@ -409,17 +404,17 @@ abstract contract GetPastRawBalance is FlexVotingClientTest { _advanceTimeBy(1); uint48 _zeroTimepoint = 0; - assertEq(flexClient().getPastRawBalance(_user, _zeroTimepoint), 0); - assertEq(flexClient().getPastRawBalance(_user, _initTimepoint), _amountA); - assertEq(flexClient().getPastRawBalance(_user, _timepoint), _amountA + _amountB); + 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); + assertEq(flexClient.getPastTotalBalance(_zeroTimepoint), 0); + assertEq(flexClient.getPastTotalBalance(_future), 0); } function testFuzz_ReturnsCurrentValueForFutureTimepoints( @@ -427,18 +422,18 @@ abstract contract GetPastTotalBalance is FlexVotingClientTest { uint208 _amount, uint48 _future ) public { - vm.assume(_user != client); + 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); + assertEq(flexClient.getPastTotalBalance(_now()), _amount); + assertEq(flexClient.getPastTotalBalance(_future), _amount); _advanceTimeTo(_future); - assertEq(flexClient().getPastTotalBalance(_now()), _amount); + assertEq(flexClient.getPastTotalBalance(_now()), _amount); } function testFuzz_SumsAllUserDeposits( @@ -447,8 +442,8 @@ abstract contract GetPastTotalBalance is FlexVotingClientTest { address _userB, uint208 _amountB ) public { - vm.assume(_userA != client); - vm.assume(_userB != client); + vm.assume(_userA != address(flexClient)); + vm.assume(_userB != address(flexClient)); vm.assume(_userA != _userB); _amountA = uint208(bound(_amountA, 1, MAX_VOTES)); @@ -459,7 +454,7 @@ abstract contract GetPastTotalBalance is FlexVotingClientTest { _advanceTimeBy(1); - assertEq(flexClient().getPastTotalBalance(_now()), _amountA + _amountB); + assertEq(flexClient.getPastTotalBalance(_now()), _amountA + _amountB); } function testFuzz_ReturnsTotalDepositsAtAGivenTimepoint( @@ -469,22 +464,22 @@ abstract contract GetPastTotalBalance is FlexVotingClientTest { uint208 _amountB, uint48 _future ) public { - vm.assume(_userA != client); - vm.assume(_userB != client); + 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); + 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); + assertEq(flexClient.getPastTotalBalance(_now() - _future + 1), _amountA); + assertEq(flexClient.getPastTotalBalance(_now()), _amountA + _amountB); } } @@ -493,28 +488,28 @@ abstract contract Withdraw is FlexVotingClientTest { public { _amount = uint208(bound(_amount, 0, type(uint208).max)); - vm.assume(_lender != client); - vm.assume(_borrower != client); + 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); + assertEq(flexClient.deposits(_borrower), 0); + assertEq(flexClient.borrowTotal(_borrower), 0); _mintGovAndDepositIntoFlexClient(_lender, _amount); - assertEq(flexClient().deposits(_lender), _amount); + assertEq(flexClient.deposits(_lender), _amount); // Borrow the funds. vm.prank(_borrower); - flexClient().borrow(_amount); + flexClient.borrow(_amount); assertEq(token.balanceOf(_borrower), _initBalance + _amount); - assertEq(flexClient().borrowTotal(_borrower), _amount); + assertEq(flexClient.borrowTotal(_borrower), _amount); // Deposit totals are unaffected. - assertEq(flexClient().deposits(_lender), _amount); - assertEq(flexClient().deposits(_borrower), 0); + assertEq(flexClient.deposits(_lender), _amount); + assertEq(flexClient.deposits(_borrower), 0); } // `borrow`s affects on vote weights are tested in Vote contract below. @@ -523,18 +518,18 @@ abstract contract Withdraw is FlexVotingClientTest { abstract contract Deposit is FlexVotingClientTest { function testFuzz_UserCanDepositGovTokens(address _user, uint208 _amount) public { _amount = uint208(bound(_amount, 0, type(uint208).max)); - vm.assume(_user != client); + vm.assume(_user != address(flexClient)); uint256 initialBalance = token.balanceOf(_user); - assertEq(flexClient().deposits(_user), 0); + assertEq(flexClient.deposits(_user), 0); _mintGovAndDepositIntoFlexClient(_user, _amount); - assertEq(token.balanceOf(client), _amount); + assertEq(token.balanceOf(address(flexClient)), _amount); assertEq(token.balanceOf(_user), initialBalance); - assertEq(token.getVotes(client), _amount); + assertEq(token.getVotes(address(flexClient)), _amount); // Confirm internal accounting has updated. - assertEq(flexClient().deposits(_user), _amount); + assertEq(flexClient.deposits(_user), _amount); } function testFuzz_DepositsAreCheckpointed( @@ -548,14 +543,14 @@ abstract contract Deposit is FlexVotingClientTest { // Deposit some gov. _mintGovAndDepositIntoFlexClient(_user, _amountA); - assertEq(flexClient().deposits(_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), + flexClient.getPastRawBalance(_user, _checkpoint1), _amountA, "user's first deposit was not properly checkpointed" ); @@ -565,17 +560,17 @@ abstract contract Deposit is FlexVotingClientTest { // Deposit some more. _mintGovAndDepositIntoFlexClient(_user, _amountB); - assertEq(flexClient().deposits(_user), _amountA + _amountB); + assertEq(flexClient.deposits(_user), _amountA + _amountB); _advanceTimeBy(1); // Advance so that we can look at checkpoints. assertEq( - flexClient().getPastRawBalance(_user, _checkpoint1), + flexClient.getPastRawBalance(_user, _checkpoint1), _amountA, "user's first deposit was not properly checkpointed" ); assertEq( - flexClient().getPastRawBalance(_user, _checkpoint2), + flexClient.getPastRawBalance(_user, _checkpoint2), _amountA + _amountB, "user's second deposit was not properly checkpointed" ); @@ -599,9 +594,9 @@ abstract contract ExpressVote is FlexVotingClientTest { // _user should now be able to express his/her vote on the proposal. vm.prank(_user); - flexClient().expressVote(_proposalId, uint8(_voteType)); + flexClient.expressVote(_proposalId, uint8(_voteType)); (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient().proposalVotes(_proposalId); + 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); @@ -629,10 +624,10 @@ abstract contract ExpressVote is FlexVotingClientTest { _mintGovAndDepositIntoFlexClient(_user, _voteWeight); // Now try to express a voting preference on the proposal. - assertEq(flexClient().deposits(_user), _voteWeight); + assertEq(flexClient.deposits(_user), _voteWeight); vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); vm.prank(_user); - flexClient().expressVote(_proposalId, uint8(_voteType)); + flexClient.expressVote(_proposalId, uint8(_voteType)); } function testFuzz_RevertWhen_NoClientWeightButTokenWeight( @@ -646,7 +641,7 @@ abstract contract ExpressVote is FlexVotingClientTest { // Mint gov but do not deposit. _mintGovAndApproveFlexClient(_user, _voteWeight); assertEq(token.balanceOf(_user), _voteWeight); - assertEq(flexClient().deposits(_user), 0); + assertEq(flexClient.deposits(_user), 0); // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -654,19 +649,19 @@ abstract contract ExpressVote is FlexVotingClientTest { // _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)); + flexClient.expressVote(_proposalId, uint8(_voteType)); // Deposit into the client. vm.prank(_user); - flexClient().deposit(_voteWeight); - assertEq(flexClient().deposits(_user), _voteWeight); + 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)); + flexClient.expressVote(_proposalId, uint8(_voteType)); } function testFuzz_RevertOn_DoubleVotes(address _user, uint208 _voteWeight, uint8 _supportType) @@ -683,13 +678,13 @@ abstract contract ExpressVote is FlexVotingClientTest { // _user should now be able to express his/her vote on the proposal. vm.prank(_user); - flexClient().expressVote(_proposalId, uint8(_voteType)); + flexClient.expressVote(_proposalId, uint8(_voteType)); ( uint256 _againstVotesExpressedInit, uint256 _forVotesExpressedInit, uint256 _abstainVotesExpressedInit - ) = flexClient().proposalVotes(_proposalId); + ) = 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); @@ -697,11 +692,11 @@ abstract contract ExpressVote is FlexVotingClientTest { // Vote early and often! vm.expectRevert(FVC.FlexVotingClient__AlreadyVoted.selector); vm.prank(_user); - flexClient().expressVote(_proposalId, uint8(_voteType)); + flexClient.expressVote(_proposalId, uint8(_voteType)); // No votes changed. (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient().proposalVotes(_proposalId); + flexClient.proposalVotes(_proposalId); assertEq(_forVotesExpressed, _forVotesExpressedInit); assertEq(_againstVotesExpressed, _againstVotesExpressedInit); assertEq(_abstainVotesExpressed, _abstainVotesExpressedInit); @@ -725,7 +720,7 @@ abstract contract ExpressVote is FlexVotingClientTest { // 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); + flexClient.expressVote(_proposalId, _supportType); } function testFuzz_RevertOn_UnknownProposal( @@ -756,7 +751,7 @@ abstract contract ExpressVote is FlexVotingClientTest { // Now try to express a voting preference on the bogus proposal. vm.expectRevert(FVC.FlexVotingClient__NoVotingWeight.selector); vm.prank(_user); - flexClient().expressVote(_proposalId, _supportType); + flexClient.expressVote(_proposalId, _supportType); } } @@ -775,9 +770,9 @@ abstract contract CastVote is FlexVotingClientTest { // _user should now be able to express his/her vote on the proposal. vm.prank(_user); - flexClient().expressVote(_proposalId, uint8(_voteType)); + flexClient.expressVote(_proposalId, uint8(_voteType)); (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient().proposalVotes(_proposalId); + 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); @@ -790,7 +785,7 @@ abstract contract CastVote is FlexVotingClientTest { assertEq(_abstainVotes, 0); // Submit votes on behalf of the flexClient. - flexClient().castVote(_proposalId); + flexClient.castVote(_proposalId); // Governor should now record votes from the flexClient. (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); @@ -820,17 +815,17 @@ abstract contract CastVote is FlexVotingClientTest { _mintGovAndDepositIntoFlexClient(_user, _voteWeightB); vm.prank(_user); - flexClient().expressVote(_proposalId, uint8(_voteType)); + 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); + 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); + flexClient.castVote(_proposalId); // Votes cast should likewise reflect only the earlier balance. (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = @@ -861,12 +856,12 @@ abstract contract CastVote is FlexVotingClientTest { // users should now be able to express their votes on the proposal. vm.prank(_userA); - flexClient().expressVote(_proposalId, uint8(GCS.VoteType.Against)); + flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Against)); vm.prank(_userB); - flexClient().expressVote(_proposalId, uint8(GCS.VoteType.Abstain)); + flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Abstain)); (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient().proposalVotes(_proposalId); + flexClient.proposalVotes(_proposalId); assertEq(_forVotesExpressed, 0); assertEq(_againstVotesExpressed, _voteWeightA); assertEq(_abstainVotesExpressed, _voteWeightB); @@ -879,7 +874,7 @@ abstract contract CastVote is FlexVotingClientTest { assertEq(_abstainVotes, 0); // Submit votes on behalf of the flexClient. - flexClient().castVote(_proposalId); + flexClient.castVote(_proposalId); // Governor should now record votes for the flexClient. (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); @@ -927,33 +922,33 @@ abstract contract CastVote is FlexVotingClientTest { // Mint and deposit. _mintGovAndDepositIntoFlexClient(_vars.userA, _vars.voteWeightA); _mintGovAndDepositIntoFlexClient(_vars.userB, _vars.voteWeightB); - uint256 _initDepositWeight = token.balanceOf(client); + uint256 _initDepositWeight = token.balanceOf(address(flexClient)); // Borrow from the flexClient, decreasing its token balance. vm.prank(_vars.userC); - flexClient().borrow(_vars.borrowAmountC); + 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(client); + uint256 _expectedVotingWeight = token.balanceOf(address(flexClient)); assert(_expectedVotingWeight < _initDepositWeight); // A+B express votes vm.prank(_vars.userA); - flexClient().expressVote(_proposalId, _vars.supportTypeA); + flexClient.expressVote(_proposalId, _vars.supportTypeA); vm.prank(_vars.userB); - flexClient().expressVote(_proposalId, _vars.supportTypeB); + 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); + flexClient.borrow(_vars.borrowAmountD); // Submit votes on behalf of the flexClient. - flexClient().castVote(_proposalId); + 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. @@ -1028,29 +1023,29 @@ abstract contract CastVote is FlexVotingClientTest { // Mint and deposit. _mintGovAndDepositIntoFlexClient(_users[0], _voteWeightA); _mintGovAndDepositIntoFlexClient(_users[1], _voteWeightB); - uint256 _initDepositWeight = token.balanceOf(client); + uint256 _initDepositWeight = token.balanceOf(address(flexClient)); // Borrow from the flexClient, decreasing its token balance. vm.prank(_users[2]); - flexClient().borrow(_borrowAmount); + 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(client); + uint256 _totalPossibleVotingWeight = token.balanceOf(address(flexClient)); - uint256 _fullVotingWeight = token.balanceOf(client); + 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)); + flexClient.expressVote(_proposalId, uint8(_voteTypeA)); // Submit votes on behalf of the flexClient. - flexClient().castVote(_proposalId); + 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. @@ -1103,7 +1098,7 @@ abstract contract CastVote is FlexVotingClientTest { // Mint and deposit for just userA. _mintGovAndDepositIntoFlexClient(_users[0], _voteWeightA); - uint256 _initDepositWeight = token.balanceOf(client); + uint256 _initDepositWeight = token.balanceOf(address(flexClient)); // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -1114,16 +1109,16 @@ abstract contract CastVote is FlexVotingClientTest { // Now mint and deposit for userB. _mintGovAndDepositIntoFlexClient(_users[1], _voteWeightB); - uint256 _fullVotingWeight = token.balanceOf(client); + 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)); + flexClient.expressVote(_proposalId, uint8(_voteTypeA)); // Submit votes on behalf of the flexClient. - flexClient().castVote(_proposalId); + flexClient.castVote(_proposalId); (uint256 _againstVotes, uint256 _forVotes, uint256 _abstainVotes) = governor.proposalVotes(_proposalId); @@ -1143,8 +1138,8 @@ abstract contract CastVote is FlexVotingClientTest { _voteWeightA = uint208(bound(_voteWeightA, 1, type(uint120).max)); _voteWeightB = uint208(bound(_voteWeightB, 1, type(uint120).max)); - vm.assume(_userA != client); - vm.assume(_userB != client); + vm.assume(_userA != address(flexClient)); + vm.assume(_userB != address(flexClient)); vm.assume(_userA != _userB); // Deposit some funds. @@ -1156,10 +1151,10 @@ abstract contract CastVote is FlexVotingClientTest { // users should now be able to express their votes on the proposal. vm.prank(_userA); - flexClient().expressVote(_proposalId, uint8(GCS.VoteType.Against)); + flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Against)); (uint256 _againstVotesExpressed, uint256 _forVotesExpressed, uint256 _abstainVotesExpressed) = - flexClient().proposalVotes(_proposalId); + flexClient.proposalVotes(_proposalId); assertEq(_forVotesExpressed, 0); assertEq(_againstVotesExpressed, _voteWeightA); assertEq(_abstainVotesExpressed, 0); @@ -1172,7 +1167,7 @@ abstract contract CastVote is FlexVotingClientTest { assertEq(_abstainVotes, 0); // Submit votes on behalf of the flexClient. - flexClient().castVote(_proposalId); + flexClient.castVote(_proposalId); // Governor should now record votes for the flexClient. (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); @@ -1182,8 +1177,8 @@ abstract contract CastVote is FlexVotingClientTest { // The second user now decides to express and cast. vm.prank(_userB); - flexClient().expressVote(_proposalId, uint8(GCS.VoteType.Abstain)); - flexClient().castVote(_proposalId); + flexClient.expressVote(_proposalId, uint8(GCS.VoteType.Abstain)); + flexClient.castVote(_proposalId); // Governor should now record votes for both users. (_againstVotes, _forVotes, _abstainVotes) = governor.proposalVotes(_proposalId); @@ -1206,18 +1201,18 @@ abstract contract CastVote is FlexVotingClientTest { // No one has expressed, there are no votes to cast. vm.expectRevert(FVC.FlexVotingClient__NoVotesExpressed.selector); - flexClient().castVote(_proposalId); + flexClient.castVote(_proposalId); // _user expresses his/her vote on the proposal. vm.prank(_user); - flexClient().expressVote(_proposalId, uint8(_voteType)); + flexClient.expressVote(_proposalId, uint8(_voteType)); // Submit votes on behalf of the flexClient. - flexClient().castVote(_proposalId); + 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); + flexClient.castVote(_proposalId); } function testFuzz_RevertWhen_AfterVotingPeriod( @@ -1236,7 +1231,7 @@ abstract contract CastVote is FlexVotingClientTest { // Express vote preference. vm.prank(_user); - flexClient().expressVote(_proposalId, uint8(_voteType)); + flexClient.expressVote(_proposalId, uint8(_voteType)); // Jump ahead so that we're outside of the proposal's voting period. _advanceTimeTo(governor.proposalDeadline(_proposalId) + 1); @@ -1251,7 +1246,7 @@ abstract contract CastVote is FlexVotingClientTest { bytes32(1 << uint8(IGovernor.ProposalState.Active)) ) ); - flexClient().castVote(_proposalId); + flexClient.castVote(_proposalId); } } @@ -1272,21 +1267,21 @@ abstract contract Borrow is FlexVotingClientTest { // Borrow some funds. uint256 _initBalance = token.balanceOf(_borrower); vm.prank(_borrower); - flexClient().borrow(_borrowAmount); + flexClient.borrow(_borrowAmount); // Tokens should have been transferred. assertEq(token.balanceOf(_borrower), _initBalance + _borrowAmount); - assertEq(token.balanceOf(client), _depositAmount - _borrowAmount); + assertEq(token.balanceOf(address(flexClient)), _depositAmount - _borrowAmount); // Borrow total has been tracked. - assertEq(flexClient().borrowTotal(_borrower), _borrowAmount); + assertEq(flexClient.borrowTotal(_borrower), _borrowAmount); // The deposit balance of the depositer should not have changed. - assertEq(flexClient().deposits(_depositer), _depositAmount); + 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); + assertEq(flexClient.getPastTotalBalance(_now() - 1), _depositAmount); } } From 94644ec3c2b7e06561c557b203a333c75c669078 Mon Sep 17 00:00:00 2001 From: David Laprade Date: Fri, 17 Jan 2025 17:18:23 -0500 Subject: [PATCH 05/21] Add more tests --- test/FlexVotingDelegatable.t.sol | 329 ++++++++++++++++++++++++++++++- 1 file changed, 320 insertions(+), 9 deletions(-) diff --git a/test/FlexVotingDelegatable.t.sol b/test/FlexVotingDelegatable.t.sol index 98e2691..2a61780 100644 --- a/test/FlexVotingDelegatable.t.sol +++ b/test/FlexVotingDelegatable.t.sol @@ -34,11 +34,6 @@ abstract contract Delegation is FlexVotingClientTest { return MockFlexVotingDelegatableClient(address(flexClient)); } - // TODO - // - delegating adds to a delegate's own votes - // - test multiple delegatees to the same delegate - // - test no double voting for delegatee - // - test that delegator can't vote after delegate votes function testFuzz_selfDelegationByDefault( address _delegator ) public { @@ -111,7 +106,6 @@ abstract contract Delegation is FlexVotingClientTest { assertEq(client().getPastRawBalance(_delegate, _now() - 1), _combined); // Create the proposal. - uint48 _proposalTimepoint = _now(); uint256 _proposalId = _createAndSubmitProposal(); // The delegate expresses a vote. @@ -125,6 +119,83 @@ abstract contract Delegation is FlexVotingClientTest { assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _combined : 0); } + struct Delegator { + address addr; + uint208 weight; + } + + 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); + + _delegatorA.weight = uint208(bound(_delegatorA.weight, 1, MAX_VOTES - 4)); + _delegatorB.weight = uint208(bound(_delegatorB.weight, 1, MAX_VOTES - _delegatorA.weight - 3)); + _delegatorC.weight = uint208(bound(_delegatorC.weight, 1, MAX_VOTES - _delegatorA.weight - _delegatorB.weight - 2)); + _delegatorD.weight = uint208(bound(_delegatorD.weight, 1, MAX_VOTES - _delegatorA.weight - _delegatorB.weight - _delegatorC.weight - 1)); + _delegate.weight = uint208(bound(_delegate.weight, 1, MAX_VOTES - _delegatorA.weight - _delegatorB.weight - _delegatorC.weight - _delegatorD.weight)); + + 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, @@ -145,7 +216,6 @@ abstract contract Delegation is FlexVotingClientTest { assertEq(client().delegates(_delegator), _delegate); // Create the proposal. - uint48 _proposalTimepoint = _now(); uint256 _proposalId = _createAndSubmitProposal(); // The delegator withdraws their funds without voting. @@ -164,6 +234,38 @@ abstract contract Delegation is FlexVotingClientTest { 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_delegateCanExpressVoteWithoutDepositing( address _delegator, address _delegate, @@ -222,6 +324,110 @@ contract BlockNumberClock_Deployment is Deployment { flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_governor))); + } +} +contract BlockNumber__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { + function _timestampClock() internal pure override returns (bool) { + return false; + } + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + } +} +contract BlockNumber__CheckpointTotalBalance is _CheckpointTotalBalance { + function _timestampClock() internal pure override returns (bool) { + return false; + } + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_governor))); + } +} contract BlockNumberClock_Delegation is Delegation { function _timestampClock() internal pure override returns (bool) { return false; @@ -230,7 +436,112 @@ contract BlockNumberClock_Delegation is Delegation { flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); } } -contract TimestampClock_Deployment is Deployment { + +contract TimestampClockClock_Deployment is Deployment { + function _timestampClock() internal pure override returns (bool) { + return true; + } + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_governor))); + } +} +contract TimestampClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { + function _timestampClock() internal pure override returns (bool) { + return true; + } + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + } +} +contract TimestampClock__CheckpointTotalBalance is _CheckpointTotalBalance { + function _timestampClock() internal pure override returns (bool) { + return true; + } + function _deployFlexClient(address _governor) internal override { + flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_governor))); + } +} +contract TimestampClock_Borrow is Borrow { function _timestampClock() internal pure override returns (bool) { return true; } @@ -238,7 +549,7 @@ contract TimestampClock_Deployment is Deployment { flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); } } -contract TimestampClock_Delegation is Delegation { +contract TimestampClockClock_Delegation is Delegation { function _timestampClock() internal pure override returns (bool) { return true; } From 6562f65f9805d33e856bfc4b542bf3eea9f041eb Mon Sep 17 00:00:00 2001 From: David Laprade Date: Mon, 20 Jan 2025 09:36:26 -0500 Subject: [PATCH 06/21] Get tests passing --- test/MockFlexVotingClient.sol | 4 +++ test/MockFlexVotingDelegatableClient.sol | 32 ++++++++++++++++++++++++ test/SharedFlexVoting.t.sol | 14 +++++------ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/test/MockFlexVotingClient.sol b/test/MockFlexVotingClient.sol index de55db4..82cd57e 100644 --- a/test/MockFlexVotingClient.sol +++ b/test/MockFlexVotingClient.sol @@ -29,6 +29,8 @@ contract MockFlexVotingClient is FlexVotingClient { return deposits[_user]; } + // Test hooks + // --------------------------------------------------------------------------- function exposed_rawBalanceOf(address _user) external view returns (uint208) { return _rawBalanceOf(_user); } @@ -56,6 +58,8 @@ contract MockFlexVotingClient is FlexVotingClient { function exposed_checkpointRawBalanceOf(address _user) external { return _checkpointRawBalanceOf(_user); } + // End test hooks + // --------------------------------------------------------------------------- /// @notice Allow a holder of the governance token to deposit it into the pool. /// @param _amount The amount to be deposited. diff --git a/test/MockFlexVotingDelegatableClient.sol b/test/MockFlexVotingDelegatableClient.sol index 95d620a..36f077a 100644 --- a/test/MockFlexVotingDelegatableClient.sol +++ b/test/MockFlexVotingDelegatableClient.sol @@ -30,6 +30,38 @@ contract MockFlexVotingDelegatableClient is FlexVotingDelegatable { 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_checkpointTotalBalance(int256 _delta) external { + return _checkpointTotalBalance(_delta); + } + + function exposed_castVoteReasonString() external returns (string memory) { + return _castVoteReasonString(); + } + + function exposed_selfDelegate() external { + return _selfDelegate(); + } + + function exposed_setDeposits(address _user, uint208 _amount) external { + deposits[_user] = _amount; + } + + function exposed_checkpointRawBalanceOf(address _user) external { + return _checkpointRawBalanceOf(_user); + } + // 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 { diff --git a/test/SharedFlexVoting.t.sol b/test/SharedFlexVoting.t.sol index 572d049..0900e50 100644 --- a/test/SharedFlexVoting.t.sol +++ b/test/SharedFlexVoting.t.sol @@ -233,19 +233,19 @@ abstract contract _CheckpointRawBalanceOf is FlexVotingClientTest { function testFuzz_StoresTheRawBalanceWithTheTimepoint( address _user, uint208 _amount, - uint48 _timepoint + uint48 _future ) public { vm.assume(_user != address(flexClient)); - _timepoint = uint48(bound(_timepoint, _now() + 1, type(uint48).max)); + _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); - assertEq(flexClient.getPastRawBalance(_user, _timepoint), 0); - - _advanceTimeTo(_timepoint); - flexClient.exposed_checkpointRawBalanceOf(_user); - assertEq(flexClient.getPastRawBalance(_user, _timepoint), _amount); + + assertEq(flexClient.getPastRawBalance(_user, _past), 0); + assertEq(flexClient.getPastRawBalance(_user, _future), _amount); } } From 78254c90d47636fcf0602e5d74d063eef5d3c7ad Mon Sep 17 00:00:00 2001 From: David Laprade Date: Mon, 20 Jan 2025 09:37:37 -0500 Subject: [PATCH 07/21] `forge fmt` --- src/FlexVotingClient.sol | 10 +- src/FlexVotingDelegatable.sol | 154 +++++++++++++++---------------- test/FlexVotingClient.t.sol | 28 ++++++ test/FlexVotingDelegatable.t.sol | 94 ++++++++++++++++--- 4 files changed, 186 insertions(+), 100 deletions(-) diff --git a/src/FlexVotingClient.sol b/src/FlexVotingClient.sol index 2bf9412..6118a0d 100644 --- a/src/FlexVotingClient.sol +++ b/src/FlexVotingClient.sol @@ -128,12 +128,10 @@ abstract contract FlexVotingClient { _expressVote(voter, proposalId, support, weight); } - function _expressVote( - address voter, - uint256 proposalId, - uint8 support, - uint256 weight - ) internal virtual { + function _expressVote(address voter, uint256 proposalId, uint8 support, uint256 weight) + internal + virtual + { if (weight == 0) revert FlexVotingClient__NoVotingWeight(); if (proposalVotersHasVoted[proposalId][voter]) revert FlexVotingClient__AlreadyVoted(); diff --git a/src/FlexVotingDelegatable.sol b/src/FlexVotingDelegatable.sol index be66d37..a8ffd59 100644 --- a/src/FlexVotingDelegatable.sol +++ b/src/FlexVotingDelegatable.sol @@ -9,86 +9,80 @@ import {IVotingToken} from "src/interfaces/IVotingToken.sol"; import {FlexVotingClient} from "src/FlexVotingClient.sol"; abstract contract FlexVotingDelegatable is Context, FlexVotingClient { - 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; - - function expressVote(uint256 proposalId, uint8 support) external override virtual { - address voter = _msgSender(); - uint256 weight = FlexVotingClient.getPastRawBalance(voter, GOVERNOR.proposalSnapshot(proposalId)); - _expressVote(voter, proposalId, support, weight); - } - - // @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; - - emit DelegateChanged(account, oldDelegate, delegatee); - _updateDelegateBalance(oldDelegate, delegatee, _rawBalanceOf(account)); + 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; + + function expressVote(uint256 proposalId, uint8 support) external virtual override { + address voter = _msgSender(); + uint256 weight = + FlexVotingClient.getPastRawBalance(voter, GOVERNOR.proposalSnapshot(proposalId)); + _expressVote(voter, proposalId, support, weight); + } + + // @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; + + emit DelegateChanged(account, oldDelegate, delegatee); + _updateDelegateBalance(oldDelegate, delegatee, _rawBalanceOf(account)); + } + + // @dev Moves delegated votes from one delegate to another. + function _updateDelegateBalance(address from, address to, uint208 amount) internal virtual { + if (from == to || amount == 0) return; + + if (from != address(0)) { + (uint256 oldValue, uint256 newValue) = + _push(FlexVotingClient.balanceCheckpoints[from], _subtract, amount); + emit DelegateWeightChanged(from, oldValue, newValue); } - - // @dev Moves delegated votes from one delegate to another. - function _updateDelegateBalance(address from, address to, uint208 amount) internal virtual { - if (from == to || amount == 0) return; - - if (from != address(0)) { - (uint256 oldValue, uint256 newValue) = _push( - FlexVotingClient.balanceCheckpoints[from], - _subtract, - amount - ); - emit DelegateWeightChanged(from, oldValue, newValue); - } - if (to != address(0)) { - (uint256 oldValue, uint256 newValue) = _push( - FlexVotingClient.balanceCheckpoints[to], - _add, - amount - ); - emit DelegateWeightChanged(to, oldValue, newValue); - } - } - - function _push( - Checkpoints.Trace208 storage store, - function(uint208, uint208) view returns (uint208) fn, - uint208 delta - ) private returns (uint208 oldValue, uint208 newValue) { - return store.push( - IVotingToken(GOVERNOR.token()).clock(), - fn(store.latest(), delta) - ); - } - - function _add(uint208 a, uint208 b) private pure returns (uint208) { - return a + b; - } - - function _subtract(uint208 a, uint208 b) private pure returns (uint208) { - return a - b; + if (to != address(0)) { + (uint256 oldValue, uint256 newValue) = + _push(FlexVotingClient.balanceCheckpoints[to], _add, amount); + emit DelegateWeightChanged(to, oldValue, newValue); } + } + + function _push( + Checkpoints.Trace208 storage store, + function(uint208, uint208) view returns (uint208) fn, + uint208 delta + ) private returns (uint208 oldValue, uint208 newValue) { + return store.push(IVotingToken(GOVERNOR.token()).clock(), fn(store.latest(), delta)); + } + + function _add(uint208 a, uint208 b) private pure returns (uint208) { + return a + b; + } + + function _subtract(uint208 a, uint208 b) private pure returns (uint208) { + return a - b; + } } diff --git a/test/FlexVotingClient.t.sol b/test/FlexVotingClient.t.sol index 272a58f..d9c5486 100644 --- a/test/FlexVotingClient.t.sol +++ b/test/FlexVotingClient.t.sol @@ -24,6 +24,7 @@ contract BlockNumberClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -33,6 +34,7 @@ contract BlockNumberClock_Constructor is Constructor { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -42,6 +44,7 @@ contract BlockNumberClock__RawBalanceOf is _RawBalanceOf { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -51,6 +54,7 @@ contract BlockNumberClock__CastVoteReasonString is _CastVoteReasonString { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -60,6 +64,7 @@ contract BlockNumberClock__SelfDelegate is _SelfDelegate { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -69,6 +74,7 @@ contract BlockNumberClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -78,6 +84,7 @@ contract BlockNumberClock_GetPastRawBalance is GetPastRawBalance { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -87,6 +94,7 @@ contract BlockNumber__CheckpointTotalBalance is _CheckpointTotalBalance { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -96,6 +104,7 @@ contract BlockNumberClock_GetPastTotalBalance is GetPastTotalBalance { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -105,6 +114,7 @@ contract BlockNumberClock_Withdraw is Withdraw { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -114,6 +124,7 @@ contract BlockNumberClock_Deposit is Deposit { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -123,6 +134,7 @@ contract BlockNumberClock_ExpressVote is ExpressVote { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -132,6 +144,7 @@ contract BlockNumberClock_CastVote is CastVote { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -141,6 +154,7 @@ contract BlockNumberClock_Borrow is Borrow { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -151,6 +165,7 @@ contract TimestampClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -160,6 +175,7 @@ contract TimestampClock_Constructor is Constructor { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -169,6 +185,7 @@ contract TimestampClock__RawBalanceOf is _RawBalanceOf { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -178,6 +195,7 @@ contract TimestampClock__CastVoteReasonString is _CastVoteReasonString { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -187,6 +205,7 @@ contract TimestampClock__SelfDelegate is _SelfDelegate { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -196,6 +215,7 @@ contract TimestampClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -205,6 +225,7 @@ contract TimestampClock_GetPastRawBalance is GetPastRawBalance { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -214,6 +235,7 @@ contract TimestampClock__CheckpointTotalBalance is _CheckpointTotalBalance { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -223,6 +245,7 @@ contract TimestampClock_GetPastTotalBalance is GetPastTotalBalance { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -232,6 +255,7 @@ contract TimestampClock_Withdraw is Withdraw { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -241,6 +265,7 @@ contract TimestampClock_Deposit is Deposit { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -250,6 +275,7 @@ contract TimestampClock_ExpressVote is ExpressVote { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -259,6 +285,7 @@ contract TimestampClock_CastVote is CastVote { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = new MockFlexVotingClient(_governor); } @@ -268,6 +295,7 @@ 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/FlexVotingDelegatable.t.sol b/test/FlexVotingDelegatable.t.sol index 2a61780..315a014 100644 --- a/test/FlexVotingDelegatable.t.sol +++ b/test/FlexVotingDelegatable.t.sol @@ -34,9 +34,7 @@ abstract contract Delegation is FlexVotingClientTest { return MockFlexVotingDelegatableClient(address(flexClient)); } - function testFuzz_selfDelegationByDefault( - address _delegator - ) public { + function testFuzz_selfDelegationByDefault(address _delegator) public { _assumeSafeUser(_delegator); // By default, the delegator should delegate to themselves. @@ -48,11 +46,9 @@ abstract contract Delegation is FlexVotingClientTest { assertEq(client().delegates(_delegator), _delegator); } - function testFuzz_delegateEmitsEvents( - address _delegator, - address _delegate, - uint208 _weight - ) public { + function testFuzz_delegateEmitsEvents(address _delegator, address _delegate, uint208 _weight) + public + { _assumeSafeUser(_delegator); _assumeSafeUser(_delegate); vm.assume(_delegator != _delegate); @@ -93,9 +89,7 @@ abstract contract Delegation is FlexVotingClientTest { // Delegate. vm.expectEmit(); emit FlexVotingDelegatable.DelegateWeightChanged( - _delegate, - _delegateWeight, - _delegateWeight + _delegatorWeight + _delegate, _delegateWeight, _delegateWeight + _delegatorWeight ); vm.prank(_delegator); client().delegate(_delegate); @@ -156,9 +150,23 @@ abstract contract Delegation is FlexVotingClientTest { _delegatorA.weight = uint208(bound(_delegatorA.weight, 1, MAX_VOTES - 4)); _delegatorB.weight = uint208(bound(_delegatorB.weight, 1, MAX_VOTES - _delegatorA.weight - 3)); - _delegatorC.weight = uint208(bound(_delegatorC.weight, 1, MAX_VOTES - _delegatorA.weight - _delegatorB.weight - 2)); - _delegatorD.weight = uint208(bound(_delegatorD.weight, 1, MAX_VOTES - _delegatorA.weight - _delegatorB.weight - _delegatorC.weight - 1)); - _delegate.weight = uint208(bound(_delegate.weight, 1, MAX_VOTES - _delegatorA.weight - _delegatorB.weight - _delegatorC.weight - _delegatorD.weight)); + _delegatorC.weight = + uint208(bound(_delegatorC.weight, 1, MAX_VOTES - _delegatorA.weight - _delegatorB.weight - 2)); + _delegatorD.weight = uint208( + bound( + _delegatorD.weight, + 1, + MAX_VOTES - _delegatorA.weight - _delegatorB.weight - _delegatorC.weight - 1 + ) + ); + _delegate.weight = uint208( + bound( + _delegate.weight, + 1, + MAX_VOTES - _delegatorA.weight - _delegatorB.weight - _delegatorC.weight + - _delegatorD.weight + ) + ); GCS.VoteType _voteType = _randVoteType(_supportType); @@ -320,118 +328,147 @@ contract BlockNumberClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_governor))); } } + contract BlockNumber__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); } } + contract BlockNumber__CheckpointTotalBalance is _CheckpointTotalBalance { function _timestampClock() internal pure override returns (bool) { return false; } + function _deployFlexClient(address _governor) internal override { flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_governor))); } @@ -441,118 +478,147 @@ contract TimestampClockClock_Deployment is Deployment { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_governor))); } } + contract TimestampClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); } } + contract TimestampClock__CheckpointTotalBalance is _CheckpointTotalBalance { function _timestampClock() internal pure override returns (bool) { return true; } + function _deployFlexClient(address _governor) internal override { flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_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 MockFlexVotingDelegatableClient(_governor))); } From aaa9953f4e316f4cb5dd6a97dd581a052457c90c Mon Sep 17 00:00:00 2001 From: David Laprade Date: Mon, 20 Jan 2025 09:50:40 -0500 Subject: [PATCH 08/21] Didn't need to do this after all --- src/FlexVotingClient.sol | 7 ------- src/FlexVotingDelegatable.sol | 7 ------- 2 files changed, 14 deletions(-) diff --git a/src/FlexVotingClient.sol b/src/FlexVotingClient.sol index 6118a0d..3311a5b 100644 --- a/src/FlexVotingClient.sol +++ b/src/FlexVotingClient.sol @@ -125,13 +125,6 @@ abstract contract FlexVotingClient { function expressVote(uint256 proposalId, uint8 support) external virtual { address voter = msg.sender; uint256 weight = getPastRawBalance(voter, GOVERNOR.proposalSnapshot(proposalId)); - _expressVote(voter, proposalId, support, weight); - } - - function _expressVote(address voter, uint256 proposalId, uint8 support, uint256 weight) - internal - virtual - { if (weight == 0) revert FlexVotingClient__NoVotingWeight(); if (proposalVotersHasVoted[proposalId][voter]) revert FlexVotingClient__AlreadyVoted(); diff --git a/src/FlexVotingDelegatable.sol b/src/FlexVotingDelegatable.sol index a8ffd59..6ddc58f 100644 --- a/src/FlexVotingDelegatable.sol +++ b/src/FlexVotingDelegatable.sol @@ -22,13 +22,6 @@ abstract contract FlexVotingDelegatable is Context, FlexVotingClient { mapping(address account => address) private _delegatee; - function expressVote(uint256 proposalId, uint8 support) external virtual override { - address voter = _msgSender(); - uint256 weight = - FlexVotingClient.getPastRawBalance(voter, GOVERNOR.proposalSnapshot(proposalId)); - _expressVote(voter, proposalId, support, weight); - } - // @dev Delegates votes from the sender to `delegatee`. function delegate(address delegatee) public virtual { address account = _msgSender(); From 751454bcfb2c2ba5022eca7fab4de317df4a0302 Mon Sep 17 00:00:00 2001 From: David Laprade Date: Mon, 20 Jan 2025 16:51:28 -0500 Subject: [PATCH 09/21] Simplify subdelegation implementations --- src/FlexVotingClient.sol | 33 ++++++--- src/FlexVotingDelegatable.sol | 46 +++++------- test/MockFlexVotingClient.sol | 14 ++-- test/MockFlexVotingDelegatableClient.sol | 93 ++---------------------- test/SharedFlexVoting.t.sol | 3 +- 5 files changed, 59 insertions(+), 130 deletions(-) diff --git a/src/FlexVotingClient.sol b/src/FlexVotingClient.sol index 3311a5b..adb7edf 100644 --- a/src/FlexVotingClient.sol +++ b/src/FlexVotingClient.sol @@ -202,13 +202,11 @@ 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)); - } + function _applyDeltaToCheckpoint( + Checkpoints.Trace208 storage _checkpoint, + int256 _delta + ) internal returns (uint208 _prevTotal, uint208 _newTotal) { - /// @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 @@ -226,12 +224,25 @@ abstract contract FlexVotingClient { // 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); + _prevTotal = _checkpoint.latest(); + int256 _castTotal = int256(uint256(_prevTotal)); + _newTotal = SafeCast.toUint208(uint256(_castTotal + _delta)); - totalBalanceCheckpoints.push( - IVotingToken(GOVERNOR.token()).clock(), SafeCast.toUint208(_newTotal) - ); + uint48 _timepoint = IVotingToken(GOVERNOR.token()).clock(); + _checkpoint.push(_timepoint, _newTotal); + } + + /// @dev Checkpoints the _user's current raw balance. + function _checkpointRawBalanceOf( + address _user, + int256 _delta + ) internal virtual { + _applyDeltaToCheckpoint(balanceCheckpoints[_user], _delta); + } + + /// @dev Checkpoints the total balance after applying `_delta`. + function _checkpointTotalBalance(int256 _delta) internal virtual { + _applyDeltaToCheckpoint(totalBalanceCheckpoints, _delta); } /// @notice Returns the `_user`'s raw balance at `_timepoint`. diff --git a/src/FlexVotingDelegatable.sol b/src/FlexVotingDelegatable.sol index 6ddc58f..a76eb28 100644 --- a/src/FlexVotingDelegatable.sol +++ b/src/FlexVotingDelegatable.sol @@ -43,39 +43,31 @@ abstract contract FlexVotingDelegatable is Context, FlexVotingClient { address oldDelegate = delegates(account); _delegatee[account] = delegatee; + int256 _delta = int256(uint256(_rawBalanceOf(account))); emit DelegateChanged(account, oldDelegate, delegatee); - _updateDelegateBalance(oldDelegate, delegatee, _rawBalanceOf(account)); + _updateDelegateBalance(oldDelegate, delegatee, _delta); } - // @dev Moves delegated votes from one delegate to another. - function _updateDelegateBalance(address from, address to, uint208 amount) internal virtual { - if (from == to || amount == 0) return; - - if (from != address(0)) { - (uint256 oldValue, uint256 newValue) = - _push(FlexVotingClient.balanceCheckpoints[from], _subtract, amount); - emit DelegateWeightChanged(from, oldValue, newValue); - } - if (to != address(0)) { - (uint256 oldValue, uint256 newValue) = - _push(FlexVotingClient.balanceCheckpoints[to], _add, amount); - emit DelegateWeightChanged(to, oldValue, newValue); - } + function _checkpointRawBalanceOf( + address _user, + int256 _delta + ) internal virtual override { + address _proxy = delegates(_user); + _applyDeltaToCheckpoint(balanceCheckpoints[_proxy], _delta); } - function _push( - Checkpoints.Trace208 storage store, - function(uint208, uint208) view returns (uint208) fn, - uint208 delta - ) private returns (uint208 oldValue, uint208 newValue) { - return store.push(IVotingToken(GOVERNOR.token()).clock(), fn(store.latest(), 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; - function _add(uint208 a, uint208 b) private pure returns (uint208) { - return a + b; - } + // Decrement old delegate's weight. + (uint208 _oldFrom, uint208 _newFrom) = + _applyDeltaToCheckpoint(balanceCheckpoints[from], -_delta); + emit DelegateWeightChanged(from, _oldFrom, _newFrom); - function _subtract(uint208 a, uint208 b) private pure returns (uint208) { - return a - b; + // Increment new delegate's weight. + (uint208 _oldTo, uint208 _newTo) = + _applyDeltaToCheckpoint(balanceCheckpoints[to], _delta); + emit DelegateWeightChanged(to, _oldTo, _newTo); } } diff --git a/test/MockFlexVotingClient.sol b/test/MockFlexVotingClient.sol index 82cd57e..239c131 100644 --- a/test/MockFlexVotingClient.sol +++ b/test/MockFlexVotingClient.sol @@ -55,8 +55,8 @@ contract MockFlexVotingClient is FlexVotingClient { deposits[_user] = _amount; } - function exposed_checkpointRawBalanceOf(address _user) external { - return _checkpointRawBalanceOf(_user); + function exposed_checkpointRawBalanceOf(address _user, int256 _delta) external { + _checkpointRawBalanceOf(_user, _delta); } // End test hooks // --------------------------------------------------------------------------- @@ -66,8 +66,9 @@ contract MockFlexVotingClient is FlexVotingClient { function deposit(uint208 _amount) public { deposits[msg.sender] += _amount; - FlexVotingClient._checkpointRawBalanceOf(msg.sender); - FlexVotingClient._checkpointTotalBalance(int256(uint256(_amount))); + int256 _delta = int256(uint256(_amount)); + _checkpointRawBalanceOf(msg.sender, _delta); + _checkpointTotalBalance(_delta); // Assumes revert on failure. TOKEN.transferFrom(msg.sender, address(this), _amount); @@ -79,8 +80,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)); + _checkpointRawBalanceOf(msg.sender, _delta); + _checkpointTotalBalance(_delta); TOKEN.transfer(msg.sender, _amount); // Assumes revert on failure. } diff --git a/test/MockFlexVotingDelegatableClient.sol b/test/MockFlexVotingDelegatableClient.sol index 36f077a..9e21c31 100644 --- a/test/MockFlexVotingDelegatableClient.sol +++ b/test/MockFlexVotingDelegatableClient.sol @@ -9,92 +9,15 @@ import {IVotingToken} from "src/interfaces/IVotingToken.sol"; import {FlexVotingClient} from "src/FlexVotingClient.sol"; import {FlexVotingDelegatable} from "src/FlexVotingDelegatable.sol"; -contract MockFlexVotingDelegatableClient is FlexVotingDelegatable { - using Checkpoints for Checkpoints.Trace208; +import {MockFlexVotingClient} from "test/MockFlexVotingClient.sol"; - /// @notice The governance token held and lent by this pool. - ERC20Votes public immutable TOKEN; +contract MockFlexVotingDelegatableClient is MockFlexVotingClient, FlexVotingDelegatable { + constructor(address _governor) MockFlexVotingClient(_governor) {} - /// @notice Map depositor to deposit amount. - mapping(address => uint208) public deposits; - - /// @notice Map borrower to total amount borrowed. - mapping(address => uint256) public borrowTotal; - - constructor(address _governor) FlexVotingClient(_governor) { - TOKEN = ERC20Votes(GOVERNOR.token()); - _selfDelegate(); - } - - function _rawBalanceOf(address _user) internal view override returns (uint208) { - 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_checkpointTotalBalance(int256 _delta) external { - return _checkpointTotalBalance(_delta); - } - - function exposed_castVoteReasonString() external returns (string memory) { - return _castVoteReasonString(); - } - - function exposed_selfDelegate() external { - return _selfDelegate(); - } - - function exposed_setDeposits(address _user, uint208 _amount) external { - deposits[_user] = _amount; - } - - function exposed_checkpointRawBalanceOf(address _user) external { - return _checkpointRawBalanceOf(_user); - } - // 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._checkpointTotalBalance(int256(uint256(_amount))); - - address _delegate = delegates(msg.sender); - FlexVotingDelegatable._updateDelegateBalance(address(0), _delegate, _amount); - - // Assumes revert on failure. - TOKEN.transferFrom(msg.sender, address(this), _amount); - } - - /// @notice Allow a depositor to withdraw funds previously deposited to the pool. - /// @param _amount The amount to be withdrawn. - function withdraw(uint208 _amount) public { - // Overflows & reverts if user does not have sufficient deposits. - deposits[msg.sender] -= _amount; - - FlexVotingClient._checkpointTotalBalance(-1 * int256(uint256(_amount))); - - address _delegate = delegates(msg.sender); - FlexVotingDelegatable._updateDelegateBalance(_delegate, address(0), _amount); - - TOKEN.transfer(msg.sender, _amount); // Assumes revert on failure. - } - - /// @notice Arbitrarily remove tokens from the pool. This is to simulate a borrower, hence the - /// method name. Since this is just a proof-of-concept, nothing else is actually done here. - /// @param _amount The amount to "borrow." - function borrow(uint256 _amount) public { - borrowTotal[msg.sender] += _amount; - TOKEN.transfer(msg.sender, _amount); + function _checkpointRawBalanceOf( + address _user, + int256 _delta + ) internal override(FlexVotingClient, FlexVotingDelegatable) { + return FlexVotingDelegatable._checkpointRawBalanceOf(_user, _delta); } } diff --git a/test/SharedFlexVoting.t.sol b/test/SharedFlexVoting.t.sol index 0900e50..dda862b 100644 --- a/test/SharedFlexVoting.t.sol +++ b/test/SharedFlexVoting.t.sol @@ -242,7 +242,8 @@ abstract contract _CheckpointRawBalanceOf is FlexVotingClientTest { _advanceTimeTo(_future); flexClient.exposed_setDeposits(_user, _amount); - flexClient.exposed_checkpointRawBalanceOf(_user); + int256 _delta = int256(uint256(_amount)); + flexClient.exposed_checkpointRawBalanceOf(_user, _delta); assertEq(flexClient.getPastRawBalance(_user, _past), 0); assertEq(flexClient.getPastRawBalance(_user, _future), _amount); From d227c104e51ad23fc22cb6f354318a8e5f1f865f Mon Sep 17 00:00:00 2001 From: David Laprade Date: Mon, 20 Jan 2025 16:54:06 -0500 Subject: [PATCH 10/21] Clean up the mess left by `forge fmt` --- test/FlexVotingDelegatable.t.sol | 35 +++++++++++++++----------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/test/FlexVotingDelegatable.t.sol b/test/FlexVotingDelegatable.t.sol index 315a014..aae1326 100644 --- a/test/FlexVotingDelegatable.t.sol +++ b/test/FlexVotingDelegatable.t.sol @@ -148,25 +148,22 @@ abstract contract Delegation is FlexVotingClientTest { vm.assume(_delegatorC.addr != _delegate.addr); vm.assume(_delegatorD.addr != _delegate.addr); - _delegatorA.weight = uint208(bound(_delegatorA.weight, 1, MAX_VOTES - 4)); - _delegatorB.weight = uint208(bound(_delegatorB.weight, 1, MAX_VOTES - _delegatorA.weight - 3)); - _delegatorC.weight = - uint208(bound(_delegatorC.weight, 1, MAX_VOTES - _delegatorA.weight - _delegatorB.weight - 2)); - _delegatorD.weight = uint208( - bound( - _delegatorD.weight, - 1, - MAX_VOTES - _delegatorA.weight - _delegatorB.weight - _delegatorC.weight - 1 - ) - ); - _delegate.weight = uint208( - bound( - _delegate.weight, - 1, - MAX_VOTES - _delegatorA.weight - _delegatorB.weight - _delegatorC.weight - - _delegatorD.weight - ) - ); + 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); From a8160d38bb69db0ca54df7606537408559f9476b Mon Sep 17 00:00:00 2001 From: David Laprade Date: Tue, 21 Jan 2025 10:31:44 -0500 Subject: [PATCH 11/21] Test changing delegates --- test/FlexVotingDelegatable.t.sol | 90 ++++++++++++++++++++++++++++++-- test/SharedFlexVoting.t.sol | 6 ++- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/test/FlexVotingDelegatable.t.sol b/test/FlexVotingDelegatable.t.sol index aae1326..a92789c 100644 --- a/test/FlexVotingDelegatable.t.sol +++ b/test/FlexVotingDelegatable.t.sol @@ -28,6 +28,11 @@ import { } 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 (MockFlexVotingDelegatableClient) { @@ -113,11 +118,6 @@ abstract contract Delegation is FlexVotingClientTest { assertEq(_abstainVotesExpressed, _voteType == GCS.VoteType.Abstain ? _combined : 0); } - struct Delegator { - address addr; - uint208 weight; - } - function testFuzz_multipleAddressesDelegate( Delegator memory _delegatorA, Delegator memory _delegatorB, @@ -271,6 +271,86 @@ abstract contract Delegation is FlexVotingClientTest { 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, diff --git a/test/SharedFlexVoting.t.sol b/test/SharedFlexVoting.t.sol index dda862b..1b600e4 100644 --- a/test/SharedFlexVoting.t.sol +++ b/test/SharedFlexVoting.t.sol @@ -79,11 +79,15 @@ abstract contract FlexVotingClientTest is Test { } 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("mockReceiverFunction()"); + bytes memory receiverCallData = abi.encodeWithSignature(_sig); address[] memory targets = new address[](1); uint256[] memory values = new uint256[](1); bytes[] memory calldatas = new bytes[](1); From 3ad35c03acf8148e0178bf1288f1b652884555a8 Mon Sep 17 00:00:00 2001 From: David Laprade Date: Tue, 21 Jan 2025 11:26:40 -0500 Subject: [PATCH 12/21] Rename all the things --- src/FlexVotingClient.sol | 81 ++++++++++++----------- src/FlexVotingDelegatable.sol | 8 +-- test/FlexVotingClient.invariants.t.sol | 6 +- test/FlexVotingClient.t.sol | 12 ++-- test/FlexVotingDelegatable.t.sol | 24 +++---- test/MockFlexVotingClient.sol | 20 +++--- test/MockFlexVotingDelegatableClient.sol | 10 +-- test/SharedFlexVoting.t.sol | 80 +++++++++++----------- test/handlers/FlexVotingClientHandler.sol | 2 +- 9 files changed, 119 insertions(+), 124 deletions(-) diff --git a/src/FlexVotingClient.sol b/src/FlexVotingClient.sol index adb7edf..f2e0211 100644 --- a/src/FlexVotingClient.sol +++ b/src/FlexVotingClient.sol @@ -79,14 +79,15 @@ abstract contract FlexVotingClient { /// 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) internal balanceCheckpoints; + /// @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 raw balances in the system. May or may + /// @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 totalBalanceCheckpoints; + Checkpoints.Trace208 internal totalVoteWeightCheckpoints; // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7b74442c5e87ea51dde41c7f18a209fa5154f1a4/contracts/governance/extensions/GovernorCountingFractional.sol#L37 uint8 internal constant VOTE_TYPE_FRACTIONAL = 255; @@ -124,7 +125,7 @@ abstract contract FlexVotingClient { /// @param support The depositor's vote preferences in accordance with the `VoteType` enum. function expressVote(uint256 proposalId, uint8 support) external virtual { address voter = msg.sender; - uint256 weight = getPastRawBalance(voter, GOVERNOR.proposalSnapshot(proposalId)); + uint256 weight = getPastVoteWeight(voter, GOVERNOR.proposalSnapshot(proposalId)); if (weight == 0) revert FlexVotingClient__NoVotingWeight(); if (proposalVotersHasVoted[proposalId][voter]) revert FlexVotingClient__AlreadyVoted(); @@ -143,10 +144,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) { @@ -155,7 +158,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 @@ -168,28 +171,28 @@ 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 + // userVoteWeight = userVotesInternal * totalTokenWeight / totalVotesInternal uint128 _forVotesToCast = SafeCast.toUint128( - (_votingWeightAtSnapshot * _proposalVote.forVotes) / _totalRawBalanceAtSnapshot + (_totalTokenWeight * _proposalVote.forVotes) / _totalVotesInternal ); uint128 _againstVotesToCast = SafeCast.toUint128( - (_votingWeightAtSnapshot * _proposalVote.againstVotes) / _totalRawBalanceAtSnapshot + (_totalTokenWeight * _proposalVote.againstVotes) / _totalVotesInternal ); uint128 _abstainVotesToCast = SafeCast.toUint128( - (_votingWeightAtSnapshot * _proposalVote.abstainVotes) / _totalRawBalanceAtSnapshot + (_totalTokenWeight * _proposalVote.abstainVotes) / _totalVotesInternal ); // Clear the stored votes so that we don't double-cast them. @@ -206,11 +209,9 @@ abstract contract FlexVotingClient { 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 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. @@ -232,35 +233,35 @@ abstract contract FlexVotingClient { _checkpoint.push(_timepoint, _newTotal); } - /// @dev Checkpoints the _user's current raw balance. - function _checkpointRawBalanceOf( + /// @dev Checkpoints internal voting weight of `user` after applying `_delta`. + function _checkpointVoteWeightOf( address _user, int256 _delta ) internal virtual { - _applyDeltaToCheckpoint(balanceCheckpoints[_user], _delta); + _applyDeltaToCheckpoint(voteWeightCheckpoints[_user], _delta); } - /// @dev Checkpoints the total balance after applying `_delta`. - function _checkpointTotalBalance(int256 _delta) internal virtual { - _applyDeltaToCheckpoint(totalBalanceCheckpoints, _delta); + /// @dev Checkpoints the total vote weight after applying `_delta`. + function _checkpointTotalVoteWeight(int256 _delta) internal virtual { + _applyDeltaToCheckpoint(totalVoteWeightCheckpoints, _delta); } - /// @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/FlexVotingDelegatable.sol b/src/FlexVotingDelegatable.sol index a76eb28..07075e1 100644 --- a/src/FlexVotingDelegatable.sol +++ b/src/FlexVotingDelegatable.sol @@ -48,12 +48,12 @@ abstract contract FlexVotingDelegatable is Context, FlexVotingClient { _updateDelegateBalance(oldDelegate, delegatee, _delta); } - function _checkpointRawBalanceOf( + function _checkpointVoteWeightOf( address _user, int256 _delta ) internal virtual override { address _proxy = delegates(_user); - _applyDeltaToCheckpoint(balanceCheckpoints[_proxy], _delta); + _applyDeltaToCheckpoint(voteWeightCheckpoints[_proxy], _delta); } // @dev Moves delegated votes from one delegate to another. @@ -62,12 +62,12 @@ abstract contract FlexVotingDelegatable is Context, FlexVotingClient { // Decrement old delegate's weight. (uint208 _oldFrom, uint208 _newFrom) = - _applyDeltaToCheckpoint(balanceCheckpoints[from], -_delta); + _applyDeltaToCheckpoint(voteWeightCheckpoints[from], -_delta); emit DelegateWeightChanged(from, _oldFrom, _newFrom); // Increment new delegate's weight. (uint208 _oldTo, uint208 _newTo) = - _applyDeltaToCheckpoint(balanceCheckpoints[to], _delta); + _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 d9c5486..fc41291 100644 --- a/test/FlexVotingClient.t.sol +++ b/test/FlexVotingClient.t.sol @@ -8,8 +8,8 @@ import { _RawBalanceOf, _CastVoteReasonString, _SelfDelegate, - _CheckpointRawBalanceOf, - _CheckpointTotalBalance, + _CheckpointVoteWeightOf, + _CheckpointTotalVoteWeight, GetPastRawBalance, GetPastTotalBalance, Withdraw, @@ -70,7 +70,7 @@ contract BlockNumberClock__SelfDelegate is _SelfDelegate { } } -contract BlockNumberClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { +contract BlockNumberClock__CheckpointVoteWeightOf is _CheckpointVoteWeightOf { function _timestampClock() internal pure override returns (bool) { return false; } @@ -90,7 +90,7 @@ contract BlockNumberClock_GetPastRawBalance is GetPastRawBalance { } } -contract BlockNumber__CheckpointTotalBalance is _CheckpointTotalBalance { +contract BlockNumber__CheckpointTotalVoteWeight is _CheckpointTotalVoteWeight { function _timestampClock() internal pure override returns (bool) { return false; } @@ -211,7 +211,7 @@ contract TimestampClock__SelfDelegate is _SelfDelegate { } } -contract TimestampClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { +contract TimestampClock__CheckpointVoteWeightOf is _CheckpointVoteWeightOf { function _timestampClock() internal pure override returns (bool) { return true; } @@ -231,7 +231,7 @@ contract TimestampClock_GetPastRawBalance is GetPastRawBalance { } } -contract TimestampClock__CheckpointTotalBalance is _CheckpointTotalBalance { +contract TimestampClock__CheckpointTotalVoteWeight is _CheckpointTotalVoteWeight { function _timestampClock() internal pure override returns (bool) { return true; } diff --git a/test/FlexVotingDelegatable.t.sol b/test/FlexVotingDelegatable.t.sol index a92789c..a23d0f3 100644 --- a/test/FlexVotingDelegatable.t.sol +++ b/test/FlexVotingDelegatable.t.sol @@ -16,8 +16,8 @@ import { _RawBalanceOf, _CastVoteReasonString, _SelfDelegate, - _CheckpointRawBalanceOf, - _CheckpointTotalBalance, + _CheckpointVoteWeightOf, + _CheckpointTotalVoteWeight, GetPastRawBalance, GetPastTotalBalance, Withdraw, @@ -88,8 +88,8 @@ abstract contract Delegation is FlexVotingClientTest { _mintGovAndDepositIntoFlexClient(_delegate, _delegateWeight); _advanceTimeBy(1); // Make past balances retrievable. - assertEq(client().getPastRawBalance(_delegate, _now() - 1), _delegateWeight); - assertEq(client().getPastRawBalance(_delegator, _now() - 1), _delegatorWeight); + assertEq(client().getPastVoteWeight(_delegate, _now() - 1), _delegateWeight); + assertEq(client().getPastVoteWeight(_delegator, _now() - 1), _delegatorWeight); // Delegate. vm.expectEmit(); @@ -101,8 +101,8 @@ abstract contract Delegation is FlexVotingClientTest { uint256 _combined = _delegatorWeight + _delegateWeight; _advanceTimeBy(1); // Make past balances retrievable. - assertEq(client().getPastRawBalance(_delegator, _now() - 1), 0); - assertEq(client().getPastRawBalance(_delegate, _now() - 1), _combined); + assertEq(client().getPastVoteWeight(_delegator, _now() - 1), 0); + assertEq(client().getPastVoteWeight(_delegate, _now() - 1), _combined); // Create the proposal. uint256 _proposalId = _createAndSubmitProposal(); @@ -382,14 +382,14 @@ abstract contract Delegation is FlexVotingClientTest { // The delegator has no weight to vote with, despite having a deposit balance. assertEq(client().deposits(_delegator), _weight); - assertEq(client().getPastRawBalance(_delegator, _proposalTimepoint), 0); + 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().getPastRawBalance(_delegate, _proposalTimepoint), _weight); + assertEq(client().getPastVoteWeight(_delegate, _proposalTimepoint), _weight); vm.prank(_delegate); client().expressVote(_proposalId, uint8(_voteType)); @@ -451,7 +451,7 @@ contract BlockNumber__SelfDelegate is _SelfDelegate { } } -contract BlockNumber__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { +contract BlockNumber__CheckpointVoteWeightOf is _CheckpointVoteWeightOf { function _timestampClock() internal pure override returns (bool) { return false; } @@ -461,7 +461,7 @@ contract BlockNumber__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { } } -contract BlockNumber__CheckpointTotalBalance is _CheckpointTotalBalance { +contract BlockNumber__CheckpointTotalVoteWeight is _CheckpointTotalVoteWeight { function _timestampClock() internal pure override returns (bool) { return false; } @@ -601,7 +601,7 @@ contract TimestampClock__SelfDelegate is _SelfDelegate { } } -contract TimestampClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { +contract TimestampClock__CheckpointVoteWeightOf is _CheckpointVoteWeightOf { function _timestampClock() internal pure override returns (bool) { return true; } @@ -611,7 +611,7 @@ contract TimestampClock__CheckpointRawBalanceOf is _CheckpointRawBalanceOf { } } -contract TimestampClock__CheckpointTotalBalance is _CheckpointTotalBalance { +contract TimestampClock__CheckpointTotalVoteWeight is _CheckpointTotalVoteWeight { function _timestampClock() internal pure override returns (bool) { return true; } diff --git a/test/MockFlexVotingClient.sol b/test/MockFlexVotingClient.sol index 239c131..e75c385 100644 --- a/test/MockFlexVotingClient.sol +++ b/test/MockFlexVotingClient.sol @@ -35,12 +35,12 @@ contract MockFlexVotingClient is FlexVotingClient { 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) { @@ -55,8 +55,8 @@ contract MockFlexVotingClient is FlexVotingClient { deposits[_user] = _amount; } - function exposed_checkpointRawBalanceOf(address _user, int256 _delta) external { - _checkpointRawBalanceOf(_user, _delta); + function exposed_checkpointVoteWeightOf(address _user, int256 _delta) external { + _checkpointVoteWeightOf(_user, _delta); } // End test hooks // --------------------------------------------------------------------------- @@ -67,8 +67,8 @@ contract MockFlexVotingClient is FlexVotingClient { deposits[msg.sender] += _amount; int256 _delta = int256(uint256(_amount)); - _checkpointRawBalanceOf(msg.sender, _delta); - _checkpointTotalBalance(_delta); + _checkpointVoteWeightOf(msg.sender, _delta); + _checkpointTotalVoteWeight(_delta); // Assumes revert on failure. TOKEN.transferFrom(msg.sender, address(this), _amount); @@ -81,8 +81,8 @@ contract MockFlexVotingClient is FlexVotingClient { deposits[msg.sender] -= _amount; int256 _delta = -1 * int256(uint256(_amount)); - _checkpointRawBalanceOf(msg.sender, _delta); - _checkpointTotalBalance(_delta); + _checkpointVoteWeightOf(msg.sender, _delta); + _checkpointTotalVoteWeight(_delta); TOKEN.transfer(msg.sender, _amount); // Assumes revert on failure. } diff --git a/test/MockFlexVotingDelegatableClient.sol b/test/MockFlexVotingDelegatableClient.sol index 9e21c31..f7a453c 100644 --- a/test/MockFlexVotingDelegatableClient.sol +++ b/test/MockFlexVotingDelegatableClient.sol @@ -1,23 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.10; -import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; -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 {FlexVotingClient} from "src/FlexVotingClient.sol"; import {FlexVotingDelegatable} from "src/FlexVotingDelegatable.sol"; - import {MockFlexVotingClient} from "test/MockFlexVotingClient.sol"; contract MockFlexVotingDelegatableClient is MockFlexVotingClient, FlexVotingDelegatable { constructor(address _governor) MockFlexVotingClient(_governor) {} - function _checkpointRawBalanceOf( + function _checkpointVoteWeightOf( address _user, int256 _delta ) internal override(FlexVotingClient, FlexVotingDelegatable) { - return FlexVotingDelegatable._checkpointRawBalanceOf(_user, _delta); + return FlexVotingDelegatable._checkpointVoteWeightOf(_user, _delta); } } diff --git a/test/SharedFlexVoting.t.sol b/test/SharedFlexVoting.t.sol index 1b600e4..f69baf4 100644 --- a/test/SharedFlexVoting.t.sol +++ b/test/SharedFlexVoting.t.sol @@ -233,7 +233,7 @@ abstract contract _SelfDelegate is FlexVotingClientTest { } // Contract name has a leading underscore for scopelint spec support. -abstract contract _CheckpointRawBalanceOf is FlexVotingClientTest { +abstract contract _CheckpointVoteWeightOf is FlexVotingClientTest { function testFuzz_StoresTheRawBalanceWithTheTimepoint( address _user, uint208 _amount, @@ -247,27 +247,27 @@ abstract contract _CheckpointRawBalanceOf is FlexVotingClientTest { _advanceTimeTo(_future); flexClient.exposed_setDeposits(_user, _amount); int256 _delta = int256(uint256(_amount)); - flexClient.exposed_checkpointRawBalanceOf(_user, _delta); + flexClient.exposed_checkpointVoteWeightOf(_user, _delta); - assertEq(flexClient.getPastRawBalance(_user, _past), 0); - assertEq(flexClient.getPastRawBalance(_user, _future), _amount); + assertEq(flexClient.getPastVoteWeight(_user, _past), 0); + assertEq(flexClient.getPastVoteWeight(_user, _future), _amount); } } -abstract contract _CheckpointTotalBalance is FlexVotingClientTest { +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_latestTotalBalance(), 0); + assertEq(flexClient.exposed_latestTotalWeight(), 0); _advanceTimeTo(_timepoint); - flexClient.exposed_checkpointTotalBalance(_value); + flexClient.exposed_checkpointTotalVoteWeight(_value); _advanceTimeBy(1); - assertEq(flexClient.getPastTotalBalance(_timepoint), uint256(_value)); - assertEq(flexClient.exposed_latestTotalBalance(), uint256(_value)); + assertEq(flexClient.getPastTotalVoteWeight(_timepoint), uint256(_value)); + assertEq(flexClient.exposed_latestTotalWeight(), uint256(_value)); } function testFuzz_checkpointsTheTotalBalanceDeltaAtClockTime( @@ -278,13 +278,13 @@ abstract contract _CheckpointTotalBalance is FlexVotingClientTest { _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); + flexClient.exposed_checkpointTotalVoteWeight(_initBalance); _advanceTimeTo(_timepoint); - flexClient.exposed_checkpointTotalBalance(_delta); + flexClient.exposed_checkpointTotalVoteWeight(_delta); _advanceTimeBy(1); - assertEq(flexClient.getPastTotalBalance(_timepoint), uint256(_initBalance + _delta)); + assertEq(flexClient.getPastTotalVoteWeight(_timepoint), uint256(_initBalance + _delta)); } function testFuzz_RevertIf_negativeDeltaWraps(int256 delta, uint208 balance) public { @@ -325,16 +325,16 @@ abstract contract _CheckpointTotalBalance is FlexVotingClientTest { function testFuzz_RevertIf_withdrawalFromZero(int256 _withdraw) public { _withdraw = bound(_withdraw, type(int208).min, -1); vm.expectRevert(); - flexClient.exposed_checkpointTotalBalance(_withdraw); + 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_checkpointTotalBalance(_deposit); + flexClient.exposed_checkpointTotalVoteWeight(_deposit); vm.expectRevert(); - flexClient.exposed_checkpointTotalBalance(_withdraw); + flexClient.exposed_checkpointTotalVoteWeight(_withdraw); } function testFuzz_RevertIf_depositsOverflow(int256 _deposit1, int256 _deposit2) public { @@ -342,9 +342,9 @@ abstract contract _CheckpointTotalBalance is FlexVotingClientTest { _deposit1 = bound(_deposit1, 1, _max); _deposit2 = bound(_deposit2, 1 + _max - _deposit1, _max); - flexClient.exposed_checkpointTotalBalance(_deposit1); + flexClient.exposed_checkpointTotalVoteWeight(_deposit1); vm.expectRevert(); - flexClient.exposed_checkpointTotalBalance(_deposit2); + flexClient.exposed_checkpointTotalVoteWeight(_deposit2); } } @@ -360,14 +360,14 @@ abstract contract GetPastRawBalance is FlexVotingClientTest { _amount = uint208(bound(_amount, 1, MAX_VOTES)); _advanceTimeBy(1); - assertEq(flexClient.getPastRawBalance(_depositor, 0), 0); - assertEq(flexClient.getPastRawBalance(_nonDepositor, 0), 0); + assertEq(flexClient.getPastVoteWeight(_depositor, 0), 0); + assertEq(flexClient.getPastVoteWeight(_nonDepositor, 0), 0); _mintGovAndDepositIntoFlexClient(_depositor, _amount); _advanceTimeBy(1); - assertEq(flexClient.getPastRawBalance(_depositor, _now() - 1), _amount); - assertEq(flexClient.getPastRawBalance(_nonDepositor, _now() - 1), 0); + assertEq(flexClient.getPastVoteWeight(_depositor, _now() - 1), _amount); + assertEq(flexClient.getPastVoteWeight(_nonDepositor, _now() - 1), 0); } function testFuzz_ReturnsCurrentValueForFutureTimepoints( @@ -381,12 +381,12 @@ abstract contract GetPastRawBalance is FlexVotingClientTest { _mintGovAndDepositIntoFlexClient(_user, _amount); - assertEq(flexClient.getPastRawBalance(_user, _now()), _amount); - assertEq(flexClient.getPastRawBalance(_user, _timepoint), _amount); + assertEq(flexClient.getPastVoteWeight(_user, _now()), _amount); + assertEq(flexClient.getPastVoteWeight(_user, _timepoint), _amount); _advanceTimeTo(_timepoint); - assertEq(flexClient.getPastRawBalance(_user, _now()), _amount); + assertEq(flexClient.getPastVoteWeight(_user, _now()), _amount); } function testFuzz_ReturnsUserBalanceAtAGivenTimepoint( @@ -409,17 +409,17 @@ abstract contract GetPastRawBalance is FlexVotingClientTest { _advanceTimeBy(1); uint48 _zeroTimepoint = 0; - assertEq(flexClient.getPastRawBalance(_user, _zeroTimepoint), 0); - assertEq(flexClient.getPastRawBalance(_user, _initTimepoint), _amountA); - assertEq(flexClient.getPastRawBalance(_user, _timepoint), _amountA + _amountB); + 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.getPastTotalBalance(_zeroTimepoint), 0); - assertEq(flexClient.getPastTotalBalance(_future), 0); + assertEq(flexClient.getPastTotalVoteWeight(_zeroTimepoint), 0); + assertEq(flexClient.getPastTotalVoteWeight(_future), 0); } function testFuzz_ReturnsCurrentValueForFutureTimepoints( @@ -433,12 +433,12 @@ abstract contract GetPastTotalBalance is FlexVotingClientTest { _mintGovAndDepositIntoFlexClient(_user, _amount); - assertEq(flexClient.getPastTotalBalance(_now()), _amount); - assertEq(flexClient.getPastTotalBalance(_future), _amount); + assertEq(flexClient.getPastTotalVoteWeight(_now()), _amount); + assertEq(flexClient.getPastTotalVoteWeight(_future), _amount); _advanceTimeTo(_future); - assertEq(flexClient.getPastTotalBalance(_now()), _amount); + assertEq(flexClient.getPastTotalVoteWeight(_now()), _amount); } function testFuzz_SumsAllUserDeposits( @@ -459,7 +459,7 @@ abstract contract GetPastTotalBalance is FlexVotingClientTest { _advanceTimeBy(1); - assertEq(flexClient.getPastTotalBalance(_now()), _amountA + _amountB); + assertEq(flexClient.getPastTotalVoteWeight(_now()), _amountA + _amountB); } function testFuzz_ReturnsTotalDepositsAtAGivenTimepoint( @@ -477,14 +477,14 @@ abstract contract GetPastTotalBalance is FlexVotingClientTest { _amountA = uint208(bound(_amountA, 1, MAX_VOTES)); _amountB = uint208(bound(_amountB, 0, MAX_VOTES - _amountA)); - assertEq(flexClient.getPastTotalBalance(_now()), 0); + assertEq(flexClient.getPastTotalVoteWeight(_now()), 0); _mintGovAndDepositIntoFlexClient(_userA, _amountA); _advanceTimeTo(_future); _mintGovAndDepositIntoFlexClient(_userB, _amountB); - assertEq(flexClient.getPastTotalBalance(_now() - _future + 1), _amountA); - assertEq(flexClient.getPastTotalBalance(_now()), _amountA + _amountB); + assertEq(flexClient.getPastTotalVoteWeight(_now() - _future + 1), _amountA); + assertEq(flexClient.getPastTotalVoteWeight(_now()), _amountA + _amountB); } } @@ -555,7 +555,7 @@ abstract contract Deposit is FlexVotingClientTest { // We can still retrieve the user's balance at the given time. uint256 _checkpoint1 = _now() - 1; assertEq( - flexClient.getPastRawBalance(_user, _checkpoint1), + flexClient.getPastVoteWeight(_user, _checkpoint1), _amountA, "user's first deposit was not properly checkpointed" ); @@ -570,12 +570,12 @@ abstract contract Deposit is FlexVotingClientTest { _advanceTimeBy(1); // Advance so that we can look at checkpoints. assertEq( - flexClient.getPastRawBalance(_user, _checkpoint1), + flexClient.getPastVoteWeight(_user, _checkpoint1), _amountA, "user's first deposit was not properly checkpointed" ); assertEq( - flexClient.getPastRawBalance(_user, _checkpoint2), + flexClient.getPastVoteWeight(_user, _checkpoint2), _amountA + _amountB, "user's second deposit was not properly checkpointed" ); @@ -1287,6 +1287,6 @@ abstract contract Borrow is FlexVotingClientTest { _advanceTimeBy(1); // Advance so we can check the snapshot. // The total deposit snapshot should not have changed. - assertEq(flexClient.getPastTotalBalance(_now() - 1), _depositAmount); + assertEq(flexClient.getPastTotalVoteWeight(_now() - 1), _depositAmount); } } diff --git a/test/handlers/FlexVotingClientHandler.sol b/test/handlers/FlexVotingClientHandler.sol index fa77df6..7191a1b 100644 --- a/test/handlers/FlexVotingClientHandler.sol +++ b/test/handlers/FlexVotingClientHandler.sol @@ -326,7 +326,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; From da4ba5628bb471644791f48613786ca59601cb03 Mon Sep 17 00:00:00 2001 From: David Laprade Date: Tue, 21 Jan 2025 11:27:20 -0500 Subject: [PATCH 13/21] `forge fmt` --- src/FlexVotingClient.sol | 28 ++++++++++-------------- src/FlexVotingDelegatable.sol | 8 ++----- test/FlexVotingDelegatable.t.sol | 8 +++---- test/MockFlexVotingDelegatableClient.sol | 8 +++---- 4 files changed, 20 insertions(+), 32 deletions(-) diff --git a/src/FlexVotingClient.sol b/src/FlexVotingClient.sol index f2e0211..a7dd9af 100644 --- a/src/FlexVotingClient.sol +++ b/src/FlexVotingClient.sol @@ -185,15 +185,12 @@ abstract contract FlexVotingClient { // totalVotesInternal totalTokenWeight // // 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 - ); + 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]; @@ -205,10 +202,10 @@ abstract contract FlexVotingClient { ); } - function _applyDeltaToCheckpoint( - Checkpoints.Trace208 storage _checkpoint, - int256 _delta - ) internal returns (uint208 _prevTotal, uint208 _newTotal) { + 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 @@ -234,10 +231,7 @@ abstract contract FlexVotingClient { } /// @dev Checkpoints internal voting weight of `user` after applying `_delta`. - function _checkpointVoteWeightOf( - address _user, - int256 _delta - ) internal virtual { + function _checkpointVoteWeightOf(address _user, int256 _delta) internal virtual { _applyDeltaToCheckpoint(voteWeightCheckpoints[_user], _delta); } diff --git a/src/FlexVotingDelegatable.sol b/src/FlexVotingDelegatable.sol index 07075e1..ab73e3b 100644 --- a/src/FlexVotingDelegatable.sol +++ b/src/FlexVotingDelegatable.sol @@ -48,10 +48,7 @@ abstract contract FlexVotingDelegatable is Context, FlexVotingClient { _updateDelegateBalance(oldDelegate, delegatee, _delta); } - function _checkpointVoteWeightOf( - address _user, - int256 _delta - ) internal virtual override { + function _checkpointVoteWeightOf(address _user, int256 _delta) internal virtual override { address _proxy = delegates(_user); _applyDeltaToCheckpoint(voteWeightCheckpoints[_proxy], _delta); } @@ -66,8 +63,7 @@ abstract contract FlexVotingDelegatable is Context, FlexVotingClient { emit DelegateWeightChanged(from, _oldFrom, _newFrom); // Increment new delegate's weight. - (uint208 _oldTo, uint208 _newTo) = - _applyDeltaToCheckpoint(voteWeightCheckpoints[to], _delta); + (uint208 _oldTo, uint208 _newTo) = _applyDeltaToCheckpoint(voteWeightCheckpoints[to], _delta); emit DelegateWeightChanged(to, _oldTo, _newTo); } } diff --git a/test/FlexVotingDelegatable.t.sol b/test/FlexVotingDelegatable.t.sol index a23d0f3..125151c 100644 --- a/test/FlexVotingDelegatable.t.sol +++ b/test/FlexVotingDelegatable.t.sol @@ -338,14 +338,12 @@ abstract contract Delegation is FlexVotingClientTest { 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); + (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); + (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); diff --git a/test/MockFlexVotingDelegatableClient.sol b/test/MockFlexVotingDelegatableClient.sol index f7a453c..64b3c47 100644 --- a/test/MockFlexVotingDelegatableClient.sol +++ b/test/MockFlexVotingDelegatableClient.sol @@ -8,10 +8,10 @@ import {MockFlexVotingClient} from "test/MockFlexVotingClient.sol"; contract MockFlexVotingDelegatableClient is MockFlexVotingClient, FlexVotingDelegatable { constructor(address _governor) MockFlexVotingClient(_governor) {} - function _checkpointVoteWeightOf( - address _user, - int256 _delta - ) internal override(FlexVotingClient, FlexVotingDelegatable) { + function _checkpointVoteWeightOf(address _user, int256 _delta) + internal + override(FlexVotingClient, FlexVotingDelegatable) + { return FlexVotingDelegatable._checkpointVoteWeightOf(_user, _delta); } } From 05c57fb68b3782c4a091476aa5962924b772ad8e Mon Sep 17 00:00:00 2001 From: David Laprade Date: Tue, 21 Jan 2025 13:34:24 -0500 Subject: [PATCH 14/21] Restructure inheritance for FlexVotingClient --- src/FlexVotingBase.sol | 129 ++++++++++++++++++++++ src/FlexVotingClient.sol | 131 +++-------------------- src/FlexVotingDelegatable.sol | 5 +- test/MockFlexVotingClient.sol | 3 +- test/MockFlexVotingDelegatableClient.sol | 9 +- 5 files changed, 153 insertions(+), 124 deletions(-) create mode 100644 src/FlexVotingBase.sol diff --git a/src/FlexVotingBase.sol b/src/FlexVotingBase.sol new file mode 100644 index 0000000..91e9c13 --- /dev/null +++ b/src/FlexVotingBase.sol @@ -0,0 +1,129 @@ +// 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 rename to avoid collision with FlexVotingDelegatable. + /// @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 a7dd9af..010a9f4 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 proprosals, 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,26 +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 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; - - // 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(); @@ -97,27 +50,11 @@ 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. @@ -128,8 +65,8 @@ abstract contract FlexVotingClient { uint256 weight = getPastVoteWeight(voter, GOVERNOR.proposalSnapshot(proposalId)); if (weight == 0) revert FlexVotingClient__NoVotingWeight(); - if (proposalVotersHasVoted[proposalId][voter]) revert FlexVotingClient__AlreadyVoted(); - proposalVotersHasVoted[proposalId][voter] = true; + if (proposalVoterHasVoted[proposalId][voter]) revert FlexVotingClient__AlreadyVoted(); + proposalVoterHasVoted[proposalId][voter] = true; if (support == uint8(VoteType.Against)) { proposalVotes[proposalId].againstVotes += SafeCast.toUint128(weight); @@ -202,44 +139,6 @@ abstract contract FlexVotingClient { ); } - 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); - } - /// @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 diff --git a/src/FlexVotingDelegatable.sol b/src/FlexVotingDelegatable.sol index ab73e3b..41f29f3 100644 --- a/src/FlexVotingDelegatable.sol +++ b/src/FlexVotingDelegatable.sol @@ -5,10 +5,9 @@ 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 {IVotingToken} from "src/interfaces/IVotingToken.sol"; -import {FlexVotingClient} from "src/FlexVotingClient.sol"; +import {FlexVotingBase} from "src/FlexVotingBase.sol"; -abstract contract FlexVotingDelegatable is Context, FlexVotingClient { +abstract contract FlexVotingDelegatable is Context, FlexVotingBase { using Checkpoints for Checkpoints.Trace208; // @dev Emitted when an account changes its delegate. diff --git a/test/MockFlexVotingClient.sol b/test/MockFlexVotingClient.sol index e75c385..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(); } diff --git a/test/MockFlexVotingDelegatableClient.sol b/test/MockFlexVotingDelegatableClient.sol index 64b3c47..054d093 100644 --- a/test/MockFlexVotingDelegatableClient.sol +++ b/test/MockFlexVotingDelegatableClient.sol @@ -4,14 +4,15 @@ pragma solidity >=0.8.10; import {FlexVotingClient} from "src/FlexVotingClient.sol"; import {FlexVotingDelegatable} from "src/FlexVotingDelegatable.sol"; import {MockFlexVotingClient} from "test/MockFlexVotingClient.sol"; +import {FlexVotingBase} from "src/FlexVotingBase.sol"; contract MockFlexVotingDelegatableClient is MockFlexVotingClient, FlexVotingDelegatable { constructor(address _governor) MockFlexVotingClient(_governor) {} - function _checkpointVoteWeightOf(address _user, int256 _delta) - internal - override(FlexVotingClient, FlexVotingDelegatable) - { + function _checkpointVoteWeightOf( + address _user, + int256 _delta + ) internal override(FlexVotingBase, FlexVotingDelegatable) { return FlexVotingDelegatable._checkpointVoteWeightOf(_user, _delta); } } From 3d058c7d52c123a6dd36d055e46bbfe60b965d6d Mon Sep 17 00:00:00 2001 From: David Laprade Date: Tue, 21 Jan 2025 13:43:06 -0500 Subject: [PATCH 15/21] Add contract description for FlexVotingDelegatable --- src/FlexVotingBase.sol | 6 +++--- src/FlexVotingDelegatable.sol | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/FlexVotingBase.sol b/src/FlexVotingBase.sol index 91e9c13..4639ac2 100644 --- a/src/FlexVotingBase.sol +++ b/src/FlexVotingBase.sol @@ -6,9 +6,9 @@ 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. +/// @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: diff --git a/src/FlexVotingDelegatable.sol b/src/FlexVotingDelegatable.sol index 41f29f3..bb1a874 100644 --- a/src/FlexVotingDelegatable.sol +++ b/src/FlexVotingDelegatable.sol @@ -7,6 +7,22 @@ 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 FlexVotingDelegatable is Context, FlexVotingBase { using Checkpoints for Checkpoints.Trace208; From 83c50d8bf41a1ddc7d65b4e1cc73defe593557d4 Mon Sep 17 00:00:00 2001 From: David Laprade Date: Tue, 21 Jan 2025 13:43:35 -0500 Subject: [PATCH 16/21] `forge fmt` --- src/FlexVotingBase.sol | 13 +++++-------- test/MockFlexVotingDelegatableClient.sol | 8 ++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/FlexVotingBase.sol b/src/FlexVotingBase.sol index 4639ac2..3f11ffc 100644 --- a/src/FlexVotingBase.sol +++ b/src/FlexVotingBase.sol @@ -86,10 +86,10 @@ abstract contract FlexVotingBase { IVotingToken(GOVERNOR.token()).delegate(address(this)); } - function _applyDeltaToCheckpoint( - Checkpoints.Trace208 storage _checkpoint, - int256 _delta - ) internal returns (uint208 _prevTotal, uint208 _newTotal) { + 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 @@ -115,10 +115,7 @@ abstract contract FlexVotingBase { } /// @dev Checkpoints internal voting weight of `user` after applying `_delta`. - function _checkpointVoteWeightOf( - address _user, - int256 _delta - ) internal virtual { + function _checkpointVoteWeightOf(address _user, int256 _delta) internal virtual { _applyDeltaToCheckpoint(voteWeightCheckpoints[_user], _delta); } diff --git a/test/MockFlexVotingDelegatableClient.sol b/test/MockFlexVotingDelegatableClient.sol index 054d093..9527653 100644 --- a/test/MockFlexVotingDelegatableClient.sol +++ b/test/MockFlexVotingDelegatableClient.sol @@ -9,10 +9,10 @@ import {FlexVotingBase} from "src/FlexVotingBase.sol"; contract MockFlexVotingDelegatableClient is MockFlexVotingClient, FlexVotingDelegatable { constructor(address _governor) MockFlexVotingClient(_governor) {} - function _checkpointVoteWeightOf( - address _user, - int256 _delta - ) internal override(FlexVotingBase, FlexVotingDelegatable) { + function _checkpointVoteWeightOf(address _user, int256 _delta) + internal + override(FlexVotingBase, FlexVotingDelegatable) + { return FlexVotingDelegatable._checkpointVoteWeightOf(_user, _delta); } } From cb90ea6c99b6e57430bf3f36453399dec14b1884 Mon Sep 17 00:00:00 2001 From: David Laprade Date: Wed, 9 Apr 2025 15:24:43 -0400 Subject: [PATCH 17/21] Fix spelling error --- src/FlexVotingClient.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FlexVotingClient.sol b/src/FlexVotingClient.sol index 010a9f4..1d95e2a 100644 --- a/src/FlexVotingClient.sol +++ b/src/FlexVotingClient.sol @@ -13,7 +13,7 @@ import {FlexVotingBase} from "src/FlexVotingBase.sol"; /// /// This contract extends FlexVotingBase, adding two features: /// (a) the ability for depositors to express voting preferences on -/// {Governor}'s proprosals, and +/// {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; From 3e16491a4c5e77eb8d39a722272dda9b39ba607b Mon Sep 17 00:00:00 2001 From: David Laprade Date: Wed, 9 Apr 2025 16:29:48 -0400 Subject: [PATCH 18/21] rename Delegatable --> Delegable --- src/FlexVotingBase.sol | 2 +- ...elegatable.sol => FlexVotingDelegable.sol} | 2 +- ...atable.t.sol => FlexVotingDelegable.t.sol} | 74 +++++++++---------- ....sol => MockFlexVotingDelegableClient.sol} | 8 +- 4 files changed, 43 insertions(+), 43 deletions(-) rename src/{FlexVotingDelegatable.sol => FlexVotingDelegable.sol} (98%) rename test/{FlexVotingDelegatable.t.sol => FlexVotingDelegable.t.sol} (87%) rename test/{MockFlexVotingDelegatableClient.sol => MockFlexVotingDelegableClient.sol} (58%) diff --git a/src/FlexVotingBase.sol b/src/FlexVotingBase.sol index 3f11ffc..f0d4f42 100644 --- a/src/FlexVotingBase.sol +++ b/src/FlexVotingBase.sol @@ -80,7 +80,7 @@ abstract contract FlexVotingBase { /// e.g. if the internal representation of balance has been scaled down. function _rawBalanceOf(address _user) internal view virtual returns (uint208); - // TODO rename to avoid collision with FlexVotingDelegatable. + // TODO rename to avoid collision with FlexVotingDelegable. /// @dev Delegates the present contract's voting rights with `GOVERNOR` to itself. function _selfDelegate() internal { IVotingToken(GOVERNOR.token()).delegate(address(this)); diff --git a/src/FlexVotingDelegatable.sol b/src/FlexVotingDelegable.sol similarity index 98% rename from src/FlexVotingDelegatable.sol rename to src/FlexVotingDelegable.sol index bb1a874..6ddcbb0 100644 --- a/src/FlexVotingDelegatable.sol +++ b/src/FlexVotingDelegable.sol @@ -23,7 +23,7 @@ import {FlexVotingBase} from "src/FlexVotingBase.sol"; /// - 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 FlexVotingDelegatable is Context, FlexVotingBase { +abstract contract FlexVotingDelegable is Context, FlexVotingBase { using Checkpoints for Checkpoints.Trace208; // @dev Emitted when an account changes its delegate. diff --git a/test/FlexVotingDelegatable.t.sol b/test/FlexVotingDelegable.t.sol similarity index 87% rename from test/FlexVotingDelegatable.t.sol rename to test/FlexVotingDelegable.t.sol index 125151c..24bd027 100644 --- a/test/FlexVotingDelegatable.t.sol +++ b/test/FlexVotingDelegable.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {FlexVotingDelegatable} from "src/FlexVotingDelegatable.sol"; +import {FlexVotingDelegable} from "src/FlexVotingDelegable.sol"; import {MockFlexVotingClient as MFVC} from "test/MockFlexVotingClient.sol"; -import {MockFlexVotingDelegatableClient} from "test/MockFlexVotingDelegatableClient.sol"; +import {MockFlexVotingDelegableClient} from "test/MockFlexVotingDelegableClient.sol"; import {GovernorCountingSimple as GCS} from "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; @@ -35,8 +35,8 @@ abstract contract Delegation is FlexVotingClientTest { // We cast the flexClient to the delegatable client to access the delegate // function. - function client() internal view returns (MockFlexVotingDelegatableClient) { - return MockFlexVotingDelegatableClient(address(flexClient)); + function client() internal view returns (MockFlexVotingDelegableClient) { + return MockFlexVotingDelegableClient(address(flexClient)); } function testFuzz_selfDelegationByDefault(address _delegator) public { @@ -62,9 +62,9 @@ abstract contract Delegation is FlexVotingClientTest { _mintGovAndDepositIntoFlexClient(_delegator, _weight); vm.expectEmit(); - emit FlexVotingDelegatable.DelegateChanged(_delegator, _delegator, _delegate); + emit FlexVotingDelegable.DelegateChanged(_delegator, _delegator, _delegate); vm.expectEmit(); - emit FlexVotingDelegatable.DelegateWeightChanged(_delegate, 0, _weight); + emit FlexVotingDelegable.DelegateWeightChanged(_delegate, 0, _weight); vm.prank(_delegator); client().delegate(_delegate); } @@ -93,7 +93,7 @@ abstract contract Delegation is FlexVotingClientTest { // Delegate. vm.expectEmit(); - emit FlexVotingDelegatable.DelegateWeightChanged( + emit FlexVotingDelegable.DelegateWeightChanged( _delegate, _delegateWeight, _delegateWeight + _delegatorWeight ); vm.prank(_delegator); @@ -405,7 +405,7 @@ contract BlockNumberClock_Deployment is Deployment { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -415,7 +415,7 @@ contract BlockNumber_Constructor is Constructor { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -425,7 +425,7 @@ contract BlockNumber__RawBalanceOf is _RawBalanceOf { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -435,7 +435,7 @@ contract BlockNumber__CastVoteReasonString is _CastVoteReasonString { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -445,7 +445,7 @@ contract BlockNumber__SelfDelegate is _SelfDelegate { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -455,7 +455,7 @@ contract BlockNumber__CheckpointVoteWeightOf is _CheckpointVoteWeightOf { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -465,7 +465,7 @@ contract BlockNumber__CheckpointTotalVoteWeight is _CheckpointTotalVoteWeight { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -475,7 +475,7 @@ contract BlockNumber_GetPastRawBalance is GetPastRawBalance { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -485,7 +485,7 @@ contract BlockNumber_GetPastTotalBalance is GetPastTotalBalance { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -495,7 +495,7 @@ contract BlockNumber_Withdraw is Withdraw { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -505,7 +505,7 @@ contract BlockNumber_Deposit is Deposit { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -515,7 +515,7 @@ contract BlockNumber_ExpressVote is ExpressVote { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -525,7 +525,7 @@ contract BlockNumber_CastVote is CastVote { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -535,7 +535,7 @@ contract BlockNumber_Borrow is Borrow { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -545,7 +545,7 @@ contract BlockNumberClock_Delegation is Delegation { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -555,7 +555,7 @@ contract TimestampClockClock_Deployment is Deployment { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -565,7 +565,7 @@ contract TimestampClock_Constructor is Constructor { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -575,7 +575,7 @@ contract TimestampClock__RawBalanceOf is _RawBalanceOf { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -585,7 +585,7 @@ contract TimestampClock__CastVoteReasonString is _CastVoteReasonString { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -595,7 +595,7 @@ contract TimestampClock__SelfDelegate is _SelfDelegate { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -605,7 +605,7 @@ contract TimestampClock__CheckpointVoteWeightOf is _CheckpointVoteWeightOf { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -615,7 +615,7 @@ contract TimestampClock__CheckpointTotalVoteWeight is _CheckpointTotalVoteWeight } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -625,7 +625,7 @@ contract TimestampClock_GetPastRawBalance is GetPastRawBalance { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -635,7 +635,7 @@ contract TimestampClock_GetPastTotalBalance is GetPastTotalBalance { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -645,7 +645,7 @@ contract TimestampClock_Withdraw is Withdraw { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -655,7 +655,7 @@ contract TimestampClock_Deposit is Deposit { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -665,7 +665,7 @@ contract TimestampClock_ExpressVote is ExpressVote { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -675,7 +675,7 @@ contract TimestampClock_CastVote is CastVote { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -685,7 +685,7 @@ contract TimestampClock_Borrow is Borrow { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } @@ -695,6 +695,6 @@ contract TimestampClockClock_Delegation is Delegation { } function _deployFlexClient(address _governor) internal override { - flexClient = MFVC(address(new MockFlexVotingDelegatableClient(_governor))); + flexClient = MFVC(address(new MockFlexVotingDelegableClient(_governor))); } } diff --git a/test/MockFlexVotingDelegatableClient.sol b/test/MockFlexVotingDelegableClient.sol similarity index 58% rename from test/MockFlexVotingDelegatableClient.sol rename to test/MockFlexVotingDelegableClient.sol index 9527653..62cb03c 100644 --- a/test/MockFlexVotingDelegatableClient.sol +++ b/test/MockFlexVotingDelegableClient.sol @@ -2,17 +2,17 @@ pragma solidity >=0.8.10; import {FlexVotingClient} from "src/FlexVotingClient.sol"; -import {FlexVotingDelegatable} from "src/FlexVotingDelegatable.sol"; +import {FlexVotingDelegable} from "src/FlexVotingDelegable.sol"; import {MockFlexVotingClient} from "test/MockFlexVotingClient.sol"; import {FlexVotingBase} from "src/FlexVotingBase.sol"; -contract MockFlexVotingDelegatableClient is MockFlexVotingClient, FlexVotingDelegatable { +contract MockFlexVotingDelegableClient is MockFlexVotingClient, FlexVotingDelegable { constructor(address _governor) MockFlexVotingClient(_governor) {} function _checkpointVoteWeightOf(address _user, int256 _delta) internal - override(FlexVotingBase, FlexVotingDelegatable) + override(FlexVotingBase, FlexVotingDelegable) { - return FlexVotingDelegatable._checkpointVoteWeightOf(_user, _delta); + return FlexVotingDelegable._checkpointVoteWeightOf(_user, _delta); } } From 1e1d9bb11e14c976944d0138f401e413c6af503c Mon Sep 17 00:00:00 2001 From: David Laprade Date: Wed, 9 Apr 2025 16:34:29 -0400 Subject: [PATCH 19/21] Fix tests --- test/SharedFlexVoting.t.sol | 15 ++++++++++++++- test/handlers/FlexVotingClientHandler.sol | 3 ++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/test/SharedFlexVoting.t.sol b/test/SharedFlexVoting.t.sol index f69baf4..7a60eed 100644 --- a/test/SharedFlexVoting.t.sol +++ b/test/SharedFlexVoting.t.sol @@ -18,6 +18,14 @@ 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; @@ -318,8 +326,13 @@ abstract contract _CheckpointTotalVoteWeight is FlexVotingClientTest { // 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); + _safeCast.toUint208(netBalanceUint256); } function testFuzz_RevertIf_withdrawalFromZero(int256 _withdraw) public { diff --git a/test/handlers/FlexVotingClientHandler.sol b/test/handlers/FlexVotingClientHandler.sol index 7191a1b..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. From 345804b4524879c74a1c935546bbf5993894cc3b Mon Sep 17 00:00:00 2001 From: David Laprade Date: Tue, 15 Apr 2025 08:31:02 -0400 Subject: [PATCH 20/21] Add reference to github issue RE function naming --- src/FlexVotingBase.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/FlexVotingBase.sol b/src/FlexVotingBase.sol index f0d4f42..e76b4be 100644 --- a/src/FlexVotingBase.sol +++ b/src/FlexVotingBase.sol @@ -80,7 +80,8 @@ abstract contract FlexVotingBase { /// e.g. if the internal representation of balance has been scaled down. function _rawBalanceOf(address _user) internal view virtual returns (uint208); - // TODO rename to avoid collision with FlexVotingDelegable. + // 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)); From 178c0f0c3e48bba1247c6dcbeb7651cccbfbcbfd Mon Sep 17 00:00:00 2001 From: David Laprade Date: Tue, 15 Apr 2025 14:43:57 -0400 Subject: [PATCH 21/21] Disable nightly warning to stop forge fmt from failing --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) 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 }}