Last active
November 8, 2021 01:07
-
-
Save z0r0z/1154cece644b5d3d1efe43b8d77a06c2 to your computer and use it in GitHub Desktop.
LiteDAOfactory.sol
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 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