-
-
Save saintsal/57a965749acfafe1b0fc03ed63f32283 to your computer and use it in GitHub Desktop.
EIP-712-signed multi-signature contract with NFT identifiers for signers and ragequit
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 | |
import "https://github.com/Rari-Capital/solmate/src/tokens/ERC721.sol"; | |
import "https://github.com/kalidao/kali-contracts/blob/main/contracts/utils/NFThelper.sol"; | |
import "https://github.com/kalidao/kali-contracts/blob/main/contracts/libraries/SafeTransferLib.sol"; | |
import "https://github.com/kalidao/kali-contracts/blob/main/contracts/interfaces/IERC20minimal.sol"; | |
error NoArrayParity(); | |
error SigBounds(); | |
error NotSigner(); | |
error SigOutOfOrder(); | |
error ExecuteFailed(); | |
error Forbidden(); | |
pragma solidity >=0.8.4; | |
/// @notice EIP-712-signed multi-signature contract with NFT identifiers for signers and ragequit. | |
/// @dev This design allows signers to transfer role - consider overriding transfers as alternative. | |
/// @author Modified from MultiSignatureWallet (https://github.com/SilentCicero/MultiSignatureWallet) | |
/// and LilGnosis (https://github.com/m1guelpf/lil-web3/blob/main/src/LilGnosis.sol) | |
contract ClubSig is ERC721 { | |
using SafeTransferLib for address; | |
/// EVENTS | |
event Execute(address[] targets, uint256[] values, bytes[] payloads); | |
event Govern(address[] signers, uint256 requiredSignatures); | |
/// STORAGE | |
string public baseURI; | |
uint256 public nonce = 1; | |
uint256 public requiredSignatures; | |
uint256 public totalSupply; | |
/// EIP-712 | |
uint256 private INITIAL_CHAIN_ID; | |
bytes32 private INITIAL_DOMAIN_SEPARATOR; | |
bytes32 private constant EXEC_HASH = | |
keccak256('Exec(address[] targets,uint256[] values,bytes[] payloads,uint256 nonce)'); | |
struct Signature { | |
uint8 v; | |
bytes32 r; | |
bytes32 s; | |
} | |
/// INIT | |
constructor( | |
address[] memory signers, | |
uint256[] memory ids, | |
uint256 requiredSignatures_, | |
string memory name_, | |
string memory symbol_, | |
string memory baseURI_ | |
) ERC721(name_, symbol_) { | |
uint256 length = signers.length; | |
if (length != ids.length) revert NoArrayParity(); | |
if (requiredSignatures_ > length) revert SigBounds(); | |
// cannot realistically overflow on human timescales | |
unchecked { | |
for (uint256 i = 0; i < length; i++) | |
_mint(signers[i], ids[i]); | |
totalSupply++; | |
} | |
baseURI = baseURI_; | |
requiredSignatures = requiredSignatures_; | |
INITIAL_CHAIN_ID = block.chainid; | |
INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator(); | |
} | |
/// GETTERS | |
function tokenURI(uint256) public view override virtual returns (string memory) { | |
return baseURI; | |
} | |
function DOMAIN_SEPARATOR() internal 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('ClubMultiSig')), | |
bytes('1'), | |
block.chainid, | |
address(this) | |
) | |
); | |
} | |
/// OPERATIONS | |
function execute( | |
address[] calldata targets, | |
uint256[] calldata values, | |
bytes[] calldata payloads, | |
Signature[] calldata sigs | |
) public virtual returns (bytes[] memory results) { | |
uint256 length = targets.length; | |
if (length != values.length || length != payloads.length) revert NoArrayParity(); | |
bytes32 digest = | |
keccak256( | |
abi.encodePacked( | |
'\x19\x01', | |
DOMAIN_SEPARATOR(), | |
keccak256( | |
abi.encode( | |
EXEC_HASH, | |
targets, | |
values, | |
payloads, | |
nonce++ | |
) | |
) | |
) | |
); | |
address previous; | |
// cannot realistically overflow on human timescales | |
unchecked { | |
for (uint256 i = 0; i < requiredSignatures; i++) { | |
address recoveredAddress = ecrecover(digest, sigs[i].v, sigs[i].r, sigs[i].s); | |
if (balanceOf[recoveredAddress] == 0) revert NotSigner(); | |
// check for duplicates or zero value | |
if (recoveredAddress < previous) revert SigOutOfOrder(); | |
previous = recoveredAddress; | |
} | |
} | |
// cannot realistically overflow on human timescales | |
unchecked { | |
for (uint256 i = 0; i < length; i++) { | |
results = new bytes[](length); | |
(bool success, bytes memory result) = targets[i].call{value: values[i]}(payloads[i]); | |
results[i] = result; | |
if (!success) revert ExecuteFailed(); | |
} | |
} | |
emit Execute(targets, values, payloads); | |
} | |
function govern( | |
address[] calldata signers, | |
uint256[] calldata ids, | |
bool[] calldata mints, | |
uint256 requiredSignatures_ | |
) public virtual { | |
if (msg.sender != address(this)) revert Forbidden(); | |
uint256 length = signers.length; | |
if (length != ids.length || length != mints.length) revert NoArrayParity(); | |
// cannot realistically overflow on human timescales | |
unchecked { | |
for (uint256 i = 0; i < length; i++) { | |
if (mints[i]) { | |
_mint(signers[i], ids[i]); | |
totalSupply++; | |
} else { | |
_burn(ids[i]); | |
totalSupply--; | |
} | |
} | |
} | |
if (requiredSignatures_ > totalSupply) revert SigBounds(); | |
requiredSignatures = requiredSignatures_; | |
emit Govern(signers, requiredSignatures_); | |
} | |
/// FINANCE | |
function ragequit(address[] calldata redeemables, uint256[] calldata sigsToBurn) public virtual { | |
for (uint256 i; i < redeemables.length; i++) { | |
// calculate fair share of given token for redemption | |
uint256 amountToRedeem = sigsToBurn.length * | |
IERC20minimal(redeemables[i]).balanceOf(address(this)) / | |
totalSupply; | |
// transfer to redeemer | |
if (amountToRedeem != 0) redeemables[i]. | |
_safeTransfer(msg.sender, amountToRedeem); | |
_burn(sigsToBurn[i]); | |
totalSupply--; | |
} | |
// if full exit, reduce quorum | |
if (balanceOf[msg.sender] == 0) requiredSignatures--; | |
} | |
receive() external payable {} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment