Skip to content

Commit

Permalink
Use our optimized Checkpoints implementation in T token contract
Browse files Browse the repository at this point in the history
* Reduces average transfer cost from 108k gas to 88k.
* Removes ~130 LOC from T token contract by using common code from the Checkpoints contract and OpenZeppelin utils
  • Loading branch information
cygnusv committed Oct 5, 2021
1 parent a819207 commit 41bdc80
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 156 deletions.
180 changes: 25 additions & 155 deletions contracts/token/T.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,27 @@

pragma solidity 0.8.4;

import "../utils/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;

contract T is ERC20WithPermit, MisfundRecovery, Checkpoints {
/// @notice The EIP-712 typehash for the delegation struct used by
/// `delegateBySig`.
bytes32 public constant DELEGATION_TYPEHASH =
keccak256(
"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);
function delegate(address delegatee) public virtual {
return delegate(msg.sender, delegatee); // _msgSender()?
}

/// @notice Delegates votes from signatory to `delegatee`
Expand Down Expand Up @@ -108,9 +80,7 @@ contract T is ERC20WithPermit, MisfundRecovery {
/// @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;
return uint96(getVotes(account));
}

/// @notice Determine the prior number of votes for an account as of
Expand All @@ -125,37 +95,7 @@ contract T is ERC20WithPermit, MisfundRecovery {
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;
return uint96(getPastVotes(account, blockNumber));
}

// slither-disable-next-line dead-code
Expand All @@ -164,104 +104,34 @@ 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);
}
// When burning:
if (to == address(0)) {
_writeCheckpoint(_totalSupplyCheckpoints, _subtract, safeAmount);
}
}

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;
}

emit DelegateVotesChanged(delegatee, oldVotes, newVotes);
_moveVotingPower(delegates(from), delegates(to), safeAmount);
}

function safe32(uint256 n, string memory errorMessage)
internal
pure
returns (uint32)
{
require(n < type(uint32).max, errorMessage);
return uint32(n);
}
function delegate(address delegator, address delegatee) internal virtual {
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);
}
}
2 changes: 1 addition & 1 deletion test/token/T.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe("T token", () => {
it("should revert", async () => {
await expect(
t.getPriorVotes(tokenHolder.address, await lastBlockNumber())
).to.be.revertedWith("Not yet determined")
).to.be.revertedWith("Checkpoints: block not yet mined")
})
})
})
Expand Down

0 comments on commit 41bdc80

Please sign in to comment.