From d6c1343b5c5e3f935d9835c29517a3853d280cf9 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Wed, 20 Dec 2023 12:58:39 -0300 Subject: [PATCH 01/19] feat: add wonderGovernor and wonderVotes contract examples Signed-off-by: 0xRaccoon --- .gitignore | 1 + .../contracts/governance/WonderGovernor.sol | 4 + .../governance/utils/WonderVotes.sol | 6 +- solidity/examples/AliceGovernor.sol | 134 ++++++++++++++++++ solidity/examples/RabbitToken.sol | 35 +++++ .../governance/utils/IWonderVotes.sol | 1 + 6 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 solidity/examples/AliceGovernor.sol create mode 100644 solidity/examples/RabbitToken.sol diff --git a/.gitignore b/.gitignore index 9be8a61..96330bd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ out-via-ir # Config files .env +.vscode # Avoid ignoring gitkeep !/**/.gitkeep diff --git a/solidity/contracts/governance/WonderGovernor.sol b/solidity/contracts/governance/WonderGovernor.sol index 67d2f87..bdc84ca 100644 --- a/solidity/contracts/governance/WonderGovernor.sol +++ b/solidity/contracts/governance/WonderGovernor.sol @@ -889,4 +889,8 @@ abstract contract WonderGovernor is function proposalTypes() public view virtual override returns (uint8[] memory) { return _proposalTypes(); } + + function _getProposal(uint256 _proposalId) internal view virtual returns (ProposalCore storage) { + return _proposals[_proposalId]; + } } diff --git a/solidity/contracts/governance/utils/WonderVotes.sol b/solidity/contracts/governance/utils/WonderVotes.sol index f2903f2..1b3db47 100644 --- a/solidity/contracts/governance/utils/WonderVotes.sol +++ b/solidity/contracts/governance/utils/WonderVotes.sol @@ -252,7 +252,11 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes if (_weightSum != _weightNormalizer()) revert InvalidWeightSum(_weightSum); Delegate[] memory _oldDelegates = delegates(account, proposalType); - _delegatees[account][proposalType] = delegatees; + + delete _delegatees[account][proposalType]; + for (uint256 i = 0; i < delegatees.length; i++) { + _delegatees[account][proposalType].push(delegatees[i]); + } emit DelegateChanged(account, proposalType, _oldDelegates, delegatees); _moveDelegateVotes(proposalType, _oldDelegates, delegatees, _getVotingUnits(account)); diff --git a/solidity/examples/AliceGovernor.sol b/solidity/examples/AliceGovernor.sol new file mode 100644 index 0000000..75b2930 --- /dev/null +++ b/solidity/examples/AliceGovernor.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import 'contracts/governance/WonderGovernor.sol'; +import 'contracts/governance/utils/WonderVotes.sol'; + +contract AliceGovernor is WonderGovernor { + WonderVotes public votes; + string internal _countingMode = 'support=bravo&quorum=bravo'; + uint8[] internal __proposalTypes = [1, 2, 3]; + + mapping(uint256 proposalId => mapping(address => Receipt)) public receipts; + mapping(uint256 proposalId => ProposalTrack) public proposalTracks; + + /// @notice Ballot receipt record for a voter + struct Receipt { + /// @notice Whether or not a vote has been cast + bool hasVoted; + /// @notice 0 = Against, 1 = For, 2 = Abstain + uint8 support; + /// @notice The number of votes the voter had, which were cast + uint256 votes; + } + + struct ProposalTrack { + uint256 proposalId; + uint256 votes; + uint256 forVotes; + uint256 againstVotes; + uint256 abstainVotes; + } + + constructor(address _wonderToken) WonderGovernor('AliceGovernor') { + votes = WonderVotes(_wonderToken); + } + + function CLOCK_MODE() public view override returns (string memory _clockMode) { + return votes.CLOCK_MODE(); + } + + function COUNTING_MODE() external view override returns (string memory) { + return _countingMode; + } + + function _getVotes( + address _account, + uint8 _proposalType, + uint256 _timepoint, + bytes memory _params + ) internal view virtual override returns (uint256) { + return votes.getPastVotes(_account, _proposalType, _timepoint); + } + + function clock() public view override returns (uint48) { + return votes.clock(); + } + + function votingPeriod() public view override returns (uint256) { + // ~3 days in blocks (assuming 15s blocks) + return 17_280; + } + + function votingDelay() public view override returns (uint256) { + // 1 block + return 1; + } + + function quorum(uint256 _timepoint, uint8 _proposalType) public view override returns (uint256) { + // same quorum for all proposals types and timepoints + return 400_000e18; + } + + function _proposalTypes() internal view override returns (uint8[] memory) { + return __proposalTypes; + } + + function _isValidProposalType(uint8 _proposalType) internal view virtual override returns (bool) { + return _proposalType < __proposalTypes.length; + } + + function _countVote( + uint256 _proposalId, + address _account, + uint8 _support, + uint256 _weight, + bytes memory _params + ) internal virtual override { + if (_support == 0) { + proposalTracks[_proposalId].againstVotes += _weight; + } else if (_support == 1) { + proposalTracks[_proposalId].forVotes += _weight; + } else if (_support == 2) { + proposalTracks[_proposalId].abstainVotes += _weight; + } else { + revert InvalidVoteType(_support); + } + + Receipt storage receipt = receipts[_proposalId][_account]; + + receipt.hasVoted = true; + receipt.support = _support; + receipt.votes = _weight; + } + + function hasVoted(uint256 _proposalId, address _account) external view override returns (bool) { + return receipts[_proposalId][_account].hasVoted; + } + + function _quorumReached(uint256 _proposalId) internal view virtual override returns (bool) { + ProposalTrack memory _proposalTrack = proposalTracks[_proposalId]; + ProposalCore memory _proposal = _getProposal(_proposalId); + + uint256 _totalVotes = _proposalTrack.forVotes + _proposalTrack.againstVotes + _proposalTrack.abstainVotes; + return _totalVotes >= quorum(_proposal.voteStart, _proposal.proposalType); + } + + function _voteSucceeded(uint256 _proposalId) internal view virtual override returns (bool) { + ProposalTrack memory _proposalTrack = proposalTracks[_proposalId]; + + bool _succeded = _quorumReached(_proposalId) && _proposalTrack.forVotes > _proposalTrack.againstVotes; + return _succeded; + } + + function proposalThreshold(uint8 _proposalType) public view override returns (uint256) { + // same threshold for all proposals types + return 100_000e18; + } + + function isValidProposalType(uint8 _proposalType) external view returns (bool) { + return _isValidProposalType(_proposalType); + } + + error InvalidVoteType(uint8 voteType); +} diff --git a/solidity/examples/RabbitToken.sol b/solidity/examples/RabbitToken.sol new file mode 100644 index 0000000..f0112a8 --- /dev/null +++ b/solidity/examples/RabbitToken.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {WonderERC20Votes} from 'contracts/token/ERC20/extensions/WonderERC20Votes.sol'; +import {AliceGovernor} from './AliceGovernor.sol'; +import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; +import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +contract RabbitToken is WonderERC20Votes { + AliceGovernor public governor; + + constructor(AliceGovernor _governor) EIP712('RabbitToken', '1') ERC20('RabbitToken', 'RBT') { + governor = _governor; + } + + function _getProposalTypes() internal view virtual override returns (uint8[] memory) { + return governor.proposalTypes(); + } + + function _maxDelegates() internal view virtual override returns (uint8) { + return 4; + } + + function _validProposalType(uint8 _proposalType) internal view virtual override returns (bool) { + return governor.isValidProposalType(_proposalType); + } + + function _weightNormalizer() internal view virtual override returns (uint256) { + return 100; + } + + function proposalTypes() external view returns (uint8[] memory) { + return governor.proposalTypes(); + } +} diff --git a/solidity/interfaces/governance/utils/IWonderVotes.sol b/solidity/interfaces/governance/utils/IWonderVotes.sol index c595243..db510b8 100644 --- a/solidity/interfaces/governance/utils/IWonderVotes.sol +++ b/solidity/interfaces/governance/utils/IWonderVotes.sol @@ -112,6 +112,7 @@ interface IWonderVotes { */ function delegateBySig( Delegate[] memory delegates, + uint8 proposalType, uint256 nonce, uint256 expiry, uint8 v, From b4410f12073f7c2065a86d0809d03c954e12c6e9 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Wed, 20 Dec 2023 13:05:10 -0300 Subject: [PATCH 02/19] fix: return memory proposal instead of storage Signed-off-by: 0xRaccoon --- solidity/contracts/governance/WonderGovernor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/contracts/governance/WonderGovernor.sol b/solidity/contracts/governance/WonderGovernor.sol index bdc84ca..f564ded 100644 --- a/solidity/contracts/governance/WonderGovernor.sol +++ b/solidity/contracts/governance/WonderGovernor.sol @@ -890,7 +890,7 @@ abstract contract WonderGovernor is return _proposalTypes(); } - function _getProposal(uint256 _proposalId) internal view virtual returns (ProposalCore storage) { + function _getProposal(uint256 _proposalId) internal view virtual returns (ProposalCore memory) { return _proposals[_proposalId]; } } From d7300bc7078446486efef5047adf11d950e03b37 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Thu, 21 Dec 2023 09:31:41 -0300 Subject: [PATCH 03/19] test: add Governor unit tests Signed-off-by: 0xRaccoon --- package.json | 3 +- remappings.txt | 1 + solidity/examples/AliceGovernor.sol | 12 +- solidity/test/smock/SmockHelper.sol | 20 ++ .../test/smock/examples/MockAliceGovernor.sol | 271 ++++++++++++++++++ .../test/smock/examples/MockRabbitToken.sol | 147 ++++++++++ solidity/test/unit/WonderGovernor.t.sol | 94 ++++++ yarn.lock | 46 ++- 8 files changed, 584 insertions(+), 10 deletions(-) create mode 100644 solidity/test/smock/SmockHelper.sol create mode 100644 solidity/test/smock/examples/MockAliceGovernor.sol create mode 100644 solidity/test/smock/examples/MockRabbitToken.sol create mode 100644 solidity/test/unit/WonderGovernor.t.sol diff --git a/package.json b/package.json index ffa408b..01ed179 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "package.json": "sort-package-json" }, "dependencies": { - "@openzeppelin/contracts": "^5.0.1", + "@defi-wonderland/smock-foundry": "1.0.6", + "@openzeppelin/contracts": "5.0.1", "isolmate": "github:defi-wonderland/isolmate#59e1804" }, "devDependencies": { diff --git a/remappings.txt b/remappings.txt index 83b675a..db861fd 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,4 +4,5 @@ isolmate/=node_modules/isolmate/src contracts/=solidity/contracts interfaces/=solidity/interfaces +examples/=solidity/examples test/=solidity/test \ No newline at end of file diff --git a/solidity/examples/AliceGovernor.sol b/solidity/examples/AliceGovernor.sol index 75b2930..74eee89 100644 --- a/solidity/examples/AliceGovernor.sol +++ b/solidity/examples/AliceGovernor.sol @@ -9,11 +9,11 @@ contract AliceGovernor is WonderGovernor { string internal _countingMode = 'support=bravo&quorum=bravo'; uint8[] internal __proposalTypes = [1, 2, 3]; - mapping(uint256 proposalId => mapping(address => Receipt)) public receipts; + mapping(uint256 proposalId => mapping(address => BallotReceipt)) public receipts; mapping(uint256 proposalId => ProposalTrack) public proposalTracks; /// @notice Ballot receipt record for a voter - struct Receipt { + struct BallotReceipt { /// @notice Whether or not a vote has been cast bool hasVoted; /// @notice 0 = Against, 1 = For, 2 = Abstain @@ -95,11 +95,11 @@ contract AliceGovernor is WonderGovernor { revert InvalidVoteType(_support); } - Receipt storage receipt = receipts[_proposalId][_account]; + BallotReceipt storage _receipt = receipts[_proposalId][_account]; - receipt.hasVoted = true; - receipt.support = _support; - receipt.votes = _weight; + _receipt.hasVoted = true; + _receipt.support = _support; + _receipt.votes = _weight; } function hasVoted(uint256 _proposalId, address _account) external view override returns (bool) { diff --git a/solidity/test/smock/SmockHelper.sol b/solidity/test/smock/SmockHelper.sol new file mode 100644 index 0000000..442961e --- /dev/null +++ b/solidity/test/smock/SmockHelper.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; + +contract SmockHelper is Test { + function deployMock( + string memory _label, + bytes memory _creationCode, + bytes memory _encodedArgs + ) internal returns (address _deployed) { + bytes memory _bytecode = abi.encodePacked(_creationCode, _encodedArgs); + assembly { + mstore(0x0, _creationCode) + _deployed := create2(0, add(_bytecode, 0x20), mload(_bytecode), 'Wonderland') + } + vm.label(_deployed, _label); + vm.allowCheatcodes(_deployed); + } +} diff --git a/solidity/test/smock/examples/MockAliceGovernor.sol b/solidity/test/smock/examples/MockAliceGovernor.sol new file mode 100644 index 0000000..5b2623d --- /dev/null +++ b/solidity/test/smock/examples/MockAliceGovernor.sol @@ -0,0 +1,271 @@ +/// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; +import { + Address, + AliceGovernor, + Checkpoints, + Context, + DoubleEndedQueue, + ECDSA, + EIP712, + ERC165, + IERC1155Receiver, + IERC165, + IERC6372, + IERC721Receiver, + IWonderGovernor, + IWonderVotes, + Nonces, + SafeCast, + SignatureChecker, + Time, + WonderGovernor, + WonderVotes +} from 'solidity/examples/AliceGovernor.sol'; +import 'solidity/contracts/governance/WonderGovernor.sol'; +import 'solidity/contracts/governance/utils/WonderVotes.sol'; + +contract MockAliceGovernor is AliceGovernor, Test { + constructor(address _wonderToken) AliceGovernor(_wonderToken) {} + + /// Mocked State Variables + + function set_votes(WonderVotes _votes) public { + votes = _votes; + } + + function mock_call_votes(WonderVotes _votes) public { + vm.mockCall(address(this), abi.encodeWithSignature('votes()'), abi.encode(_votes)); + } + + function set__countingMode(string memory __countingMode) public { + _countingMode = __countingMode; + } + + function set___proposalTypes(uint8[] memory ___proposalTypes) public { + __proposalTypes = ___proposalTypes; + } + + function set_receipts(uint256 _key0, address _key1, AliceGovernor.BallotReceipt memory _value) public { + receipts[_key0][_key1] = _value; + } + + function mock_call_receipts(uint256 _key0, address _key1, AliceGovernor.BallotReceipt memory _value) public { + vm.mockCall(address(this), abi.encodeWithSignature('receipts(uint256,address)', _key0, _key1), abi.encode(_value)); + } + + function set_proposalTracks(uint256 _key0, AliceGovernor.ProposalTrack memory _value) public { + proposalTracks[_key0] = _value; + } + + function mock_call_proposalTracks(uint256 _key0, AliceGovernor.ProposalTrack memory _value) public { + vm.mockCall(address(this), abi.encodeWithSignature('proposalTracks(uint256)', _key0), abi.encode(_value)); + } + + /// Mocked External Functions + + function mock_call_CLOCK_MODE(string memory _clockMode) public { + vm.mockCall(address(this), abi.encodeWithSignature('CLOCK_MODE()'), abi.encode(_clockMode)); + } + + function mock_call_COUNTING_MODE(string memory _return0) public { + vm.mockCall(address(this), abi.encodeWithSignature('COUNTING_MODE()'), abi.encode(_return0)); + } + + function mock_call_clock(uint48 _return0) public { + vm.mockCall(address(this), abi.encodeWithSignature('clock()'), abi.encode(_return0)); + } + + function mock_call_votingPeriod(uint256 _return0) public { + vm.mockCall(address(this), abi.encodeWithSignature('votingPeriod()'), abi.encode(_return0)); + } + + function mock_call_votingDelay(uint256 _return0) public { + vm.mockCall(address(this), abi.encodeWithSignature('votingDelay()'), abi.encode(_return0)); + } + + function mock_call_quorum(uint256 _timepoint, uint8 _proposalType, uint256 _return0) public { + vm.mockCall( + address(this), abi.encodeWithSignature('quorum(uint256,uint8)', _timepoint, _proposalType), abi.encode(_return0) + ); + } + + function mock_call_hasVoted(uint256 _proposalId, address _account, bool _return0) public { + vm.mockCall( + address(this), abi.encodeWithSignature('hasVoted(uint256,address)', _proposalId, _account), abi.encode(_return0) + ); + } + + function mock_call_proposalThreshold(uint8 _proposalType, uint256 _return0) public { + vm.mockCall(address(this), abi.encodeWithSignature('proposalThreshold(uint8)', _proposalType), abi.encode(_return0)); + } + + function mock_call_isValidProposalType(uint8 _proposalType, bool _return0) public { + vm.mockCall( + address(this), abi.encodeWithSignature('isValidProposalType(uint8)', _proposalType), abi.encode(_return0) + ); + } + + /// Mocked Internal Functions + + struct _getVotesOutput { + uint256 _returnParam0; + } + + mapping(bytes32 => _getVotesOutput) private _getVotesOutputs; + bytes32[] private _getVotesInputHashes; + + function mock_call__getVotes( + address _account, + uint8 _proposalType, + uint256 _timepoint, + bytes memory _params, + uint256 _returnParam0 + ) public { + bytes32 _key = keccak256(abi.encode(_account, _proposalType, _timepoint, _params)); + _getVotesOutputs[_key] = _getVotesOutput(_returnParam0); + for (uint256 _i; _i < _getVotesInputHashes.length; ++_i) { + if (_key == _getVotesInputHashes[_i]) { + return; + } + } + _getVotesInputHashes.push(_key); + } + + function _getVotes( + address _account, + uint8 _proposalType, + uint256 _timepoint, + bytes memory _params + ) internal view override returns (uint256 _returnParam0) { + bytes32 _key = keccak256(abi.encode(_account, _proposalType, _timepoint, _params)); + for (uint256 _i; _i < _getVotesInputHashes.length; ++_i) { + if (_key == _getVotesInputHashes[_i]) { + _getVotesOutput memory _output = _getVotesOutputs[_key]; + return (_output._returnParam0); + } + } + return super._getVotes(_account, _proposalType, _timepoint, _params); + } + + struct _isValidProposalTypeOutput { + bool _returnParam0; + } + + mapping(bytes32 => _isValidProposalTypeOutput) private _isValidProposalTypeOutputs; + bytes32[] private _isValidProposalTypeInputHashes; + + function mock_call__isValidProposalType(uint8 _proposalType, bool _returnParam0) public { + bytes32 _key = keccak256(abi.encode(_proposalType)); + _isValidProposalTypeOutputs[_key] = _isValidProposalTypeOutput(_returnParam0); + for (uint256 _i; _i < _isValidProposalTypeInputHashes.length; ++_i) { + if (_key == _isValidProposalTypeInputHashes[_i]) { + return; + } + } + _isValidProposalTypeInputHashes.push(_key); + } + + function _isValidProposalType(uint8 _proposalType) internal view override returns (bool _returnParam0) { + bytes32 _key = keccak256(abi.encode(_proposalType)); + for (uint256 _i; _i < _isValidProposalTypeInputHashes.length; ++_i) { + if (_key == _isValidProposalTypeInputHashes[_i]) { + _isValidProposalTypeOutput memory _output = _isValidProposalTypeOutputs[_key]; + return (_output._returnParam0); + } + } + return super._isValidProposalType(_proposalType); + } + + function mock_call__countVote( + uint256 _proposalId, + address _account, + uint8 _support, + uint256 _weight, + bytes memory _params + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + '_countVote(uint256,address,uint8,uint256,bytes)', _proposalId, _account, _support, _weight, _params + ), + abi.encode() + ); + } + + function _countVote( + uint256 _proposalId, + address _account, + uint8 _support, + uint256 _weight, + bytes memory _params + ) internal override { + (bool _success, bytes memory _data) = address(this).call( + abi.encodeWithSignature( + '_countVote(uint256,address,uint8,uint256,bytes)', _proposalId, _account, _support, _weight, _params + ) + ); + if (_success) return abi.decode(_data, ()); + else return super._countVote(_proposalId, _account, _support, _weight, _params); + } + + struct _quorumReachedOutput { + bool _returnParam0; + } + + mapping(bytes32 => _quorumReachedOutput) private _quorumReachedOutputs; + bytes32[] private _quorumReachedInputHashes; + + function mock_call__quorumReached(uint256 _proposalId, bool _returnParam0) public { + bytes32 _key = keccak256(abi.encode(_proposalId)); + _quorumReachedOutputs[_key] = _quorumReachedOutput(_returnParam0); + for (uint256 _i; _i < _quorumReachedInputHashes.length; ++_i) { + if (_key == _quorumReachedInputHashes[_i]) { + return; + } + } + _quorumReachedInputHashes.push(_key); + } + + function _quorumReached(uint256 _proposalId) internal view override returns (bool _returnParam0) { + bytes32 _key = keccak256(abi.encode(_proposalId)); + for (uint256 _i; _i < _quorumReachedInputHashes.length; ++_i) { + if (_key == _quorumReachedInputHashes[_i]) { + _quorumReachedOutput memory _output = _quorumReachedOutputs[_key]; + return (_output._returnParam0); + } + } + return super._quorumReached(_proposalId); + } + + struct _voteSucceededOutput { + bool _returnParam0; + } + + mapping(bytes32 => _voteSucceededOutput) private _voteSucceededOutputs; + bytes32[] private _voteSucceededInputHashes; + + function mock_call__voteSucceeded(uint256 _proposalId, bool _returnParam0) public { + bytes32 _key = keccak256(abi.encode(_proposalId)); + _voteSucceededOutputs[_key] = _voteSucceededOutput(_returnParam0); + for (uint256 _i; _i < _voteSucceededInputHashes.length; ++_i) { + if (_key == _voteSucceededInputHashes[_i]) { + return; + } + } + _voteSucceededInputHashes.push(_key); + } + + function _voteSucceeded(uint256 _proposalId) internal view override returns (bool _returnParam0) { + bytes32 _key = keccak256(abi.encode(_proposalId)); + for (uint256 _i; _i < _voteSucceededInputHashes.length; ++_i) { + if (_key == _voteSucceededInputHashes[_i]) { + _voteSucceededOutput memory _output = _voteSucceededOutputs[_key]; + return (_output._returnParam0); + } + } + return super._voteSucceeded(_proposalId); + } +} diff --git a/solidity/test/smock/examples/MockRabbitToken.sol b/solidity/test/smock/examples/MockRabbitToken.sol new file mode 100644 index 0000000..c1fa222 --- /dev/null +++ b/solidity/test/smock/examples/MockRabbitToken.sol @@ -0,0 +1,147 @@ +/// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; +import {AliceGovernor, EIP712, ERC20, RabbitToken, WonderERC20Votes} from 'solidity/examples/RabbitToken.sol'; +import {WonderERC20Votes} from 'solidity/contracts/token/ERC20/extensions/WonderERC20Votes.sol'; +import {AliceGovernor} from 'solidity/examples/AliceGovernor.sol'; +import {EIP712} from 'node_modules/@openzeppelin/contracts/utils/cryptography/EIP712.sol'; +import {ERC20} from 'node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +contract MockRabbitToken is RabbitToken, Test { + constructor(AliceGovernor _governor) RabbitToken(_governor) {} + + /// Mocked State Variables + + function set_governor(AliceGovernor _governor) public { + governor = _governor; + } + + function mock_call_governor(AliceGovernor _governor) public { + vm.mockCall(address(this), abi.encodeWithSignature('governor()'), abi.encode(_governor)); + } + + /// Mocked External Functions + + function mock_call_proposalTypes(uint8[] memory _return0) public { + vm.mockCall(address(this), abi.encodeWithSignature('proposalTypes()'), abi.encode(_return0)); + } + + /// Mocked Internal Functions + + struct _getProposalTypesOutput { + uint8[] _returnParam0; + } + + mapping(bytes32 => _getProposalTypesOutput) private _getProposalTypesOutputs; + bytes32[] private _getProposalTypesInputHashes; + + function mock_call__getProposalTypes(uint8[] memory _returnParam0) public { + bytes32 _key = keccak256(abi.encode()); + _getProposalTypesOutputs[_key] = _getProposalTypesOutput(_returnParam0); + for (uint256 _i; _i < _getProposalTypesInputHashes.length; ++_i) { + if (_key == _getProposalTypesInputHashes[_i]) { + return; + } + } + _getProposalTypesInputHashes.push(_key); + } + + function _getProposalTypes() internal view override returns (uint8[] memory _returnParam0) { + bytes32 _key = keccak256(abi.encode()); + for (uint256 _i; _i < _getProposalTypesInputHashes.length; ++_i) { + if (_key == _getProposalTypesInputHashes[_i]) { + _getProposalTypesOutput memory _output = _getProposalTypesOutputs[_key]; + return (_output._returnParam0); + } + } + return super._getProposalTypes(); + } + + struct _maxDelegatesOutput { + uint8 _returnParam0; + } + + mapping(bytes32 => _maxDelegatesOutput) private _maxDelegatesOutputs; + bytes32[] private _maxDelegatesInputHashes; + + function mock_call__maxDelegates(uint8 _returnParam0) public { + bytes32 _key = keccak256(abi.encode()); + _maxDelegatesOutputs[_key] = _maxDelegatesOutput(_returnParam0); + for (uint256 _i; _i < _maxDelegatesInputHashes.length; ++_i) { + if (_key == _maxDelegatesInputHashes[_i]) { + return; + } + } + _maxDelegatesInputHashes.push(_key); + } + + function _maxDelegates() internal view override returns (uint8 _returnParam0) { + bytes32 _key = keccak256(abi.encode()); + for (uint256 _i; _i < _maxDelegatesInputHashes.length; ++_i) { + if (_key == _maxDelegatesInputHashes[_i]) { + _maxDelegatesOutput memory _output = _maxDelegatesOutputs[_key]; + return (_output._returnParam0); + } + } + return super._maxDelegates(); + } + + struct _validProposalTypeOutput { + bool _returnParam0; + } + + mapping(bytes32 => _validProposalTypeOutput) private _validProposalTypeOutputs; + bytes32[] private _validProposalTypeInputHashes; + + function mock_call__validProposalType(uint8 _proposalType, bool _returnParam0) public { + bytes32 _key = keccak256(abi.encode(_proposalType)); + _validProposalTypeOutputs[_key] = _validProposalTypeOutput(_returnParam0); + for (uint256 _i; _i < _validProposalTypeInputHashes.length; ++_i) { + if (_key == _validProposalTypeInputHashes[_i]) { + return; + } + } + _validProposalTypeInputHashes.push(_key); + } + + function _validProposalType(uint8 _proposalType) internal view override returns (bool _returnParam0) { + bytes32 _key = keccak256(abi.encode(_proposalType)); + for (uint256 _i; _i < _validProposalTypeInputHashes.length; ++_i) { + if (_key == _validProposalTypeInputHashes[_i]) { + _validProposalTypeOutput memory _output = _validProposalTypeOutputs[_key]; + return (_output._returnParam0); + } + } + return super._validProposalType(_proposalType); + } + + struct _weightNormalizerOutput { + uint256 _returnParam0; + } + + mapping(bytes32 => _weightNormalizerOutput) private _weightNormalizerOutputs; + bytes32[] private _weightNormalizerInputHashes; + + function mock_call__weightNormalizer(uint256 _returnParam0) public { + bytes32 _key = keccak256(abi.encode()); + _weightNormalizerOutputs[_key] = _weightNormalizerOutput(_returnParam0); + for (uint256 _i; _i < _weightNormalizerInputHashes.length; ++_i) { + if (_key == _weightNormalizerInputHashes[_i]) { + return; + } + } + _weightNormalizerInputHashes.push(_key); + } + + function _weightNormalizer() internal view override returns (uint256 _returnParam0) { + bytes32 _key = keccak256(abi.encode()); + for (uint256 _i; _i < _weightNormalizerInputHashes.length; ++_i) { + if (_key == _weightNormalizerInputHashes[_i]) { + _weightNormalizerOutput memory _output = _weightNormalizerOutputs[_key]; + return (_output._returnParam0); + } + } + return super._weightNormalizer(); + } +} diff --git a/solidity/test/unit/WonderGovernor.t.sol b/solidity/test/unit/WonderGovernor.t.sol new file mode 100644 index 0000000..df6909c --- /dev/null +++ b/solidity/test/unit/WonderGovernor.t.sol @@ -0,0 +1,94 @@ +import 'forge-std/Test.sol'; + +import {WonderGovernor} from 'contracts/governance/WonderGovernor.sol'; +import {IWonderGovernor} from 'interfaces/governance/IWonderGovernor.sol'; +import {AliceGovernor} from 'examples/AliceGovernor.sol'; +import {MockRabbitToken} from '../smock/examples/MockRabbitToken.sol'; +import {IWonderVotes} from 'interfaces/governance/utils/IWonderVotes.sol'; + +contract BaseTest is Test { + address deployer = makeAddr('deployer'); + address hatter = makeAddr('hatter'); + + IWonderGovernor governor; + MockRabbitToken rabbit; + + function _mockGetPastVotes(address _account, uint8 _proposalType, uint256 _timePoint, uint256 _votes) internal { + vm.mockCall( + address(rabbit), + abi.encodeWithSelector(IWonderVotes.getPastVotes.selector, _account, _proposalType, _timePoint), + abi.encode(_votes) + ); + } + + function setUp() public { + vm.startPrank(deployer); + + address tokenAddress = vm.computeCreateAddress(deployer, vm.getNonce(deployer) + 1); + governor = new AliceGovernor(tokenAddress); + rabbit = new MockRabbitToken(AliceGovernor(payable(address(governor)))); + + vm.stopPrank(); + } + + function _expectEmit(address _contract) internal { + vm.expectEmit(true, true, true, true, _contract); + } +} + +contract Unit_Propose is BaseTest { + event ProposalCreated( + uint256 proposalId, + uint8 proposalType, + address proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + uint256 voteStart, + uint256 voteEnd, + string description + ); + + function test_propose_Emit_ProposalCreated( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + + // hatter will pass the proposal threshold limit + _mockGetPastVotes(hatter, _proposalType, block.number - 1, governor.proposalThreshold(_proposalType)); + + address[] memory _targets = new address[](1); + _targets[0] = _target; + + uint256[] memory _values = new uint256[](1); + _values[0] = _value; + + bytes[] memory _calldatas = new bytes[](1); + _calldatas[0] = _calldata; + + uint256 _precomputedProposalId = + governor.hashProposal(_proposalType, _targets, _values, _calldatas, keccak256(bytes(_description))); + _expectEmit(address(governor)); + + emit ProposalCreated( + _precomputedProposalId, + _proposalType, + address(hatter), + _targets, + _values, + new string[](1), + _calldatas, + block.number + 1, + block.number + governor.votingPeriod() + 1, + _description + ); + + vm.prank(hatter); + uint256 _proposeId = governor.propose(_proposalType, _targets, _values, _calldatas, _description); + } +} diff --git a/yarn.lock b/yarn.lock index a80fc74..6d6371a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -192,6 +192,14 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@defi-wonderland/smock-foundry@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@defi-wonderland/smock-foundry/-/smock-foundry-1.0.6.tgz#25cef951d3ab4d038581a64d9d7cb55538bee53e" + integrity sha512-TEvPrkTBao1bMUnmIasBPdDQ46AC/pUFVrcvtvJ9Mc3krZkeZWGP+UDW786B9lGoGWkXFTWoiR6fZuoekczM1w== + dependencies: + handlebars "4.7.7" + yargs "17.7.2" + "@jridgewell/resolve-uri@^3.0.3": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" @@ -231,7 +239,7 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@openzeppelin/contracts@^5.0.1": +"@openzeppelin/contracts@5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.1.tgz#93da90fc209a0a4ff09c1deb037fbb35e4020890" integrity sha512-yQJaT5HDp9hYOOp4jTYxMsR02gdFZFXhewX5HW9Jo4fsqSVqqyIO/xTHdWDaKX5a3pv1txmf076Lziz+sO7L1w== @@ -1192,6 +1200,18 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +handlebars@4.7.7: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + hard-rejection@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" @@ -1701,7 +1721,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.6: +minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -1728,6 +1748,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -2262,6 +2287,11 @@ sort-package-json@1.53.1: is-plain-obj "2.1.0" sort-object-keys "^1.1.3" +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + spdx-correct@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" @@ -2534,6 +2564,11 @@ type-fest@^1.0.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.2.tgz#00d1c7c1c46928c5845c1ee8d0cc2791031d4c43" integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ== +uglify-js@^3.1.4: + version "3.17.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" + integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" @@ -2588,6 +2623,11 @@ word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -2643,7 +2683,7 @@ yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.0.0: +yargs@17.7.2, yargs@^17.0.0: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== From d07cb6c9134d0fa637102aa1b9fa7c005f49c875 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Thu, 21 Dec 2023 10:14:57 -0300 Subject: [PATCH 04/19] test: add governor unit tests Signed-off-by: 0xRaccoon --- .../contracts/governance/WonderGovernor.sol | 2 +- .../interfaces/governance/IWonderGovernor.sol | 2 +- solidity/test/unit/WonderGovernor.t.sol | 132 +++++++++++++++++- 3 files changed, 130 insertions(+), 6 deletions(-) diff --git a/solidity/contracts/governance/WonderGovernor.sol b/solidity/contracts/governance/WonderGovernor.sol index f564ded..820d254 100644 --- a/solidity/contracts/governance/WonderGovernor.sol +++ b/solidity/contracts/governance/WonderGovernor.sol @@ -79,7 +79,7 @@ abstract contract WonderGovernor is */ modifier validProposalType(uint8 proposalType) { if (!_isValidProposalType(proposalType)) { - revert InvalidProposalType(proposalType); + revert GovernorInvalidProposalType(proposalType); } _; } diff --git a/solidity/interfaces/governance/IWonderGovernor.sol b/solidity/interfaces/governance/IWonderGovernor.sol index 2e49684..eb8663f 100644 --- a/solidity/interfaces/governance/IWonderGovernor.sol +++ b/solidity/interfaces/governance/IWonderGovernor.sol @@ -105,7 +105,7 @@ interface IWonderGovernor is IERC165, IERC6372 { /** * @dev The proposalType is not supported by the governor. */ - error InvalidProposalType(uint8 proposalType); + error GovernorInvalidProposalType(uint8 proposalType); /** * @dev Emitted when a proposal is created. diff --git a/solidity/test/unit/WonderGovernor.t.sol b/solidity/test/unit/WonderGovernor.t.sol index df6909c..576bb2e 100644 --- a/solidity/test/unit/WonderGovernor.t.sol +++ b/solidity/test/unit/WonderGovernor.t.sol @@ -5,6 +5,15 @@ import {IWonderGovernor} from 'interfaces/governance/IWonderGovernor.sol'; import {AliceGovernor} from 'examples/AliceGovernor.sol'; import {MockRabbitToken} from '../smock/examples/MockRabbitToken.sol'; import {IWonderVotes} from 'interfaces/governance/utils/IWonderVotes.sol'; +import {IWonderGovernor} from 'interfaces/governance/IWonderGovernor.sol'; + +contract GovernorForTest is AliceGovernor { + constructor(address _wonderToken) AliceGovernor(_wonderToken) {} + + function getProposal(uint256 _proposalId) public view returns (ProposalCore memory) { + return _getProposal(_proposalId); + } +} contract BaseTest is Test { address deployer = makeAddr('deployer'); @@ -25,7 +34,7 @@ contract BaseTest is Test { vm.startPrank(deployer); address tokenAddress = vm.computeCreateAddress(deployer, vm.getNonce(deployer) + 1); - governor = new AliceGovernor(tokenAddress); + governor = new GovernorForTest(tokenAddress); rabbit = new MockRabbitToken(AliceGovernor(payable(address(governor)))); vm.stopPrank(); @@ -50,17 +59,19 @@ contract Unit_Propose is BaseTest { string description ); - function test_propose_Emit_ProposalCreated( + function test_Emit_ProposalCreated( uint8 _proposalType, address _target, uint256 _value, bytes memory _calldata, - string memory _description + string memory _description, + uint256 _proposerVotes ) public { vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); // hatter will pass the proposal threshold limit - _mockGetPastVotes(hatter, _proposalType, block.number - 1, governor.proposalThreshold(_proposalType)); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); address[] memory _targets = new address[](1); _targets[0] = _target; @@ -91,4 +102,117 @@ contract Unit_Propose is BaseTest { vm.prank(hatter); uint256 _proposeId = governor.propose(_proposalType, _targets, _values, _calldatas, _description); } + + function test_Stores_New_Proposal( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + + address[] memory _targets = new address[](1); + _targets[0] = _target; + + uint256[] memory _values = new uint256[](1); + _values[0] = _value; + + bytes[] memory _calldatas = new bytes[](1); + _calldatas[0] = _calldata; + + vm.prank(hatter); + uint256 _proposeId = governor.propose(_proposalType, _targets, _values, _calldatas, _description); + + WonderGovernor.ProposalCore memory _proposal = GovernorForTest(payable(address(governor))).getProposal(_proposeId); + + assertEq(_proposal.proposer, hatter); + assertEq(_proposal.proposalType, _proposalType); + assertEq(_proposal.voteStart, block.number + 1); + assertEq(_proposal.voteDuration, governor.votingPeriod()); + assertEq(_proposal.executed, false); + assertEq(_proposal.canceled, false); + assertEq(_proposal.etaSeconds, 0); + } + + function test_Revert_GovernorInvalidProposalType( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata + ) public { + vm.assume(_proposalType >= governor.proposalTypes().length); + + _mockGetPastVotes(hatter, _proposalType, block.number - 1, governor.proposalThreshold(_proposalType)); + + address[] memory _targets = new address[](1); + _targets[0] = _target; + + uint256[] memory _values = new uint256[](1); + _values[0] = _value; + + bytes[] memory _calldatas = new bytes[](1); + _calldatas[0] = _calldata; + + vm.prank(hatter); + + vm.expectRevert(abi.encodeWithSelector(IWonderGovernor.GovernorInvalidProposalType.selector, _proposalType)); + governor.propose(_proposalType, _targets, _values, _calldatas, ''); + } + + function test_Revert_GovernorInsufficientProposerVotes( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + uint256 _proposerVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + uint256 _votesThreshold = governor.proposalThreshold(_proposalType); + vm.assume(_proposerVotes < _votesThreshold); + + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + + address[] memory _targets = new address[](1); + _targets[0] = _target; + + uint256[] memory _values = new uint256[](1); + _values[0] = _value; + + bytes[] memory _calldatas = new bytes[](1); + _calldatas[0] = _calldata; + + vm.prank(hatter); + + vm.expectRevert( + abi.encodeWithSelector( + IWonderGovernor.GovernorInsufficientProposerVotes.selector, hatter, _proposerVotes, _votesThreshold + ) + ); + governor.propose(_proposalType, _targets, _values, _calldatas, ''); + } + + function test_Revert_GovernorInvalidProposalLength( + uint8 _proposalType, + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_targets.length != _values.length || _targets.length != _calldatas.length || _targets.length == 0); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, governor.proposalThreshold(_proposalType)); + + vm.prank(hatter); + vm.expectRevert( + abi.encodeWithSelector( + IWonderGovernor.GovernorInvalidProposalLength.selector, _targets.length, _calldatas.length, _values.length + ) + ); + + governor.propose(_proposalType, _targets, _values, _calldatas, ''); + } } From 3a3a65db28b195920ed7893b98f12be0cb060e40 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Thu, 21 Dec 2023 12:52:38 -0300 Subject: [PATCH 05/19] test: add governor castVote tests Signed-off-by: 0xRaccoon --- solidity/test/unit/WonderGovernor.t.sol | 217 ++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/solidity/test/unit/WonderGovernor.t.sol b/solidity/test/unit/WonderGovernor.t.sol index 576bb2e..35730b9 100644 --- a/solidity/test/unit/WonderGovernor.t.sol +++ b/solidity/test/unit/WonderGovernor.t.sol @@ -6,6 +6,7 @@ import {AliceGovernor} from 'examples/AliceGovernor.sol'; import {MockRabbitToken} from '../smock/examples/MockRabbitToken.sol'; import {IWonderVotes} from 'interfaces/governance/utils/IWonderVotes.sol'; import {IWonderGovernor} from 'interfaces/governance/IWonderGovernor.sol'; +import {WonderVotes} from 'contracts/governance/utils/WonderVotes.sol'; contract GovernorForTest is AliceGovernor { constructor(address _wonderToken) AliceGovernor(_wonderToken) {} @@ -18,6 +19,7 @@ contract GovernorForTest is AliceGovernor { contract BaseTest is Test { address deployer = makeAddr('deployer'); address hatter = makeAddr('hatter'); + address cat = makeAddr('cat'); IWonderGovernor governor; MockRabbitToken rabbit; @@ -139,6 +141,39 @@ contract Unit_Propose is BaseTest { assertEq(_proposal.etaSeconds, 0); } + function test_Call_IWonderVotes_GetVotes( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + + // hatter will pass the proposal threshold limit + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + + address[] memory _targets = new address[](1); + _targets[0] = _target; + + uint256[] memory _values = new uint256[](1); + _values[0] = _value; + + bytes[] memory _calldatas = new bytes[](1); + _calldatas[0] = _calldata; + + vm.expectCall( + address(rabbit), + abi.encodeWithSelector(IWonderVotes.getPastVotes.selector, hatter, _proposalType, block.number - 1), + 1 + ); + + vm.prank(hatter); + governor.propose(_proposalType, _targets, _values, _calldatas, _description); + } + function test_Revert_GovernorInvalidProposalType( uint8 _proposalType, address _target, @@ -216,3 +251,185 @@ contract Unit_Propose is BaseTest { governor.propose(_proposalType, _targets, _values, _calldatas, ''); } } + +contract Unit_CastVote is BaseTest { + event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason); + + function _createProposal( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes + ) internal returns (uint256) { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + + address[] memory _targets = new address[](1); + _targets[0] = _target; + + uint256[] memory _values = new uint256[](1); + _values[0] = _value; + + bytes[] memory _calldatas = new bytes[](1); + _calldatas[0] = _calldata; + + vm.prank(hatter); + return governor.propose(_proposalType, _targets, _values, _calldatas, _description); + } + + function test_Emit_VoteCast( + uint8 _proposalType, + uint8 _support, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + vm.assume(_support < 2); + + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, block.number + governor.votingDelay(), _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + _expectEmit(address(governor)); + emit VoteCast(cat, _proposalId, _support, _voterVotes, ''); + + vm.prank(cat); + governor.castVote(_proposalId, _support); + } + + function test_Call_GetVotes( + uint8 _proposalType, + uint8 _support, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + vm.assume(_voterVotes > 0); + vm.assume(_support < 2); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.expectCall( + address(rabbit), abi.encodeWithSelector(IWonderVotes.getPastVotes.selector, cat, _proposalType, _voteStart), 1 + ); + + vm.prank(cat); + governor.castVote(_proposalId, _support); + } + + function test_Count_VoteFor( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.prank(cat); + governor.castVote(_proposalId, 1); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, _voterVotes); + assertEq(_againstVotes, 0); + assertEq(_abstainVotes, 0); + } + + function test_Count_VoteAgainst( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.prank(cat); + governor.castVote(_proposalId, 0); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, 0); + assertEq(_againstVotes, _voterVotes); + assertEq(_abstainVotes, 0); + } + + function test_Count_VoteAbstain( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.prank(cat); + governor.castVote(_proposalId, 2); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, 0); + assertEq(_againstVotes, 0); + assertEq(_abstainVotes, _voterVotes); + } +} From 6414e1a27cb998ed8b1228956c800327f81a1927 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Thu, 21 Dec 2023 14:29:18 -0300 Subject: [PATCH 06/19] test: add governor unit tests Signed-off-by: 0xRaccoon --- solidity/test/unit/WonderGovernor.t.sol | 377 ++++++++++++++++++++++-- 1 file changed, 355 insertions(+), 22 deletions(-) diff --git a/solidity/test/unit/WonderGovernor.t.sol b/solidity/test/unit/WonderGovernor.t.sol index 35730b9..cd0a0aa 100644 --- a/solidity/test/unit/WonderGovernor.t.sol +++ b/solidity/test/unit/WonderGovernor.t.sol @@ -45,6 +45,32 @@ contract BaseTest is Test { function _expectEmit(address _contract) internal { vm.expectEmit(true, true, true, true, _contract); } + + function _createProposal( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes + ) internal returns (uint256) { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + + address[] memory _targets = new address[](1); + _targets[0] = _target; + + uint256[] memory _values = new uint256[](1); + _values[0] = _value; + + bytes[] memory _calldatas = new bytes[](1); + _calldatas[0] = _calldata; + + vm.prank(hatter); + return governor.propose(_proposalType, _targets, _values, _calldatas, _description); + } } contract Unit_Propose is BaseTest { @@ -255,33 +281,35 @@ contract Unit_Propose is BaseTest { contract Unit_CastVote is BaseTest { event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason); - function _createProposal( + function test_Emit_VoteCast( uint8 _proposalType, + uint8 _support, address _target, uint256 _value, bytes memory _calldata, string memory _description, - uint256 _proposerVotes - ) internal returns (uint256) { + uint256 _proposerVotes, + uint256 _voterVotes + ) public { vm.assume(_proposalType < governor.proposalTypes().length); vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + vm.assume(_support < 2); _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, block.number + governor.votingDelay(), _voterVotes); - address[] memory _targets = new address[](1); - _targets[0] = _target; + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); - uint256[] memory _values = new uint256[](1); - _values[0] = _value; + vm.roll(block.number + governor.votingDelay() + 1); - bytes[] memory _calldatas = new bytes[](1); - _calldatas[0] = _calldata; + _expectEmit(address(governor)); + emit VoteCast(cat, _proposalId, _support, _voterVotes, ''); - vm.prank(hatter); - return governor.propose(_proposalType, _targets, _values, _calldatas, _description); + vm.prank(cat); + governor.castVote(_proposalId, _support); } - function test_Emit_VoteCast( + function test_Call_GetVotes( uint8 _proposalType, uint8 _support, address _target, @@ -290,6 +318,135 @@ contract Unit_CastVote is BaseTest { string memory _description, uint256 _proposerVotes, uint256 _voterVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + vm.assume(_voterVotes > 0); + vm.assume(_support < 2); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.expectCall( + address(rabbit), abi.encodeWithSelector(IWonderVotes.getPastVotes.selector, cat, _proposalType, _voteStart), 1 + ); + + vm.prank(cat); + governor.castVote(_proposalId, _support); + } + + function test_Count_VoteFor( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.prank(cat); + governor.castVote(_proposalId, 1); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, _voterVotes); + assertEq(_againstVotes, 0); + assertEq(_abstainVotes, 0); + } + + function test_Count_VoteAgainst( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.prank(cat); + governor.castVote(_proposalId, 0); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, 0); + assertEq(_againstVotes, _voterVotes); + assertEq(_abstainVotes, 0); + } + + function test_Count_VoteAbstain( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.prank(cat); + governor.castVote(_proposalId, 2); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, 0); + assertEq(_againstVotes, 0); + assertEq(_abstainVotes, _voterVotes); + } +} + +contract Unit_CastVoteWithReason is BaseTest { + event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason); + + function test_Emit_VoteCast( + uint8 _proposalType, + uint8 _support, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes, + string memory _reason ) public { vm.assume(_proposalType < governor.proposalTypes().length); vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); @@ -303,10 +460,10 @@ contract Unit_CastVote is BaseTest { vm.roll(block.number + governor.votingDelay() + 1); _expectEmit(address(governor)); - emit VoteCast(cat, _proposalId, _support, _voterVotes, ''); + emit VoteCast(cat, _proposalId, _support, _voterVotes, _reason); vm.prank(cat); - governor.castVote(_proposalId, _support); + governor.castVoteWithReason(_proposalId, _support, _reason); } function test_Call_GetVotes( @@ -317,7 +474,8 @@ contract Unit_CastVote is BaseTest { bytes memory _calldata, string memory _description, uint256 _proposerVotes, - uint256 _voterVotes + uint256 _voterVotes, + string memory _reason ) public { vm.assume(_proposalType < governor.proposalTypes().length); vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); @@ -337,7 +495,7 @@ contract Unit_CastVote is BaseTest { ); vm.prank(cat); - governor.castVote(_proposalId, _support); + governor.castVoteWithReason(_proposalId, _support, _reason); } function test_Count_VoteFor( @@ -347,7 +505,8 @@ contract Unit_CastVote is BaseTest { bytes memory _calldata, string memory _description, uint256 _proposerVotes, - uint256 _voterVotes + uint256 _voterVotes, + string memory _reason ) public { vm.assume(_proposalType < governor.proposalTypes().length); vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); @@ -361,7 +520,7 @@ contract Unit_CastVote is BaseTest { vm.roll(block.number + governor.votingDelay() + 1); vm.prank(cat); - governor.castVote(_proposalId, 1); + governor.castVoteWithReason(_proposalId, 1, _reason); (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); @@ -378,7 +537,8 @@ contract Unit_CastVote is BaseTest { bytes memory _calldata, string memory _description, uint256 _proposerVotes, - uint256 _voterVotes + uint256 _voterVotes, + string memory _reason ) public { vm.assume(_proposalType < governor.proposalTypes().length); vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); @@ -392,7 +552,7 @@ contract Unit_CastVote is BaseTest { vm.roll(block.number + governor.votingDelay() + 1); vm.prank(cat); - governor.castVote(_proposalId, 0); + governor.castVoteWithReason(_proposalId, 0, _reason); (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); @@ -409,7 +569,8 @@ contract Unit_CastVote is BaseTest { bytes memory _calldata, string memory _description, uint256 _proposerVotes, - uint256 _voterVotes + uint256 _voterVotes, + string memory _reason ) public { vm.assume(_proposalType < governor.proposalTypes().length); vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); @@ -423,7 +584,179 @@ contract Unit_CastVote is BaseTest { vm.roll(block.number + governor.votingDelay() + 1); vm.prank(cat); - governor.castVote(_proposalId, 2); + governor.castVoteWithReason(_proposalId, 2, _reason); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, 0); + assertEq(_againstVotes, 0); + assertEq(_abstainVotes, _voterVotes); + } +} + +contract Unit_CastVoteWithReasonAndParams is BaseTest { + event VoteCastWithParams( + address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason, bytes params + ); + + function test_Emit_VoteCastWithParams( + uint8 _proposalType, + uint8 _support, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes, + string memory _reason, + bytes memory _params + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + vm.assume(_support < 2); + vm.assume(_params.length > 0); + + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, block.number + governor.votingDelay(), _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + _expectEmit(address(governor)); + emit VoteCastWithParams(cat, _proposalId, _support, _voterVotes, _reason, _params); + + vm.prank(cat); + governor.castVoteWithReasonAndParams(_proposalId, _support, _reason, _params); + } + + function test_Call_GetVotes( + uint8 _proposalType, + uint8 _support, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes, + bytes memory _params + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + vm.assume(_voterVotes > 0); + vm.assume(_support < 2); + vm.assume(_params.length > 0); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.expectCall( + address(rabbit), abi.encodeWithSelector(IWonderVotes.getPastVotes.selector, cat, _proposalType, _voteStart), 1 + ); + + vm.prank(cat); + governor.castVoteWithReasonAndParams(_proposalId, _support, '', _params); + } + + function test_Count_VoteFor( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes, + string memory _reason, + bytes memory _params + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + vm.assume(_params.length > 0); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.prank(cat); + governor.castVoteWithReasonAndParams(_proposalId, 1, _reason, _params); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, _voterVotes); + assertEq(_againstVotes, 0); + assertEq(_abstainVotes, 0); + } + + function test_Count_VoteAgainst( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes, + string memory _reason, + bytes memory _params + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + vm.assume(_params.length > 0); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.prank(cat); + governor.castVoteWithReasonAndParams(_proposalId, 0, _reason, _params); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, 0); + assertEq(_againstVotes, _voterVotes); + assertEq(_abstainVotes, 0); + } + + function test_Count_VoteAbstain( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes, + uint256 _voterVotes, + string memory _reason, + bytes memory _params + ) public { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + vm.assume(_params.length > 0); + + uint256 _voteStart = block.number + governor.votingDelay(); + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + _mockGetPastVotes(cat, _proposalType, _voteStart, _voterVotes); + + uint256 _proposalId = _createProposal(_proposalType, _target, _value, _calldata, _description, _proposerVotes); + + vm.roll(block.number + governor.votingDelay() + 1); + + vm.prank(cat); + governor.castVoteWithReasonAndParams(_proposalId, 2, _reason, _params); (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); From 681175bdf22cdb0e3e7a68feed1bb1cf480c7330 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Fri, 22 Dec 2023 13:49:57 -0300 Subject: [PATCH 07/19] test: add wonder votes unit tests Signed-off-by: 0xRaccoon --- solidity/examples/AliceGovernor.sol | 2 +- solidity/test/unit/WonderVotes.t.sol | 617 +++++++++++++++++++++++++++ 2 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 solidity/test/unit/WonderVotes.t.sol diff --git a/solidity/examples/AliceGovernor.sol b/solidity/examples/AliceGovernor.sol index 74eee89..2c2488d 100644 --- a/solidity/examples/AliceGovernor.sol +++ b/solidity/examples/AliceGovernor.sol @@ -7,7 +7,7 @@ import 'contracts/governance/utils/WonderVotes.sol'; contract AliceGovernor is WonderGovernor { WonderVotes public votes; string internal _countingMode = 'support=bravo&quorum=bravo'; - uint8[] internal __proposalTypes = [1, 2, 3]; + uint8[] internal __proposalTypes = [0, 1, 2, 3]; mapping(uint256 proposalId => mapping(address => BallotReceipt)) public receipts; mapping(uint256 proposalId => ProposalTrack) public proposalTracks; diff --git a/solidity/test/unit/WonderVotes.t.sol b/solidity/test/unit/WonderVotes.t.sol new file mode 100644 index 0000000..62e911b --- /dev/null +++ b/solidity/test/unit/WonderVotes.t.sol @@ -0,0 +1,617 @@ +import 'forge-std/Test.sol'; + +import {WonderGovernor} from 'contracts/governance/WonderGovernor.sol'; +import {IWonderGovernor} from 'interfaces/governance/IWonderGovernor.sol'; +import {IWonderVotes} from 'interfaces/governance/utils/IWonderVotes.sol'; +import {IWonderGovernor} from 'interfaces/governance/IWonderGovernor.sol'; +import {WonderVotes} from 'contracts/governance/utils/WonderVotes.sol'; +import {RabbitToken} from 'examples/RabbitToken.sol'; +import {MockAliceGovernor} from '../smock/examples/MockAliceGovernor.sol'; +import {AliceGovernor} from 'examples/AliceGovernor.sol'; + +contract WonderVotesForTest is RabbitToken { + constructor(AliceGovernor _governor) RabbitToken(_governor) {} + + function mint(address _account, uint256 _amount) public { + _mint(_account, _amount); + } + + function burn(uint256 _amount) public { + _burn(msg.sender, _amount); + } +} + +contract BaseTest is Test { + address deployer = makeAddr('deployer'); + address hatter = makeAddr('hatter'); + address cat = makeAddr('cat'); + + MockAliceGovernor governor; + RabbitToken rabbitToken; + + event DelegateVotesChanged(address indexed delegate, uint8 proposalType, uint256 previousVotes, uint256 newVotes); + + function _mockGetPastVotes(address _account, uint8 _proposalType, uint256 _timePoint, uint256 _votes) internal { + vm.mockCall( + address(rabbitToken), + abi.encodeWithSelector(IWonderVotes.getPastVotes.selector, _account, _proposalType, _timePoint), + abi.encode(_votes) + ); + } + + function setUp() public virtual { + vm.startPrank(deployer); + + address tokenAddress = vm.computeCreateAddress(deployer, vm.getNonce(deployer) + 1); + governor = new MockAliceGovernor(tokenAddress); + rabbitToken = new WonderVotesForTest(AliceGovernor(payable(address(governor)))); + + vm.stopPrank(); + } + + function _expectEmit(address _contract) internal { + vm.expectEmit(true, true, true, true, _contract); + } + + function _createProposal( + uint8 _proposalType, + address _target, + uint256 _value, + bytes memory _calldata, + string memory _description, + uint256 _proposerVotes + ) internal returns (uint256) { + vm.assume(_proposalType < governor.proposalTypes().length); + vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); + + _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); + + address[] memory _targets = new address[](1); + _targets[0] = _target; + + uint256[] memory _values = new uint256[](1); + _values[0] = _value; + + bytes[] memory _calldatas = new bytes[](1); + _calldatas[0] = _calldata; + + vm.prank(hatter); + return governor.propose(_proposalType, _targets, _values, _calldatas, _description); + } +} + +contract Unit_Delegate_Simple is BaseTest { + function test_Minting_WithoutTracking_Add_Zero(uint128 _amount) public { + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); + } + } + + function test_Minting_SelfDelegate_Before(uint128 _amount) public { + // To start tracking votes the account delegates himself + vm.prank(hatter); + rabbitToken.delegate(hatter); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), _amount); + } + } + + function test_Minting_SelfDelegate_After(uint128 _amount) public { + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + // If the account does not have delegates it will not track votes + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); + } + + // To start tracking votes the account delegates himself + vm.prank(hatter); + rabbitToken.delegate(hatter); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), _amount); + } + } + + function test_SelfDelegate_Changes(uint128 _amount) public { + // To start tracking votes the account delegates himself + vm.prank(hatter); + rabbitToken.delegate(hatter); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), _amount); + } + + vm.prank(hatter); + rabbitToken.delegate(cat); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(cat, _proposalTypes[i]), _amount); + } + } + + function test_SelfDelegate_Burns(uint128 _amount) public { + vm.prank(hatter); + rabbitToken.delegate(hatter); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + vm.prank(hatter); + WonderVotesForTest(address(rabbitToken)).burn(_amount); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); + } + } + + function test_Emit_DelegateVotesChanged(uint128 _amount) public { + vm.prank(hatter); + rabbitToken.delegate(hatter); + + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _expectEmit(address(rabbitToken)); + emit DelegateVotesChanged(hatter, _proposalTypes[i], 0, _amount); + } + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + } +} + +contract Unit_Delegate_Smart is BaseTest { + function test_Minting_SmartDelegation_Before(uint128 _amount) public { + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + // Will define one delegate per proposal type + address[] memory _delegates = new address[](_proposalTypes.length); + + vm.startPrank(hatter); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + + // 100% voting power to the delegate for the proposalType + rabbitToken.delegate(_delegates[i], _proposalTypes[i]); + } + vm.stopPrank(); + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(_delegates[i], _proposalTypes[i]), _amount); + } + } + + function test_Minting_SmartDelegation_After(uint128 _amount) public { + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + + // Will define one delegate per proposal type + address[] memory _delegates = new address[](_proposalTypes.length); + + vm.startPrank(hatter); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + + // 100% voting power to the delegate for the proposalType + rabbitToken.delegate(_delegates[i], _proposalTypes[i]); + } + vm.stopPrank(); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(_delegates[i], _proposalTypes[i]), _amount); + } + } + + function test_Minting_SmartDelegation_Changes(uint128 _amount) public { + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + // Will define one delegate per proposal type + address[] memory _delegates = new address[](_proposalTypes.length); + address[] memory _delegatesChange = new address[](_proposalTypes.length); + + vm.startPrank(hatter); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + + // 100% voting power to the delegate for the proposalType + rabbitToken.delegate(_delegates[i], _proposalTypes[i]); + } + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(_delegates[i], _proposalTypes[i]), _amount); + } + + // Delegates changes + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegatesChange[i] = makeAddr(string(abi.encodePacked('delegateChange', i))); + + // 100% voting power to the delegate for the proposalType + rabbitToken.delegate(_delegatesChange[i], _proposalTypes[i]); + } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(_delegates[i], _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(_delegatesChange[i], _proposalTypes[i]), _amount); + } + } + + function test_Revert_InvalidProposalType(uint8 _proposalType) public { + vm.assume(_proposalType >= rabbitToken.proposalTypes().length); + + vm.expectRevert(abi.encodeWithSelector(IWonderVotes.InvalidProposalType.selector, _proposalType)); + + vm.prank(hatter); + rabbitToken.delegate(hatter, _proposalType); + } + + function test_Emit_DelegateVotesChanged(uint128 _amount) public { + vm.prank(hatter); + rabbitToken.delegate(hatter); + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + address[] memory _delegates = new address[](_proposalTypes.length); + + vm.startPrank(hatter); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + + _expectEmit(address(rabbitToken)); + emit DelegateVotesChanged(_delegates[i], _proposalTypes[i], 0, _amount); + + rabbitToken.delegate(_delegates[i], _proposalTypes[i]); + } + vm.stopPrank(); + } +} + +contract Unit_Delegate_SmartAndPartial is BaseTest { + function test_Minting_SmartAndPartialDelegation_Before(uint128 _amount) public { + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + // To simply we will divide the voting power into 2 delegates 50% each + // We can add a more complex test of this further + uint256 _weightNormalizer = rabbitToken.weightNormalizer(); + uint256 _weight = _weightNormalizer / 2; + + address[] memory _delegates = new address[](_proposalTypes.length); + address[] memory _delegates2 = new address[](_proposalTypes.length); + + vm.startPrank(hatter); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + _delegates2[i] = makeAddr(string(abi.encodePacked('delegate2', i))); + + IWonderVotes.Delegate memory _delegate = IWonderVotes.Delegate({account: _delegates[i], weight: _weight}); + IWonderVotes.Delegate memory _delegate2 = IWonderVotes.Delegate({account: _delegates2[i], weight: _weight}); + IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](2); + _delegatesStruct[0] = _delegate; + _delegatesStruct[1] = _delegate2; + + rabbitToken.delegate(_delegatesStruct, _proposalTypes[i]); + } + + vm.stopPrank(); + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); + emit log_uint(rabbitToken.getVotes(_delegates[i], _proposalTypes[i])); + emit log_uint(rabbitToken.getVotes(_delegates2[i], _proposalTypes[i])); + + assertEq(rabbitToken.getVotes(_delegates[i], _proposalTypes[i]), _amount / 2); + assertEq(rabbitToken.getVotes(_delegates2[i], _proposalTypes[i]), _amount / 2); + } + } + + function test_Minting_SmartAndPartialDelegation_After(uint128 _amount) public { + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + // To simply we will divide the voting power into 2 delegates 50% each + // We can add a more complex test of this further + uint256 _weightNormalizer = rabbitToken.weightNormalizer(); + uint256 _weight = _weightNormalizer / 2; + + address[] memory _delegates = new address[](_proposalTypes.length); + address[] memory _delegates2 = new address[](_proposalTypes.length); + + vm.startPrank(hatter); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + _delegates2[i] = makeAddr(string(abi.encodePacked('delegate2', i))); + + IWonderVotes.Delegate memory _delegate = IWonderVotes.Delegate({account: _delegates[i], weight: _weight}); + IWonderVotes.Delegate memory _delegate2 = IWonderVotes.Delegate({account: _delegates2[i], weight: _weight}); + IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](2); + _delegatesStruct[0] = _delegate; + _delegatesStruct[1] = _delegate2; + + rabbitToken.delegate(_delegatesStruct, _proposalTypes[i]); + } + vm.stopPrank(); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); + emit log_uint(rabbitToken.getVotes(_delegates[i], _proposalTypes[i])); + emit log_uint(rabbitToken.getVotes(_delegates2[i], _proposalTypes[i])); + + assertEq(rabbitToken.getVotes(_delegates[i], _proposalTypes[i]), _amount / 2); + assertEq(rabbitToken.getVotes(_delegates2[i], _proposalTypes[i]), _amount / 2); + } + } + + function test_Revert_InvalidProposalType(uint8 _proposalType) public { + vm.assume(_proposalType >= rabbitToken.proposalTypes().length); + + IWonderVotes.Delegate[] memory _delegates = new IWonderVotes.Delegate[](1); + _delegates[0] = IWonderVotes.Delegate({account: makeAddr('delegate'), weight: rabbitToken.weightNormalizer()}); + + vm.expectRevert(abi.encodeWithSelector(IWonderVotes.InvalidProposalType.selector, _proposalType)); + + vm.prank(hatter); + rabbitToken.delegate(_delegates, _proposalType); + } + + function test_Emit_DelegateVotesChanged(uint128 _amount) public { + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + WonderVotesForTest(address(rabbitToken)).mint(hatter, _amount); + + // To simply we will divide the voting power into 2 delegates 50% each + // We can add a more complex test of this further + uint256 _weightNormalizer = rabbitToken.weightNormalizer(); + uint256 _weight = _weightNormalizer / 2; + + address[] memory _delegates = new address[](_proposalTypes.length); + address[] memory _delegates2 = new address[](_proposalTypes.length); + + vm.startPrank(hatter); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + _delegates2[i] = makeAddr(string(abi.encodePacked('delegate2', i))); + + IWonderVotes.Delegate memory _delegate = IWonderVotes.Delegate({account: _delegates[i], weight: _weight}); + IWonderVotes.Delegate memory _delegate2 = IWonderVotes.Delegate({account: _delegates2[i], weight: _weight}); + IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](2); + _delegatesStruct[0] = _delegate; + _delegatesStruct[1] = _delegate2; + + _expectEmit(address(rabbitToken)); + + emit DelegateVotesChanged(_delegates[i], _proposalTypes[i], 0, _amount / 2); + emit DelegateVotesChanged(_delegates2[i], _proposalTypes[i], 0, _amount / 2); + + rabbitToken.delegate(_delegatesStruct, _proposalTypes[i]); + } + + vm.stopPrank(); + } + + function test_Revert_ZeroWeight(uint8 _proposalType) public { + vm.assume(_proposalType < rabbitToken.proposalTypes().length); + + IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](1); + _delegatesStruct[0] = IWonderVotes.Delegate({account: makeAddr('delegate'), weight: 0}); + + vm.expectRevert(abi.encodeWithSelector(IWonderVotes.ZeroWeight.selector)); + + vm.prank(hatter); + rabbitToken.delegate(_delegatesStruct, _proposalType); + } + + function test_Revert_InvalidWeightSum_LessThan_WeighNormalizer(uint8 _proposalType, uint256 _weightSum) public { + vm.assume(_proposalType < rabbitToken.proposalTypes().length); + vm.assume(_weightSum > 0 && (_weightSum > rabbitToken.weightNormalizer())); + + IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](1); + _delegatesStruct[0] = IWonderVotes.Delegate({account: makeAddr('delegate'), weight: _weightSum}); + + vm.expectRevert(abi.encodeWithSelector(IWonderVotes.InvalidWeightSum.selector, _weightSum)); + + vm.prank(hatter); + rabbitToken.delegate(_delegatesStruct, _proposalType); + } + + function test_Revert_InvalidWeightSum_MoreThan_WeighNormalizer(uint8 _proposalType, uint256 _weightSum) public { + vm.assume(_proposalType < rabbitToken.proposalTypes().length); + vm.assume(_weightSum > 0 && (_weightSum > rabbitToken.weightNormalizer())); + + IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](1); + _delegatesStruct[0] = IWonderVotes.Delegate({account: makeAddr('delegate'), weight: _weightSum}); + + vm.expectRevert(abi.encodeWithSelector(IWonderVotes.InvalidWeightSum.selector, _weightSum)); + + vm.prank(hatter); + rabbitToken.delegate(_delegatesStruct, _proposalType); + } +} + +contract Unit_TransferVotes is BaseTest { + function setUp() public override { + super.setUp(); + + // To start tracking votes the accounts delegates themselves + vm.prank(hatter); + rabbitToken.delegate(hatter); + vm.prank(cat); + rabbitToken.delegate(cat); + } + + function test_TransferVotes_SimpleDelegation(uint128 _balance, uint128 _transferAmount) public { + vm.assume(_balance >= _transferAmount); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _balance); + + vm.prank(hatter); + rabbitToken.transfer(cat, _transferAmount); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), _balance - _transferAmount); + assertEq(rabbitToken.getVotes(cat, _proposalTypes[i]), _transferAmount); + } + } + + function test_TransferVotes_SimpleDelegation_Emit_DelegateVotesChanged( + uint128 _balance, + uint128 _transferAmount + ) public { + vm.assume(_balance >= _transferAmount); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _balance); + + _expectEmit(address(rabbitToken)); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + emit DelegateVotesChanged(hatter, _proposalTypes[i], _balance, _balance - _transferAmount); + emit DelegateVotesChanged(cat, _proposalTypes[i], 0, _transferAmount); + } + + vm.prank(hatter); + rabbitToken.transfer(cat, _transferAmount); + } + + function test_TransferVotes_SmartDelegation(uint128 _balance, uint128 _transferAmount) public { + vm.assume(_balance >= _transferAmount); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + // Will define one delegate per proposal type + address[] memory _hatterDelegates = new address[](_proposalTypes.length); + address[] memory _catDelegates = new address[](_proposalTypes.length); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _hatterDelegates[i] = makeAddr(string(abi.encodePacked('hatterDelegate', i))); + _catDelegates[i] = makeAddr(string(abi.encodePacked('catDelegate', i))); + + // 100% voting power to the delegate for the proposalType + + vm.prank(hatter); + rabbitToken.delegate(_hatterDelegates[i], _proposalTypes[i]); + + vm.prank(cat); + rabbitToken.delegate(_catDelegates[i], _proposalTypes[i]); + } + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _balance); + + vm.prank(hatter); + rabbitToken.transfer(cat, _transferAmount); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(_hatterDelegates[i], _proposalTypes[i]), _balance - _transferAmount); + assertEq(rabbitToken.getVotes(_catDelegates[i], _proposalTypes[i]), _transferAmount); + } + } + + function test_TransferVotes_SmartDelegation_Emits_DelegateVotesChanged( + uint128 _balance, + uint128 _transferAmount + ) public { + vm.assume(_balance >= _transferAmount); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + // Will define one delegate per proposal type + address[] memory _hatterDelegates = new address[](_proposalTypes.length); + address[] memory _catDelegates = new address[](_proposalTypes.length); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _hatterDelegates[i] = makeAddr(string(abi.encodePacked('hatterDelegate', i))); + _catDelegates[i] = makeAddr(string(abi.encodePacked('catDelegate', i))); + + // 100% voting power to the delegate for the proposalType + + vm.prank(hatter); + rabbitToken.delegate(_hatterDelegates[i], _proposalTypes[i]); + + vm.prank(cat); + rabbitToken.delegate(_catDelegates[i], _proposalTypes[i]); + } + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _balance); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + emit DelegateVotesChanged(_hatterDelegates[i], _proposalTypes[i], _balance, _balance - _transferAmount); + emit DelegateVotesChanged(_catDelegates[i], _proposalTypes[i], 0, _transferAmount); + } + + vm.prank(hatter); + rabbitToken.transfer(cat, _transferAmount); + } + + function _partialDelegate( + string memory _nameHash, + address _account, + uint8[] memory _proposalTypes + ) internal returns (address[] memory, address[] memory) { + // To simply we will divide the voting power into 2 delegates 50% each + uint256 _weightNormalizer = rabbitToken.weightNormalizer(); + uint256 _weight = _weightNormalizer / 2; + + address[] memory _delegates = new address[](_proposalTypes.length); + address[] memory _delegates2 = new address[](_proposalTypes.length); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + // smart partial delegation + _delegates[i] = makeAddr(string(abi.encodePacked('1', _nameHash, i))); + _delegates2[i] = makeAddr(string(abi.encodePacked('2', _nameHash, i))); + + IWonderVotes.Delegate memory _delegate = IWonderVotes.Delegate({account: _delegates[i], weight: _weight}); + IWonderVotes.Delegate memory _delegate2 = IWonderVotes.Delegate({account: _delegates2[i], weight: _weight}); + IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](2); + _delegatesStruct[0] = _delegate; + _delegatesStruct[1] = _delegate2; + + vm.prank(_account); + rabbitToken.delegate(_delegatesStruct, _proposalTypes[i]); + } + + return (_delegates, _delegates2); + } + + function test_TransferVotes_SmartAndPartialDelegation(uint128 _balance, uint128 _transferAmount) public { + vm.assume(_balance >= _transferAmount); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + (address[] memory _hatterDelegates1, address[] memory _hatterDelegates2) = + _partialDelegate('hatter', hatter, _proposalTypes); + (address[] memory _catDelegates1, address[] memory _catDelegates2) = _partialDelegate('cat', cat, _proposalTypes); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _balance); + + vm.prank(hatter); + rabbitToken.transfer(cat, _transferAmount); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertApproxEqAbs( + rabbitToken.getVotes(_hatterDelegates1[i], _proposalTypes[i]), (_balance - _transferAmount) / 2, 1, '' + ); + assertApproxEqAbs( + rabbitToken.getVotes(_hatterDelegates2[i], _proposalTypes[i]), (_balance - _transferAmount) / 2, 1, '' + ); + + assertApproxEqAbs(rabbitToken.getVotes(_catDelegates1[i], _proposalTypes[i]), _transferAmount / 2, 1, ''); + assertApproxEqAbs(rabbitToken.getVotes(_catDelegates2[i], _proposalTypes[i]), _transferAmount / 2, 1, ''); + } + } +} From 71d224240a42dd7c4411cf1fca538f27befd4cef Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Fri, 22 Dec 2023 18:44:14 -0300 Subject: [PATCH 08/19] test: add wonder votes unit tests Signed-off-by: 0xRaccoon --- solidity/test/unit/WonderVotes.t.sol | 481 ++++++++++++++++++++++++++- 1 file changed, 476 insertions(+), 5 deletions(-) diff --git a/solidity/test/unit/WonderVotes.t.sol b/solidity/test/unit/WonderVotes.t.sol index 62e911b..a87209a 100644 --- a/solidity/test/unit/WonderVotes.t.sol +++ b/solidity/test/unit/WonderVotes.t.sol @@ -191,6 +191,14 @@ contract Unit_Delegate_Smart is BaseTest { assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); assertEq(rabbitToken.getVotes(_delegates[i], _proposalTypes[i]), _amount); } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _delegates.length; j++) { + if (j != i) { + assertEq(rabbitToken.getVotes(_delegates[j], _proposalTypes[i]), 0); + } + } + } } function test_Minting_SmartDelegation_After(uint128 _amount) public { @@ -214,6 +222,14 @@ contract Unit_Delegate_Smart is BaseTest { assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); assertEq(rabbitToken.getVotes(_delegates[i], _proposalTypes[i]), _amount); } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _delegates.length; j++) { + if (j != i) { + assertEq(rabbitToken.getVotes(_delegates[j], _proposalTypes[i]), 0); + } + } + } } function test_Minting_SmartDelegation_Changes(uint128 _amount) public { @@ -251,6 +267,15 @@ contract Unit_Delegate_Smart is BaseTest { assertEq(rabbitToken.getVotes(_delegates[i], _proposalTypes[i]), 0); assertEq(rabbitToken.getVotes(_delegatesChange[i], _proposalTypes[i]), _amount); } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _delegates.length; j++) { + if (j != i) { + assertEq(rabbitToken.getVotes(_delegates[j], _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(_delegatesChange[j], _proposalTypes[i]), 0); + } + } + } } function test_Revert_InvalidProposalType(uint8 _proposalType) public { @@ -315,12 +340,18 @@ contract Unit_Delegate_SmartAndPartial is BaseTest { for (uint256 i = 0; i < _proposalTypes.length; i++) { assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); - emit log_uint(rabbitToken.getVotes(_delegates[i], _proposalTypes[i])); - emit log_uint(rabbitToken.getVotes(_delegates2[i], _proposalTypes[i])); - assertEq(rabbitToken.getVotes(_delegates[i], _proposalTypes[i]), _amount / 2); assertEq(rabbitToken.getVotes(_delegates2[i], _proposalTypes[i]), _amount / 2); } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _delegates.length; j++) { + if (j != i) { + assertEq(rabbitToken.getVotes(_delegates[j], _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(_delegates2[j], _proposalTypes[i]), 0); + } + } + } } function test_Minting_SmartAndPartialDelegation_After(uint128 _amount) public { @@ -353,12 +384,19 @@ contract Unit_Delegate_SmartAndPartial is BaseTest { for (uint256 i = 0; i < _proposalTypes.length; i++) { assertEq(rabbitToken.getVotes(hatter, _proposalTypes[i]), 0); - emit log_uint(rabbitToken.getVotes(_delegates[i], _proposalTypes[i])); - emit log_uint(rabbitToken.getVotes(_delegates2[i], _proposalTypes[i])); assertEq(rabbitToken.getVotes(_delegates[i], _proposalTypes[i]), _amount / 2); assertEq(rabbitToken.getVotes(_delegates2[i], _proposalTypes[i]), _amount / 2); } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _delegates.length; j++) { + if (j != i) { + assertEq(rabbitToken.getVotes(_delegates[j], _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(_delegates2[j], _proposalTypes[i]), 0); + } + } + } } function test_Revert_InvalidProposalType(uint8 _proposalType) public { @@ -522,6 +560,15 @@ contract Unit_TransferVotes is BaseTest { assertEq(rabbitToken.getVotes(_hatterDelegates[i], _proposalTypes[i]), _balance - _transferAmount); assertEq(rabbitToken.getVotes(_catDelegates[i], _proposalTypes[i]), _transferAmount); } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _catDelegates.length; j++) { + if (j != i) { + assertEq(rabbitToken.getVotes(_hatterDelegates[j], _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(_catDelegates[j], _proposalTypes[i]), 0); + } + } + } } function test_TransferVotes_SmartDelegation_Emits_DelegateVotesChanged( @@ -613,5 +660,429 @@ contract Unit_TransferVotes is BaseTest { assertApproxEqAbs(rabbitToken.getVotes(_catDelegates1[i], _proposalTypes[i]), _transferAmount / 2, 1, ''); assertApproxEqAbs(rabbitToken.getVotes(_catDelegates2[i], _proposalTypes[i]), _transferAmount / 2, 1, ''); } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _catDelegates1.length; j++) { + if (j != i) { + assertEq(rabbitToken.getVotes(_hatterDelegates1[j], _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(_hatterDelegates2[j], _proposalTypes[i]), 0); + + assertEq(rabbitToken.getVotes(_catDelegates1[j], _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(_catDelegates2[j], _proposalTypes[i]), 0); + } + } + } + } +} + +contract Unit_GetPastVotes is BaseTest { + function setUp() public override { + super.setUp(); + + // To track votes, if not it will be always 0 + vm.prank(hatter); + rabbitToken.delegate(hatter); + vm.prank(cat); + rabbitToken.delegate(cat); + } + + // Simple delegation + function test_GetPastVotes_After_Mint(uint128 _previousBalance, uint128 _addBalance) public { + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); + vm.roll(block.number + 1); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _addBalance); + vm.roll(block.number + 1); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 2), _previousBalance); + assertEq( + rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 1), uint256(_previousBalance) + _addBalance + ); + } + } + + function test_GetPastVotes_After_Burn(uint128 _previousBalance, uint128 _subsBalance) public { + vm.assume(_previousBalance >= _subsBalance); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); + vm.roll(block.number + 1); + + vm.prank(hatter); + WonderVotesForTest(address(rabbitToken)).burn(_subsBalance); + vm.roll(block.number + 1); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 2), _previousBalance); + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 1), _previousBalance - _subsBalance); + } + } + + function test_GetPastVotes_After_Transfer(uint128 _previousBalance, uint128 _addBalance) public { + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); + WonderVotesForTest(address(rabbitToken)).mint(cat, _addBalance); + + vm.roll(block.number + 1); + + vm.prank(cat); + rabbitToken.transfer(hatter, _addBalance); + vm.roll(block.number + 1); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 2), _previousBalance); + assertEq( + rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 1), uint256(_previousBalance) + _addBalance + ); + + assertEq(rabbitToken.getPastVotes(cat, _proposalTypes[i], block.number - 2), _addBalance); + assertEq(rabbitToken.getPastVotes(cat, _proposalTypes[i], block.number - 1), 0); + } + } + + // Smart Delegation + function test_GetPastVotes_After_Mint_SmartDelegation(uint128 _previousBalance, uint128 _addBalance) public { + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + address[] memory _delegates = new address[](_proposalTypes.length); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + + // 100% voting power to the delegate for the proposalType + vm.prank(hatter); + rabbitToken.delegate(_delegates[i], _proposalTypes[i]); + } + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); + vm.roll(block.number + 1); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _addBalance); + vm.roll(block.number + 1); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 2), 0); + + assertEq(rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 2), _previousBalance); + assertEq( + rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 1), + uint256(_previousBalance) + _addBalance + ); + } + } + + function test_GetPastVotes_After_Burn_SmartDelegation(uint128 _previousBalance, uint128 _subsBalance) public { + vm.assume(_previousBalance >= _subsBalance); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + address[] memory _delegates = new address[](_proposalTypes.length); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + + // 100% voting power to the delegate for the proposalType + vm.prank(hatter); + rabbitToken.delegate(_delegates[i], _proposalTypes[i]); + } + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); + vm.roll(block.number + 1); + + vm.prank(hatter); + WonderVotesForTest(address(rabbitToken)).burn(_subsBalance); + vm.roll(block.number + 1); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 1), 0); + + assertEq(rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 2), _previousBalance); + assertEq( + rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 1), _previousBalance - _subsBalance + ); + } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _delegates.length; j++) { + if (j != i) { + assertEq(rabbitToken.getPastVotes(_delegates[j], _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(_delegates[j], _proposalTypes[i], block.number - 1), 0); + } + } + } + } + + function test_GetPastVotes_After_Transfer_SmartDelegation(uint128 _previousBalance, uint128 _addBalance) public { + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + address[] memory _delegates = new address[](_proposalTypes.length); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + + // 100% voting power to the delegate for the proposalType + vm.prank(hatter); + rabbitToken.delegate(_delegates[i], _proposalTypes[i]); + } + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); + WonderVotesForTest(address(rabbitToken)).mint(cat, _addBalance); + + vm.roll(block.number + 1); + + vm.prank(cat); + rabbitToken.transfer(hatter, _addBalance); + vm.roll(block.number + 1); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 1), 0); + + assertEq(rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 2), _previousBalance); + assertEq( + rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 1), + uint256(_previousBalance) + _addBalance + ); + + assertEq(rabbitToken.getPastVotes(cat, _proposalTypes[i], block.number - 2), _addBalance); + assertEq(rabbitToken.getPastVotes(cat, _proposalTypes[i], block.number - 1), 0); + } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _delegates.length; j++) { + if (j != i) { + assertEq(rabbitToken.getPastVotes(_delegates[j], _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(_delegates[j], _proposalTypes[i], block.number - 1), 0); + } + } + } + } + + // Smart and partial delegation + function test_GetPastVotes_After_Mint_SmartAndPartialDelegation(uint128 _previousBalance, uint128 _addBalance) public { + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + // 2 delegates 50% each + uint256 _weightNormalizer = rabbitToken.weightNormalizer(); + uint256 _weight = _weightNormalizer / 2; + + address[] memory _delegates = new address[](_proposalTypes.length); + address[] memory _delegates2 = new address[](_proposalTypes.length); + + vm.startPrank(hatter); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + _delegates2[i] = makeAddr(string(abi.encodePacked('delegate2', i))); + + IWonderVotes.Delegate memory _delegate = IWonderVotes.Delegate({account: _delegates[i], weight: _weight}); + IWonderVotes.Delegate memory _delegate2 = IWonderVotes.Delegate({account: _delegates2[i], weight: _weight}); + IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](2); + _delegatesStruct[0] = _delegate; + _delegatesStruct[1] = _delegate2; + + rabbitToken.delegate(_delegatesStruct, _proposalTypes[i]); + } + + vm.stopPrank(); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); + vm.roll(block.number + 1); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _addBalance); + vm.roll(block.number + 1); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 1), 0); + + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 2), _previousBalance / 2, 1, '' + ); + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates2[i], _proposalTypes[i], block.number - 2), _previousBalance / 2, 1, '' + ); + + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 1), + (uint256(_previousBalance) + _addBalance) / 2, + 1, + '' + ); + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates2[i], _proposalTypes[i], block.number - 1), + (uint256(_previousBalance) + _addBalance) / 2, + 1, + '' + ); + } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _delegates.length; j++) { + if (j != i) { + assertEq(rabbitToken.getPastVotes(_delegates[j], _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(_delegates[j], _proposalTypes[i], block.number - 1), 0); + + assertEq(rabbitToken.getPastVotes(_delegates2[j], _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(_delegates2[j], _proposalTypes[i], block.number - 1), 0); + } + } + } + } + + function test_GetPastVotes_After_Burn_SmartAndPartialDelegation( + uint128 _previousBalance, + uint128 _subsBalance + ) public { + vm.assume(_previousBalance >= _subsBalance); + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + // 2 delegates 50% each + uint256 _weightNormalizer = rabbitToken.weightNormalizer(); + uint256 _weight = _weightNormalizer / 2; + + address[] memory _delegates = new address[](_proposalTypes.length); + address[] memory _delegates2 = new address[](_proposalTypes.length); + + vm.startPrank(hatter); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + _delegates2[i] = makeAddr(string(abi.encodePacked('delegate2', i))); + + IWonderVotes.Delegate memory _delegate = IWonderVotes.Delegate({account: _delegates[i], weight: _weight}); + IWonderVotes.Delegate memory _delegate2 = IWonderVotes.Delegate({account: _delegates2[i], weight: _weight}); + IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](2); + _delegatesStruct[0] = _delegate; + _delegatesStruct[1] = _delegate2; + + rabbitToken.delegate(_delegatesStruct, _proposalTypes[i]); + } + + vm.stopPrank(); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); + vm.roll(block.number + 1); + + vm.prank(hatter); + WonderVotesForTest(address(rabbitToken)).burn(_subsBalance); + vm.roll(block.number + 1); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 1), 0); + + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 2), _previousBalance / 2, 1, '' + ); + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates2[i], _proposalTypes[i], block.number - 2), _previousBalance / 2, 1, '' + ); + + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 1), + (uint256(_previousBalance) - _subsBalance) / 2, + 1, + '' + ); + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates2[i], _proposalTypes[i], block.number - 1), + (uint256(_previousBalance) - _subsBalance) / 2, + 1, + '' + ); + } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _delegates.length; j++) { + if (j != i) { + assertEq(rabbitToken.getPastVotes(_delegates[j], _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(_delegates[j], _proposalTypes[i], block.number - 1), 0); + + assertEq(rabbitToken.getPastVotes(_delegates2[j], _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(_delegates2[j], _proposalTypes[i], block.number - 1), 0); + } + } + } + } + + function test_GetPastVotes_After_Transfer_SmartAndPartialDelegation( + uint128 _previousBalance, + uint128 _addBalance + ) public { + uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); + + // 2 delegates 50% each + uint256 _weightNormalizer = rabbitToken.weightNormalizer(); + uint256 _weight = _weightNormalizer / 2; + + address[] memory _delegates = new address[](_proposalTypes.length); + address[] memory _delegates2 = new address[](_proposalTypes.length); + + vm.startPrank(hatter); + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); + _delegates2[i] = makeAddr(string(abi.encodePacked('delegate2', i))); + + IWonderVotes.Delegate memory _delegate = IWonderVotes.Delegate({account: _delegates[i], weight: _weight}); + IWonderVotes.Delegate memory _delegate2 = IWonderVotes.Delegate({account: _delegates2[i], weight: _weight}); + IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](2); + _delegatesStruct[0] = _delegate; + _delegatesStruct[1] = _delegate2; + + rabbitToken.delegate(_delegatesStruct, _proposalTypes[i]); + } + vm.stopPrank(); + + WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); + WonderVotesForTest(address(rabbitToken)).mint(cat, _addBalance); + + vm.roll(block.number + 1); + + vm.prank(cat); + rabbitToken.transfer(hatter, _addBalance); + vm.roll(block.number + 1); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(hatter, _proposalTypes[i], block.number - 1), 0); + + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 2), _previousBalance / 2, 1, '' + ); + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates2[i], _proposalTypes[i], block.number - 2), _previousBalance / 2, 1, '' + ); + + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates[i], _proposalTypes[i], block.number - 1), + (uint256(_previousBalance) + _addBalance) / 2, + 1, + '' + ); + assertApproxEqAbs( + rabbitToken.getPastVotes(_delegates2[i], _proposalTypes[i], block.number - 1), + (uint256(_previousBalance) + _addBalance) / 2, + 1, + '' + ); + + assertEq(rabbitToken.getPastVotes(cat, _proposalTypes[i], block.number - 2), _addBalance); + assertEq(rabbitToken.getPastVotes(cat, _proposalTypes[i], block.number - 1), 0); + } + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + for (uint256 j = 0; j < _delegates.length; j++) { + if (j != i) { + assertEq(rabbitToken.getPastVotes(_delegates[j], _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(_delegates[j], _proposalTypes[i], block.number - 1), 0); + + assertEq(rabbitToken.getPastVotes(_delegates2[j], _proposalTypes[i], block.number - 2), 0); + assertEq(rabbitToken.getPastVotes(_delegates2[j], _proposalTypes[i], block.number - 1), 0); + } + } + } } } From 3856d2d16e9a500a383d21eb2c37649764bd8283 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Fri, 22 Dec 2023 18:59:30 -0300 Subject: [PATCH 09/19] test: add wonder votes unit tests Signed-off-by: 0xRaccoon --- solidity/test/unit/WonderVotes.t.sol | 152 +++++++-------------------- 1 file changed, 37 insertions(+), 115 deletions(-) diff --git a/solidity/test/unit/WonderVotes.t.sol b/solidity/test/unit/WonderVotes.t.sol index a87209a..03bec0d 100644 --- a/solidity/test/unit/WonderVotes.t.sol +++ b/solidity/test/unit/WonderVotes.t.sol @@ -52,32 +52,6 @@ contract BaseTest is Test { function _expectEmit(address _contract) internal { vm.expectEmit(true, true, true, true, _contract); } - - function _createProposal( - uint8 _proposalType, - address _target, - uint256 _value, - bytes memory _calldata, - string memory _description, - uint256 _proposerVotes - ) internal returns (uint256) { - vm.assume(_proposalType < governor.proposalTypes().length); - vm.assume(_proposerVotes >= governor.proposalThreshold(_proposalType)); - - _mockGetPastVotes(hatter, _proposalType, block.number - 1, _proposerVotes); - - address[] memory _targets = new address[](1); - _targets[0] = _target; - - uint256[] memory _values = new uint256[](1); - _values[0] = _value; - - bytes[] memory _calldatas = new bytes[](1); - _calldatas[0] = _calldata; - - vm.prank(hatter); - return governor.propose(_proposalType, _targets, _values, _calldatas, _description); - } } contract Unit_Delegate_Simple is BaseTest { @@ -676,6 +650,15 @@ contract Unit_TransferVotes is BaseTest { } contract Unit_GetPastVotes is BaseTest { + uint8[] internal _proposalTypes; + + // 2 delegates 50% each + uint256 internal _weightNormalizer; + uint256 internal _weight; + + address[] internal _delegates; + address[] internal _delegates2; + function setUp() public override { super.setUp(); @@ -684,12 +667,19 @@ contract Unit_GetPastVotes is BaseTest { rabbitToken.delegate(hatter); vm.prank(cat); rabbitToken.delegate(cat); + + _proposalTypes = rabbitToken.proposalTypes(); + + // 2 delegates 50% each + _weightNormalizer = rabbitToken.weightNormalizer(); + _weight = _weightNormalizer / 2; + + _delegates = new address[](_proposalTypes.length); + _delegates2 = new address[](_proposalTypes.length); } // Simple delegation function test_GetPastVotes_After_Mint(uint128 _previousBalance, uint128 _addBalance) public { - uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); - WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); vm.roll(block.number + 1); @@ -706,7 +696,6 @@ contract Unit_GetPastVotes is BaseTest { function test_GetPastVotes_After_Burn(uint128 _previousBalance, uint128 _subsBalance) public { vm.assume(_previousBalance >= _subsBalance); - uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); vm.roll(block.number + 1); @@ -722,8 +711,6 @@ contract Unit_GetPastVotes is BaseTest { } function test_GetPastVotes_After_Transfer(uint128 _previousBalance, uint128 _addBalance) public { - uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); - WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); WonderVotesForTest(address(rabbitToken)).mint(cat, _addBalance); @@ -744,19 +731,21 @@ contract Unit_GetPastVotes is BaseTest { } } - // Smart Delegation - function test_GetPastVotes_After_Mint_SmartDelegation(uint128 _previousBalance, uint128 _addBalance) public { - uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); - - address[] memory _delegates = new address[](_proposalTypes.length); + function _smartDelegate() internal { + vm.startPrank(hatter); for (uint256 i = 0; i < _proposalTypes.length; i++) { _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); // 100% voting power to the delegate for the proposalType - vm.prank(hatter); rabbitToken.delegate(_delegates[i], _proposalTypes[i]); } + vm.stopPrank(); + } + + // Smart Delegation + function test_GetPastVotes_After_Mint_SmartDelegation(uint128 _previousBalance, uint128 _addBalance) public { + _smartDelegate(); WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); vm.roll(block.number + 1); @@ -778,17 +767,8 @@ contract Unit_GetPastVotes is BaseTest { function test_GetPastVotes_After_Burn_SmartDelegation(uint128 _previousBalance, uint128 _subsBalance) public { vm.assume(_previousBalance >= _subsBalance); - uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); - address[] memory _delegates = new address[](_proposalTypes.length); - - for (uint256 i = 0; i < _proposalTypes.length; i++) { - _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); - - // 100% voting power to the delegate for the proposalType - vm.prank(hatter); - rabbitToken.delegate(_delegates[i], _proposalTypes[i]); - } + _smartDelegate(); WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); vm.roll(block.number + 1); @@ -818,16 +798,7 @@ contract Unit_GetPastVotes is BaseTest { } function test_GetPastVotes_After_Transfer_SmartDelegation(uint128 _previousBalance, uint128 _addBalance) public { - uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); - address[] memory _delegates = new address[](_proposalTypes.length); - - for (uint256 i = 0; i < _proposalTypes.length; i++) { - _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); - - // 100% voting power to the delegate for the proposalType - vm.prank(hatter); - rabbitToken.delegate(_delegates[i], _proposalTypes[i]); - } + _smartDelegate(); WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); WonderVotesForTest(address(rabbitToken)).mint(cat, _addBalance); @@ -862,18 +833,9 @@ contract Unit_GetPastVotes is BaseTest { } } - // Smart and partial delegation - function test_GetPastVotes_After_Mint_SmartAndPartialDelegation(uint128 _previousBalance, uint128 _addBalance) public { - uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); - - // 2 delegates 50% each - uint256 _weightNormalizer = rabbitToken.weightNormalizer(); - uint256 _weight = _weightNormalizer / 2; - - address[] memory _delegates = new address[](_proposalTypes.length); - address[] memory _delegates2 = new address[](_proposalTypes.length); - + function _smartAndPartialDelegate() internal { vm.startPrank(hatter); + for (uint256 i = 0; i < _proposalTypes.length; i++) { _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); _delegates2[i] = makeAddr(string(abi.encodePacked('delegate2', i))); @@ -886,8 +848,12 @@ contract Unit_GetPastVotes is BaseTest { rabbitToken.delegate(_delegatesStruct, _proposalTypes[i]); } - vm.stopPrank(); + } + + // Smart and partial delegation + function test_GetPastVotes_After_Mint_SmartAndPartialDelegation(uint128 _previousBalance, uint128 _addBalance) public { + _smartAndPartialDelegate(); WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); vm.roll(block.number + 1); @@ -938,30 +904,8 @@ contract Unit_GetPastVotes is BaseTest { uint128 _subsBalance ) public { vm.assume(_previousBalance >= _subsBalance); - uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); - // 2 delegates 50% each - uint256 _weightNormalizer = rabbitToken.weightNormalizer(); - uint256 _weight = _weightNormalizer / 2; - - address[] memory _delegates = new address[](_proposalTypes.length); - address[] memory _delegates2 = new address[](_proposalTypes.length); - - vm.startPrank(hatter); - for (uint256 i = 0; i < _proposalTypes.length; i++) { - _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); - _delegates2[i] = makeAddr(string(abi.encodePacked('delegate2', i))); - - IWonderVotes.Delegate memory _delegate = IWonderVotes.Delegate({account: _delegates[i], weight: _weight}); - IWonderVotes.Delegate memory _delegate2 = IWonderVotes.Delegate({account: _delegates2[i], weight: _weight}); - IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](2); - _delegatesStruct[0] = _delegate; - _delegatesStruct[1] = _delegate2; - - rabbitToken.delegate(_delegatesStruct, _proposalTypes[i]); - } - - vm.stopPrank(); + _smartAndPartialDelegate(); WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); vm.roll(block.number + 1); @@ -1012,29 +956,7 @@ contract Unit_GetPastVotes is BaseTest { uint128 _previousBalance, uint128 _addBalance ) public { - uint8[] memory _proposalTypes = rabbitToken.proposalTypes(); - - // 2 delegates 50% each - uint256 _weightNormalizer = rabbitToken.weightNormalizer(); - uint256 _weight = _weightNormalizer / 2; - - address[] memory _delegates = new address[](_proposalTypes.length); - address[] memory _delegates2 = new address[](_proposalTypes.length); - - vm.startPrank(hatter); - for (uint256 i = 0; i < _proposalTypes.length; i++) { - _delegates[i] = makeAddr(string(abi.encodePacked('delegate', i))); - _delegates2[i] = makeAddr(string(abi.encodePacked('delegate2', i))); - - IWonderVotes.Delegate memory _delegate = IWonderVotes.Delegate({account: _delegates[i], weight: _weight}); - IWonderVotes.Delegate memory _delegate2 = IWonderVotes.Delegate({account: _delegates2[i], weight: _weight}); - IWonderVotes.Delegate[] memory _delegatesStruct = new IWonderVotes.Delegate[](2); - _delegatesStruct[0] = _delegate; - _delegatesStruct[1] = _delegate2; - - rabbitToken.delegate(_delegatesStruct, _proposalTypes[i]); - } - vm.stopPrank(); + _smartAndPartialDelegate(); WonderVotesForTest(address(rabbitToken)).mint(hatter, _previousBalance); WonderVotesForTest(address(rabbitToken)).mint(cat, _addBalance); From fbeccaf03fdb5a01b0e6b24c88a99c02b55d87c7 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Tue, 26 Dec 2023 20:14:31 -0300 Subject: [PATCH 10/19] test: add integration tests Signed-off-by: 0xRaccoon --- .env.example | 2 + foundry.toml | 3 +- solidity/examples/AliceGovernor.sol | 1 + solidity/test/integration/Delegation.t.sol | 152 ++++++++++++++++++ .../test/integration/IntegrationBase.t.sol | 72 +++++++++ solidity/test/integration/Propose.t.sol | 87 ++++++++++ solidity/test/smock/SmockHelper.sol | 2 +- .../test/smock/examples/MockAliceGovernor.sol | 4 +- .../test/smock/examples/MockRabbitToken.sol | 4 +- solidity/test/unit/WonderGovernor.t.sol | 20 ++- solidity/test/unit/WonderVotes.t.sol | 11 +- solidity/test/utils/TestExtended.sol | 21 +++ 12 files changed, 361 insertions(+), 18 deletions(-) create mode 100644 solidity/test/integration/Delegation.t.sol create mode 100644 solidity/test/integration/IntegrationBase.t.sol create mode 100644 solidity/test/integration/Propose.t.sol create mode 100644 solidity/test/utils/TestExtended.sol diff --git a/.env.example b/.env.example index aa5efdc..5b6781a 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,5 @@ GOERLI_RPC= GOERLI_DEPLOYER_PK= ETHERSCAN_API_KEY= + +OPTIMISM_RPC= \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 3f6d2a0..f2d2201 100644 --- a/foundry.toml +++ b/foundry.toml @@ -31,4 +31,5 @@ src = 'solidity/interfaces/' runs = 1000 [rpc_endpoints] -mainnet = "${MAINNET_RPC}" \ No newline at end of file +mainnet = "${MAINNET_RPC}" +optimism = "${OPTIMISM_RPC}" \ No newline at end of file diff --git a/solidity/examples/AliceGovernor.sol b/solidity/examples/AliceGovernor.sol index 2c2488d..e8e1691 100644 --- a/solidity/examples/AliceGovernor.sol +++ b/solidity/examples/AliceGovernor.sol @@ -85,6 +85,7 @@ contract AliceGovernor is WonderGovernor { uint256 _weight, bytes memory _params ) internal virtual override { + proposalTracks[_proposalId].votes += _weight; if (_support == 0) { proposalTracks[_proposalId].againstVotes += _weight; } else if (_support == 1) { diff --git a/solidity/test/integration/Delegation.t.sol b/solidity/test/integration/Delegation.t.sol new file mode 100644 index 0000000..779863d --- /dev/null +++ b/solidity/test/integration/Delegation.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import './IntegrationBase.t.sol'; + +import {WonderVotes} from 'contracts/governance/utils/WonderVotes.sol'; +import {WonderGovernor} from 'contracts/governance/WonderGovernor.sol'; + +contract Integration_Delegation is IntegrationBase { + function test_AllVotersDelegateToProposer() public { + // AllVoters delegates to proposer + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address holder = holders[_i]; + vm.prank(holder); + rabbitToken.delegate(proposer); + } + + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address holder = holders[_i]; + + for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) { + uint8 proposalType = governor.proposalTypes()[_j]; + assertEq(rabbitToken.getVotes(holder, proposalType), 0); + } + } + + for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) { + uint8 proposalType = governor.proposalTypes()[_j]; + assertEq(rabbitToken.getVotes(proposer, proposalType), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER); + } + } + + function test_AllVotersDelegateByProposalType() public { + uint8[] memory proposalTypes = governor.proposalTypes(); + + for (uint256 _i = 0; _i < proposalTypes.length; _i++) { + address _holder = holders[_i]; + vm.prank(_holder); + rabbitToken.delegate(proposer, proposalTypes[_i]); + } + + for (uint256 _i = 0; _i < proposalTypes.length; _i++) { + address _holder = holders[_i]; + assertEq(rabbitToken.getVotes(_holder, proposalTypes[_i]), 0); + assertEq(rabbitToken.getVotes(proposer, proposalTypes[_i]), INITIAL_VOTERS_BALANCE); + } + } + + function test_AllVotersDelegatePartially() public { + uint8[] memory proposalTypes = governor.proposalTypes(); + + // 50% of votes + uint256 _weight = rabbitToken.weightNormalizer() / 2; + + IWonderVotes.Delegate memory _delegate = IWonderVotes.Delegate({account: proposer, weight: _weight}); + IWonderVotes.Delegate memory _delegate2 = IWonderVotes.Delegate({account: proposer2, weight: _weight}); + + IWonderVotes.Delegate[] memory _delegates = new IWonderVotes.Delegate[](2); + _delegates[0] = _delegate; + _delegates[1] = _delegate2; + + for (uint256 _i = 0; _i < proposalTypes.length; _i++) { + for (uint256 _j = 0; _j < VOTERS_NUMBER; _j++) { + address _holder = holders[_j]; + vm.prank(_holder); + rabbitToken.delegate(_delegates, proposalTypes[_i]); + } + } + + for (uint256 _i = 0; _i < proposalTypes.length; _i++) { + assertEq(rabbitToken.getVotes(proposer, proposalTypes[_i]), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER / 2); + assertEq(rabbitToken.getVotes(proposer2, proposalTypes[_i]), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER / 2); + + for (uint256 _j = 0; _j < VOTERS_NUMBER; _j++) { + address _holder = holders[_j]; + assertEq(rabbitToken.getVotes(_holder, proposalTypes[_i]), 0); + } + } + } + + function test_ProposeWithDelegatedVotes() public { + address _voter1 = holders[0]; + + vm.prank(proposer); + rabbitToken.delegate(proposer); + + // delegate to proposer + vm.prank(_voter1); + rabbitToken.delegate(proposer); + + address[] memory _targets = new address[](1); + _targets[0] = address(governor); + + uint256[] memory _values = new uint256[](1); + _values[0] = 1; + + bytes[] memory _calldatas = new bytes[](1); + _calldatas[0] = abi.encode(0); + + string memory _description = 'test proposal'; + + // To propose Governor controls the proposal threshold calling getPastVotes, so we need to mine a block to be able to propose + _mineBlock(); + + uint8[] memory _proposalTypes = governor.proposalTypes(); + + vm.startPrank(proposer); + for (uint256 _i = 0; _i < _proposalTypes.length; _i++) { + uint8 _proposalType = _proposalTypes[_i]; + + uint256 _precomputedProposalId = + governor.hashProposal(_proposalType, _targets, _values, _calldatas, keccak256(bytes(_description))); + _expectEmit(address(governor)); + + emit ProposalCreated( + _precomputedProposalId, + _proposalType, + address(proposer), + _targets, + _values, + new string[](1), + _calldatas, + block.number + 1, + block.number + governor.votingPeriod() + 1, + _description + ); + governor.propose(_proposalType, _targets, _values, _calldatas, _description); + } + vm.stopPrank(); + } + + function test_AllVotersChangeDelegation() public { + uint8[] memory _proposalTypes = governor.proposalTypes(); + + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address _holder = holders[_i]; + vm.prank(_holder); + rabbitToken.delegate(proposer); + } + + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address _holder = holders[_i]; + vm.prank(_holder); + rabbitToken.delegate(proposer2); + } + + for (uint8 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(proposer, _proposalTypes[i]), 0); + assertEq(rabbitToken.getVotes(proposer2, _proposalTypes[i]), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER); + } + } +} diff --git a/solidity/test/integration/IntegrationBase.t.sol b/solidity/test/integration/IntegrationBase.t.sol new file mode 100644 index 0000000..8c2765c --- /dev/null +++ b/solidity/test/integration/IntegrationBase.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// solhint-disable no-unused-import +// solhint-disable-next-line no-console +import {console} from 'forge-std/console.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {IWonderVotes} from 'interfaces/governance/utils/IWonderVotes.sol'; +import {IWonderGovernor} from 'interfaces/governance/IWonderGovernor.sol'; + +import {AliceGovernor} from 'examples/AliceGovernor.sol'; +import {RabbitToken} from 'examples/RabbitToken.sol'; + +import {TestExtended} from '../utils/TestExtended.sol'; + +contract IntegrationBase is TestExtended { + uint256 public constant FORK_BLOCK = 111_361_902; + + uint256 internal _initialBalance = 100_000 ether; + + address public deployer = makeAddr('deployer'); + address public proposer = makeAddr('proposer'); + address public proposer2 = makeAddr('proposer2'); + + address[] public holders; + + IWonderVotes public rabbitToken; + IWonderGovernor public governor; + + uint256 public constant INITIAL_VOTERS_BALANCE = 100_000e18; + uint8 public constant VOTERS_NUMBER = 10; + + function setUp() public virtual { + vm.createSelectFork(vm.rpcUrl('optimism'), FORK_BLOCK); + + // Deploy the governance contracts + vm.startPrank(deployer); + + address tokenAddress = vm.computeCreateAddress(deployer, vm.getNonce(deployer) + 1); + governor = new AliceGovernor(tokenAddress); + rabbitToken = new RabbitToken(AliceGovernor(payable(address(governor)))); + + vm.stopPrank(); + + for (uint256 i = 0; i < VOTERS_NUMBER; i++) { + address holder = makeAddr(string(abi.encodePacked('holder', i))); + holders.push(holder); + deal(tokenAddress, holder, INITIAL_VOTERS_BALANCE); + vm.prank(holder); + + // start tracking votes + rabbitToken.delegate(holder); + } + + _mineBlock(); + } + + event ProposalCreated( + uint256 proposalId, + uint8 proposalType, + address proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + uint256 voteStart, + uint256 voteEnd, + string description + ); +} diff --git a/solidity/test/integration/Propose.t.sol b/solidity/test/integration/Propose.t.sol new file mode 100644 index 0000000..57087c7 --- /dev/null +++ b/solidity/test/integration/Propose.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import './IntegrationBase.t.sol'; + +import {WonderVotes} from 'contracts/governance/utils/WonderVotes.sol'; +import {WonderGovernor} from 'contracts/governance/WonderGovernor.sol'; + +contract Integration_Propose is IntegrationBase { + function _propose() internal returns (uint256 _proposalId) { + address[] memory _targets = new address[](1); + _targets[0] = address(governor); + + uint256[] memory _values = new uint256[](1); + _values[0] = 1; + + bytes[] memory _calldatas = new bytes[](1); + _calldatas[0] = abi.encode(0); + + string memory _description = 'test proposal'; + + vm.prank(holders[0]); + + // Propose + return governor.propose(0, _targets, _values, _calldatas, _description); + } + + function _vote(uint256 _proposalId, uint256 _forVoters) internal { + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address _holder = holders[_i]; + vm.prank(_holder); + + // for 60% , against 40% + uint8 _vote = _forVoters > _i ? 1 : 0; + // Vote + governor.castVote(_proposalId, _vote); + } + } + + function test_ProposalSucceeded() public { + uint256 _proposalId = _propose(); + + _mineBlocks(governor.votingDelay() + 1); + + uint256 _forVoters = VOTERS_NUMBER / 2 + 1; + uint256 _againstVoters = VOTERS_NUMBER - _forVoters; + + _vote(_proposalId, _forVoters); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, INITIAL_VOTERS_BALANCE * _forVoters); + assertEq(_againstVotes, INITIAL_VOTERS_BALANCE * _againstVoters); + assertEq(_abstainVotes, 0); + assertEq(_votes, INITIAL_VOTERS_BALANCE * VOTERS_NUMBER); + + // End voting period + _mineBlocks(governor.votingPeriod()); + + assertEq(uint256(governor.state(_proposalId)), 4); + } + + function test_ProposalDefeated() public { + uint256 _proposalId = _propose(); + + _mineBlocks(governor.votingDelay() + 1); + + uint256 _againstVoters = VOTERS_NUMBER / 2 + 1; + uint256 _forVoters = VOTERS_NUMBER - _againstVoters; + + _vote(_proposalId, _forVoters); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, INITIAL_VOTERS_BALANCE * _forVoters); + assertEq(_againstVotes, INITIAL_VOTERS_BALANCE * _againstVoters); + assertEq(_abstainVotes, 0); + assertEq(_votes, INITIAL_VOTERS_BALANCE * VOTERS_NUMBER); + + // End voting period + _mineBlocks(governor.votingPeriod()); + + assertEq(uint256(governor.state(_proposalId)), 3); + } +} diff --git a/solidity/test/smock/SmockHelper.sol b/solidity/test/smock/SmockHelper.sol index 442961e..14ef54d 100644 --- a/solidity/test/smock/SmockHelper.sol +++ b/solidity/test/smock/SmockHelper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import {Test} from 'forge-std/Test.sol'; diff --git a/solidity/test/smock/examples/MockAliceGovernor.sol b/solidity/test/smock/examples/MockAliceGovernor.sol index 5b2623d..9bf7328 100644 --- a/solidity/test/smock/examples/MockAliceGovernor.sol +++ b/solidity/test/smock/examples/MockAliceGovernor.sol @@ -1,5 +1,5 @@ -/// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; import {Test} from 'forge-std/Test.sol'; import { diff --git a/solidity/test/smock/examples/MockRabbitToken.sol b/solidity/test/smock/examples/MockRabbitToken.sol index c1fa222..1042fc5 100644 --- a/solidity/test/smock/examples/MockRabbitToken.sol +++ b/solidity/test/smock/examples/MockRabbitToken.sol @@ -1,5 +1,5 @@ -/// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; import {Test} from 'forge-std/Test.sol'; import {AliceGovernor, EIP712, ERC20, RabbitToken, WonderERC20Votes} from 'solidity/examples/RabbitToken.sol'; diff --git a/solidity/test/unit/WonderGovernor.t.sol b/solidity/test/unit/WonderGovernor.t.sol index cd0a0aa..3320a10 100644 --- a/solidity/test/unit/WonderGovernor.t.sol +++ b/solidity/test/unit/WonderGovernor.t.sol @@ -1,4 +1,5 @@ -import 'forge-std/Test.sol'; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; import {WonderGovernor} from 'contracts/governance/WonderGovernor.sol'; import {IWonderGovernor} from 'interfaces/governance/IWonderGovernor.sol'; @@ -8,6 +9,8 @@ import {IWonderVotes} from 'interfaces/governance/utils/IWonderVotes.sol'; import {IWonderGovernor} from 'interfaces/governance/IWonderGovernor.sol'; import {WonderVotes} from 'contracts/governance/utils/WonderVotes.sol'; +import {TestExtended} from '../utils/TestExtended.sol'; + contract GovernorForTest is AliceGovernor { constructor(address _wonderToken) AliceGovernor(_wonderToken) {} @@ -16,7 +19,7 @@ contract GovernorForTest is AliceGovernor { } } -contract BaseTest is Test { +contract BaseTest is TestExtended { address deployer = makeAddr('deployer'); address hatter = makeAddr('hatter'); address cat = makeAddr('cat'); @@ -42,10 +45,6 @@ contract BaseTest is Test { vm.stopPrank(); } - function _expectEmit(address _contract) internal { - vm.expectEmit(true, true, true, true, _contract); - } - function _createProposal( uint8 _proposalType, address _target, @@ -366,6 +365,7 @@ contract Unit_CastVote is BaseTest { (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + assertEq(_votes, _voterVotes); assertEq(_forVotes, _voterVotes); assertEq(_againstVotes, 0); assertEq(_abstainVotes, 0); @@ -397,6 +397,7 @@ contract Unit_CastVote is BaseTest { (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + assertEq(_votes, _voterVotes); assertEq(_forVotes, 0); assertEq(_againstVotes, _voterVotes); assertEq(_abstainVotes, 0); @@ -428,6 +429,7 @@ contract Unit_CastVote is BaseTest { (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + assertEq(_votes, _voterVotes); assertEq(_forVotes, 0); assertEq(_againstVotes, 0); assertEq(_abstainVotes, _voterVotes); @@ -525,6 +527,7 @@ contract Unit_CastVoteWithReason is BaseTest { (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + assertEq(_votes, _voterVotes); assertEq(_forVotes, _voterVotes); assertEq(_againstVotes, 0); assertEq(_abstainVotes, 0); @@ -557,6 +560,7 @@ contract Unit_CastVoteWithReason is BaseTest { (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + assertEq(_votes, _voterVotes); assertEq(_forVotes, 0); assertEq(_againstVotes, _voterVotes); assertEq(_abstainVotes, 0); @@ -589,6 +593,7 @@ contract Unit_CastVoteWithReason is BaseTest { (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + assertEq(_votes, _voterVotes); assertEq(_forVotes, 0); assertEq(_againstVotes, 0); assertEq(_abstainVotes, _voterVotes); @@ -693,6 +698,7 @@ contract Unit_CastVoteWithReasonAndParams is BaseTest { (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + assertEq(_votes, _voterVotes); assertEq(_forVotes, _voterVotes); assertEq(_againstVotes, 0); assertEq(_abstainVotes, 0); @@ -727,6 +733,7 @@ contract Unit_CastVoteWithReasonAndParams is BaseTest { (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + assertEq(_votes, _voterVotes); assertEq(_forVotes, 0); assertEq(_againstVotes, _voterVotes); assertEq(_abstainVotes, 0); @@ -761,6 +768,7 @@ contract Unit_CastVoteWithReasonAndParams is BaseTest { (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + assertEq(_votes, _voterVotes); assertEq(_forVotes, 0); assertEq(_againstVotes, 0); assertEq(_abstainVotes, _voterVotes); diff --git a/solidity/test/unit/WonderVotes.t.sol b/solidity/test/unit/WonderVotes.t.sol index 03bec0d..3607876 100644 --- a/solidity/test/unit/WonderVotes.t.sol +++ b/solidity/test/unit/WonderVotes.t.sol @@ -1,4 +1,5 @@ -import 'forge-std/Test.sol'; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; import {WonderGovernor} from 'contracts/governance/WonderGovernor.sol'; import {IWonderGovernor} from 'interfaces/governance/IWonderGovernor.sol'; @@ -9,6 +10,8 @@ import {RabbitToken} from 'examples/RabbitToken.sol'; import {MockAliceGovernor} from '../smock/examples/MockAliceGovernor.sol'; import {AliceGovernor} from 'examples/AliceGovernor.sol'; +import {TestExtended} from '../utils/TestExtended.sol'; + contract WonderVotesForTest is RabbitToken { constructor(AliceGovernor _governor) RabbitToken(_governor) {} @@ -21,7 +24,7 @@ contract WonderVotesForTest is RabbitToken { } } -contract BaseTest is Test { +contract BaseTest is TestExtended { address deployer = makeAddr('deployer'); address hatter = makeAddr('hatter'); address cat = makeAddr('cat'); @@ -48,10 +51,6 @@ contract BaseTest is Test { vm.stopPrank(); } - - function _expectEmit(address _contract) internal { - vm.expectEmit(true, true, true, true, _contract); - } } contract Unit_Delegate_Simple is BaseTest { diff --git a/solidity/test/utils/TestExtended.sol b/solidity/test/utils/TestExtended.sol new file mode 100644 index 0000000..e3dd99d --- /dev/null +++ b/solidity/test/utils/TestExtended.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; + +contract TestExtended is Test { + uint256 public constant BLOCK_TIME = 12 seconds; + + function _mineBlock() internal { + _mineBlocks(1); + } + + function _mineBlocks(uint256 _blocks) internal { + vm.warp(block.timestamp + _blocks * BLOCK_TIME); + vm.roll(block.number + _blocks); + } + + function _expectEmit(address _contract) internal { + vm.expectEmit(true, true, true, true, _contract); + } +} From ef0afb9d312afa91aa526dfa32471e15c13cd059 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Tue, 26 Dec 2023 20:44:17 -0300 Subject: [PATCH 11/19] fix: add optimism rpc Signed-off-by: 0xRaccoon --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 010bb44..5db6910 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,7 @@ jobs: touch .env echo MAINNET_RPC="${{ secrets.MAINNET_RPC }}" >> .env echo GOERLI_RPC="${{ secrets.GOERLI_RPC }}" >> .env + echo GOERLI_RPC="${{ secrets.OPTIMISM_RPC }}" >> .env cat .env - name: Run tests From d8440ad1fd77167e4b6da409eb9c92d7af440c3e Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Tue, 26 Dec 2023 20:45:56 -0300 Subject: [PATCH 12/19] fix: add optimism rpc Signed-off-by: 0xRaccoon --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5db6910..7f5c524 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: touch .env echo MAINNET_RPC="${{ secrets.MAINNET_RPC }}" >> .env echo GOERLI_RPC="${{ secrets.GOERLI_RPC }}" >> .env - echo GOERLI_RPC="${{ secrets.OPTIMISM_RPC }}" >> .env + echo OPTIMISM_RPC="${{ secrets.OPTIMISM_RPC }}" >> .env cat .env - name: Run tests From 66a61f254dc8a76877cbdced0c69eeb0accb2be4 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Wed, 27 Dec 2023 11:08:55 -0300 Subject: [PATCH 13/19] test: add integration tests Signed-off-by: 0xRaccoon --- solidity/test/integration/Propose.t.sol | 94 +++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/solidity/test/integration/Propose.t.sol b/solidity/test/integration/Propose.t.sol index 57087c7..1645691 100644 --- a/solidity/test/integration/Propose.t.sol +++ b/solidity/test/integration/Propose.t.sol @@ -7,6 +7,8 @@ import {WonderVotes} from 'contracts/governance/utils/WonderVotes.sol'; import {WonderGovernor} from 'contracts/governance/WonderGovernor.sol'; contract Integration_Propose is IntegrationBase { + event ProposalCanceled(uint256 _proposalId); + function _propose() internal returns (uint256 _proposalId) { address[] memory _targets = new address[](1); _targets[0] = address(governor); @@ -25,15 +27,15 @@ contract Integration_Propose is IntegrationBase { return governor.propose(0, _targets, _values, _calldatas, _description); } - function _vote(uint256 _proposalId, uint256 _forVoters) internal { + function _vote(uint256 _proposalId, uint256 _forVoters, uint256 _againstVoters) internal { for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { address _holder = holders[_i]; vm.prank(_holder); // for 60% , against 40% - uint8 _vote = _forVoters > _i ? 1 : 0; + uint8 _support = _forVoters > _i ? 1 : _forVoters + _againstVoters > _i ? 0 : 2; // Vote - governor.castVote(_proposalId, _vote); + governor.castVote(_proposalId, _support); } } @@ -45,7 +47,7 @@ contract Integration_Propose is IntegrationBase { uint256 _forVoters = VOTERS_NUMBER / 2 + 1; uint256 _againstVoters = VOTERS_NUMBER - _forVoters; - _vote(_proposalId, _forVoters); + _vote(_proposalId, _forVoters, _againstVoters); (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); @@ -69,7 +71,31 @@ contract Integration_Propose is IntegrationBase { uint256 _againstVoters = VOTERS_NUMBER / 2 + 1; uint256 _forVoters = VOTERS_NUMBER - _againstVoters; - _vote(_proposalId, _forVoters); + _vote(_proposalId, _forVoters, _againstVoters); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, INITIAL_VOTERS_BALANCE * _forVoters); + assertEq(_againstVotes, INITIAL_VOTERS_BALANCE * _againstVoters); + assertEq(_abstainVotes, 0); + assertEq(_votes, INITIAL_VOTERS_BALANCE * VOTERS_NUMBER); + + // End voting period + _mineBlocks(governor.votingPeriod()); + + assertEq(uint256(governor.state(_proposalId)), 3); + } + + function test_ProposalEven() public { + uint256 _proposalId = _propose(); + + _mineBlocks(governor.votingDelay() + 1); + + uint256 _againstVoters = VOTERS_NUMBER / 2; + uint256 _forVoters = VOTERS_NUMBER - _againstVoters; + + _vote(_proposalId, _forVoters, _againstVoters); (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); @@ -84,4 +110,62 @@ contract Integration_Propose is IntegrationBase { assertEq(uint256(governor.state(_proposalId)), 3); } + + function test_ProposalSucceedWithAbstentions() public { + uint256 _proposalId = _propose(); + + _mineBlocks(governor.votingDelay() + 1); + + uint256 _abstainVoters = VOTERS_NUMBER / 2 + 1; + uint256 _againstVoters = (VOTERS_NUMBER - _abstainVoters) / 2 - 1; + uint256 _forVoters = VOTERS_NUMBER - _abstainVoters - _againstVoters; + + _vote(_proposalId, _forVoters, _againstVoters); + + (uint256 _id, uint256 _votes, uint256 _forVotes, uint256 _againstVotes, uint256 _abstainVotes) = + AliceGovernor(payable(address(governor))).proposalTracks(_proposalId); + + assertEq(_forVotes, INITIAL_VOTERS_BALANCE * _forVoters); + assertEq(_againstVotes, INITIAL_VOTERS_BALANCE * _againstVoters); + assertEq(_abstainVotes, INITIAL_VOTERS_BALANCE * _abstainVoters); + assertEq(_votes, INITIAL_VOTERS_BALANCE * VOTERS_NUMBER); + + // End voting period + _mineBlocks(governor.votingPeriod()); + + assertEq(uint256(governor.state(_proposalId)), 4); + } + + function test_ProposeAndCancel() public { + address[] memory _targets = new address[](1); + _targets[0] = address(governor); + + uint256[] memory _values = new uint256[](1); + _values[0] = 1; + + bytes[] memory _calldatas = new bytes[](1); + _calldatas[0] = abi.encode(0); + + string memory _description = 'test proposal'; + + vm.startPrank(holders[0]); + + // Propose + uint256 _proposalId = governor.propose(0, _targets, _values, _calldatas, _description); + + _expectEmit(address(governor)); + emit ProposalCanceled(_proposalId); + + // Cancel proposal + governor.cancel(0, _targets, _values, _calldatas, keccak256(bytes(_description))); + + vm.stopPrank(); + + _mineBlocks(governor.votingDelay() + 1); + + vm.prank(holders[1]); + + vm.expectRevert(abi.encodeWithSelector(IWonderGovernor.GovernorUnexpectedProposalState.selector, _proposalId, 2, 2)); + governor.castVote(_proposalId, 1); + } } From 8f0f9b0098fba4e78da28e494041206ec23b4f72 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Wed, 27 Dec 2023 11:25:10 -0300 Subject: [PATCH 14/19] fix: add missing file headers Signed-off-by: 0xRaccoon --- solidity/test/smock/SmockHelper.sol | 2 +- solidity/test/smock/examples/MockAliceGovernor.sol | 4 ++-- solidity/test/smock/examples/MockRabbitToken.sol | 4 ++-- solidity/test/unit/WonderGovernor.t.sol | 3 +++ 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/solidity/test/smock/SmockHelper.sol b/solidity/test/smock/SmockHelper.sol index 442961e..7ef3520 100644 --- a/solidity/test/smock/SmockHelper.sol +++ b/solidity/test/smock/SmockHelper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {Test} from 'forge-std/Test.sol'; diff --git a/solidity/test/smock/examples/MockAliceGovernor.sol b/solidity/test/smock/examples/MockAliceGovernor.sol index 5b2623d..759c1a5 100644 --- a/solidity/test/smock/examples/MockAliceGovernor.sol +++ b/solidity/test/smock/examples/MockAliceGovernor.sol @@ -1,5 +1,5 @@ -/// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; import {Test} from 'forge-std/Test.sol'; import { diff --git a/solidity/test/smock/examples/MockRabbitToken.sol b/solidity/test/smock/examples/MockRabbitToken.sol index c1fa222..ded229b 100644 --- a/solidity/test/smock/examples/MockRabbitToken.sol +++ b/solidity/test/smock/examples/MockRabbitToken.sol @@ -1,5 +1,5 @@ -/// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; import {Test} from 'forge-std/Test.sol'; import {AliceGovernor, EIP712, ERC20, RabbitToken, WonderERC20Votes} from 'solidity/examples/RabbitToken.sol'; diff --git a/solidity/test/unit/WonderGovernor.t.sol b/solidity/test/unit/WonderGovernor.t.sol index cd0a0aa..fa480e7 100644 --- a/solidity/test/unit/WonderGovernor.t.sol +++ b/solidity/test/unit/WonderGovernor.t.sol @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + import 'forge-std/Test.sol'; import {WonderGovernor} from 'contracts/governance/WonderGovernor.sol'; From 278f18aee8e23d3039e828f9235564c71374f4ba Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Wed, 27 Dec 2023 11:28:03 -0300 Subject: [PATCH 15/19] fix: add missing headers Signed-off-by: 0xRaccoon --- solidity/test/unit/WonderVotes.t.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/solidity/test/unit/WonderVotes.t.sol b/solidity/test/unit/WonderVotes.t.sol index 03bec0d..8b7c1c8 100644 --- a/solidity/test/unit/WonderVotes.t.sol +++ b/solidity/test/unit/WonderVotes.t.sol @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + import 'forge-std/Test.sol'; import {WonderGovernor} from 'contracts/governance/WonderGovernor.sol'; From 5a3bbf85431ff9317035724346453584dfe5c3d5 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Thu, 28 Dec 2023 09:30:10 -0300 Subject: [PATCH 16/19] feat: add non delegable flag feature Signed-off-by: 0xRaccoon --- .gitignore | 4 ++ .../governance/utils/WonderVotes.sol | 52 ++++++++++++++++--- .../governance/utils/IWonderVotes.sol | 20 +++++++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 96330bd..8910467 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ broadcast/*/*/* # Out dir out + +# Coverage +lcov.info + diff --git a/solidity/contracts/governance/utils/WonderVotes.sol b/solidity/contracts/governance/utils/WonderVotes.sol index 1b3db47..8fa6300 100644 --- a/solidity/contracts/governance/utils/WonderVotes.sol +++ b/solidity/contracts/governance/utils/WonderVotes.sol @@ -42,6 +42,8 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes mapping(uint8 proposalType => Checkpoints.Trace208) private _totalCheckpoints; + mapping(address account => bool) private _nonDelegatableAddresses; + /** * @dev The clock was incorrectly modified. */ @@ -132,7 +134,10 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes /** * @dev Delegates votes from the sender to `delegatee`. */ - function delegate(Delegate[] calldata delegatees, uint8 proposalType) public virtual validProposalType(proposalType) { + function delegate( + Delegate[] calldata delegatees, + uint8 proposalType + ) public virtual validProposalType(proposalType) activeDelegations(delegatees) { address account = _msgSender(); _delegate(account, proposalType, delegatees); } @@ -140,7 +145,10 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes /** * @dev See {IWonderVotes-delegate}. */ - function delegate(address delegatee, uint8 proposalType) public virtual validProposalType(proposalType) { + function delegate( + address delegatee, + uint8 proposalType + ) public virtual validProposalType(proposalType) activeDelegation(delegatee) { address account = _msgSender(); Delegate[] memory _singleDelegate = new Delegate[](1); _singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); @@ -150,7 +158,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes /** * @dev See {IWonderVotes-delegate}. */ - function delegate(address delegatee) public virtual { + function delegate(address delegatee) public virtual activeDelegation(delegatee) { address account = _msgSender(); Delegate[] memory _singleDelegate = new Delegate[](1); _singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); @@ -187,7 +195,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes uint8 v, bytes32 r, bytes32 s - ) public virtual validProposalType(proposalType) { + ) public virtual validProposalType(proposalType) activeDelegations(delegatees) { if (block.timestamp > expiry) { revert VotesExpiredSignature(expiry); } @@ -209,7 +217,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes uint8 v, bytes32 r, bytes32 s - ) public virtual validProposalType(proposalType) { + ) public virtual validProposalType(proposalType) activeDelegation(delegatee) { Delegate[] memory _singleDelegate = new Delegate[](1); _singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); delegateBySig(_singleDelegate, proposalType, nonce, expiry, v, r, s); @@ -225,7 +233,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes uint8 v, bytes32 r, bytes32 s - ) public virtual { + ) public virtual activeDelegation(delegatee) { Delegate[] memory _singleDelegate = new Delegate[](1); _singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); @@ -236,6 +244,20 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes } } + /** + * @dev See {IWonderVotes-isSuspendedDelegation}. + */ + function isSuspendedDelegation(address account) external view returns (bool) { + return _nonDelegatableAddresses[account]; + } + + /** + * @dev See {IWonderVotes-suspendDelegation}. + */ + function suspendDelegation(bool allow) external { + _nonDelegatableAddresses[msg.sender] = allow; + } + /** * @dev Delegate all of `account`'s voting units to `delegatee`. * @@ -381,4 +403,22 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes if (!_validProposalType(proposalType)) revert InvalidProposalType(proposalType); _; } + + /** + * @dev checks if the delegation is active for the `account` + */ + modifier activeDelegation(address account) { + if (_nonDelegatableAddresses[account]) revert DelegationSuspended(account); + _; + } + + /** + * @dev checks if the delegation is active for the `delegatees` + */ + modifier activeDelegations(Delegate[] memory delegatees) { + for (uint256 i = 0; i < delegatees.length; i++) { + if (_nonDelegatableAddresses[delegatees[i].account]) revert DelegationSuspended(delegatees[i].account); + } + _; + } } diff --git a/solidity/interfaces/governance/utils/IWonderVotes.sol b/solidity/interfaces/governance/utils/IWonderVotes.sol index db510b8..f87efdd 100644 --- a/solidity/interfaces/governance/utils/IWonderVotes.sol +++ b/solidity/interfaces/governance/utils/IWonderVotes.sol @@ -36,6 +36,11 @@ interface IWonderVotes { */ error DelegatesMaxNumberExceeded(uint256 delegateesNumber); + /** + * @dev The delegation of votes is suspended for the account. + */ + error DelegationSuspended(address account); + /** * @dev Emitted when an account changes their delegates. */ @@ -120,6 +125,15 @@ interface IWonderVotes { bytes32 s ) external; + /** + * @dev The caller account can enable or disable the ability to be delegated votes by a delegator. + * If set to true, the caller account is not eligible to be a delegatee; if set to false, it can be a delegatee. + * + * NOTE: changing the delegation status does not affect the already delegated votes to the account. + * By default, all accounts are allowed to be delegated. + */ + function suspendDelegation(bool suspend) external; + /** * @dev Returns the amount that represents 100% of the weight sum for every delegation * used to calculate the amount of votes when partial delegating to more than 1 delegate. @@ -136,4 +150,10 @@ interface IWonderVotes { * @dev Returns the `proposalTypes` supported. */ function proposalTypes() external view returns (uint8[] memory); + + /** + * @dev Returns whether the delegation of votes is suspended for a given account. + * Note: changing the delegation status does not affect the already delegated votes to the account. + */ + function isSuspendedDelegation(address account) external view returns (bool); } From 6490b89f64042049f323b1debfb82844584b7fe5 Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Thu, 28 Dec 2023 09:34:18 -0300 Subject: [PATCH 17/19] feat: add non-delegable flag feature Signed-off-by: 0xRaccoon --- solidity/contracts/governance/utils/WonderVotes.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/solidity/contracts/governance/utils/WonderVotes.sol b/solidity/contracts/governance/utils/WonderVotes.sol index 8fa6300..6ad06ef 100644 --- a/solidity/contracts/governance/utils/WonderVotes.sol +++ b/solidity/contracts/governance/utils/WonderVotes.sol @@ -42,7 +42,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes mapping(uint8 proposalType => Checkpoints.Trace208) private _totalCheckpoints; - mapping(address account => bool) private _nonDelegatableAddresses; + mapping(address account => bool) private _nonDelegableAddresses; /** * @dev The clock was incorrectly modified. @@ -248,14 +248,14 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes * @dev See {IWonderVotes-isSuspendedDelegation}. */ function isSuspendedDelegation(address account) external view returns (bool) { - return _nonDelegatableAddresses[account]; + return _nonDelegableAddresses[account]; } /** * @dev See {IWonderVotes-suspendDelegation}. */ function suspendDelegation(bool allow) external { - _nonDelegatableAddresses[msg.sender] = allow; + _nonDelegableAddresses[msg.sender] = allow; } /** @@ -408,7 +408,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes * @dev checks if the delegation is active for the `account` */ modifier activeDelegation(address account) { - if (_nonDelegatableAddresses[account]) revert DelegationSuspended(account); + if (_nonDelegableAddresses[account]) revert DelegationSuspended(account); _; } @@ -417,7 +417,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes */ modifier activeDelegations(Delegate[] memory delegatees) { for (uint256 i = 0; i < delegatees.length; i++) { - if (_nonDelegatableAddresses[delegatees[i].account]) revert DelegationSuspended(delegatees[i].account); + if (_nonDelegableAddresses[delegatees[i].account]) revert DelegationSuspended(delegatees[i].account); } _; } From 21250cb1b52583c7aaf1eb176082776c569192ef Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Thu, 28 Dec 2023 12:57:11 -0300 Subject: [PATCH 18/19] test: add delegation suspension integration tests Signed-off-by: 0xRaccoon --- .../governance/utils/WonderVotes.sol | 20 +-- solidity/test/integration/Delegation.t.sol | 133 ++++++++++++++++++ solidity/test/unit/WonderVotes.t.sol | 15 ++ 3 files changed, 158 insertions(+), 10 deletions(-) diff --git a/solidity/contracts/governance/utils/WonderVotes.sol b/solidity/contracts/governance/utils/WonderVotes.sol index 3fb3876..9911cba 100644 --- a/solidity/contracts/governance/utils/WonderVotes.sol +++ b/solidity/contracts/governance/utils/WonderVotes.sol @@ -137,7 +137,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes function delegate( Delegate[] calldata delegatees, uint8 proposalType - ) public virtual validProposalType(proposalType) activeDelegations(delegatees) { + ) public virtual validProposalType(proposalType) activeDelegates(delegatees) { address account = _msgSender(); _delegate(account, proposalType, delegatees); } @@ -148,7 +148,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes function delegate( address delegatee, uint8 proposalType - ) public virtual validProposalType(proposalType) activeDelegation(delegatee) { + ) public virtual validProposalType(proposalType) activeDelegate(delegatee) { address account = _msgSender(); Delegate[] memory _singleDelegate = new Delegate[](1); _singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); @@ -158,7 +158,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes /** * @dev See {IWonderVotes-delegate}. */ - function delegate(address delegatee) public virtual activeDelegation(delegatee) { + function delegate(address delegatee) public virtual activeDelegate(delegatee) { address account = _msgSender(); Delegate[] memory _singleDelegate = new Delegate[](1); _singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); @@ -195,7 +195,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes uint8 v, bytes32 r, bytes32 s - ) public virtual validProposalType(proposalType) activeDelegations(delegatees) { + ) public virtual validProposalType(proposalType) activeDelegates(delegatees) { if (block.timestamp > expiry) { revert VotesExpiredSignature(expiry); } @@ -217,7 +217,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes uint8 v, bytes32 r, bytes32 s - ) public virtual validProposalType(proposalType) activeDelegation(delegatee) { + ) public virtual validProposalType(proposalType) activeDelegate(delegatee) { Delegate[] memory _singleDelegate = new Delegate[](1); _singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); delegateBySig(_singleDelegate, proposalType, nonce, expiry, v, r, s); @@ -233,7 +233,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes uint8 v, bytes32 r, bytes32 s - ) public virtual activeDelegation(delegatee) { + ) public virtual activeDelegate(delegatee) { Delegate[] memory _singleDelegate = new Delegate[](1); _singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); @@ -406,17 +406,17 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes } /** - * @dev checks if the delegation is active for the `account` + * @dev checks if the delegation is active for the `delegatee` */ - modifier activeDelegation(address account) { - if (_nonDelegableAddresses[account]) revert VotesDelegationSuspended(account); + modifier activeDelegate(address delegatee) { + if (_nonDelegableAddresses[delegatee]) revert VotesDelegationSuspended(delegatee); _; } /** * @dev checks if the delegation is active for the `delegatees` */ - modifier activeDelegations(Delegate[] memory delegatees) { + modifier activeDelegates(Delegate[] memory delegatees) { for (uint256 i = 0; i < delegatees.length; i++) { if (_nonDelegableAddresses[delegatees[i].account]) revert VotesDelegationSuspended(delegatees[i].account); } diff --git a/solidity/test/integration/Delegation.t.sol b/solidity/test/integration/Delegation.t.sol index 779863d..a9fbc9a 100644 --- a/solidity/test/integration/Delegation.t.sol +++ b/solidity/test/integration/Delegation.t.sol @@ -30,6 +30,96 @@ contract Integration_Delegation is IntegrationBase { } } + function test_AllVotersDelegateAndTransferToProposerWithoutProposerDelegation() public { + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address holder = holders[_i]; + vm.prank(holder); + rabbitToken.delegate(proposer); + } + + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address holder = holders[_i]; + + for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) { + uint8 proposalType = governor.proposalTypes()[_j]; + assertEq(rabbitToken.getVotes(holder, proposalType), 0); + } + } + + for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) { + uint8 proposalType = governor.proposalTypes()[_j]; + assertEq(rabbitToken.getVotes(proposer, proposalType), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER); + } + + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address holder = holders[_i]; + vm.prank(holder); + IERC20(address(rabbitToken)).transfer(proposer, INITIAL_VOTERS_BALANCE); + } + + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address holder = holders[_i]; + + for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) { + uint8 proposalType = governor.proposalTypes()[_j]; + assertEq(rabbitToken.getVotes(holder, proposalType), 0); + } + } + + // Since proposer never delegated himself, and the accounts that delegates proposer has 0 tokens, proposer has now NO votes + for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) { + uint8 proposalType = governor.proposalTypes()[_j]; + assertEq(rabbitToken.getVotes(proposer, proposalType), 0); + } + } + + function test_AllVotersDelegateAndTransferToProposerWithProposerDelegation() public { + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address holder = holders[_i]; + vm.prank(holder); + rabbitToken.delegate(proposer); + } + + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address holder = holders[_i]; + + for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) { + uint8 proposalType = governor.proposalTypes()[_j]; + assertEq(rabbitToken.getVotes(holder, proposalType), 0); + } + } + + for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) { + uint8 proposalType = governor.proposalTypes()[_j]; + assertEq(rabbitToken.getVotes(proposer, proposalType), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER); + } + + // Proposer delegates to himself + vm.prank(proposer); + rabbitToken.delegate(proposer); + + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address holder = holders[_i]; + vm.prank(holder); + IERC20(address(rabbitToken)).transfer(proposer, INITIAL_VOTERS_BALANCE); + } + + for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) { + address holder = holders[_i]; + + for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) { + uint8 proposalType = governor.proposalTypes()[_j]; + assertEq(rabbitToken.getVotes(holder, proposalType), 0); + } + } + + // Since proposer delegated himself, all the token transfers are now voting power of proposer + for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) { + uint8 proposalType = governor.proposalTypes()[_j]; + assertEq(rabbitToken.getVotes(proposer, proposalType), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER); + } + } + function test_AllVotersDelegateByProposalType() public { uint8[] memory proposalTypes = governor.proposalTypes(); @@ -149,4 +239,47 @@ contract Integration_Delegation is IntegrationBase { assertEq(rabbitToken.getVotes(proposer2, _proposalTypes[i]), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER); } } + + function test_DelegationSuspensionDoesNotAffectPreviousVotesDelegation() public { + // Delegates himself befor suspending + vm.prank(proposer); + rabbitToken.delegate(proposer); + + uint8[] memory _proposalTypes = governor.proposalTypes(); + + // Holder 0 delegates to proposer before suspending + vm.prank(holders[0]); + rabbitToken.delegate(proposer); + + for (uint8 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(proposer, _proposalTypes[i]), INITIAL_VOTERS_BALANCE); + } + + // Suspend delegation + vm.prank(proposer); + rabbitToken.suspendDelegation(true); + + // Checks that delegation suspension does not affect previous delegation + for (uint8 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(proposer, _proposalTypes[i]), INITIAL_VOTERS_BALANCE); + } + + // Previous delegation also includes transfers that the holder 0 receives + vm.prank(holders[1]); + IERC20(address(rabbitToken)).transfer(holders[0], INITIAL_VOTERS_BALANCE); + + // Checks that the votes is increased + for (uint8 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(proposer, _proposalTypes[i]), INITIAL_VOTERS_BALANCE * 2); + } + + // Holder 2 delegates to proposer which previously delegated himself before suspending + vm.prank(holders[2]); + IERC20(address(rabbitToken)).transfer(proposer, INITIAL_VOTERS_BALANCE); + + // Checks that the votes of proposer is increased + for (uint8 i = 0; i < _proposalTypes.length; i++) { + assertEq(rabbitToken.getVotes(proposer, _proposalTypes[i]), INITIAL_VOTERS_BALANCE * 3); + } + } } diff --git a/solidity/test/unit/WonderVotes.t.sol b/solidity/test/unit/WonderVotes.t.sol index e1348ba..b0c2a5b 100644 --- a/solidity/test/unit/WonderVotes.t.sol +++ b/solidity/test/unit/WonderVotes.t.sol @@ -1069,4 +1069,19 @@ contract Unit_SuspendDelegation is BaseTest { vm.prank(_account); rabbitToken.suspendDelegation(_suspend); } + + function test_CanDelegateAfterDelegationIsResumed(address _delegator, address _delegate) public { + vm.prank(_delegate); + rabbitToken.suspendDelegation(true); + + vm.expectRevert(abi.encodeWithSelector(IWonderVotes.VotesDelegationSuspended.selector, _delegate)); + vm.prank(_delegator); + rabbitToken.delegate(_delegate); + + vm.prank(_delegate); + rabbitToken.suspendDelegation(false); + + vm.prank(_delegator); + rabbitToken.delegate(_delegate); + } } From 04fe0a3214a7abb19aa7dfcdf10cfb12b4af049d Mon Sep 17 00:00:00 2001 From: 0xRaccoon Date: Tue, 2 Jan 2024 16:17:44 -0300 Subject: [PATCH 19/19] chore: add readme and deploy scripts Signed-off-by: 0xRaccoon --- .env.example | 8 +- README.md | 160 +++++++++--------------------------- package.json | 9 +- solidity/scripts/Deploy.sol | 44 ++++++++-- yarn.lock | 20 +++++ 5 files changed, 109 insertions(+), 132 deletions(-) diff --git a/.env.example b/.env.example index 5b6781a..1730836 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,10 @@ MAINNET_DEPLOYER_PK= GOERLI_RPC= GOERLI_DEPLOYER_PK= -ETHERSCAN_API_KEY= +OPTIMISM_RPC= +OPTIMISM_DEPLOYER_PK= + +LOCAL_RPC= +LOCAL_DEPLOYER_PK= -OPTIMISM_RPC= \ No newline at end of file +ETHERSCAN_API_KEY= diff --git a/README.md b/README.md index 4aeda6c..2c46fa8 100644 --- a/README.md +++ b/README.md @@ -1,146 +1,62 @@ -wonderland banner -
+# Wonderland Governor -
Start your next Solidity project with Foundry in seconds
-
A highly scalable foundation focused on DX and best practices
+⚠️ The code has not been audited yet, tread with caution. -
+## Overview -## Features +Wonderland Governor is a DAO governance solution designed to address the current limitations in delegation within governance protocols. Unlike old systems where users give all their voting power to one delegate, it offers a flexible solution, introducing innovative features to empower users and enhance governance processes in decentralized organizations. -
-
Sample contracts
-
Basic Greeter contract with an external interface.
+Key Features: -
Foundry setup
-
Foundry configuration with multiple custom profiles and remappings.
+Proposal Types: -
Deployment scripts
-
Sample scripts to deploy contracts on both mainnet and testnet.
+Wonderland Governor lets organizations set different types of proposals for specific needs like legal or technical matters. +Every proposal now has a required type, making it clear which category it falls into. -
Sample e2e & unit tests
-
Example tests showcasing mocking, assertions and configuration for mainnet forking. As well it includes everything needed in order to check code coverage.
+Better Delegation: -
Linter
-
Simple and fast solidity linting thanks to forge fmt.
+Users can now spread their voting power across different categories. +Users can choose multiple delegates for each proposal type, assigning a percentage of their voting power to each. +Users can still delegate 100% of their voting power to one person if they prefer. -
Github workflows CI
-
Run all tests and see the coverage as you push your changes.
-
Export your Solidity interfaces and contracts as packages, and publish them to NPM.
-
+Wonderland Governor is founded on the OpenZeppelin contracts and enhances their functionalities to achieve the described features. ## Setup -1. Install Foundry by following the instructions from [their repository](https://github.com/foundry-rs/foundry#installation). -2. Copy the `.env.example` file to `.env` and fill in the variables. -3. Install the dependencies by running: `yarn install`. In case there is an error with the commands, run `foundryup` and try them again. +This project uses [Foundry](https://book.getfoundry.sh/). To build it locally, run: -## Build - -The default way to build the code is suboptimal but fast, you can run it via: - -```bash +```sh +git clone git@github.com:defi-wonderland/wonderland-governooor-poc.git +cd wonderland-governooor-poc +yarn install yarn build ``` -In order to build a more optimized code ([via IR](https://docs.soliditylang.org/en/v0.8.15/ir-breaking-changes.html#solidity-ir-based-codegen-changes)), run: - -```bash -yarn build:optimized -``` - -## Running tests - -Unit tests should be isolated from any externalities, while E2E usually run in a fork of the blockchain. In this boilerplate you will find example of both. - -In order to run both unit and E2E tests, run: - -```bash -yarn test -``` - -In order to just run unit tests, run: - -```bash -yarn test:unit -``` - -In order to run unit tests and run way more fuzzing than usual (5x), run: - -```bash -yarn test:unit:deep -``` - -In order to just run e2e tests, run: - -```bash -yarn test:e2e -``` - -In order to check your current code coverage, run: +### Available Commands -```bash -yarn coverage -``` - -
- -## Deploy & verify - -### Setup - -Configure the `.env` variables. +Make sure to set `OPTIMISM_RPC` environment variable before running end-to-end tests. -### Goerli - -```bash -yarn deploy:goerli -``` - -### Mainnet - -```bash -yarn deploy:mainnet -``` +| Yarn Command | Description | +| ----------------------- | ---------------------------------------------------------- | +| `yarn build` | Compile all contracts. | +| `yarn coverage` | See `forge coverage` report. | +| `yarn deploy:local` | Deploy the contracts to a local fork. | +| `yarn deploy:goerli` | Deploy the contracts to Goerli testnet. | +| `yarn deploy:optimism` | Deploy the contracts to Optimism mainnet. | +| `yarn deploy:mainnet` | Deploy the contracts to Ethereum mainnet. | +| `yarn test` | Run all unit and integration tests. | +| `yarn test:unit` | Run unit tests. | +| `yarn test:integration` | Run integration tests. | +| `yarn test:gas` | Run all unit and integration tests, and make a gas report. | -The deployments are stored in ./broadcast +## Licensing -See the [Foundry Book for available options](https://book.getfoundry.sh/reference/forge/forge-create.html). +The primary license for Wonderland Governor contracts is MIT, see [`LICENSE`](./LICENSE). -## Export And Publish +## Contributors -Export TypeScript interfaces from Solidity contracts and interfaces providing compatibility with TypeChain. Publish the exported packages to NPM. +Wonderland Governor was built with ❤️ by [Wonderland](https://defi.sucks). -To enable this feature, make sure you've set the `NPM_TOKEN` on your org's secrets. Then set the job's conditional to `true`: - -```yaml -solidity-exporter.yml - -jobs: - export: - name: Generate Interfaces And Contracts - # Remove the following line if you wish to export your Solidity contracts and interfaces and publish them to NPM - if: true - ... -``` - -Also, remember to update the `package_name` param to your package name: - -```yaml -solidity-exporter.yml - -- name: Export Solidity - ${{ matrix.export_type }} - uses: defi-wonderland/solidity-exporter-action@1dbf5371c260add4a354e7a8d3467e5d3b9580b8 - with: - # Update package_name with your package name - package_name: "my-cool-project" - ... - - -- name: Publish to NPM - ${{ matrix.export_type }} - # Update `my-cool-project` with your package name - run: cd export/my-cool-project-${{ matrix.export_type }} && npm publish --access public - ... -``` +Wonderland is a team of top Web3 researchers, developers, and operators who believe that the future needs to be open-source, permissionless, and decentralized. -You can take a look at our [solidity-exporter-action](https://github.com/defi-wonderland/solidity-exporter-action) repository more information and usage examples. +[DeFi sucks](https://defi.sucks), but Wonderland is here to make it better. \ No newline at end of file diff --git a/package.json b/package.json index 01ed179..4ecc32f 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "build": "forge build", "build:optimized": "FOUNDRY_PROFILE=optimized forge build", "coverage": "forge coverage --match-contract Unit", - "deploy:goerli": "bash -c 'source .env && forge script DeployGoerli --rpc-url $GOERLI_RPC --broadcast --private-key $GOERLI_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", - "deploy:mainnet": "bash -c 'source .env && forge script DeployMainnet --rpc-url $MAINNET_RPC --broadcast --private-key $MAINNET_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", + "deploy:goerli": "dotenv -- bash -c 'forge script DeployGoerli -vv --slow --broadcast --rpc-url $GOERLI_RPC'", + "deploy:local": "dotenv -- bash -c 'forge script DeployLocal -vv --slow --broadcast --rpc-url $LOCAL_RPC'", + "deploy:mainnet": "dotenv -- bash -c 'forge script DeployMainnet -vv --slow --broadcast --rpc-url $MAINNET_RPC'", + "deploy:optimism": "dotenv -- bash -c 'forge script DeployOptimism -vv --slow --broadcast --rpc-url $OPTIMISM_RPC'", "lint:check": "yarn lint:sol-tests && yarn lint:sol-logic && forge fmt --check", "lint:fix": "sort-package-json && forge fmt && yarn lint:sol-tests --fix && yarn lint:sol-logic --fix", "lint:sol-logic": "solhint -c .solhint.json 'solidity/contracts/**/*.sol' 'solidity/interfaces/**/*.sol'", @@ -23,6 +25,8 @@ "prepare": "husky install", "test": "forge test -vvv", "test:e2e": "forge test --match-contract E2E -vvv", + "test:gas": "forge test --match-contract Integration -vvv --gas-report", + "test:integration": "forge test --match-contract Integration -vvv", "test:unit": "forge test --match-contract Unit -vvv", "test:unit:deep": "FOUNDRY_FUZZ_RUNS=5000 yarn test:unit" }, @@ -39,6 +43,7 @@ "devDependencies": { "@commitlint/cli": "17.0.3", "@commitlint/config-conventional": "17.0.3", + "dotenv-cli": "7.2.1", "ds-test": "github:dapphub/ds-test#e282159", "forge-std": "github:foundry-rs/forge-std#v1.7.3", "husky": ">=8", diff --git a/solidity/scripts/Deploy.sol b/solidity/scripts/Deploy.sol index 21e875f..574a1e8 100644 --- a/solidity/scripts/Deploy.sol +++ b/solidity/scripts/Deploy.sol @@ -1,24 +1,56 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {Script} from 'forge-std/Script.sol'; +import {IWonderGovernor} from 'interfaces/governance/IWonderGovernor.sol'; +import {IWonderVotes} from 'interfaces/governance/utils/IWonderVotes.sol'; +import {AliceGovernor} from 'examples/AliceGovernor.sol'; +import {RabbitToken} from 'examples/RabbitToken.sol'; + +import {Script, console} from 'forge-std/Script.sol'; abstract contract Deploy is Script { - function _deploy() internal { - vm.startBroadcast(); - // Deploy the contract + function _deploy(address _deployer) internal { + vm.startBroadcast(_deployer); + + // Deploy the contracts + address tokenAddress = vm.computeCreateAddress(_deployer, vm.getNonce(_deployer) + 1); + AliceGovernor governor = new AliceGovernor(tokenAddress); + console.log('WonderGovernor:', address(governor)); + RabbitToken rabbitToken = new RabbitToken(AliceGovernor(payable(address(governor)))); + console.log('WonderVotes:', address(rabbitToken)); + vm.stopBroadcast(); } } contract DeployMainnet is Deploy { function run() external { - _deploy(); + address _deployer = vm.rememberKey(vm.envUint('MAINNET_DEPLOYER_PK')); + + _deploy(_deployer); } } contract DeployGoerli is Deploy { function run() external { - _deploy(); + address _deployer = vm.rememberKey(vm.envUint('GOERLI_DEPLOYER_PK')); + + _deploy(_deployer); + } +} + +contract DeployLocal is Deploy { + function run() external { + address _deployer = vm.rememberKey(vm.envUint('LOCAL_DEPLOYER_PK')); + + _deploy(_deployer); + } +} + +contract DeployOptimism is Deploy { + function run() external { + address _deployer = vm.rememberKey(vm.envUint('OPTIMISM_DEPLOYER_PK')); + + _deploy(_deployer); } } diff --git a/yarn.lock b/yarn.lock index 6d6371a..fafabe9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -801,6 +801,26 @@ dot-prop@^5.1.0: dependencies: is-obj "^2.0.0" +dotenv-cli@7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-7.2.1.tgz#e595afd9ebfb721df9da809a435b9aa966c92062" + integrity sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ== + dependencies: + cross-spawn "^7.0.3" + dotenv "^16.0.0" + dotenv-expand "^10.0.0" + minimist "^1.2.6" + +dotenv-expand@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== + +dotenv@^16.0.0: + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + "ds-test@github:dapphub/ds-test#e282159": version "1.0.0" resolved "https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0"