pragma solidity 0.8.20; import {Test} from "forge-std/Test.sol"; import "@openzeppelin-contracts/token/ERC20/extensions/ERC20Votes.sol"; import "@openzeppelin-contracts/utils/structs/Checkpoints.sol"; abstract contract ERC20VotesTest is Test { ERC20Votes tokenVotes; uint256 amountVotes; address public aliceVotes; address public bobVotes; address constant public charlieVotes = 0x0000000000000000000000000000000000000069; address constant public eveVotes = 0x0000000000000000000000000000000000001337; event DelegateChanged( address indexed delegator, address indexed fromDelegate, address indexed toDelegate ); event DelegateVotesChanged( address indexed delegate, uint256 previousBalance, uint256 newBalance ); function initializeVotes( address alice, address bob, address token, uint256 amount ) public { aliceVotes = alice; bobVotes = bob; amountVotes = amount; tokenVotes = ERC20Votes(token); } function test_votes_delegate() external { // case 1: first delegation without balance assertEq(tokenVotes.delegates(address(this)), address(0)); vm.expectEmit(true, true, true, false, address(tokenVotes)); emit DelegateChanged(address(this), address(0), aliceVotes); tokenVotes.delegate(aliceVotes); assertEq(tokenVotes.delegates(address(this)), aliceVotes); // no votes in aliceVotes assertEq(tokenVotes.getVotes(aliceVotes), 0); // no Checkpoint generated assertEq(tokenVotes.numCheckpoints(aliceVotes), 0); // case 2: first delegate with balance _mintVotesTokens(aliceVotes, amountVotes); assertEq(tokenVotes.delegates(aliceVotes), address(0)); vm.expectEmit(true, true, true, false, address(tokenVotes)); emit DelegateChanged(aliceVotes, address(0), bobVotes); vm.expectEmit(true, false, false, true, address(tokenVotes)); emit DelegateVotesChanged(bobVotes, 0, amountVotes); vm.prank(aliceVotes); tokenVotes.delegate(bobVotes); assertEq(tokenVotes.delegates(aliceVotes), bobVotes); // amountVotes votes in bobVotes assertEq(tokenVotes.getVotes(bobVotes), amountVotes); // 1 Checkpoint generated assertEq(tokenVotes.numCheckpoints(bobVotes), 1); Checkpoints.Checkpoint208 memory ckpt = tokenVotes.checkpoints(bobVotes, 0); assertEq(ckpt._key, 1); assertEq(ckpt._value, amountVotes); // case 3: delegate with balance not first vm.roll(2); vm.expectEmit(true, true, true, false, address(tokenVotes)); emit DelegateChanged(aliceVotes, bobVotes, charlieVotes); vm.expectEmit(true, false, false, true, address(tokenVotes)); emit DelegateVotesChanged(bobVotes, amountVotes, 0); vm.expectEmit(true, false, false, true, address(tokenVotes)); emit DelegateVotesChanged(charlieVotes, 0, amountVotes); vm.prank(aliceVotes); tokenVotes.delegate(charlieVotes); assertEq(tokenVotes.delegates(aliceVotes), charlieVotes); // amountVotes votes in charlieVotes assertEq(tokenVotes.getVotes(charlieVotes), amountVotes); // 1 Checkpoint generated assertEq(tokenVotes.numCheckpoints(charlieVotes), 1); ckpt = tokenVotes.checkpoints(charlieVotes, 0); assertEq(ckpt._key, 2); assertEq(ckpt._value, amountVotes); // 0 votes in bobVotes assertEq(tokenVotes.getVotes(bobVotes), 0); // 1 Checkpoint generated assertEq(tokenVotes.numCheckpoints(bobVotes), 2); ckpt = tokenVotes.checkpoints(bobVotes, 1); assertEq(ckpt._key, 2); assertEq(ckpt._value, 0); } function test_votes_mintAndBurnAndMaxSupply() external { // test for {_mint} // case 1: receiver has no delegatee assertEq(tokenVotes.totalSupply(), 0); _mintVotesTokens(address(this), 1); assertEq(tokenVotes.totalSupply(), 1); assertEq(tokenVotes.balanceOf(address(this)), 1); // revert if total supply exceeds the ceiling vm.expectRevert(); _mintVotesTokens(aliceVotes, type(uint224).max + 1); // case 2: receiver has a delegatee vm.prank(aliceVotes); tokenVotes.delegate(address(this)); assertEq(tokenVotes.getVotes(address(this)), 0); _mintVotesTokens(aliceVotes, 2); assertEq(tokenVotes.totalSupply(), 1 + 2); assertEq(tokenVotes.balanceOf(aliceVotes), 0 + 2); // delegatee's votes increased assertEq(tokenVotes.getVotes(address(this)), 0 + 2); // revert if total supply exceeds the ceiling (happens in {_afterTokenTransfer}) vm.expectRevert(); _mintVotesTokens(aliceVotes, type(uint224).max); // test for {_burn} // case 3: receiver has no delegatee _burnVotesTokens(address(this), 1); assertEq(tokenVotes.totalSupply(), 3 - 1); // case 4: receiver has a delegatee _burnVotesTokens(aliceVotes, 1); assertEq(tokenVotes.totalSupply(), 2 - 1); // delegatee's votes decreased assertEq(tokenVotes.getVotes(address(this)), 2 - 1); } function test_votes_afterTokenTransfer() external { _mintVotesTokens(address(this), 100); tokenVotes.delegate(aliceVotes); assertEq(tokenVotes.delegates(address(this)), aliceVotes); assertEq(tokenVotes.numCheckpoints(aliceVotes), 1); // test for {transfer} // case 1: 'to' has no delegatee vm.roll(2); vm.expectEmit(true, false, false, true, address(tokenVotes)); emit DelegateVotesChanged(aliceVotes, 100, 100 - 1); vm.prank(address(this)); tokenVotes.transfer(bobVotes, 1); assertEq(tokenVotes.getVotes(aliceVotes), 100 - 1); // case 2: 'to' has a delegatee _mintVotesTokens(charlieVotes, 100); vm.prank(charlieVotes); tokenVotes.delegate(eveVotes); vm.roll(3); vm.expectEmit(true, false, false, true, address(tokenVotes)); emit DelegateVotesChanged(aliceVotes, 99, 99 - 1); vm.expectEmit(true, false, false, true, address(tokenVotes)); emit DelegateVotesChanged(eveVotes, 100, 100 + 1); tokenVotes.transfer(charlieVotes, 1); assertEq(tokenVotes.getVotes(aliceVotes), 99 - 1); assertEq(tokenVotes.getVotes(eveVotes), 100 + 1); // test for {transferFrom} // case 3: 'to' has no delegatee vm.roll(4); assertEq(tokenVotes.delegates(bobVotes), address(0)); tokenVotes.approve(aliceVotes, 100); vm.startPrank(aliceVotes); vm.expectEmit(true, false, false, true, address(tokenVotes)); emit DelegateVotesChanged(aliceVotes, 98, 98 - 1); tokenVotes.transferFrom(address(this), bobVotes, 1); // case 4: 'to' has a delegatee vm.roll(5); assertEq(tokenVotes.delegates(charlieVotes), eveVotes); vm.expectEmit(true, false, false, true, address(tokenVotes)); emit DelegateVotesChanged(aliceVotes, 97, 97 - 1); vm.expectEmit(true, false, false, true, address(tokenVotes)); emit DelegateVotesChanged(eveVotes, 101, 101 + 1); tokenVotes.transferFrom(address(this), charlieVotes, 1); } function test_votes_getPastVotesAndGetPastTotalSupply() external { // 6 Checkpoints of aliceVotes: // block votes index // 2 10 0 // 3 15 1 // 6 19 2 // 10 20 3 // 11 23 4 // 13 31 5 // // 6 Checkpoints of total supply: // block total supply index // 2 10 0 // 3 15 1 // 6 19 2 // 10 20 3 // 11 23 4 // 13 31 5 tokenVotes.delegate(aliceVotes); vm.roll(2); _mintVotesTokens(address(this), 10); vm.roll(3); _mintVotesTokens(address(this), 15 - 10); vm.roll(6); _mintVotesTokens(address(this), 19 - 15); vm.roll(10); _mintVotesTokens(address(this), 20 - 19); vm.roll(11); _mintVotesTokens(address(this), 23 - 20); vm.roll(13); _mintVotesTokens(address(this), 31 - 23); vm.roll(20); // check {getPastVotes} && {getPastTotalSupply} assertEq(tokenVotes.numCheckpoints(aliceVotes), 6); assertEq(tokenVotes.getPastVotes(aliceVotes, 1), 0); assertEq(tokenVotes.getPastTotalSupply(1), 0); assertEq(tokenVotes.getPastVotes(aliceVotes, 2), 10); assertEq(tokenVotes.getPastTotalSupply(2), 10); assertEq(tokenVotes.getPastVotes(aliceVotes, 4), 15); assertEq(tokenVotes.getPastTotalSupply(4), 15); assertEq(tokenVotes.getPastVotes(aliceVotes, 6), 19); assertEq(tokenVotes.getPastTotalSupply(6), 19); assertEq(tokenVotes.getPastVotes(aliceVotes, 9), 19); assertEq(tokenVotes.getPastTotalSupply(9), 19); assertEq(tokenVotes.getPastVotes(aliceVotes, 10), 20); assertEq(tokenVotes.getPastTotalSupply(10), 20); assertEq(tokenVotes.getPastVotes(aliceVotes, 12), 23); assertEq(tokenVotes.getPastTotalSupply(12), 23); assertEq(tokenVotes.getPastVotes(aliceVotes, 13), 31); assertEq(tokenVotes.getPastTotalSupply(13), 31); assertEq(tokenVotes.getPastVotes(aliceVotes, 19), 31); assertEq(tokenVotes.getPastTotalSupply(19), 31); // revert if block not mined vm.expectRevert(); tokenVotes.getPastVotes(aliceVotes, 9999); vm.expectRevert(); tokenVotes.getPastTotalSupply(9999); } function _mintVotesTokens(address who, uint256 value) internal virtual; function _burnVotesTokens(address who, uint256 amount) internal virtual; }