270 lines
10 KiB
Solidity
270 lines
10 KiB
Solidity
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;
|
|
}
|