Created
October 30, 2021 18:09
-
-
Save z0r0z/9192899285690f6dd5ad2d7a67d82ebb to your computer and use it in GitHub Desktop.
LexToken with owned minting, Compound-style governance and EIP-1238 'badge' nontransferability.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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