Skip to content

Instantly share code, notes, and snippets.

@z0r0z
Last active November 8, 2021 01:07
Show Gist options
  • Save z0r0z/1154cece644b5d3d1efe43b8d77a06c2 to your computer and use it in GitHub Desktop.
Save z0r0z/1154cece644b5d3d1efe43b8d77a06c2 to your computer and use it in GitHub Desktop.
LiteDAOfactory.sol
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.0;
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation with COMP-style governance,
/// @author Adapted from RariCapital, https://github.com/Rari-Capital/solmate/blob/main/src/erc20/ERC20.sol,
/// License-Identifier: AGPL-3.0-only.
contract LiteDAOtoken {
/*///////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);
event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance);
event TogglePause(bool indexed paused);
/*///////////////////////////////////////////////////////////////
METADATA STORAGE
//////////////////////////////////////////////////////////////*/
string public name;
string public symbol;
uint8 public constant decimals = 18;
/*///////////////////////////////////////////////////////////////
ERC20 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/*///////////////////////////////////////////////////////////////
DAO STORAGE
//////////////////////////////////////////////////////////////*/
bool public paused;
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;
struct Checkpoint {
uint256 fromTimestamp;
uint256 votes;
}
/*///////////////////////////////////////////////////////////////
EIP-2612 STORAGE
//////////////////////////////////////////////////////////////*/
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
//////////////////////////////////////////////////////////////*/
constructor(
string memory name_,
string memory symbol_,
bool paused_,
address[] memory voters,
uint256[] memory shares
) {
require(voters.length == shares.length, "NO_ARRAY_PARITY");
name = name_;
symbol = symbol_;
paused = paused_;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator();
for (uint256 i; i < voters.length; i++) {
_mint(voters[i], shares[i]);
_delegate(voters[i], voters[i]);
}
}
/*///////////////////////////////////////////////////////////////
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount) external notPaused 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
) external notPaused 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;
}
/*///////////////////////////////////////////////////////////////
DAO LOGIC
//////////////////////////////////////////////////////////////*/
modifier notPaused() {
require(!paused, "PAUSED");
_;
}
function getCurrentVotes(address account) external view returns (uint256 votes) {
unchecked {
uint256 nCheckpoints = numCheckpoints[account];
votes = nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0;
}
}
function delegate(address delegatee) external {
_delegate(msg.sender, delegatee);
}
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);
}
function getPriorVotes(address account, uint256 timestamp) public view returns (uint256 votes) {
require(block.timestamp > 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) internal {
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) internal {
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) internal {
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);
}
/*///////////////////////////////////////////////////////////////
EIP-2612 LOGIC
//////////////////////////////////////////////////////////////*/
function _computeDomainSeparator() internal view returns (bytes32 domainSeparator) {
domainSeparator = 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 domainSeparator) {
domainSeparator = block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _computeDomainSeparator();
}
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
require(block.timestamp <= deadline, "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);
}
/*///////////////////////////////////////////////////////////////
MINT/BURN LOGIC
//////////////////////////////////////////////////////////////*/
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);
}
/*///////////////////////////////////////////////////////////////
PAUSE LOGIC
//////////////////////////////////////////////////////////////*/
function _togglePause() internal {
paused = !paused;
emit TogglePause(paused);
}
}
/// @notice Helper for NFT 'safe' transfers.
contract LiteDAOnftHelper {
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure returns (bytes4 sig) {
sig = 0x150b7a02; // 'onERC721Received(address,address,uint,bytes)'
}
function onERC1155Received(
address,
address,
uint256,
uint256,
bytes calldata
) external pure returns (bytes4 sig) {
sig = 0xf23a6e61; // 'onERC1155Received(address,address,uint,uint,bytes)'
}
}
/// @notice Simple gas-optimized DAO core module.
contract LiteDAO is LiteDAOtoken, LiteDAOnftHelper {
event NewProposal(uint256 proposalId);
event ProposalProcessed(uint256 proposalId);
uint256 public proposalCount;
uint256 public votingPeriod;
mapping(uint256 => Proposal) public proposals;
enum ProposalType {
MINT,
BURN,
CALL,
GOV
}
struct Proposal {
ProposalType proposalType;
string description;
address account; // member being added/kicked; account to send money; or account receiving loot
address asset; // asset considered for payment
uint256 amount; // value to be minted/burned/spent
bytes payload; // data for CALL proposals
uint256 yesVotes;
uint256 noVotes;
uint256 creationTime;
}
/*///////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(
string memory name_,
string memory symbol_,
bool paused_,
address[] memory voters,
uint256[] memory shares,
uint256 votingPeriod_
)
LiteDAOtoken(
name_,
symbol_,
paused_,
voters,
shares
)
{
votingPeriod = votingPeriod_;
}
/*///////////////////////////////////////////////////////////////
PROPOSAL LOGIC
//////////////////////////////////////////////////////////////*/
modifier onlyTokenHolders() {
require(balanceOf[msg.sender] > 0, "NOT_TOKEN_HOLDER");
_;
}
function propose(
ProposalType proposalType,
string calldata description,
address account,
address asset,
uint256 amount,
bytes calldata payload
) external onlyTokenHolders {
uint256 proposalId = proposalCount;
proposals[proposalId] = Proposal({
proposalType: proposalType,
description: description,
account: account,
asset: asset,
amount: amount,
payload: payload,
yesVotes: 0,
noVotes: 0,
creationTime: block.timestamp
});
unchecked {
proposalCount++;
}
emit NewProposal(proposalId);
}
function vote(uint256 proposal, bool approve) external onlyTokenHolders {
Proposal storage prop = proposals[proposal];
unchecked {
require(block.timestamp <= prop.creationTime + votingPeriod, "VOTING_ENDED");
}
uint256 weight = getPriorVotes(msg.sender, prop.creationTime);
if (approve) {
prop.yesVotes += weight;
} else {
prop.noVotes += weight;
}
}
function processProposal(uint256 proposal) external returns (bool success) {
Proposal storage prop = proposals[proposal];
// * COMMENTED OUT FOR TESTING * ///
// require(block.timestamp > prop.creationTime + votingPeriod, "VOTING_NOT_ENDED");
bool didProposalPass = _weighVotes(prop.yesVotes, prop.noVotes);
if (didProposalPass) { // simple majority; can use module to override this
if (prop.proposalType == ProposalType.MINT) {
_mint(prop.account, prop.amount);
}
if (prop.proposalType == ProposalType.BURN) {
_burn(prop.account, prop.amount);
}
if (prop.proposalType == ProposalType.CALL) {
(success, ) = prop.account.call{value: prop.amount}(prop.payload);
}
if (prop.proposalType == ProposalType.GOV) {
if (prop.amount > 0) votingPeriod = prop.amount;
if (prop.payload.length > 0) _togglePause();
}
}
delete proposals[proposal];
emit ProposalProcessed(proposal);
}
function _weighVotes(uint256 yesVotes, uint256 noVotes) internal virtual returns (bool didProposalPass) {
if (yesVotes > noVotes) {
didProposalPass = true;
}
}
}
contract LiteDAOquorumRequired is LiteDAO {
uint256 immutable public quorum;
constructor(
string memory name_,
string memory symbol_,
bool paused_,
address[] memory voters,
uint256[] memory shares,
uint256 votingPeriod_,
uint256 quorum_
)
LiteDAO(
name_,
symbol_,
paused_,
voters,
shares,
votingPeriod_
)
{
quorum = quorum_;
}
function _weighVotes(uint256 yesVotes, uint256 noVotes) internal view override returns (bool didProposalPass) {
unchecked {
uint256 quor = totalSupply / quorum;
uint256 votes = yesVotes + noVotes;
require(votes >= quor, "QUORUM_REQUIRED");
}
if (yesVotes > noVotes) {
didProposalPass = true;
}
}
}
contract LiteDAOfactory {
address[] public daoRegistry;
mapping(address => VoteType) public voteType;
enum VoteType {
SIMPLE_MAJORITY,
SIMPLE_MAJORITY_QUORUM_REQUIRED
}
function deployDAO(
string calldata name_,
string calldata symbol_,
bool paused_,
address[] calldata voters_,
uint256[] calldata shares_,
uint256 votingPeriod_,
uint256 quorum_,
VoteType voteType_
) external {
if (voteType_ == VoteType.SIMPLE_MAJORITY) {
LiteDAO dao = new LiteDAO(
name_,
symbol_,
paused_,
voters_,
shares_,
votingPeriod_
);
daoRegistry.push(address(dao));
voteType[address(dao)] = voteType_;
}
if (voteType_ == VoteType.SIMPLE_MAJORITY_QUORUM_REQUIRED) {
LiteDAOquorumRequired dao = new LiteDAOquorumRequired(
name_,
symbol_,
paused_,
voters_,
shares_,
votingPeriod_,
quorum_
);
daoRegistry.push(address(dao));
voteType[address(dao)] = voteType_;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment