Skip to content

Instantly share code, notes, and snippets.

@z0r0z
Created October 30, 2021 18:09
Show Gist options
  • Save z0r0z/9192899285690f6dd5ad2d7a67d82ebb to your computer and use it in GitHub Desktop.
Save z0r0z/9192899285690f6dd5ad2d7a67d82ebb to your computer and use it in GitHub Desktop.
LexToken with owned minting, Compound-style governance and EIP-1238 'badge' nontransferability.
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.0;
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Adapted from RariCapital, https://github.com/Rari-Capital/solmate/blob/main/src/erc20/ERC20.sol,
/// License-Identifier: AGPL-3.0-only.
abstract contract LexToken {
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
string public name;
string public symbol;
uint8 public immutable decimals;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
bytes32 public constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
uint256 internal immutable INITIAL_CHAIN_ID;
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = _calculateDomainSeparator();
}
function _calculateDomainSeparator() internal view returns (bytes32 domainSeperator) {
domainSeperator = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes("1")),
block.chainid,
address(this)
)
);
}
function DOMAIN_SEPARATOR() public view returns (bytes32 domainSeperator) {
domainSeperator = block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _calculateDomainSeparator();
}
function approve(address spender, uint256 amount) public virtual returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount) public virtual returns (bool) {
balanceOf[msg.sender] -= amount;
// This is safe because the sum of all user
// balances can't exceed 'type(uint256).max'.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
if (allowance[from][msg.sender] != type(uint256).max) {
allowance[from][msg.sender] -= amount;
}
balanceOf[from] -= amount;
// This is safe because the sum of all user
// balances can't exceed 'type(uint256).max'.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
// This is reasonably safe from overflow because incrementing `nonces` beyond
// 'type(uint256).max' is exceedingly unlikely compared to optimization benefits.
unchecked {
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_PERMIT_SIGNATURE");
allowance[recoveredAddress][spender] = value;
}
emit Approval(owner, spender, value);
}
function _mint(address to, uint256 amount) internal {
totalSupply += amount;
// This is safe because the sum of all user
// balances can't exceed 'type(uint256).max'.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal {
balanceOf[from] -= amount;
// This is safe because a user won't ever
// have a balance larger than `totalSupply`.
unchecked {
totalSupply -= amount;
}
emit Transfer(from, address(0), amount);
}
}
/// @notice Single owner function access control module.
abstract contract LexOwnable {
event TransferOwner(address indexed from, address indexed to);
event TransferOwnerClaim(address indexed from, address indexed to);
address public owner;
address public pendingOwner;
/// @notice Initialize ownership module for function access control.
/// @param _owner Account to grant ownership.
constructor(address _owner) {
owner = _owner;
emit TransferOwner(address(0), _owner);
}
/// @notice Access control modifier that conditions function to be restricted to `owner` account.
modifier onlyOwner() {
require(msg.sender == owner, "NOT_OWNER");
_;
}
/// @notice `pendingOwner` can claim `owner` account.
function claimOwner() external {
require(msg.sender == pendingOwner, "NOT_PENDING_OWNER");
emit TransferOwner(owner, msg.sender);
owner = msg.sender;
pendingOwner = address(0);
}
/// @notice Transfer `owner` account.
/// @param to Account granted `owner` access control.
/// @param direct If 'true', ownership is directly transferred.
function transferOwner(address to, bool direct) external onlyOwner {
require(to != address(0), "ZERO_ADDRESS");
if (direct) {
owner = to;
emit TransferOwner(msg.sender, to);
} else {
pendingOwner = to;
emit TransferOwnerClaim(msg.sender, to);
}
}
}
/// @notice LexToken with owned minting.
abstract contract LexTokenMintable is LexToken, LexOwnable {
/// @notice Initialize LexToken extension.
/// @param _name Public name for LexToken.
/// @param _symbol Public symbol for LexToken.
/// @param _decimals Unit scaling factor - default '18' to match ETH units.
/// @param _owner Account to grant minting ownership.
/// @param _initialSupply Starting LexToken supply.
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
address _owner,
uint256 _initialSupply
) LexToken(_name, _symbol, _decimals) LexOwnable(_owner) {
_mint(_owner, _initialSupply);
}
/// @notice Mints tokens by `owner`.
/// @param to Account to receive tokens.
/// @param amount Sum to mint.
function mint(address to, uint256 amount) public virtual onlyOwner {
_mint(to, amount);
}
}
/// @notice LexToken with owned minting, Compound-style governance and EIP-1238 'badge' nontransferability.
contract LexTokenVotableBadge is LexTokenMintable {
bytes32 public constant DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
mapping(address => address) public delegates;
mapping(address => mapping(uint256 => Checkpoint)) public checkpoints;
mapping(address => uint256) public numCheckpoints;
event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);
event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance);
/// @notice Marks 'votes' from a given timestamp.
struct Checkpoint {
uint256 fromTimestamp;
uint256 votes;
}
/// @notice Initialize owned mintable LexToken with Compound-style governance and EIP-1238 'badge' nontransferability.
/// @param _name Public name for LexToken.
/// @param _symbol Public symbol for LexToken.
/// @param _decimals Unit scaling factor - default '18' to match ETH units.
/// @param _owner Account to grant minting and burning ownership.
/// @param _initialSupply Starting LexToken supply.
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
address _owner,
uint256 _initialSupply
) LexTokenMintable(_name, _symbol, _decimals, _owner, _initialSupply) {
_delegate(msg.sender, msg.sender);
}
/// @notice Disables transferability by overriding {transfer} function of LexToken.
function transfer(address, uint256) public pure override returns (bool) {
revert();
}
/// @notice Disables transferability by overriding {transferFrom} function of LexToken.
function transferFrom(address, address, uint256) public pure override returns (bool) {
revert();
}
/// @notice Mints tokens by `owner`.
/// @param to Account to receive tokens.
/// @param amount Sum to mint.
function mint(address to, uint256 amount) public onlyOwner override {
_mint(to, amount);
_moveDelegates(address(0), delegates[to], amount);
}
/// @notice Delegate votes from `msg.sender` to `delegatee`.
/// @param delegatee The address to delegate votes to.
function delegate(address delegatee) external {
_delegate(msg.sender, delegatee);
}
/// @notice Delegates votes from signatory to `delegatee`.
/// @param delegatee The address to delegate votes to.
/// @param nonce The contract state required to match the signature.
/// @param expiry The time at which to expire the signature.
/// @param v The recovery byte of the signature.
/// @param r Half of the ECDSA signature pair.
/// @param s Half of the ECDSA signature pair.
function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external {
bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), structHash));
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "ZERO_ADDRESS");
unchecked {
require(nonce == nonces[signatory]++, "INVALID_NONCE");
}
require(block.timestamp <= expiry, "SIGNATURE_EXPIRED");
_delegate(signatory, delegatee);
}
/// @notice Gets the current 'votes' balance for `account`.
/// @param account The address to get votes balance.
/// @return votes The number of current 'votes' for `account`.
function getCurrentVotes(address account) external view returns (uint256 votes) {
unchecked {
uint256 nCheckpoints = numCheckpoints[account];
votes = nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0;
}
}
/// @notice Determine the prior number of 'votes' for an `account`.
/// @param account The address to check.
/// @param timestamp The unix timestamp to get the 'votes' balance at.
/// @return votes The number of 'votes' the `account` had as of the given unix timestamp.
function getPriorVotes(address account, uint256 timestamp) external view returns (uint256 votes) {
require(timestamp < block.timestamp, "NOT_YET_DETERMINED");
uint256 nCheckpoints = numCheckpoints[account];
if (nCheckpoints == 0) {
return 0;
}
unchecked {
if (checkpoints[account][nCheckpoints - 1].fromTimestamp <= timestamp) {
return checkpoints[account][nCheckpoints - 1].votes;
}
if (checkpoints[account][0].fromTimestamp > timestamp) {
return 0;
}
uint256 lower;
uint256 upper = nCheckpoints - 1;
while (upper > lower) {
uint256 center = upper - (upper - lower) / 2;
Checkpoint memory cp = checkpoints[account][center];
if (cp.fromTimestamp == timestamp) {
return cp.votes;
} else if (cp.fromTimestamp < timestamp) {
lower = center;
} else {
upper = center - 1;
}
}
return checkpoints[account][lower].votes;
}
}
function _delegate(address delegator, address delegatee) private {
address currentDelegate = delegates[delegator];
delegates[delegator] = delegatee;
_moveDelegates(currentDelegate, delegatee, balanceOf[delegator]);
emit DelegateChanged(delegator, currentDelegate, delegatee);
}
function _moveDelegates(address srcRep, address dstRep, uint256 amount) private {
unchecked {
if (srcRep != dstRep && amount > 0) {
if (srcRep != address(0)) {
uint256 srcRepNum = numCheckpoints[srcRep];
uint256 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0;
uint256 srcRepNew = srcRepOld - amount;
_writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew);
}
if (dstRep != address(0)) {
uint256 dstRepNum = numCheckpoints[dstRep];
uint256 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0;
uint256 dstRepNew = dstRepOld + amount;
_writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew);
}
}
}
}
function _writeCheckpoint(address delegatee, uint256 nCheckpoints, uint256 oldVotes, uint256 newVotes) private {
unchecked {
if (nCheckpoints > 0 && checkpoints[delegatee][nCheckpoints - 1].fromTimestamp == block.timestamp) {
checkpoints[delegatee][nCheckpoints - 1].votes = newVotes;
} else {
checkpoints[delegatee][nCheckpoints] = Checkpoint(block.timestamp, newVotes);
numCheckpoints[delegatee] = nCheckpoints + 1;
}
}
emit DelegateVotesChanged(delegatee, oldVotes, newVotes);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment