Created
November 20, 2021 07:56
-
-
Save z0r0z/2ef6f93c771613f309f0225a7e625a1e to your computer and use it in GitHub Desktop.
tribute to master chef and community of weirdos
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.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 NomiToken { | |
| /*/////////////////////////////////////////////////////////////// | |
| 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); | |
| /*/////////////////////////////////////////////////////////////// | |
| METADATA STORAGE | |
| //////////////////////////////////////////////////////////////*/ | |
| string public constant name = "Yomi Token"; | |
| string public constant symbol = "YOMI"; | |
| uint8 public constant decimals = 18; | |
| /*/////////////////////////////////////////////////////////////// | |
| ERC20 STORAGE | |
| //////////////////////////////////////////////////////////////*/ | |
| uint256 public totalSupply; | |
| mapping(address => uint256) public balanceOf; | |
| mapping(address => mapping(address => uint256)) public allowance; | |
| /*/////////////////////////////////////////////////////////////// | |
| DAO STORAGE | |
| //////////////////////////////////////////////////////////////*/ | |
| bytes32 public constant DELEGATION_TYPEHASH = keccak256('Delegation(address delegatee,uint256 nonce,uint256 expiry)'); | |
| mapping(address => address) private _delegates; | |
| mapping(address => mapping(uint256 => Checkpoint)) public checkpoints; | |
| mapping(address => uint256) public numCheckpoints; | |
| struct Checkpoint { | |
| uint32 fromTimestamp; | |
| uint224 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() { | |
| INITIAL_CHAIN_ID = block.chainid; | |
| INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator(); | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| 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 returns (bool) { | |
| balanceOf[msg.sender] -= amount; | |
| // this is safe from overflow because the sum of all user | |
| // balances can't exceed 'type(uint256).max' | |
| 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 | |
| ) external returns (bool) { | |
| if (allowance[from][msg.sender] != type(uint256).max) { | |
| allowance[from][msg.sender] -= amount; | |
| } | |
| balanceOf[from] -= amount; | |
| // this is safe from overflow because the sum of all user | |
| // balances can't exceed 'type(uint256).max' | |
| unchecked { | |
| balanceOf[to] += amount; | |
| } | |
| _moveDelegates(delegates(from), delegates(to), amount); | |
| emit Transfer(from, to, amount); | |
| return true; | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| DAO LOGIC | |
| //////////////////////////////////////////////////////////////*/ | |
| function delegates(address delegator) public view returns (address delegatee) { | |
| address current = _delegates[delegator]; | |
| delegatee = current == address(0) ? delegator : current; | |
| } | |
| function getCurrentVotes(address account) external view returns (uint256 votes) { | |
| // this is safe from underflow because decrement only occurs if `nCheckpoints` is positive | |
| 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'); | |
| // this is reasonably safe from overflow because incrementing `nonces` beyond | |
| // 'type(uint256).max' is exceedingly unlikely compared to optimization benefits | |
| 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; | |
| } | |
| // 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 { | |
| 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 { | |
| 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 { | |
| // 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 = safeCastTo224(newVotes); | |
| } else { | |
| checkpoints[delegatee][nCheckpoints] = Checkpoint(safeCastTo32(block.timestamp), safeCastTo224(newVotes)); | |
| // this is reasonably safe from overflow because incrementing `nCheckpoints` beyond | |
| // 'type(uint256).max' is exceedingly unlikely compared to optimization benefits | |
| 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); | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| SAFECAST LOGIC | |
| //////////////////////////////////////////////////////////////*/ | |
| function safeCastTo32(uint256 x) internal pure returns (uint32 y) { | |
| require(x <= type(uint32).max); | |
| y = uint32(x); | |
| } | |
| function safeCastTo224(uint256 x) internal pure returns (uint224 y) { | |
| require(x <= type(uint224).max); | |
| y = uint224(x); | |
| } | |
| } | |
| /// @notice Brief interface for entering and exiting SushiBar (xSUSHI). | |
| interface ISushiBarBasic { | |
| function balanceOf(address account) external view returns (uint256); | |
| function approve(address spender, uint256 amount) external returns (bool); | |
| function transfer(address recipient, uint256 amount) external returns (bool); | |
| function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); | |
| function enter(uint256 amount) external; | |
| function leave(uint256 share) external; | |
| } | |
| /// @notice Interface for depositing into & withdrawing from BentoBox vault. | |
| interface IBentoBoxBasic { | |
| function deposit( | |
| address token_, | |
| address from, | |
| address to, | |
| uint256 amount, | |
| uint256 share | |
| ) external payable returns (uint256 amountOut, uint256 shareOut); | |
| function withdraw( | |
| address token_, | |
| address from, | |
| address to, | |
| uint256 amount, | |
| uint256 share | |
| ) external returns (uint256 amountOut, uint256 shareOut); | |
| } | |
| /*============================ | |
| WELCOME TO THE PARTY (飲み会)! | |
| ============================*/ | |
| contract NomiDAO is NomiToken { | |
| /*/////////////////////////////////////////////////////////////// | |
| EVENTS | |
| //////////////////////////////////////////////////////////////*/ | |
| event NewProposal(uint256 indexed proposal); | |
| event VoteCast(address indexed voter, uint256 indexed proposal, bool indexed approve); | |
| event ProposalProcessed(uint256 indexed proposal); | |
| /*/////////////////////////////////////////////////////////////// | |
| DAO STORAGE | |
| //////////////////////////////////////////////////////////////*/ | |
| uint256 public proposalCount; | |
| uint224 private constant votingPeriod = 432000; // 5 days in seconds | |
| uint32 private constant quorum = 15; // % | |
| ISushiBarBasic private constant sushiToken = ISushiBarBasic(0x6B3595068778DD592e39A122f4f5a5cF09C90fE2); // SUSHI token contract | |
| ISushiBarBasic private constant sushiBar = ISushiBarBasic(0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272); // xSUSHI staking contract for SUSHI | |
| IBentoBoxBasic private constant bento = IBentoBoxBasic(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966); // BentoBox vault contract | |
| mapping(uint256 => Proposal) public proposals; | |
| mapping(uint256 => mapping(address => bool)) public voted; | |
| struct Proposal { | |
| address proposer; | |
| string description; | |
| address[] account; // account(s) receiving SUSHI | |
| uint256[] amount; // SUSHI amounts(s) | |
| uint256 bentoSum; | |
| uint224 yesVotes; | |
| uint224 noVotes; | |
| uint32 creationTime; | |
| } | |
| /// @dev reentrancy guard | |
| uint256 unlocked = 1; | |
| modifier nonReentrant() { | |
| require(unlocked == 1, "LOCKED"); | |
| unlocked = 2; | |
| _; | |
| unlocked = 1; | |
| } | |
| constructor() { | |
| sushiToken.approve(address(sushiBar), type(uint256).max); | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| DAO JOINDER | |
| //////////////////////////////////////////////////////////////*/ | |
| function join(uint256 amount, bool sushi) external nonReentrant returns (uint256 shares) { // if sushi 'true', `sushiToken` in | |
| if (sushi) { | |
| sushiToken.transferFrom(msg.sender, address(this), amount); | |
| sushiBar.enter(amount); | |
| (, shares) = bento.deposit(address(sushiBar), address(this), address(this), sushiBar.balanceOf(address(this)), 0); | |
| } else { | |
| sushiBar.transferFrom(msg.sender, address(bento), amount); | |
| (, shares) = bento.deposit(address(sushiBar), address(bento), address(this), amount, 0); | |
| } | |
| _mint(msg.sender, shares); | |
| _moveDelegates(address(0), delegates(msg.sender), shares); | |
| } | |
| function exit(uint256 shares, bool sushi) external nonReentrant returns (uint256 amount) { // if sushi 'true', `sushiToken` out | |
| _burn(msg.sender, shares); | |
| _moveDelegates(delegates(msg.sender), address(0), shares); | |
| if (sushi) { | |
| (amount, ) = bento.withdraw(address(sushiBar), address(this), address(this), 0, shares); | |
| sushiBar.leave(amount); | |
| sushiToken.transfer(msg.sender, sushiToken.balanceOf(address(this))); | |
| } else { | |
| (amount, ) = bento.withdraw(address(sushiBar), address(this), msg.sender, 0, shares); | |
| } | |
| } | |
| /*/////////////////////////////////////////////////////////////// | |
| PROPOSAL LOGIC | |
| //////////////////////////////////////////////////////////////*/ | |
| modifier onlyTokenHolders() { | |
| require(balanceOf[msg.sender] > 0, 'NOT_TOKEN_HOLDER'); | |
| _; | |
| } | |
| function propose( | |
| string calldata description, | |
| address[] calldata account, | |
| uint256[] calldata amount | |
| ) external nonReentrant onlyTokenHolders returns (uint256 bentoSum) { | |
| require(account.length == amount.length, 'NO_ARRAY_PARITY'); | |
| require(account.length <= 10, 'ARRAY_MAX'); | |
| // tally up `sum` from `amount` array | |
| uint256 sum; | |
| unchecked { | |
| for (uint256 i = 0; i < amount.length; i++) { | |
| sum += amount[i]; | |
| } | |
| } | |
| // 5% deposit of requested sushi | |
| uint256 deposit = sum / 20; | |
| sushiToken.transferFrom(msg.sender, address(this), deposit); | |
| sushiBar.enter(deposit); | |
| (, bentoSum) = bento.deposit(address(sushiBar), address(this), address(this), sushiBar.balanceOf(address(this)), 0); | |
| uint256 proposal = proposalCount; | |
| proposals[proposal] = Proposal({ | |
| proposer: msg.sender, | |
| description: description, | |
| account: account, | |
| amount: amount, | |
| bentoSum: bentoSum, | |
| yesVotes: 0, | |
| noVotes: 0, | |
| creationTime: safeCastTo32(block.timestamp) | |
| }); | |
| // this is reasonably safe from overflow because incrementing `proposalCount` beyond | |
| // 'type(uint256).max' is exceedingly unlikely compared to optimization benefits | |
| unchecked { | |
| proposalCount++; | |
| } | |
| emit NewProposal(proposal); | |
| } | |
| function vote(uint256 proposal, bool approve) external nonReentrant onlyTokenHolders { | |
| require(!voted[proposal][msg.sender], 'ALREADY_VOTED'); | |
| Proposal storage prop = proposals[proposal]; | |
| // this is safe from overflow because `votingPeriod` is capped so it will not combine | |
| // with unix time to exceed 'type(uint256).max' | |
| unchecked { | |
| require(block.timestamp <= prop.creationTime + votingPeriod, 'VOTING_ENDED'); | |
| } | |
| uint224 weight = uint224(getPriorVotes(msg.sender, prop.creationTime)); | |
| // this is safe from overflow because `yesVotes` and `noVotes` are capped by `totalSupply` | |
| // which is checked for overflow in `YomiToken` contract | |
| unchecked { | |
| if (approve) { | |
| prop.yesVotes += weight; | |
| } else { | |
| prop.noVotes += weight; | |
| } | |
| } | |
| voted[proposal][msg.sender] = true; | |
| emit VoteCast(msg.sender, proposal, approve); | |
| } | |
| function processProposal(uint256 proposal) external nonReentrant { | |
| Proposal storage prop = proposals[proposal]; | |
| // this is safe from overflow because `votingPeriod` is capped so it will not combine | |
| // with unix time to exceed 'type(uint256).max' | |
| unchecked { | |
| require(block.timestamp > prop.creationTime + votingPeriod, 'VOTING_NOT_ENDED'); | |
| } | |
| uint256 minVotes = (totalSupply * quorum) / 100; | |
| // this is safe from overflow because `yesVotes` and `noVotes` are capped by `totalSupply` | |
| // which is checked for overflow in `LiteDAOtoken` contract | |
| unchecked { | |
| uint224 votes = prop.yesVotes + prop.noVotes; | |
| require(votes >= minVotes, 'QUORUM_REQUIRED'); | |
| } | |
| if (prop.yesVotes > prop.noVotes) { | |
| // this is reasonably safe from overflow because incrementing `i` loop beyond | |
| // 'type(uint256).max' is exceedingly unlikely compared to optimization benefits | |
| unchecked { | |
| for (uint256 i; i < prop.account.length; i++) { | |
| sushiToken.transfer(prop.account[i], prop.amount[i]); | |
| } | |
| } | |
| } | |
| delete proposals[proposal]; | |
| emit ProposalProcessed(proposal); | |
| } | |
| receive() external payable {} | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment