diff --git a/contracts/governance/Checkpoints.sol b/contracts/governance/Checkpoints.sol new file mode 100644 index 00000000..f28e36ae --- /dev/null +++ b/contracts/governance/Checkpoints.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/// @title Checkpoints +/// @dev Abstract contract to support checkpoints for Compound-like voting and +/// delegation. This implementation supports token supply up to 2^96 - 1. +/// This contract keeps a history (checkpoints) of each account's vote +/// power. Vote power can be delegated either by calling the {delegate} +/// function directly, or by providing a signature to be used with +/// {delegateBySig}. Voting power can be publicly queried through +/// {getVotes} and {getPastVotes}. +/// NOTE: Extracted from OpenZeppelin ERCVotes.sol. +abstract contract Checkpoints { + struct Checkpoint { + uint32 fromBlock; + uint96 votes; + } + + mapping(address => address) internal _delegates; + mapping(address => uint128[]) internal _checkpoints; + uint128[] internal _totalSupplyCheckpoints; + + /// @notice Emitted when an account changes their delegate. + event DelegateChanged( + address indexed delegator, + address indexed fromDelegate, + address indexed toDelegate + ); + + /// @notice Emitted when a balance or delegate change results in changes + /// to an account's voting power. + event DelegateVotesChanged( + address indexed delegate, + uint256 previousBalance, + uint256 newBalance + ); + + function checkpoints(address account, uint32 pos) + public + view + virtual + returns (Checkpoint memory checkpoint) + { + (uint32 fromBlock, uint96 votes) = decodeCheckpoint( + _checkpoints[account][pos] + ); + checkpoint = Checkpoint(fromBlock, votes); + } + + /// @notice Get number of checkpoints for `account`. + function numCheckpoints(address account) + public + view + virtual + returns (uint32) + { + return SafeCast.toUint32(_checkpoints[account].length); + } + + /// @notice Get the address `account` is currently delegating to. + function delegates(address account) public view virtual returns (address) { + return _delegates[account]; + } + + /// @notice Gets the current votes balance for `account`. + /// @param account The address to get votes balance + /// @return The number of current votes for `account` + function getVotes(address account) public view returns (uint96) { + uint256 pos = _checkpoints[account].length; + return pos == 0 ? 0 : decodeValue(_checkpoints[account][pos - 1]); + } + + /// @notice Determine the prior number of votes for an account as of + /// a block number. + /// @dev Block number must be a finalized block or else this function will + /// revert to prevent misinformation. + /// @param account The address of the account to check + /// @param blockNumber The block number to get the vote balance at + /// @return The number of votes the account had as of the given block + function getPastVotes(address account, uint256 blockNumber) + public + view + returns (uint96) + { + return lookupCheckpoint(_checkpoints[account], blockNumber); + } + + /// @notice Retrieve the `totalSupply` at the end of `blockNumber`. + /// Note, this value is the sum of all balances, but it is NOT the + /// sum of all the delegated votes! + /// @param blockNumber The block number to get the total supply at + /// @dev `blockNumber` must have been already mined + function getPastTotalSupply(uint256 blockNumber) + public + view + returns (uint96) + { + return lookupCheckpoint(_totalSupplyCheckpoints, blockNumber); + } + + /// @notice Change delegation for `delegator` to `delegatee`. + function delegate(address delegator, address delegatee) internal virtual; + + /// @notice Moves voting power from one delegate to another + /// @param src Address of old delegate + /// @param dst Address of new delegate + /// @param amount Voting power amount to transfer between delegates + function moveVotingPower( + address src, + address dst, + uint256 amount + ) internal { + 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); + } + } + } + + /// @notice Writes a new checkpoint based on operating last stored value + /// with a `delta`. Usually, said operation is the `add` or + /// `subtract` functions from this contract, but more complex + /// functions can be passed as parameters. + /// @param ckpts The checkpoints array to use + /// @param op The function to apply over the last value and the `delta` + /// @param delta Variation with respect to last stored value to be used + /// for new checkpoint + function writeCheckpoint( + uint128[] storage ckpts, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) internal returns (uint256 oldWeight, uint256 newWeight) { + uint256 pos = ckpts.length; + oldWeight = pos == 0 ? 0 : decodeValue(ckpts[pos - 1]); + newWeight = op(oldWeight, delta); + + if (pos > 0) { + uint32 fromBlock = decodeBlockNumber(ckpts[pos - 1]); + if (fromBlock == block.number) { + ckpts[pos - 1] = encodeCheckpoint( + fromBlock, + SafeCast.toUint96(newWeight) + ); + return (oldWeight, newWeight); + } + } + + ckpts.push( + encodeCheckpoint( + SafeCast.toUint32(block.number), + SafeCast.toUint96(newWeight) + ) + ); + } + + /// @notice Lookup a value in a list of (sorted) checkpoints. + /// @param ckpts The checkpoints array to use + /// @param blockNumber Block number when we want to get the checkpoint at + function lookupCheckpoint(uint128[] storage ckpts, uint256 blockNumber) + internal + view + returns (uint96) + { + // We run a binary search to look for the earliest checkpoint taken + // after `blockNumber`. During the loop, the index of the wanted + // checkpoint remains in the range [low-1, high). With each iteration, + // either `low` or `high` is moved towards the middle of the range to + // maintain the invariant. + // - If the middle checkpoint is after `blockNumber`, + // we look in [low, mid) + // - If the middle checkpoint is before or equal to `blockNumber`, + // we look in [mid+1, high) + // Once we reach a single value (when low == high), we've found the + // right checkpoint at the index high-1, if not out of bounds (in that + // case we're looking too far in the past and the result is 0). + // Note that if the latest checkpoint available is exactly for + // `blockNumber`, we end up with an index that is past the end of the + // array, so we technically don't find a checkpoint after + // `blockNumber`, but it works out the same. + require(blockNumber < block.number, "Block not yet determined"); + + uint256 high = ckpts.length; + uint256 low = 0; + while (low < high) { + uint256 mid = Math.average(low, high); + uint32 midBlock = decodeBlockNumber(ckpts[mid]); + if (midBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + return high == 0 ? 0 : decodeValue(ckpts[high - 1]); + } + + /// @notice Maximum token supply. Defaults to `type(uint96).max` (2^96 - 1) + function maxSupply() internal view virtual returns (uint96) { + return type(uint96).max; + } + + /// @notice Encodes a `blockNumber` and `value` into a single `uint128` + /// checkpoint. + /// @dev `blockNumber` is stored in the first 32 bits, while `value` in the + /// remaining 96 bits. + function encodeCheckpoint(uint32 blockNumber, uint96 value) + internal + pure + returns (uint128) + { + return (uint128(blockNumber) << 96) | uint128(value); + } + + /// @notice Decodes a block number from a `uint128` `checkpoint`. + function decodeBlockNumber(uint128 checkpoint) + internal + pure + returns (uint32) + { + return uint32(bytes4(bytes16(checkpoint))); + } + + /// @notice Decodes a voting value from a `uint128` `checkpoint`. + function decodeValue(uint128 checkpoint) internal pure returns (uint96) { + return uint96(checkpoint); + } + + /// @notice Decodes a block number and voting value from a `uint128` + /// `checkpoint`. + function decodeCheckpoint(uint128 checkpoint) + internal + pure + returns (uint32 blockNumber, uint96 value) + { + blockNumber = decodeBlockNumber(checkpoint); + value = decodeValue(checkpoint); + } + + // slither-disable-next-line dead-code + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + // slither-disable-next-line dead-code + function subtract(uint256 a, uint256 b) internal pure returns (uint256) { + return a - b; + } +} diff --git a/contracts/token/T.sol b/contracts/token/T.sol index c74d6b65..5e40654b 100644 --- a/contracts/token/T.sol +++ b/contracts/token/T.sol @@ -2,27 +2,18 @@ pragma solidity 0.8.4; +import "../governance/Checkpoints.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "@thesis/solidity-contracts/contracts/token/ERC20WithPermit.sol"; import "@thesis/solidity-contracts/contracts/token/MisfundRecovery.sol"; /// @title T token /// @notice Threshold Network T token. -contract T is ERC20WithPermit, MisfundRecovery { - /// @notice A checkpoint for marking number of votes from a given block. - struct Checkpoint { - uint32 fromBlock; - uint96 votes; - } - - /// @notice A record of each account's delegate. - mapping(address => address) public delegates; - - /// @notice A record of votes checkpoints for each account, by index. - mapping(address => mapping(uint32 => Checkpoint)) public checkpoints; - - /// @notice The number of checkpoints for each account. - mapping(address => uint32) public numCheckpoints; - +/// @dev By default, token balance does not account for voting power. +/// This makes transfers cheaper. The downside is that it requires users +/// to delegate to themselves to activate checkpoints and have their +/// voting power tracked. +contract T is ERC20WithPermit, MisfundRecovery, Checkpoints { /// @notice The EIP-712 typehash for the delegation struct used by /// `delegateBySig`. bytes32 public constant DELEGATION_TYPEHASH = @@ -30,29 +21,8 @@ contract T is ERC20WithPermit, MisfundRecovery { "Delegation(address delegatee,uint256 nonce,uint256 deadline)" ); - /// @notice An event emitted when an account changes its delegate. - event DelegateChanged( - address indexed delegator, - address indexed fromDelegate, - address indexed toDelegate - ); - - /// @notice An event emitted when a delegatee account's vote balance - /// changes. - event DelegateVotesChanged( - address indexed delegatee, - uint256 previousBalance, - uint256 newBalance - ); - constructor() ERC20WithPermit("Threshold Network Token", "T") {} - /// @notice Delegate votes from `msg.sender` to `delegatee`. - /// @param delegatee The address to delegate votes to - function delegate(address delegatee) external { - return delegate(msg.sender, delegatee); - } - /// @notice Delegates votes from signatory to `delegatee` /// @param delegatee The address to delegate votes to /// @param deadline The time at which to expire the signature @@ -104,58 +74,10 @@ contract T is ERC20WithPermit, MisfundRecovery { return delegate(signatory, delegatee); } - /// @notice Gets the current votes balance for `account`. - /// @param account The address to get votes balance - /// @return The number of current votes for `account` - function getCurrentVotes(address account) external view returns (uint96) { - uint32 nCheckpoints = numCheckpoints[account]; - return - nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0; - } - - /// @notice Determine the prior number of votes for an account as of - /// a block number. - /// @dev Block number must be a finalized block or else this function will - /// revert to prevent misinformation. - /// @param account The address of the account to check - /// @param blockNumber The block number to get the vote balance at - /// @return The number of votes the account had as of the given block - function getPriorVotes(address account, uint256 blockNumber) - external - view - returns (uint96) - { - require(blockNumber < block.number, "Not yet determined"); - - uint32 nCheckpoints = numCheckpoints[account]; - if (nCheckpoints == 0) { - return 0; - } - - // First check most recent balance - if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) { - return checkpoints[account][nCheckpoints - 1].votes; - } - - // Next check implicit zero balance - if (checkpoints[account][0].fromBlock > blockNumber) { - return 0; - } - - uint32 lower = 0; - uint32 upper = nCheckpoints - 1; - while (upper > lower) { - uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow - Checkpoint memory cp = checkpoints[account][center]; - if (cp.fromBlock == blockNumber) { - return cp.votes; - } else if (cp.fromBlock < blockNumber) { - lower = center; - } else { - upper = center - 1; - } - } - return checkpoints[account][lower].votes; + /// @notice Delegate votes from `msg.sender` to `delegatee`. + /// @param delegatee The address to delegate votes to + function delegate(address delegatee) public virtual { + return delegate(msg.sender, delegatee); } // slither-disable-next-line dead-code @@ -164,104 +86,38 @@ contract T is ERC20WithPermit, MisfundRecovery { address to, uint256 amount ) internal override { - // Does not allow to mint more than uin96 can fit. Otherwise, the - // Checkpoint might not fit the balance. + uint96 safeAmount = SafeCast.toUint96(amount); + + // When minting: if (from == address(0)) { + // Does not allow to mint more than uint96 can fit. Otherwise, the + // Checkpoint might not fit the balance. require( - totalSupply + amount <= type(uint96).max, + totalSupply + amount <= maxSupply(), "Maximum total supply exceeded" ); + writeCheckpoint(_totalSupplyCheckpoints, add, safeAmount); } - moveDelegates( - delegates[from], - delegates[to], - safe96(amount, "Transfer amount overflows") - ); - } - - function delegate(address delegator, address delegatee) internal { - address currentDelegate = delegates[delegator]; - uint96 delegatorBalance = safe96( - balanceOf[delegator], - "Delegator balance overflows" - ); - delegates[delegator] = delegatee; - - emit DelegateChanged(delegator, currentDelegate, delegatee); - - moveDelegates(currentDelegate, delegatee, delegatorBalance); - } - - function moveDelegates( - address srcRep, - address dstRep, - uint96 amount - ) internal { - if (srcRep != dstRep && amount > 0) { - if (srcRep != address(0)) { - uint32 srcRepNum = numCheckpoints[srcRep]; - uint96 srcRepOld = srcRepNum > 0 - ? checkpoints[srcRep][srcRepNum - 1].votes - : 0; - uint96 srcRepNew = srcRepOld - amount; - writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew); - } - - if (dstRep != address(0)) { - uint32 dstRepNum = numCheckpoints[dstRep]; - uint96 dstRepOld = dstRepNum > 0 - ? checkpoints[dstRep][dstRepNum - 1].votes - : 0; - uint96 dstRepNew = dstRepOld + amount; - writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew); - } - } - } - - function writeCheckpoint( - address delegatee, - uint32 nCheckpoints, - uint96 oldVotes, - uint96 newVotes - ) internal { - uint32 blockNumber = safe32( - block.number, - "Block number exceeds 32 bits" - ); - - if ( - // slither-disable-next-line incorrect-equality - nCheckpoints > 0 && - checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber - ) { - checkpoints[delegatee][nCheckpoints - 1].votes = newVotes; - } else { - checkpoints[delegatee][nCheckpoints] = Checkpoint( - blockNumber, - newVotes - ); - numCheckpoints[delegatee] = nCheckpoints + 1; + // When burning: + if (to == address(0)) { + writeCheckpoint(_totalSupplyCheckpoints, subtract, safeAmount); } - emit DelegateVotesChanged(delegatee, oldVotes, newVotes); + moveVotingPower(delegates(from), delegates(to), safeAmount); } - function safe32(uint256 n, string memory errorMessage) + function delegate(address delegator, address delegatee) internal - pure - returns (uint32) + virtual + override { - require(n < type(uint32).max, errorMessage); - return uint32(n); - } + address currentDelegate = delegates(delegator); + uint96 delegatorBalance = SafeCast.toUint96(balanceOf[delegator]); + _delegates[delegator] = delegatee; - function safe96(uint256 n, string memory errorMessage) - internal - pure - returns (uint96) - { - require(n < type(uint96).max, errorMessage); - return uint96(n); + emit DelegateChanged(delegator, currentDelegate, delegatee); + + moveVotingPower(currentDelegate, delegatee, delegatorBalance); } } diff --git a/package.json b/package.json index 9c890c6b..57436094 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "typescript": "^4.3.2" }, "dependencies": { - "@thesis/solidity-contracts": "github:thesis/solidity-contracts#507c647" + "@thesis/solidity-contracts": "github:thesis/solidity-contracts#507c647", + "@openzeppelin/contracts": "^4.3" } } diff --git a/test/helpers/contract-test-helpers.js b/test/helpers/contract-test-helpers.js index eb3362ac..47d5a49f 100644 --- a/test/helpers/contract-test-helpers.js +++ b/test/helpers/contract-test-helpers.js @@ -20,11 +20,17 @@ async function lastBlockTime() { return (await ethers.provider.getBlock("latest")).timestamp } +async function mineBlock() { + await ethers.provider.send("evm_mine") + return await lastBlockNumber() +} + module.exports.to1e18 = to1e18 module.exports.to1ePrecision = to1ePrecision module.exports.getBlockTime = getBlockTime module.exports.lastBlockNumber = lastBlockNumber module.exports.lastBlockTime = lastBlockTime +module.exports.mineBlock = mineBlock module.exports.ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" module.exports.MAX_UINT96 = ethers.BigNumber.from( diff --git a/test/token/T.test.js b/test/token/T.test.js index 51cc0c33..92a2001b 100644 --- a/test/token/T.test.js +++ b/test/token/T.test.js @@ -4,6 +4,7 @@ const { to1e18, lastBlockNumber, lastBlockTime, + mineBlock, ZERO_ADDRESS, MAX_UINT96, } = require("../helpers/contract-test-helpers") @@ -38,20 +39,20 @@ describe("T token", () => { return (await lastBlockNumber()) - 1 } - describe("getCurrentVotes", () => { + describe("getVotes", () => { context("when no delegation was done", () => { it("should return zero votes", async () => { - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal(0) + expect(await t.getVotes(tokenHolder.address)).to.equal(0) }) }) }) - describe("getPriorVotes", () => { + describe("getPastVotes", () => { context("when executed for the last block", () => { it("should revert", async () => { await expect( - t.getPriorVotes(tokenHolder.address, await lastBlockNumber()) - ).to.be.revertedWith("Not yet determined") + t.getPastVotes(tokenHolder.address, await lastBlockNumber()) + ).to.be.revertedWith("Block not yet determined") }) }) }) @@ -67,10 +68,8 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(delegator.address)).to.equal(0) - expect(await t.getCurrentVotes(delegatee.address)).to.equal( - initialBalance - ) + expect(await t.getVotes(delegator.address)).to.equal(0) + expect(await t.getVotes(delegatee.address)).to.equal(initialBalance) }) it("should update delegatee address", async () => { @@ -100,9 +99,7 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(delegator.address)).to.equal( - initialBalance - ) + expect(await t.getVotes(delegator.address)).to.equal(initialBalance) }) it("should update delegatee address", async () => { @@ -143,37 +140,35 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(delegator.address)).to.equal(0) - expect(await t.getCurrentVotes(delegatee.address)).to.equal(0) - expect(await t.getCurrentVotes(delegatee2.address)).to.equal( - initialBalance - ) + expect(await t.getVotes(delegator.address)).to.equal(0) + expect(await t.getVotes(delegatee.address)).to.equal(0) + expect(await t.getVotes(delegatee2.address)).to.equal(initialBalance) }) it("should keep track of prior votes", async () => { - expect(await t.getPriorVotes(delegator.address, block1)).to.equal(0) - expect(await t.getPriorVotes(delegatee.address, block1)).to.equal( + expect(await t.getPastVotes(delegator.address, block1)).to.equal(0) + expect(await t.getPastVotes(delegatee.address, block1)).to.equal( initialBalance ) - expect(await t.getPriorVotes(delegatee2.address, block1)).to.equal(0) + expect(await t.getPastVotes(delegatee2.address, block1)).to.equal(0) - expect(await t.getPriorVotes(delegator.address, block2)).to.equal(0) - expect(await t.getPriorVotes(delegatee.address, block2)).to.equal(0) - expect(await t.getPriorVotes(delegatee2.address, block2)).to.equal( + expect(await t.getPastVotes(delegator.address, block2)).to.equal(0) + expect(await t.getPastVotes(delegatee.address, block2)).to.equal(0) + expect(await t.getPastVotes(delegatee2.address, block2)).to.equal( initialBalance ) - expect(await t.getPriorVotes(delegator.address, block3)).to.equal(0) - expect(await t.getPriorVotes(delegatee.address, block3)).to.equal( + expect(await t.getPastVotes(delegator.address, block3)).to.equal(0) + expect(await t.getPastVotes(delegatee.address, block3)).to.equal( initialBalance ) - expect(await t.getPriorVotes(delegatee2.address, block3)).to.equal(0) + expect(await t.getPastVotes(delegatee2.address, block3)).to.equal(0) - expect(await t.getPriorVotes(delegator.address, block4)).to.equal( + expect(await t.getPastVotes(delegator.address, block4)).to.equal( initialBalance ) - expect(await t.getPriorVotes(delegatee.address, block4)).to.equal(0) - expect(await t.getPriorVotes(delegatee2.address, block4)).to.equal(0) + expect(await t.getPastVotes(delegatee.address, block4)).to.equal(0) + expect(await t.getPastVotes(delegatee2.address, block4)).to.equal(0) }) }) } @@ -301,19 +296,16 @@ describe("T token", () => { }) it("should keep current votes at zero", async () => { - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal(0) - expect(await t.getCurrentVotes(tokenRecipient.address)).to.equal(0) + expect(await t.getVotes(tokenHolder.address)).to.equal(0) + expect(await t.getVotes(tokenRecipient.address)).to.equal(0) }) it("should keep prior votes at zero", async () => { expect( - await t.getPriorVotes( - tokenHolder.address, - await previousBlockNumber() - ) + await t.getPastVotes(tokenHolder.address, await previousBlockNumber()) ).to.equal(0) expect( - await t.getPriorVotes( + await t.getPastVotes( tokenRecipient.address, await previousBlockNumber() ) @@ -334,35 +326,32 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(delegatee.address)).to.equal( + expect(await t.getVotes(delegatee.address)).to.equal( initialBalance.sub(amount) ) - expect(await t.getCurrentVotes(delegatee2.address)).to.equal(amount) - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal(0) - expect(await t.getCurrentVotes(tokenRecipient.address)).to.equal(0) + expect(await t.getVotes(delegatee2.address)).to.equal(amount) + expect(await t.getVotes(tokenHolder.address)).to.equal(0) + expect(await t.getVotes(tokenRecipient.address)).to.equal(0) }) it("should keep track of prior votes", async () => { expect( - await t.getPriorVotes( - delegatee.address, - await previousBlockNumber() - ) + await t.getPastVotes(delegatee.address, await previousBlockNumber()) ).to.equal(initialBalance) expect( - await t.getPriorVotes( + await t.getPastVotes( delegatee2.address, await previousBlockNumber() ) ).to.equal(0) expect( - await t.getPriorVotes( + await t.getPastVotes( tokenHolder.address, await previousBlockNumber() ) ).to.equal(0) expect( - await t.getPriorVotes( + await t.getPastVotes( tokenRecipient.address, await previousBlockNumber() ) @@ -396,22 +385,19 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal( + expect(await t.getVotes(tokenHolder.address)).to.equal( initialBalance.sub(amount) ) - expect(await t.getCurrentVotes(tokenRecipient.address)).to.equal(amount) + expect(await t.getVotes(tokenRecipient.address)).to.equal(amount) }) it("should keep track of prior votes", async () => { expect( - await t.getPriorVotes( - tokenHolder.address, - await previousBlockNumber() - ) + await t.getPastVotes(tokenHolder.address, await previousBlockNumber()) ).to.equal(initialBalance) expect( - await t.getPriorVotes( + await t.getPastVotes( tokenRecipient.address, await previousBlockNumber() ) @@ -443,25 +429,22 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(delegatee.address)).to.equal( + expect(await t.getVotes(delegatee.address)).to.equal( initialBalance.sub(amount) ) - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal(0) - expect(await t.getCurrentVotes(tokenRecipient.address)).to.equal(0) + expect(await t.getVotes(tokenHolder.address)).to.equal(0) + expect(await t.getVotes(tokenRecipient.address)).to.equal(0) }) it("should keep track of prior votes", async () => { expect( - await t.getPriorVotes(delegatee.address, await previousBlockNumber()) + await t.getPastVotes(delegatee.address, await previousBlockNumber()) ).to.equal(initialBalance) expect( - await t.getPriorVotes( - tokenHolder.address, - await previousBlockNumber() - ) + await t.getPastVotes(tokenHolder.address, await previousBlockNumber()) ).to.equal(0) expect( - await t.getPriorVotes( + await t.getPastVotes( tokenRecipient.address, await previousBlockNumber() ) @@ -489,21 +472,18 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal( + expect(await t.getVotes(tokenHolder.address)).to.equal( initialBalance.sub(amount) ) - expect(await t.getCurrentVotes(tokenRecipient.address)).to.equal(0) + expect(await t.getVotes(tokenRecipient.address)).to.equal(0) }) it("should keep track of prior votes", async () => { expect( - await t.getPriorVotes( - tokenHolder.address, - await previousBlockNumber() - ) + await t.getPastVotes(tokenHolder.address, await previousBlockNumber()) ).to.equal(initialBalance) expect( - await t.getPriorVotes( + await t.getPastVotes( tokenRecipient.address, await previousBlockNumber() ) @@ -531,23 +511,20 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(delegatee2.address)).to.equal(amount) - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal(0) - expect(await t.getCurrentVotes(tokenRecipient.address)).to.equal(0) + expect(await t.getVotes(delegatee2.address)).to.equal(amount) + expect(await t.getVotes(tokenHolder.address)).to.equal(0) + expect(await t.getVotes(tokenRecipient.address)).to.equal(0) }) it("should keep track of prior votes", async () => { expect( - await t.getPriorVotes(delegatee2.address, await previousBlockNumber()) + await t.getPastVotes(delegatee2.address, await previousBlockNumber()) ).to.equal(0) expect( - await t.getPriorVotes( - tokenHolder.address, - await previousBlockNumber() - ) + await t.getPastVotes(tokenHolder.address, await previousBlockNumber()) ).to.equal(0) expect( - await t.getPriorVotes( + await t.getPastVotes( tokenRecipient.address, await previousBlockNumber() ) @@ -571,19 +548,16 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal(0) - expect(await t.getCurrentVotes(tokenRecipient.address)).to.equal(amount) + expect(await t.getVotes(tokenHolder.address)).to.equal(0) + expect(await t.getVotes(tokenRecipient.address)).to.equal(amount) }) it("should keep track of prior votes", async () => { expect( - await t.getPriorVotes( - tokenHolder.address, - await previousBlockNumber() - ) + await t.getPastVotes(tokenHolder.address, await previousBlockNumber()) ).to.equal(0) expect( - await t.getPriorVotes( + await t.getPastVotes( tokenRecipient.address, await previousBlockNumber() ) @@ -616,45 +590,39 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal(0) - expect(await t.getCurrentVotes(tokenRecipient.address)).to.equal(0) - expect(await t.getCurrentVotes(delegatee.address)).to.equal( + expect(await t.getVotes(tokenHolder.address)).to.equal(0) + expect(await t.getVotes(tokenRecipient.address)).to.equal(0) + expect(await t.getVotes(delegatee.address)).to.equal( initialBalance.sub(to1e18(10)) ) - expect(await t.getCurrentVotes(delegatee2.address)).to.equal(to1e18(10)) + expect(await t.getVotes(delegatee2.address)).to.equal(to1e18(10)) }) it("should keep track of prior votes", async () => { - expect(await t.getPriorVotes(tokenHolder.address, block1)).to.equal(0) - expect(await t.getPriorVotes(tokenRecipient.address, block1)).to.equal( - 0 - ) - expect(await t.getPriorVotes(delegatee.address, block1)).to.equal( + expect(await t.getPastVotes(tokenHolder.address, block1)).to.equal(0) + expect(await t.getPastVotes(tokenRecipient.address, block1)).to.equal(0) + expect(await t.getPastVotes(delegatee.address, block1)).to.equal( initialBalance.sub(to1e18(1)) ) - expect(await t.getPriorVotes(delegatee2.address, block1)).to.equal( + expect(await t.getPastVotes(delegatee2.address, block1)).to.equal( to1e18(1) ) - expect(await t.getPriorVotes(tokenHolder.address, block2)).to.equal(0) - expect(await t.getPriorVotes(tokenRecipient.address, block2)).to.equal( - 0 - ) - expect(await t.getPriorVotes(delegatee.address, block2)).to.equal( + expect(await t.getPastVotes(tokenHolder.address, block2)).to.equal(0) + expect(await t.getPastVotes(tokenRecipient.address, block2)).to.equal(0) + expect(await t.getPastVotes(delegatee.address, block2)).to.equal( initialBalance.sub(to1e18(3)) ) - expect(await t.getPriorVotes(delegatee2.address, block2)).to.equal( + expect(await t.getPastVotes(delegatee2.address, block2)).to.equal( to1e18(3) ) - expect(await t.getPriorVotes(tokenHolder.address, block3)).to.equal(0) - expect(await t.getPriorVotes(tokenRecipient.address, block3)).to.equal( - 0 - ) - expect(await t.getPriorVotes(delegatee.address, block3)).to.equal( + expect(await t.getPastVotes(tokenHolder.address, block3)).to.equal(0) + expect(await t.getPastVotes(tokenRecipient.address, block3)).to.equal(0) + expect(await t.getPastVotes(delegatee.address, block3)).to.equal( initialBalance.sub(to1e18(6)) ) - expect(await t.getPriorVotes(delegatee2.address, block3)).to.equal( + expect(await t.getPastVotes(delegatee2.address, block3)).to.equal( to1e18(6) ) }) @@ -679,6 +647,13 @@ describe("T token", () => { }) describe("mint", () => { + context("when minting", () => { + it("historic supply should be tracked", async () => { + const block = await mineBlock() + expect(await t.getPastTotalSupply(block - 1)).to.equal(initialBalance) + }) + }) + context("when trying to mint more than the checkpoint can store", () => { context("in one step", () => { it("should revert", async () => { @@ -702,11 +677,29 @@ describe("T token", () => { context("when no delegation was done", () => { it("should keep current votes at zero", async () => { await t.connect(deployer).mint(thirdParty.address, initialBalance) - expect(await t.getCurrentVotes(thirdParty.address)).to.equal(0) + expect(await t.getVotes(thirdParty.address)).to.equal(0) // one more time, when not starting from zero balance await t.connect(deployer).mint(thirdParty.address, initialBalance) - expect(await t.getCurrentVotes(thirdParty.address)).to.equal(0) + expect(await t.getVotes(thirdParty.address)).to.equal(0) + }) + + it("but historical supply should be tracked", async () => { + const block1 = await mineBlock() + expect(await t.getPastTotalSupply(block1 - 1)).to.equal(initialBalance) + + await t.connect(deployer).mint(thirdParty.address, initialBalance) + const block2 = await mineBlock() + expect(await t.getPastTotalSupply(block2 - 1)).to.equal( + initialBalance.mul(2) + ) + + // one more time, when not starting from zero balance + await t.connect(deployer).mint(thirdParty.address, initialBalance) + const block3 = await mineBlock() + expect(await t.getPastTotalSupply(block3 - 1)).to.equal( + initialBalance.mul(3) + ) }) }) @@ -719,9 +712,27 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(thirdParty.address)).to.equal(0) - expect(await t.getCurrentVotes(delegatee.address)).to.equal( - initialBalance + expect(await t.getVotes(thirdParty.address)).to.equal(0) + expect(await t.getVotes(delegatee.address)).to.equal(initialBalance) + }) + + it("historical supply should be tracked", async () => { + const block1 = await mineBlock() + expect(await t.getPastTotalSupply(block1 - 1)).to.equal( + initialBalance.mul(2) + ) + + await t.connect(deployer).mint(thirdParty.address, initialBalance) + const block2 = await mineBlock() + expect(await t.getPastTotalSupply(block2 - 1)).to.equal( + initialBalance.mul(3) + ) + + // one more time, when not starting from zero balance + await t.connect(deployer).mint(thirdParty.address, initialBalance) + const block3 = await mineBlock() + expect(await t.getPastTotalSupply(block3 - 1)).to.equal( + initialBalance.mul(4) ) }) @@ -741,8 +752,26 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(thirdParty.address)).to.equal( - initialBalance + expect(await t.getVotes(thirdParty.address)).to.equal(initialBalance) + }) + + it("historical supply should be tracked", async () => { + const block1 = await mineBlock() + expect(await t.getPastTotalSupply(block1 - 1)).to.equal( + initialBalance.mul(2) + ) + + await t.connect(deployer).mint(thirdParty.address, initialBalance) + const block2 = await mineBlock() + expect(await t.getPastTotalSupply(block2 - 1)).to.equal( + initialBalance.mul(3) + ) + + // one more time, when not starting from zero balance + await t.connect(deployer).mint(thirdParty.address, initialBalance) + const block3 = await mineBlock() + expect(await t.getPastTotalSupply(block3 - 1)).to.equal( + initialBalance.mul(4) ) }) @@ -770,18 +799,18 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(thirdParty.address)).to.equal(0) - expect(await t.getCurrentVotes(delegatee.address)).to.equal(to1e18(54)) + expect(await t.getVotes(thirdParty.address)).to.equal(0) + expect(await t.getVotes(delegatee.address)).to.equal(to1e18(54)) }) it("should keep track of prior votes", async () => { - expect(await t.getPriorVotes(delegatee.address, block1)).to.equal( + expect(await t.getPastVotes(delegatee.address, block1)).to.equal( to1e18(10) ) - expect(await t.getPriorVotes(delegatee.address, block2)).to.equal( + expect(await t.getPastVotes(delegatee.address, block2)).to.equal( to1e18(22) ) - expect(await t.getPriorVotes(delegatee.address, block3)).to.equal( + expect(await t.getPastVotes(delegatee.address, block3)).to.equal( to1e18(37) ) }) @@ -790,9 +819,21 @@ describe("T token", () => { const describeBurn = (doBurn) => { context("when no delegation was done", () => { + const amount = to1e18(10) + + beforeEach(async () => { + await doBurn(amount) + }) + it("should keep current votes at zero", async () => { - t.connect(tokenHolder).burn(to1e18(10)) - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal(0) + expect(await t.getVotes(tokenHolder.address)).to.equal(0) + }) + + it("historical supply should be reduced", async () => { + const block1 = await mineBlock() + expect(await t.getPastTotalSupply(block1 - 1)).to.equal( + initialBalance.sub(amount) + ) }) }) @@ -806,8 +847,8 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal(0) - expect(await t.getCurrentVotes(delegatee.address)).to.equal( + expect(await t.getVotes(tokenHolder.address)).to.equal(0) + expect(await t.getVotes(delegatee.address)).to.equal( initialBalance.sub(amount) ) }) @@ -821,6 +862,13 @@ describe("T token", () => { initialBalance.sub(amount) ) }) + + it("historical supply should be reduced", async () => { + const block1 = await mineBlock() + expect(await t.getPastTotalSupply(block1 - 1)).to.equal( + initialBalance.sub(amount) + ) + }) }) context("when self-delegated", () => { @@ -833,7 +881,7 @@ describe("T token", () => { }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal( + expect(await t.getVotes(tokenHolder.address)).to.equal( initialBalance.sub(amount) ) }) @@ -847,6 +895,13 @@ describe("T token", () => { initialBalance.sub(amount) ) }) + + it("historical supply should be reduced", async () => { + const block1 = await mineBlock() + expect(await t.getPastTotalSupply(block1 - 1)).to.equal( + initialBalance.sub(amount) + ) + }) }) context("when burned several times", () => { @@ -854,33 +909,55 @@ describe("T token", () => { let block2 let block3 + const amount1 = to1e18(3) + const amount2 = to1e18(1) + const amount3 = to1e18(5) + const amount4 = to1e18(2) + beforeEach(async () => { await t.connect(tokenHolder).delegate(delegatee.address) - await doBurn(to1e18(3)) + await doBurn(amount1) block1 = await lastBlockNumber() - await doBurn(to1e18(1)) + await doBurn(amount2) block2 = await lastBlockNumber() - await doBurn(to1e18(5)) + await doBurn(amount3) block3 = await lastBlockNumber() - await doBurn(to1e18(2)) + await doBurn(amount4) }) it("should update current votes", async () => { - expect(await t.getCurrentVotes(tokenHolder.address)).to.equal(0) - expect(await t.getCurrentVotes(delegatee.address)).to.equal( + expect(await t.getVotes(tokenHolder.address)).to.equal(0) + expect(await t.getVotes(delegatee.address)).to.equal( initialBalance.sub(to1e18(11)) ) }) it("should keep track of prior votes", async () => { - expect(await t.getPriorVotes(delegatee.address, block1)).to.equal( - initialBalance.sub(to1e18(3)) + expect(await t.getPastVotes(delegatee.address, block1)).to.equal( + initialBalance.sub(amount1) + ) + expect(await t.getPastVotes(delegatee.address, block2)).to.equal( + initialBalance.sub(amount1).sub(amount2) + ) + expect(await t.getPastVotes(delegatee.address, block3)).to.equal( + initialBalance.sub(amount1).sub(amount2).sub(amount3) + ) + }) + + it("historical supply should decrease every time", async () => { + expect(await t.getPastTotalSupply(block1)).to.equal( + initialBalance.sub(amount1) + ) + expect(await t.getPastTotalSupply(block2)).to.equal( + initialBalance.sub(amount1).sub(amount2) ) - expect(await t.getPriorVotes(delegatee.address, block2)).to.equal( - initialBalance.sub(to1e18(4)) + expect(await t.getPastTotalSupply(block3)).to.equal( + initialBalance.sub(amount1).sub(amount2).sub(amount3) ) - expect(await t.getPriorVotes(delegatee.address, block3)).to.equal( - initialBalance.sub(to1e18(9)) + await mineBlock() + const block4 = (await lastBlockNumber()) - 1 + expect(await t.getPastTotalSupply(block4)).to.equal( + initialBalance.sub(amount1).sub(amount2).sub(amount3).sub(amount4) ) }) }) diff --git a/yarn.lock b/yarn.lock index 291dd7c5..357fb9ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -574,6 +574,11 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.2.0.tgz#260d921d99356e48013d9d760caaa6cea35dc642" integrity sha512-LD4NnkKpHHSMo5z9MvFsG4g1xxZUDqV3A3Futu3nvyfs4wPwXxqOgMaxOoa2PeyGL2VNeSlbxT54enbQzGcgJQ== +"@openzeppelin/contracts@^4.3": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.3.1.tgz#c01f791ce6c9d3989ac1a643267501dbe336b9e3" + integrity sha512-QjgbPPlmDK2clK1hzjw2ROfY8KA5q+PfhDUUxZFEBCZP9fi6d5FuNoh/Uq0oCTMEKPmue69vhX2jcl0N/tFKGw== + "@resolver-engine/core@^0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@resolver-engine/core/-/core-0.3.3.tgz#590f77d85d45bc7ecc4e06c654f41345db6ca967" @@ -3021,8 +3026,9 @@ eslint-config-google@^0.13.0: version "0.3.0" resolved "https://codeload.github.com/keep-network/eslint-config-keep/tar.gz/0c27ade54e725f980e971c3d91ea88bab76b2330" dependencies: + "@keep-network/prettier-config-keep" "github:keep-network/prettier-config-keep" eslint-config-google "^0.13.0" - eslint-config-prettier "^6.10.0" + eslint-config-prettier "^6.15.0" eslint-plugin-no-only-tests "^2.3.1" eslint-plugin-prettier "^3.1.2"