Created
January 9, 2022 19:43
-
-
Save z0r0z/f981a3ec956458445b26acc13e840715 to your computer and use it in GitHub Desktop.
programmatically enforce membership doesn't exceed 100 investment club limit
This file contains hidden or 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.4; | |
| /// @notice Modern and gas-optimized ERC-20 + EIP-2612 implementation with COMP-style governance and pausing. | |
| /// @author Modified from RariCapital (https://github.com/Rari-Capital/solmate/blob/main/src/erc20/ERC20.sol) | |
| /// License-Identifier: AGPL-3.0-only | |
| abstract contract KaliDAOtoken { | |
| /*/////////////////////////////////////////////////////////////// | |
| 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 PauseFlipped(bool paused); | |
| /*/////////////////////////////////////////////////////////////// | |
| ERRORS | |
| //////////////////////////////////////////////////////////////*/ | |
| error NoArrayParity(); | |
| error Paused(); | |
| error SignatureExpired(); | |
| error NullAddress(); | |
| error InvalidNonce(); | |
| error NotDetermined(); | |
| error InvalidSignature(); | |
| error Uint32max(); | |
| error Uint96max(); | |
| /*/////////////////////////////////////////////////////////////// | |
| METADATA STORAGE | |
| //////////////////////////////////////////////////////////////*/ | |
| string public name; | |
| string public symbol; | |
| uint8 public constant decimals = 18; | |
| /*/////////////////////////////////////////////////////////////// | |
| ERC-20 STORAGE | |
| //////////////////////////////////////////////////////////////*/ | |
| uint256 public totalSupply; | |
| mapping(address => uint256) public balanceOf; | |
| mapping(address => mapping(address => uint256)) public allowance; | |
| /*/////////////////////////////////////////////////////////////// | |
| EIP-2612 STORAGE | |
| //////////////////////////////////////////////////////////////*/ | |
| bytes32 public constant PERMIT_TYPEHASH = | |
| keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); | |
| uint256 internal INITIAL_CHAIN_ID; | |
| bytes32 internal INITIAL_DOMAIN_SEPARATOR; | |
| mapping(address => uint256) public nonces; | |
| /*/////////////////////////////////////////////////////////////// | |
| DAO STORAGE | |
| //////////////////////////////////////////////////////////////*/ | |
| bool public paused; | |
| bytes32 public constant DELEGATION_TYPEHASH = | |
| keccak256('Delegation(address delegatee,uint256 nonce,uint256 expiry)'); | |
| mapping(address => address) internal _delegates; | |
| mapping(address => mapping(uint256 => Checkpoint)) public checkpoints; | |
| mapping(address => uint256) public numCheckpoints; | |
| struct Checkpoint { | |
| uint32 fromTimestamp; | |
| uint96 votes; | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| CONSTRUCTOR | |
| //////////////////////////////////////////////////////////////*/ | |
| function _init( | |
| string memory name_, | |
| string memory symbol_, | |
| bool paused_, | |
| address[] memory voters_, | |
| uint256[] memory shares_ | |
| ) internal virtual { | |
| if (voters_.length != shares_.length) revert NoArrayParity(); | |
| name = name_; | |
| symbol = symbol_; | |
| paused = paused_; | |
| INITIAL_CHAIN_ID = block.chainid; | |
| INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator(); | |
| // cannot realistically overflow on human timescales | |
| unchecked { | |
| for (uint256 i; i < voters_.length; i++) { | |
| _mint(voters_[i], shares_[i]); | |
| } | |
| } | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| ERC-20 LOGIC | |
| //////////////////////////////////////////////////////////////*/ | |
| 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 notPaused virtual returns (bool) { | |
| balanceOf[msg.sender] -= amount; | |
| // cannot overflow because the sum of all user | |
| // balances can't exceed the max uint256 value | |
| unchecked { | |
| balanceOf[to] += amount; | |
| } | |
| _moveDelegates(delegates(msg.sender), delegates(to), amount); | |
| emit Transfer(msg.sender, to, amount); | |
| return true; | |
| } | |
| function transferFrom( | |
| address from, | |
| address to, | |
| uint256 amount | |
| ) public notPaused virtual returns (bool) { | |
| if (allowance[from][msg.sender] != type(uint256).max) | |
| allowance[from][msg.sender] -= amount; | |
| balanceOf[from] -= amount; | |
| // cannot overflow because the sum of all user | |
| // balances can't exceed the max uint256 value | |
| unchecked { | |
| balanceOf[to] += amount; | |
| } | |
| _moveDelegates(delegates(from), delegates(to), amount); | |
| emit Transfer(from, to, amount); | |
| return true; | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| EIP-2612 LOGIC | |
| //////////////////////////////////////////////////////////////*/ | |
| function permit( | |
| address owner, | |
| address spender, | |
| uint256 value, | |
| uint256 deadline, | |
| uint8 v, | |
| bytes32 r, | |
| bytes32 s | |
| ) public virtual { | |
| if (block.timestamp > deadline) revert SignatureExpired(); | |
| // cannot realistically overflow on human timescales | |
| 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); | |
| if (recoveredAddress == address(0) || recoveredAddress != owner) revert InvalidSignature(); | |
| allowance[recoveredAddress][spender] = value; | |
| } | |
| emit Approval(owner, spender, value); | |
| } | |
| function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { | |
| return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _computeDomainSeparator(); | |
| } | |
| function _computeDomainSeparator() internal view virtual returns (bytes32) { | |
| return | |
| keccak256( | |
| abi.encode( | |
| keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), | |
| keccak256(bytes(name)), | |
| keccak256('1'), | |
| block.chainid, | |
| address(this) | |
| ) | |
| ); | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| DAO LOGIC | |
| //////////////////////////////////////////////////////////////*/ | |
| modifier notPaused() { | |
| if (paused) revert Paused(); | |
| _; | |
| } | |
| function delegates(address delegator) public view virtual returns (address) { | |
| address current = _delegates[delegator]; | |
| return current == address(0) ? delegator : current; | |
| } | |
| function getCurrentVotes(address account) public view virtual returns (uint256) { | |
| // this is safe from underflow because decrement only occurs if `nCheckpoints` is positive | |
| unchecked { | |
| uint256 nCheckpoints = numCheckpoints[account]; | |
| return nCheckpoints != 0 ? checkpoints[account][nCheckpoints - 1].votes : 0; | |
| } | |
| } | |
| function delegate(address delegatee) public virtual { | |
| _delegate(msg.sender, delegatee); | |
| } | |
| function delegateBySig( | |
| address delegatee, | |
| uint256 nonce, | |
| uint256 deadline, | |
| uint8 v, | |
| bytes32 r, | |
| bytes32 s | |
| ) public virtual { | |
| if (block.timestamp > deadline) revert SignatureExpired(); | |
| bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, deadline)); | |
| bytes32 digest = keccak256(abi.encodePacked('\x19\x01', DOMAIN_SEPARATOR(), structHash)); | |
| address signatory = ecrecover(digest, v, r, s); | |
| if (signatory == address(0)) revert NullAddress(); | |
| // cannot realistically overflow on human timescales | |
| unchecked { | |
| if (nonce != nonces[signatory]++) revert InvalidNonce(); | |
| } | |
| _delegate(signatory, delegatee); | |
| } | |
| function getPriorVotes(address account, uint256 timestamp) public view virtual returns (uint96) { | |
| if (block.timestamp <= timestamp) revert NotDetermined(); | |
| uint256 nCheckpoints = numCheckpoints[account]; | |
| if (nCheckpoints == 0) return 0; | |
| // this is safe from underflow because decrement only occurs if `nCheckpoints` is positive | |
| unchecked { | |
| if (checkpoints[account][nCheckpoints - 1].fromTimestamp <= timestamp) | |
| return checkpoints[account][nCheckpoints - 1].votes; | |
| if (checkpoints[account][0].fromTimestamp > timestamp) return 0; | |
| uint256 lower; | |
| // this is safe from underflow because decrement only occurs if `nCheckpoints` is positive | |
| uint256 upper = nCheckpoints - 1; | |
| while (upper > lower) { | |
| // this is safe from underflow because `upper` ceiling is provided | |
| 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 virtual { | |
| 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 virtual { | |
| 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 virtual { | |
| unchecked { | |
| // this is safe from underflow because decrement only occurs if `nCheckpoints` is positive | |
| if (nCheckpoints != 0 && checkpoints[delegatee][nCheckpoints - 1].fromTimestamp == block.timestamp) { | |
| checkpoints[delegatee][nCheckpoints - 1].votes = _safeCastTo96(newVotes); | |
| } else { | |
| checkpoints[delegatee][nCheckpoints] = Checkpoint(_safeCastTo32(block.timestamp), _safeCastTo96(newVotes)); | |
| // cannot realistically overflow on human timescales | |
| numCheckpoints[delegatee] = nCheckpoints + 1; | |
| } | |
| } | |
| emit DelegateVotesChanged(delegatee, oldVotes, newVotes); | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| MINT/BURN LOGIC | |
| //////////////////////////////////////////////////////////////*/ | |
| function _mint(address to, uint256 amount) internal virtual { | |
| totalSupply += amount; | |
| // cannot overflow because the sum of all user | |
| // balances can't exceed the max uint256 value | |
| unchecked { | |
| balanceOf[to] += amount; | |
| } | |
| _moveDelegates(address(0), delegates(to), amount); | |
| emit Transfer(address(0), to, amount); | |
| } | |
| function _burn(address from, uint256 amount) internal virtual { | |
| balanceOf[from] -= amount; | |
| // cannot underflow because a user's balance | |
| // will never be larger than the total supply | |
| unchecked { | |
| totalSupply -= amount; | |
| } | |
| _moveDelegates(delegates(from), address(0), amount); | |
| emit Transfer(from, address(0), amount); | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| PAUSE LOGIC | |
| //////////////////////////////////////////////////////////////*/ | |
| function _flipPause() internal virtual { | |
| paused = !paused; | |
| emit PauseFlipped(paused); | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| SAFECAST LOGIC | |
| //////////////////////////////////////////////////////////////*/ | |
| function _safeCastTo32(uint256 x) internal pure virtual returns (uint32) { | |
| if (x > type(uint32).max) revert Uint32max(); | |
| return uint32(x); | |
| } | |
| function _safeCastTo96(uint256 x) internal pure virtual returns (uint96) { | |
| if (x > type(uint96).max) revert Uint96max(); | |
| return uint96(x); | |
| } | |
| } | |
| /// @notice Helper utility that enables calling multiple local methods in a single call. | |
| /// @author Modified from Uniswap (https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/Multicall.sol) | |
| abstract contract Multicall { | |
| function multicall(bytes[] calldata data) public virtual returns (bytes[] memory results) { | |
| results = new bytes[](data.length); | |
| // cannot realistically overflow on human timescales | |
| unchecked { | |
| for (uint256 i = 0; i < data.length; i++) { | |
| (bool success, bytes memory result) = address(this).delegatecall(data[i]); | |
| if (!success) { | |
| if (result.length < 68) revert(); | |
| assembly { | |
| result := add(result, 0x04) | |
| } | |
| revert(abi.decode(result, (string))); | |
| } | |
| results[i] = result; | |
| } | |
| } | |
| } | |
| } | |
| /// @notice Helper utility for NFT 'safe' transfers. | |
| abstract contract NFThelper { | |
| function onERC721Received( | |
| address, | |
| address, | |
| uint256, | |
| bytes calldata | |
| ) external pure returns (bytes4 sig) { | |
| sig = 0x150b7a02; // 'onERC721Received(address,address,uint256,bytes)' | |
| } | |
| function onERC1155Received( | |
| address, | |
| address, | |
| uint256, | |
| uint256, | |
| bytes calldata | |
| ) external pure returns (bytes4 sig) { | |
| sig = 0xf23a6e61; // 'onERC1155Received(address,address,uint256,uint256,bytes)' | |
| } | |
| function onERC1155BatchReceived( | |
| address, | |
| address, | |
| uint256[] calldata, | |
| uint256[] calldata, | |
| bytes calldata | |
| ) external pure returns (bytes4 sig) { | |
| sig = 0xbc197c81; // 'onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)' | |
| } | |
| } | |
| /// @notice Gas-optimized reentrancy protection. | |
| /// @author Modified from OpenZeppelin | |
| /// (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) | |
| /// License-Identifier: MIT | |
| abstract contract ReentrancyGuard { | |
| error Reentrancy(); | |
| uint256 private constant NOT_ENTERED = 1; | |
| uint256 private constant ENTERED = 2; | |
| uint256 private status = NOT_ENTERED; | |
| modifier nonReentrant() { | |
| if (status == ENTERED) revert Reentrancy(); | |
| status = ENTERED; | |
| _; | |
| status = NOT_ENTERED; | |
| } | |
| } | |
| /// @notice Kali DAO membership extension interface. | |
| interface IKaliDAOextension { | |
| function setExtension(bytes calldata extensionData) external; | |
| function callExtension( | |
| address account, | |
| uint256 amount, | |
| bytes calldata extensionData | |
| ) external payable returns (bool mint, uint256 amountOut); | |
| } | |
| /// @notice Simple gas-optimized Kali DAO core module. | |
| contract KaliDAOinvestmentClub is KaliDAOtoken, Multicall, NFThelper, ReentrancyGuard { | |
| /*/////////////////////////////////////////////////////////////// | |
| EVENTS | |
| //////////////////////////////////////////////////////////////*/ | |
| event NewProposal(address indexed proposer, uint256 indexed proposal); | |
| event ProposalCancelled(address indexed proposer, uint256 indexed proposal); | |
| event ProposalSponsored(address indexed sponsor, uint256 indexed proposal); | |
| event VoteCast(address indexed voter, uint256 indexed proposal, bool indexed approve); | |
| event ProposalProcessed(uint256 indexed proposal, bool indexed didProposalPass); | |
| /*/////////////////////////////////////////////////////////////// | |
| ERRORS | |
| //////////////////////////////////////////////////////////////*/ | |
| error Initialized(); | |
| error VotingPeriodBounds(); | |
| error QuorumMax(); | |
| error SupermajorityBounds(); | |
| error TypeBounds(); | |
| error NotProposer(); | |
| error Sponsored(); | |
| error NotMember(); | |
| error NotCurrentProposal(); | |
| error AlreadyVoted(); | |
| error NotVoteable(); | |
| error VotingNotEnded(); | |
| error PrevNotProcessed(); | |
| error VoterMax(); | |
| error NotExtension(); | |
| /*/////////////////////////////////////////////////////////////// | |
| DAO STORAGE | |
| //////////////////////////////////////////////////////////////*/ | |
| string public docs; | |
| uint256 private currentSponsoredProposal; | |
| uint256 public proposalCount; | |
| uint256 public voterCount; | |
| uint32 public votingPeriod; | |
| uint8 public quorum; // 1-100 | |
| uint8 public supermajority; // 1-100 | |
| bytes32 public constant VOTE_HASH = | |
| keccak256('SignVote(address signer,uint256 proposal,bool approve)'); | |
| mapping(address => bool) public extensions; | |
| mapping(uint256 => Proposal) public proposals; | |
| mapping(uint256 => ProposalState) public proposalStates; | |
| mapping(ProposalType => VoteType) public proposalVoteTypes; | |
| mapping(uint256 => mapping(address => bool)) public voted; | |
| mapping(address => uint256) public lastYesVote; | |
| enum ProposalType { | |
| MINT, // add membership | |
| BURN, // revoke membership | |
| CALL, // call contracts | |
| PERIOD, // set `votingPeriod` | |
| QUORUM, // set `quorum` | |
| SUPERMAJORITY, // set `supermajority` | |
| TYPE, // set `VoteType` to `ProposalType` | |
| PAUSE, // flip membership transferability | |
| EXTENSION, // flip `extensions` whitelisting | |
| ESCAPE, // delete pending proposal in case of revert | |
| DOCS // amend org docs | |
| } | |
| enum VoteType { | |
| SIMPLE_MAJORITY, | |
| SIMPLE_MAJORITY_QUORUM_REQUIRED, | |
| SUPERMAJORITY, | |
| SUPERMAJORITY_QUORUM_REQUIRED | |
| } | |
| struct Proposal { | |
| ProposalType proposalType; | |
| string description; | |
| address[] accounts; // member(s) being added/kicked; account(s) receiving payload | |
| uint256[] amounts; // value(s) to be minted/burned/spent; gov setting [0] | |
| bytes[] payloads; // data for CALL proposals | |
| uint256 prevProposal; | |
| uint96 yesVotes; | |
| uint96 noVotes; | |
| uint32 creationTime; | |
| address proposer; | |
| } | |
| struct ProposalState { | |
| bool passed; | |
| bool processed; | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| CONSTRUCTOR | |
| //////////////////////////////////////////////////////////////*/ | |
| function init( | |
| string memory name_, | |
| string memory symbol_, | |
| string memory docs_, | |
| bool, | |
| address[] memory extensions_, | |
| bytes[] memory extensionsData_, | |
| address[] calldata voters_, | |
| uint256[] calldata shares_, | |
| uint32 votingPeriod_, | |
| uint8[13] memory govSettings_ | |
| ) public payable nonReentrant virtual { | |
| if (extensions_.length != extensionsData_.length) revert NoArrayParity(); | |
| if (votingPeriod != 0) revert Initialized(); | |
| if (votingPeriod_ == 0 || votingPeriod_ > 365 days) revert VotingPeriodBounds(); | |
| if (govSettings_[0] > 100) revert QuorumMax(); | |
| if (govSettings_[1] <= 51 || govSettings_[1] > 100) revert SupermajorityBounds(); | |
| KaliDAOtoken._init(name_, symbol_, true, voters_, shares_); | |
| if (extensions_.length != 0) { | |
| // cannot realistically overflow on human timescales | |
| unchecked { | |
| for (uint256 i; i < extensions_.length; i++) { | |
| extensions[extensions_[i]] = true; | |
| if (extensionsData_[i].length != 0) IKaliDAOextension(extensions_[i]) | |
| .setExtension(extensionsData_[i]); | |
| } | |
| } | |
| } | |
| for (uint256 i; i < voters_.length; i++) { | |
| voterCount++; | |
| } | |
| if (voterCount > 100) revert VoterMax(); | |
| docs = docs_; | |
| votingPeriod = votingPeriod_; | |
| quorum = govSettings_[0]; | |
| supermajority = govSettings_[1]; | |
| // set initial vote types | |
| proposalVoteTypes[ProposalType.MINT] = VoteType(govSettings_[2]); | |
| proposalVoteTypes[ProposalType.BURN] = VoteType(govSettings_[3]); | |
| proposalVoteTypes[ProposalType.CALL] = VoteType(govSettings_[4]); | |
| proposalVoteTypes[ProposalType.PERIOD] = VoteType(govSettings_[5]); | |
| proposalVoteTypes[ProposalType.QUORUM] = VoteType(govSettings_[6]); | |
| proposalVoteTypes[ProposalType.SUPERMAJORITY] = VoteType(govSettings_[7]); | |
| proposalVoteTypes[ProposalType.TYPE] = VoteType(govSettings_[8]); | |
| proposalVoteTypes[ProposalType.PAUSE] = VoteType(govSettings_[9]); | |
| proposalVoteTypes[ProposalType.EXTENSION] = VoteType(govSettings_[10]); | |
| proposalVoteTypes[ProposalType.ESCAPE] = VoteType(govSettings_[11]); | |
| proposalVoteTypes[ProposalType.DOCS] = VoteType(govSettings_[12]); | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| PROPOSAL LOGIC | |
| //////////////////////////////////////////////////////////////*/ | |
| function getProposalArrays(uint256 proposal) public view virtual returns ( | |
| address[] memory accounts, | |
| uint256[] memory amounts, | |
| bytes[] memory payloads | |
| ) { | |
| Proposal storage prop = proposals[proposal]; | |
| (accounts, amounts, payloads) = (prop.accounts, prop.amounts, prop.payloads); | |
| } | |
| function propose( | |
| ProposalType proposalType, | |
| string calldata description, | |
| address[] calldata accounts, | |
| uint256[] calldata amounts, | |
| bytes[] calldata payloads | |
| ) public nonReentrant virtual returns (uint256 proposal) { | |
| if (accounts.length != amounts.length || amounts.length != payloads.length) revert NoArrayParity(); | |
| if (proposalType == ProposalType.PERIOD) if (amounts[0] == 0 || amounts[0] > 365 days) revert VotingPeriodBounds(); | |
| if (proposalType == ProposalType.QUORUM) if (amounts[0] > 100) revert QuorumMax(); | |
| if (proposalType == ProposalType.SUPERMAJORITY) if (amounts[0] <= 51 || amounts[0] > 100) revert SupermajorityBounds(); | |
| if (proposalType == ProposalType.TYPE) if (amounts[0] > 10 || amounts[1] > 3 || amounts.length != 2) revert TypeBounds(); | |
| bool selfSponsor; | |
| // if member or extension is making proposal, include sponsorship | |
| if (balanceOf[msg.sender] != 0 || extensions[msg.sender]) selfSponsor = true; | |
| // cannot realistically overflow on human timescales | |
| unchecked { | |
| proposalCount++; | |
| } | |
| proposal = proposalCount; | |
| proposals[proposal] = Proposal({ | |
| proposalType: proposalType, | |
| description: description, | |
| accounts: accounts, | |
| amounts: amounts, | |
| payloads: payloads, | |
| prevProposal: selfSponsor ? currentSponsoredProposal : 0, | |
| yesVotes: 0, | |
| noVotes: 0, | |
| creationTime: selfSponsor ? _safeCastTo32(block.timestamp) : 0, | |
| proposer: msg.sender | |
| }); | |
| if (selfSponsor) currentSponsoredProposal = proposal; | |
| emit NewProposal(msg.sender, proposal); | |
| } | |
| function cancelProposal(uint256 proposal) public nonReentrant virtual { | |
| Proposal storage prop = proposals[proposal]; | |
| if (msg.sender != prop.proposer) revert NotProposer(); | |
| if (prop.creationTime != 0) revert Sponsored(); | |
| delete proposals[proposal]; | |
| emit ProposalCancelled(msg.sender, proposal); | |
| } | |
| function sponsorProposal(uint256 proposal) public nonReentrant virtual { | |
| Proposal storage prop = proposals[proposal]; | |
| if (balanceOf[msg.sender] == 0) revert NotMember(); | |
| if (prop.proposer == address(0)) revert NotCurrentProposal(); | |
| if (prop.creationTime != 0) revert Sponsored(); | |
| prop.prevProposal = currentSponsoredProposal; | |
| currentSponsoredProposal = proposal; | |
| prop.creationTime = _safeCastTo32(block.timestamp); | |
| emit ProposalSponsored(msg.sender, proposal); | |
| } | |
| function vote(uint256 proposal, bool approve) public nonReentrant virtual { | |
| _vote(msg.sender, proposal, approve); | |
| } | |
| function voteBySig( | |
| address signer, | |
| uint256 proposal, | |
| bool approve, | |
| uint8 v, | |
| bytes32 r, | |
| bytes32 s | |
| ) public nonReentrant virtual { | |
| bytes32 digest = | |
| keccak256( | |
| abi.encodePacked( | |
| '\x19\x01', | |
| DOMAIN_SEPARATOR(), | |
| keccak256( | |
| abi.encode( | |
| VOTE_HASH, | |
| signer, | |
| proposal, | |
| approve | |
| ) | |
| ) | |
| ) | |
| ); | |
| address recoveredAddress = ecrecover(digest, v, r, s); | |
| if (recoveredAddress != signer) revert InvalidSignature(); | |
| _vote(signer, proposal, approve); | |
| } | |
| function _vote( | |
| address signer, | |
| uint256 proposal, | |
| bool approve | |
| ) internal virtual { | |
| Proposal storage prop = proposals[proposal]; | |
| if (balanceOf[signer] == 0) revert NotMember(); | |
| if (voted[proposal][signer]) revert AlreadyVoted(); | |
| // this is safe from overflow because `votingPeriod` is capped so it will not combine | |
| // with unix time to exceed the max uint256 value | |
| unchecked { | |
| if (block.timestamp > prop.creationTime + votingPeriod) revert NotVoteable(); | |
| } | |
| uint96 weight = getPriorVotes(signer, prop.creationTime); | |
| // this is safe from overflow because `yesVotes` and `noVotes` are capped by `totalSupply` | |
| // which is checked for overflow in `KaliDAOtoken` contract | |
| unchecked { | |
| if (approve) { | |
| prop.yesVotes += weight; | |
| lastYesVote[signer] = proposal; | |
| } else { | |
| prop.noVotes += weight; | |
| } | |
| } | |
| voted[proposal][signer] = true; | |
| emit VoteCast(signer, proposal, approve); | |
| } | |
| function processProposal(uint256 proposal) public nonReentrant virtual returns ( | |
| bool didProposalPass, bytes[] memory results | |
| ) { | |
| Proposal storage prop = proposals[proposal]; | |
| VoteType voteType = proposalVoteTypes[prop.proposalType]; | |
| if (prop.creationTime == 0) revert NotCurrentProposal(); | |
| // this is safe from overflow because `votingPeriod` is capped so it will not combine | |
| // with unix time to exceed the max uint256 value | |
| unchecked { | |
| if (block.timestamp <= prop.creationTime + votingPeriod) revert VotingNotEnded(); | |
| } | |
| // skip previous proposal processing requirement in case of escape hatch | |
| if (prop.proposalType != ProposalType.ESCAPE) | |
| if (proposals[prop.prevProposal].creationTime != 0) revert PrevNotProcessed(); | |
| didProposalPass = _countVotes(voteType, prop.yesVotes, prop.noVotes); | |
| if (didProposalPass) { | |
| // cannot realistically overflow on human timescales | |
| unchecked { | |
| if (prop.proposalType == ProposalType.MINT) | |
| for (uint256 i; i < prop.accounts.length; i++) { | |
| _mint(prop.accounts[i], prop.amounts[i]); | |
| if (balanceOf[prop.accounts[i]] == 0) voterCount++; | |
| } | |
| if (voterCount > 100) revert VoterMax(); | |
| if (prop.proposalType == ProposalType.BURN) | |
| for (uint256 i; i < prop.accounts.length; i++) { | |
| _burn(prop.accounts[i], prop.amounts[i]); | |
| } | |
| if (prop.proposalType == ProposalType.CALL) | |
| for (uint256 i; i < prop.accounts.length; i++) { | |
| results = new bytes[](prop.accounts.length); | |
| (, bytes memory result) = prop.accounts[i].call{value: prop.amounts[i]} | |
| (prop.payloads[i]); | |
| results[i] = result; | |
| } | |
| // governance settings | |
| if (prop.proposalType == ProposalType.PERIOD) | |
| if (prop.amounts[0] != 0) votingPeriod = uint32(prop.amounts[0]); | |
| if (prop.proposalType == ProposalType.QUORUM) | |
| if (prop.amounts[0] != 0) quorum = uint8(prop.amounts[0]); | |
| if (prop.proposalType == ProposalType.SUPERMAJORITY) | |
| if (prop.amounts[0] != 0) supermajority = uint8(prop.amounts[0]); | |
| if (prop.proposalType == ProposalType.TYPE) | |
| proposalVoteTypes[ProposalType(prop.amounts[0])] = VoteType(prop.amounts[1]); | |
| if (prop.proposalType == ProposalType.PAUSE) | |
| _flipPause(); | |
| if (prop.proposalType == ProposalType.EXTENSION) | |
| for (uint256 i; i < prop.accounts.length; i++) { | |
| if (prop.amounts[i] != 0) | |
| extensions[prop.accounts[i]] = !extensions[prop.accounts[i]]; | |
| if (prop.payloads[i].length != 0) IKaliDAOextension(prop.accounts[i]) | |
| .setExtension(prop.payloads[i]); | |
| } | |
| if (prop.proposalType == ProposalType.ESCAPE) | |
| delete proposals[prop.amounts[0]]; | |
| if (prop.proposalType == ProposalType.DOCS) | |
| docs = prop.description; | |
| proposalStates[proposal].passed = true; | |
| } | |
| } | |
| delete proposals[proposal]; | |
| proposalStates[proposal].processed = true; | |
| emit ProposalProcessed(proposal, didProposalPass); | |
| } | |
| function _countVotes( | |
| VoteType voteType, | |
| uint256 yesVotes, | |
| uint256 noVotes | |
| ) internal view virtual returns (bool didProposalPass) { | |
| // fail proposal if no participation | |
| if (yesVotes == 0 && noVotes == 0) return false; | |
| // rule out any failed quorums | |
| if (voteType == VoteType.SIMPLE_MAJORITY_QUORUM_REQUIRED || voteType == VoteType.SUPERMAJORITY_QUORUM_REQUIRED) { | |
| uint256 minVotes = (totalSupply * quorum) / 100; | |
| // this is safe from overflow because `yesVotes` and `noVotes` | |
| // supply are checked in `KaliDAOtoken` contract | |
| unchecked { | |
| uint256 votes = yesVotes + noVotes; | |
| if (votes < minVotes) return false; | |
| } | |
| } | |
| // simple majority check | |
| if (voteType == VoteType.SIMPLE_MAJORITY || voteType == VoteType.SIMPLE_MAJORITY_QUORUM_REQUIRED) { | |
| if (yesVotes > noVotes) return true; | |
| // supermajority check | |
| } else { | |
| // example: 7 yes, 2 no, supermajority = 66 | |
| // ((7+2) * 66) / 100 = 5.94; 7 yes will pass | |
| uint256 minYes = ((yesVotes + noVotes) * supermajority) / 100; | |
| if (yesVotes >= minYes) return true; | |
| } | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| UTILITIES | |
| //////////////////////////////////////////////////////////////*/ | |
| receive() external payable virtual {} | |
| function callExtension( | |
| address extension, | |
| uint256 amount, | |
| bytes calldata extensionData | |
| ) public payable nonReentrant virtual returns (bool mint, uint256 amountOut) { | |
| if (!extensions[extension] && !extensions[msg.sender]) revert NotExtension(); | |
| address account; | |
| if (extensions[msg.sender]) { | |
| account = extension; | |
| amountOut = amount; | |
| mint = abi.decode(extensionData, (bool)); | |
| } else { | |
| account = msg.sender; | |
| (mint, amountOut) = IKaliDAOextension(extension).callExtension{value: msg.value} | |
| (msg.sender, amount, extensionData); | |
| } | |
| if (mint) { | |
| if (amountOut != 0) _mint(account, amountOut); | |
| } else { | |
| if (amountOut != 0) _burn(account, amount); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment