diff --git a/.gas-snapshot b/.gas-snapshot index 355014a..c5659db 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -320,6 +320,26 @@ ERC721PermitTest:testFailPermitReplay(uint256,address,uint256) (runs: 256, μ: 9 ERC721PermitTest:testPermit() (gas: 110677) ERC721PermitTest:testPermit(uint248,address,uint256) (runs: 256, μ: 111909, ~: 111909) ERC721PermitTest:testPermitAll() (gas: 110672) +ERC721VotesTest:testFailDelegation_WithSigBadNonce() (gas: 130065) +ERC721VotesTest:testFailDelegation_WithSigExpired() (gas: 104436) +ERC721VotesTest:testFailGetPastTotalSupply_RevertOnCurrentBlockAndGreater() (gas: 5497) +ERC721VotesTest:testFailGetPastVotes_RevertOnCurrentBlockAndGreater() (gas: 7663) +ERC721VotesTest:testFailSetDelegationWithSig_Replay() (gas: 207900) +ERC721VotesTest:testGetPastTotalSupply_ReturnsLatestBlockOnCurrentBlockOrGreater() (gas: 105819) +ERC721VotesTest:testGetPastTotalSupply_ReturnsZeroIfLessThanFirstCheckpointBlock() (gas: 106027) +ERC721VotesTest:testGetPastTotalSupply_ReturnsZeroWhenNoExistingCheckpoints() (gas: 10793) +ERC721VotesTest:testGetPastVotes_ReturnsLastestBlockIfInputIsGreaterThanCurrentBlock() (gas: 178848) +ERC721VotesTest:testGetPastVotes_ReturnsZeroIfInputIsLessThanFirstCheckpointBlock() (gas: 179039) +ERC721VotesTest:testGetPastVotes_ReturnsZeroWithoutExistingCheckpoints() (gas: 13058) +ERC721VotesTest:testNumCheckpoints() (gas: 367742) +ERC721VotesTest:testSetDelegation() (gas: 185751) +ERC721VotesTest:testSetDelegationWithSig() (gas: 209458) +ERC721VotesTest:testSetDelegation_WithExistingDelegation() (gas: 265141) +ERC721VotesTest:testSetDelegation_WithoutBalance() (gas: 39739) +ERC721VotesTest:testTransfer_WithExistingReceiverDelegation() (gas: 197518) +ERC721VotesTest:testTransfer_WithExistingSelfDelegation() (gas: 197600) +ERC721VotesTest:testTransfer_WithFullDelegation() (gas: 244105) +ERC721VotesTest:testTransfer_WithoutExistingDelegation() (gas: 124971) FixedPointMathLibTest:testAbs() (gas: 554) FixedPointMathLibTest:testAbsEdgeCases() (gas: 362) FixedPointMathLibTest:testAvg() (gas: 397) diff --git a/src/tokens/ERC20/extensions/ERC20Votes.sol b/src/tokens/ERC20/extensions/ERC20Votes.sol index 87b8472..b4cf5c6 100644 --- a/src/tokens/ERC20/extensions/ERC20Votes.sol +++ b/src/tokens/ERC20/extensions/ERC20Votes.sol @@ -94,7 +94,7 @@ abstract contract ERC20Votes is ERC20Permit { /// @dev Lookup a value in a list of (sorted) checkpoints. function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) internal view returns (uint256) { uint256 length = ckpts.length; - uint256 low = 0; + uint256 low; uint256 high = length; if (length > 5) { @@ -229,7 +229,7 @@ abstract contract ERC20Votes is ERC20Permit { Checkpoint[] storage ckpts, function(uint256, uint256) view returns (uint256) op, uint256 delta - ) private returns (uint256 oldWeight, uint256 newWeight) { + ) internal virtual returns (uint256 oldWeight, uint256 newWeight) { uint256 pos = ckpts.length; // Cannot underflow as value is confirmed as positive before arithmetic. diff --git a/src/tokens/ERC721/extensions/ERC721Votes.sol b/src/tokens/ERC721/extensions/ERC721Votes.sol new file mode 100644 index 0000000..b74d975 --- /dev/null +++ b/src/tokens/ERC721/extensions/ERC721Votes.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ERC721Permit} from "./ERC721Permit.sol"; +import {SafeCastLib} from "../../../utils/SafeCastLib.sol"; +import {FixedPointMathLib} from "../../../utils/FixedPointMathLib.sol"; + +struct Checkpoint { + uint32 fromBlock; + uint224 votes; +} + +/// @notice ERC721-compatible voting and delegation implementation. +/// @author SolDAO (https://github.com/Sol-DAO/solbase/blob/main/src/tokens/ERC721/extensions/ERC721Votes.sol) +abstract contract ERC721Votes is ERC721Permit { + /// ----------------------------------------------------------------------- + /// ERC721Votes Events + /// ----------------------------------------------------------------------- + + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + /// ----------------------------------------------------------------------- + /// Custom Errors + /// ----------------------------------------------------------------------- + + error Undetermined(); + + error DelegationSigExpired(); + + error SupplyMaxed(); + + /// ----------------------------------------------------------------------- + /// ERC721Votes Constants + /// ----------------------------------------------------------------------- + + bytes32 public constant DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + /// ----------------------------------------------------------------------- + /// ERC721Votes Storage + /// ----------------------------------------------------------------------- + + mapping(address => address) public delegates; + + mapping(address => uint256) public delegateNonces; + + mapping(address => Checkpoint[]) public checkpoints; + + Checkpoint[] public totalSupplyCheckpoints; + + /// ----------------------------------------------------------------------- + /// Constructor + /// ----------------------------------------------------------------------- + + constructor(string memory _name, string memory _symbol) ERC721Permit(_name, _symbol) {} + + /// ----------------------------------------------------------------------- + /// ERC721Votes Logic + /// ----------------------------------------------------------------------- + + /// @dev Gets the total number of checkpoints for `account`. + function numCheckpoints(address account) public view virtual returns (uint256) { + return checkpoints[account].length; + } + + /// @dev Gets the current votes balance for `account`. + function getVotes(address account) public view virtual returns (uint256) { + uint256 pos = checkpoints[account].length; + + // Cannot underflow as value is confirmed as positive before arithmetic. + unchecked { + return pos == 0 ? 0 : checkpoints[account][pos - 1].votes; + } + } + + /// @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + function getPastVotes(address account, uint256 blockNumber) public view virtual returns (uint256) { + if (block.number <= blockNumber) revert Undetermined(); + + return _checkpointsLookup(checkpoints[account], blockNumber); + } + + /// @dev Retrieve the `totalSupply` at the end of `blockNumber`. + function getPastTotalSupply(uint256 blockNumber) public view virtual returns (uint256) { + if (block.number <= blockNumber) revert Undetermined(); + + return _checkpointsLookup(totalSupplyCheckpoints, blockNumber); + } + + /// @dev Lookup a value in a list of (sorted) checkpoints. + function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) internal view returns (uint256) { + uint256 length = ckpts.length; + uint256 low; + uint256 high = length; + + if (length > 5) { + uint256 mid = length - FixedPointMathLib.sqrt(length); + + if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + while (low < high) { + uint256 mid = ((low & high) + (low ^ high)) >> 1; + + if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + // Cannot underflow as value is confirmed as positive before arithmetic. + unchecked { + return high == 0 ? 0 : _unsafeAccess(ckpts, high - 1).votes; + } + } + + /// @dev Delegate votes from the sender to `delegatee`. + function delegate(address delegatee) public virtual { + _delegate(msg.sender, delegatee); + } + + /// @dev Delegates votes from signer to `delegatee` + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + if (block.timestamp > deadline) revert DelegationSigExpired(); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + address recoveredAddress = ecrecover( + computeDigest(keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, deadline))), + v, + r, + s + ); + + if (recoveredAddress == address(0)) revert InvalidSigner(); + + if (nonce != delegateNonces[recoveredAddress]++) revert InvalidSigner(); + + _delegate(recoveredAddress, delegatee); + } + } + + /// @dev Snapshots the totalSupply after it has been increased. + function _mint(address to, uint256 id) internal virtual override { + super._mint(to, id); + + _writeCheckpoint(totalSupplyCheckpoints, _add, 1); + } + + /// @dev Snapshots the totalSupply after it has been decreased. + function _burn(uint256 id) internal virtual override { + super._burn(id); + + _writeCheckpoint(totalSupplyCheckpoints, _subtract, 1); + } + + /// @dev Performs ERC721 transferFrom with delegation tracking. + function transferFrom( + address from, + address to, + uint256 id + ) public virtual override { + _moveVotingPower(delegates[from], delegates[to], 1); + + super.transferFrom(from, to, id); + } + + /// @dev Performs ERC721 safeTransferFrom with delegation tracking. + function safeTransferFrom( + address from, + address to, + uint256 id + ) public virtual override { + _moveVotingPower(delegates[from], delegates[to], 1); + + super.safeTransferFrom(from, to, id); + } + + /// @dev Performs ERC721 safeTransferFrom (data) with delegation tracking. + function safeTransferFrom( + address from, + address to, + uint256 id, + bytes calldata data + ) public virtual override { + _moveVotingPower(delegates[from], delegates[to], 1); + + super.safeTransferFrom(from, to, id, data); + } + + /// @dev Change delegation for `delegator` to `delegatee`. + function _delegate(address delegator, address delegatee) internal virtual { + address currentDelegate = delegates[delegator]; + + delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveVotingPower(currentDelegate, delegatee, balanceOf(delegator)); + } + + function _moveVotingPower( + address src, + address dst, + uint256 amount + ) internal virtual { + if (src != dst && amount != 0) { + if (src != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(checkpoints[src], _subtract, amount); + + emit DelegateVotesChanged(src, oldWeight, newWeight); + } + + if (dst != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(checkpoints[dst], _add, amount); + + emit DelegateVotesChanged(dst, oldWeight, newWeight); + } + } + } + + function _writeCheckpoint( + Checkpoint[] storage ckpts, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) internal virtual returns (uint256 oldWeight, uint256 newWeight) { + uint256 pos = ckpts.length; + + // Cannot underflow as value is confirmed as positive before arithmetic. + unchecked { + Checkpoint memory oldCkpt = pos == 0 ? Checkpoint(0, 0) : _unsafeAccess(ckpts, pos - 1); + + oldWeight = oldCkpt.votes; + newWeight = op(oldWeight, delta); + + if (pos != 0 && oldCkpt.fromBlock == block.number) { + _unsafeAccess(ckpts, pos - 1).votes = SafeCastLib.safeCastTo224(newWeight); + } else { + ckpts.push( + Checkpoint({ + fromBlock: SafeCastLib.safeCastTo32(block.number), + votes: SafeCastLib.safeCastTo224(newWeight) + }) + ); + } + } + } + + function _add(uint256 a, uint256 b) internal pure virtual returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) internal pure virtual returns (uint256) { + return a - b; + } + + function _unsafeAccess(Checkpoint[] storage ckpts, uint256 pos) + internal + pure + virtual + returns (Checkpoint storage result) + { + assembly { + mstore(0, ckpts.slot) + result.slot := add(keccak256(0, 0x20), pos) + } + } +} diff --git a/test/ERC20Votes.t.sol b/test/ERC20Votes.t.sol index 2be6063..d4f911f 100644 --- a/test/ERC20Votes.t.sol +++ b/test/ERC20Votes.t.sol @@ -420,7 +420,7 @@ contract ERC20VotesTest is DSTestPlus { assertEq(votes, 100); }*/ - function testFailGetPastVotes_RevertOnCurrentBlockAndGreater() public { + function testFailGetPastVotes_RevertOnCurrentBlockAndGreater() public view { token.getPastVotes(holder, block.number + 1); } @@ -505,7 +505,7 @@ contract ERC20VotesTest is DSTestPlus { assertEq(token.getPastVotes(to, t4 + 1), supply); } - function testFailGetPastTotalSupply_RevertOnCurrentBlockAndGreater() public { + function testFailGetPastTotalSupply_RevertOnCurrentBlockAndGreater() public view { token.getPastTotalSupply(block.number + 1); } diff --git a/test/ERC721Votes.t.sol b/test/ERC721Votes.t.sol new file mode 100644 index 0000000..dff97dd --- /dev/null +++ b/test/ERC721Votes.t.sol @@ -0,0 +1,571 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; +import {DSInvariantTest} from "./utils/DSInvariantTest.sol"; +import {MockERC721Votes} from "./utils/mocks/MockERC721Votes.sol"; + +contract ERC721VotesTest is DSTestPlus { + bytes32 public constant DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + MockERC721Votes token; + + address holder = address(0xCAFE); + address holderDelegate = address(0xC0de); + uint256 id = 1; + + function setUp() public { + token = new MockERC721Votes("Token", "TKN"); + } + + function testSetDelegation() public { + hevm.roll(420); + + token.mint(holder, id); + + assertEq(token.delegates(holder), address(0)); + + hevm.prank(holder); + token.delegate(holder); + + assertEq(token.delegates(holder), holder); + assertEq(token.getVotes(holder), id); + assertEq(token.getPastVotes(holder, block.number - 1), 0); + + hevm.roll(block.number + 1); + + assertEq(token.getPastVotes(holder, block.number - 1), 1); + } + + function testSetDelegation_WithoutBalance() public { + assertEq(token.delegates(holder), address(0)); + + hevm.prank(holder); + token.delegate(holder); + + assertEq(token.delegates(holder), holder); + } + + function testSetDelegation_WithExistingDelegation() public { + hevm.roll(420); + + address delegatee = address(0xD3136473); + + token.mint(holder, 1); + + hevm.prank(holder); + token.delegate(holder); + + hevm.roll(block.number + 1); + + assertEq(token.getPastVotes(holder, block.number - 1), 1); + assertEq(token.getPastVotes(delegatee, block.number - 1), 0); + assertEq(token.delegates(holder), holder); + + hevm.prank(holder); + token.delegate(delegatee); + + assertEq(token.delegates(holder), delegatee); + assertEq(token.getVotes(holder), 0); + assertEq(token.getVotes(delegatee), 1); + + hevm.roll(block.number + 1); + + assertEq(token.getPastVotes(holder, block.number - 1), 0); + assertEq(token.getPastVotes(delegatee, block.number - 1), 1); + } + + function testSetDelegationWithSig() public { + hevm.roll(420); + + uint256 privateKey = uint256(0xB0b); + address owner = hevm.addr(privateKey); + uint256 nonce = 0; + uint256 expiry = type(uint256).max; + + token.mint(owner, 1); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.DELEGATION_TYPEHASH(), owner, nonce, expiry)) + ) + ) + ); + + assertEq(token.delegates(owner), address(0)); + + token.delegateBySig(owner, nonce, expiry, v, r, s); + + assertEq(token.delegates(owner), owner); + assertEq(token.getVotes(owner), 1); + assertEq(token.getPastVotes(owner, block.number - 1), 0); + + hevm.roll(block.number + 1); + + assertEq(token.getPastVotes(owner, block.number - 1), 1); + } + + function testFailSetDelegationWithSig_Replay() public { + hevm.roll(420); + + uint256 privateKey = uint256(0xB0b); + address owner = hevm.addr(privateKey); + uint256 nonce = 0; + uint256 expiry = type(uint256).max; + + token.mint(owner, 1); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.DELEGATION_TYPEHASH(), owner, nonce, expiry)) + ) + ) + ); + + assertEq(token.delegates(owner), address(0)); + + token.delegateBySig(owner, nonce, expiry, v, r, s); + token.delegateBySig(owner, nonce, expiry, v, r, s); + } + + /*function testFailDelegationWithSig_BadDelegate() public { + // it('rejects bad delegatee', async function () { + // const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + // delegator.getPrivateKey(), + // buildData(this.chainId, this.token.address, { + // delegatee: delegatorAddress, + // nonce, + // expiry: MAX_UINT256, + // }), + // )); + + // const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); + // const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); + // expect(args.delegator).to.not.be.equal(delegatorAddress); + // expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + // expect(args.toDelegate).to.be.equal(holderDelegatee); + + hevm.roll(420); + + uint256 privateKey = uint256(0xB0b); + address owner = hevm.addr(privateKey); + uint256 nonce = 0; + uint256 expiry = type(uint256).max; + + token.mint(owner, supply); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.DELEGATION_TYPEHASH(), owner, nonce, expiry)) + ) + ) + ); + + // NOTE: this doesn't fail by itself, kinda sketchy + token.delegateBySig(address(0xBAD), nonce, expiry, v, r, s); + }*/ + + function testFailDelegation_WithSigBadNonce() public { + hevm.roll(420); + + uint256 privateKey = uint256(0xB0b); + address owner = hevm.addr(privateKey); + uint256 nonce = 1; // bad nonce + uint256 expiry = type(uint256).max; + + token.mint(owner, 1); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.DELEGATION_TYPEHASH(), owner, nonce, expiry)) + ) + ) + ); + + token.delegateBySig(address(0xBAD), nonce, expiry, v, r, s); + } + + function testFailDelegation_WithSigExpired() public { + hevm.roll(420); + hevm.warp(420); + + uint256 privateKey = uint256(0xB0b); + address owner = hevm.addr(privateKey); + uint256 nonce = 0; + uint256 expiry = block.timestamp - 1; // bad expiry + + token.mint(owner, 1); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.DELEGATION_TYPEHASH(), owner, nonce, expiry)) + ) + ) + ); + + token.delegateBySig(address(0xBAD), nonce, expiry, v, r, s); + } + + function testTransfer_WithoutExistingDelegation() public { + address to = address(0xc0de); + + token.mint(holder, 1); + + hevm.prank(holder); + token.transferFrom(holder, to, 1); + + assertEq(token.getVotes(holder), 0); + assertEq(token.getVotes(to), 0); + + hevm.roll(block.number + 1); + + assertEq(token.getPastVotes(holder, block.number - 1), 0); + assertEq(token.getPastVotes(to, block.number - 1), 0); + } + + function testTransfer_WithExistingSelfDelegation() public { + address to = address(0xc0de); + + token.mint(holder, 1); + + hevm.startPrank(holder); + + token.delegate(holder); + token.transferFrom(holder, to, 1); + + hevm.stopPrank(); + + assertEq(token.getVotes(holder), 0); + assertEq(token.getVotes(to), 0); + + hevm.roll(block.number + 1); + + assertEq(token.getPastVotes(holder, block.number - 1), 0); + assertEq(token.getPastVotes(to, block.number - 1), 0); + } + + function testTransfer_WithExistingReceiverDelegation() public { + address to = address(0xc0de); + + token.mint(holder, 1); + + hevm.startPrank(holder); + + token.delegate(to); + token.transferFrom(holder, to, 1); + + hevm.stopPrank(); + + assertEq(token.getVotes(holder), 0); + assertEq(token.getVotes(to), 0); + + hevm.roll(block.number + 1); + + assertEq(token.getPastVotes(holder, block.number - 1), 0); + assertEq(token.getPastVotes(to, block.number - 1), 0); + } + + function testTransfer_WithFullDelegation() public { + address to = address(0xc0de); + + token.mint(holder, 1); + + hevm.startPrank(holder); + + token.delegate(holder); + token.delegate(to); + token.transferFrom(holder, to, 1); + + hevm.stopPrank(); + + assertEq(token.getVotes(holder), 0); + //assertEq(token.getVotes(to), 0); + + hevm.roll(block.number + 1); + + //assertEq(token.getPastVotes(holder, block.number - 1), 0); + //assertEq(token.getPastVotes(to, block.number - 1), 1); + } + + /// ----------------------------------------------------------------------- + /// Compound Tests + /// ----------------------------------------------------------------------- + + function testNumCheckpoints() public { + address to = address(0xc0de); + address otherTo = address(0xb0b); + + token.mint(holder, 0); + token.mint(holder, 1); + token.mint(to, 2); + + hevm.prank(holder); + token.transferFrom(holder, to, 0); + assertEq(token.numCheckpoints(to), 0); + + hevm.roll(block.number + 1); + uint256 t1 = block.number; + + hevm.prank(to); + token.delegate(otherTo); + assertEq(token.numCheckpoints(otherTo), 1); + + hevm.roll(block.number + 1); + uint256 t2 = block.number; + + hevm.prank(to); + token.transferFrom(to, otherTo, 0); + assertEq(token.numCheckpoints(otherTo), 2); + + hevm.roll(block.number + 1); + uint256 t3 = block.number; + + hevm.prank(to); + token.transferFrom(to, otherTo, 2); + assertEq(token.numCheckpoints(otherTo), 3); + + hevm.roll(block.number + 1); + uint256 t4 = block.number; + + hevm.prank(holder); + token.transferFrom(holder, to, 1); + assertEq(token.numCheckpoints(otherTo), 4); + /* + (uint256 fromBlock, uint256 votes) = token.checkpoints(otherTo, 0); + assertEq(fromBlock, t1); + assertEq(votes, 1); + + (fromBlock, votes) = token.checkpoints(otherTo, 1); + assertEq(fromBlock, t2); + assertEq(votes, 0); + + (fromBlock, votes) = token.checkpoints(otherTo, 2); + assertEq(fromBlock, t3); + assertEq(votes, 0); + + (fromBlock, votes) = token.checkpoints(otherTo, 3); + assertEq(fromBlock, t4); + assertEq(votes, 1); + + hevm.roll(block.number + 1); + + assertEq(token.getPastVotes(otherTo, t1), 1); + assertEq(token.getPastVotes(otherTo, t2), 0); + assertEq(token.getPastVotes(otherTo, t3), 0); + assertEq(token.getPastVotes(otherTo, t4), 1);*/ + } + + /*function testNumCheckpoints_OnlySingleCheckpointPerBlock() public { + address to = address(0xc0de); + address otherTo = address(0xb0b); + + token.mint(holder, supply); + + hevm.startPrank(holder); + + token.delegate(holder); + token.transfer(to, 100); + + hevm.stopPrank(); + + assertEq(token.numCheckpoints(otherTo), 0); + + hevm.startPrank(to); + + token.delegate(otherTo); + token.transfer(otherTo, 10); + token.transfer(otherTo, 10); + + hevm.stopPrank(); + + assertEq(token.numCheckpoints(otherTo), 1); + + (uint256 fromBlock, uint256 votes) = token.checkpoints(otherTo, 0); + assertEq(fromBlock, 0); + assertEq(votes, 80); + + hevm.roll(block.number + 1); + + hevm.prank(holder); + token.transfer(to, 20); + + assertEq(token.numCheckpoints(otherTo), 2); + + (fromBlock, votes) = token.checkpoints(otherTo, 1); + assertEq(fromBlock, 1); + assertEq(votes, 100); + }*/ + + function testFailGetPastVotes_RevertOnCurrentBlockAndGreater() public view { + token.getPastVotes(holder, block.number + 1); + } + + function testGetPastVotes_ReturnsZeroWithoutExistingCheckpoints() public { + hevm.roll(block.number + 1); + assertEq(token.getPastVotes(holder, 0), 0); + } + + function testGetPastVotes_ReturnsLastestBlockIfInputIsGreaterThanCurrentBlock() public { + address to = address(0xc0de); + + token.mint(holder, 1); + + hevm.prank(holder); + token.delegate(to); + + hevm.roll(block.number + 2); + + assertEq(token.getPastVotes(to, block.number - 1), 1); + assertEq(token.getPastVotes(to, block.number - 2), 1); + } + + function testGetPastVotes_ReturnsZeroIfInputIsLessThanFirstCheckpointBlock() public { + address to = address(0xc0de); + + hevm.roll(block.number + 1); + + token.mint(holder, 1); + + hevm.prank(holder); + token.delegate(to); + + hevm.roll(block.number + 2); + + assertEq(token.getPastVotes(to, block.number - 3), 0); + assertEq(token.getPastVotes(to, block.number - 1), 1); + } + + /* + function testGetPastVotes_ReturnsCorrectVotingBalancePerCheckpoint() public { + address to = address(0xc0de); + + hevm.roll(block.number + 1); + uint256 t1 = block.number; + + token.mint(holder, 1); + + hevm.prank(holder); + token.delegate(to); + + hevm.roll(block.number + 2); + uint256 t2 = block.number; + + hevm.prank(holder); + token.transfer(to, 10); + + hevm.roll(block.number + 2); + uint256 t3 = block.number; + + hevm.prank(holder); + token.transfer(to, 10); + + hevm.roll(block.number + 2); + uint256 t4 = block.number; + + hevm.prank(to); + token.transfer(holder, 20); + + hevm.roll(block.number + 2); + + assertEq(token.getPastVotes(to, t1 - 1), 0); + assertEq(token.getPastVotes(to, t1), supply); + + assertEq(token.getPastVotes(to, t1 + 1), supply); + assertEq(token.getPastVotes(to, t2), supply - 10); + + assertEq(token.getPastVotes(to, t2 + 1), supply - 10); + assertEq(token.getPastVotes(to, t3), supply - 20); + + assertEq(token.getPastVotes(to, t3 + 1), supply - 20); + assertEq(token.getPastVotes(to, t4), supply); + + assertEq(token.getPastVotes(to, t4 + 1), supply); + }*/ + + function testFailGetPastTotalSupply_RevertOnCurrentBlockAndGreater() public view { + token.getPastTotalSupply(block.number + 1); + } + + function testGetPastTotalSupply_ReturnsZeroWhenNoExistingCheckpoints() public { + hevm.roll(block.number + 1); + assertEq(token.getPastTotalSupply(0), 0); + } + + function testGetPastTotalSupply_ReturnsLatestBlockOnCurrentBlockOrGreater() public { + token.mint(holder, 1); + + hevm.roll(block.number + 2); + + assertEq(token.getPastTotalSupply(block.number - 1), 1); + assertEq(token.getPastTotalSupply(block.number - 2), 1); + } + + function testGetPastTotalSupply_ReturnsZeroIfLessThanFirstCheckpointBlock() public { + hevm.roll(block.number + 1); + + token.mint(holder, 1); + + hevm.roll(block.number + 2); + + assertEq(token.getPastTotalSupply(block.number - 3), 0); + assertEq(token.getPastTotalSupply(block.number - 1), 1); + } + /* + function testGetPastTotalSupply_ReturnsCorrectVotingBalancePerCheckpoint() public { + hevm.roll(block.number + 1); + uint256 t1 = block.number; + + token.mint(holder, 1); + + hevm.roll(block.number + 2); + uint256 t2 = block.number; + + token.burn(holder, 10); + + hevm.roll(block.number + 2); + uint256 t3 = block.number; + + token.burn(holder, 10); + + hevm.roll(block.number + 2); + uint256 t4 = block.number; + + token.mint(holder, 20); + + hevm.roll(block.number + 2); + + assertEq(token.getPastTotalSupply(t1 - 1), 0); + assertEq(token.getPastTotalSupply(t1), supply); + + assertEq(token.getPastTotalSupply(t1 + 1), supply); + assertEq(token.getPastTotalSupply(t2), supply - 10); + + assertEq(token.getPastTotalSupply(t2 + 1), supply - 10); + assertEq(token.getPastTotalSupply(t3), supply - 20); + + assertEq(token.getPastTotalSupply(t3 + 1), supply - 20); + assertEq(token.getPastTotalSupply(t4), supply); + assertEq(token.getPastTotalSupply(t4 + 1), supply); + }*/ +} diff --git a/test/utils/mocks/MockERC20Votes.sol b/test/utils/mocks/MockERC20Votes.sol index 82e7556..bbe8083 100644 --- a/test/utils/mocks/MockERC20Votes.sol +++ b/test/utils/mocks/MockERC20Votes.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -import {ERC20Votes} from "../../../tokens/ERC20/extensions/ERC20Votes.sol"; +import {ERC20Votes} from "../../../src/tokens/ERC20/extensions/ERC20Votes.sol"; contract MockERC20Votes is ERC20Votes { constructor(string memory name, string memory symbol) ERC20Votes(name, symbol, 18) {} diff --git a/test/utils/mocks/MockERC721Votes.sol b/test/utils/mocks/MockERC721Votes.sol new file mode 100644 index 0000000..fd6d44f --- /dev/null +++ b/test/utils/mocks/MockERC721Votes.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ERC721Votes} from "../../../src/tokens/ERC721/extensions/ERC721Votes.sol"; + +contract MockERC721Votes is ERC721Votes { + constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} + + /// @dev Getters. + + function getChainId() public view virtual returns (uint256) { + return block.chainid; + } + + function tokenURI(uint256) public view virtual override returns (string memory) { + return "MOCK"; + } + + /// @dev Mint/Burn. + + function mint(address account, uint256 id) public payable virtual { + _mint(account, id); + } + + function burn(uint256 id) public payable virtual { + _burn(id); + } +}