Last active
September 24, 2022 19:47
-
-
Save z0r0z/3a164f00ebbaf5adc490af25cfaeadc3 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 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; | |
import "https://github.com/Rari-Capital/solmate/src/tokens/ERC721.sol"; | |
import "https://github.com/kalidao/kali-contracts/blob/main/contracts/utils/NFThelper.sol"; | |
/// @notice Minimal ERC-20 interface. | |
interface IERC20minimal { | |
function balanceOf(address account) external view returns (uint256); | |
} | |
/// @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 { | |
/*/////////////////////////////////////////////////////////////// | |
EVENTS | |
//////////////////////////////////////////////////////////////*/ | |
event Execute(address target, uint256 value, bytes payload); | |
event Govern(address[] signers, uint256 quorum); | |
/*/////////////////////////////////////////////////////////////// | |
ERRORS | |
//////////////////////////////////////////////////////////////*/ | |
error NoArrayParity(); | |
error SigBounds(); | |
error InvalidSignature(); | |
error ExecuteFailed(); | |
error Forbidden(); | |
error NotSigner(); | |
error TransferFailed(); | |
/*/////////////////////////////////////////////////////////////// | |
STORAGE | |
//////////////////////////////////////////////////////////////*/ | |
string public baseURI; | |
uint256 public nonce = 1; | |
uint256 public quorum; | |
uint256 public totalSupply; | |
struct Call { | |
address target; | |
uint256 value; | |
bytes payload; | |
} | |
/*/////////////////////////////////////////////////////////////// | |
EIP-712 STORAGE | |
//////////////////////////////////////////////////////////////*/ | |
uint256 private INITIAL_CHAIN_ID; | |
bytes32 private INITIAL_DOMAIN_SEPARATOR; | |
bytes32 private constant EXEC_HASH = | |
keccak256('Exec(address target,uint256 value,bytes payload,uint256 nonce)'); | |
struct Signature { | |
uint8 v; | |
bytes32 r; | |
bytes32 s; | |
} | |
/*/////////////////////////////////////////////////////////////// | |
CONSTRUCTOR | |
//////////////////////////////////////////////////////////////*/ | |
constructor( | |
address[] memory signers, | |
uint256[] memory ids, | |
uint256 quorum_, | |
string memory name_, | |
string memory symbol_, | |
string memory baseURI_ | |
) ERC721(name_, symbol_) { | |
uint256 length = signers.length; | |
if (length != ids.length) revert NoArrayParity(); | |
if (quorum_ > length) revert SigBounds(); | |
// cannot realistically overflow on human timescales | |
unchecked { | |
for (uint256 i = 0; i < length; i++) | |
_safeMint(signers[i], ids[i]); | |
totalSupply++; | |
} | |
baseURI = baseURI_; | |
quorum = quorum_; | |
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('ClubSig')), | |
bytes('1'), | |
block.chainid, | |
address(this) | |
) | |
); | |
} | |
/*/////////////////////////////////////////////////////////////// | |
OPERATIONS | |
//////////////////////////////////////////////////////////////*/ | |
function execute( | |
Call calldata call, | |
Signature[] calldata sigs | |
) public virtual returns (bool success, bytes memory result) { | |
bytes32 digest = | |
keccak256( | |
abi.encodePacked( | |
'\x19\x01', | |
DOMAIN_SEPARATOR(), | |
keccak256( | |
abi.encode( | |
EXEC_HASH, | |
call.target, | |
call.value, | |
call.payload, | |
nonce++ | |
) | |
) | |
) | |
); | |
address previous; | |
// cannot realistically overflow on human timescales | |
unchecked { | |
for (uint256 i = 0; i < quorum; i++) { | |
address sigAddress = ecrecover(digest, sigs[i].v, sigs[i].r, sigs[i].s); | |
// check for key balance and duplicates | |
if (balanceOf[sigAddress] == 0 || previous >= sigAddress) revert InvalidSignature(); | |
previous = sigAddress; | |
} | |
} | |
// cannot realistically overflow on human timescales | |
(success, result) = call.target.call{value: call.value}(call.payload); | |
if (!success) revert ExecuteFailed(); | |
emit Execute(call.target, call.value, call.payload); | |
} | |
function govern( | |
address[] calldata signers, | |
uint256[] calldata ids, | |
bool[] calldata mints, | |
uint256 quorum_ | |
) 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]) { | |
_safeMint(signers[i], ids[i]); | |
totalSupply++; | |
} else { | |
_burn(ids[i]); | |
totalSupply--; | |
} | |
} | |
} | |
if (quorum_ > totalSupply) revert SigBounds(); | |
quorum = quorum_; | |
emit Govern(signers, quorum_); | |
} | |
/*/////////////////////////////////////////////////////////////// | |
ASSET MGMT | |
//////////////////////////////////////////////////////////////*/ | |
receive() external payable {} | |
function ragequit(address[] calldata assets, uint256[] calldata sigsToBurn) public virtual { | |
if (balanceOf[msg.sender] == 0) revert NotSigner(); | |
uint256 length = sigsToBurn.length; | |
for (uint256 i; i < length;) { | |
_burn(sigsToBurn[i]); | |
// cannot realistically overflow on human timescales | |
unchecked { | |
i++; | |
} | |
} | |
for (uint256 j; j < assets.length;) { | |
// calculate fair share of given assets for redemption | |
uint256 amountToRedeem = length * IERC20minimal(assets[j]).balanceOf(address(this)) / | |
totalSupply; | |
// transfer to redeemer | |
if (amountToRedeem != 0) | |
_safeTransfer(assets[j], msg.sender, amountToRedeem); | |
// cannot realistically overflow on human timescales | |
unchecked { | |
j++; | |
} | |
} | |
totalSupply -= length; | |
// if full exit, reduce quorum | |
if (balanceOf[msg.sender] == 0) quorum--; | |
} | |
function _safeTransfer( | |
address token, | |
address to, | |
uint256 amount | |
) internal { | |
bool callStatus; | |
assembly { | |
// get a pointer to some free memory | |
let freeMemoryPointer := mload(0x40) | |
// write the abi-encoded calldata to memory piece by piece: | |
mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) // begin with the function selector | |
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // mask and append the "to" argument | |
mstore(add(freeMemoryPointer, 36), amount) // finally append the "amount" argument - no mask as it's a full 32 byte value | |
// call the token and store if it succeeded or not | |
// we use 68 because the calldata length is 4 + 32 * 2 | |
callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0) | |
} | |
if (!_didLastOptionalReturnCallSucceed(callStatus)) revert TransferFailed(); | |
} | |
function _didLastOptionalReturnCallSucceed(bool callStatus) internal pure returns (bool success) { | |
assembly { | |
// get how many bytes the call returned | |
let returnDataSize := returndatasize() | |
// if the call reverted: | |
if iszero(callStatus) { | |
// copy the revert message into memory | |
returndatacopy(0, 0, returnDataSize) | |
// revert with the same message | |
revert(0, returnDataSize) | |
} | |
switch returnDataSize | |
case 32 { | |
// copy the return data into memory | |
returndatacopy(0, 0, returnDataSize) | |
// set success to whether it returned true | |
success := iszero(iszero(mload(0))) | |
} | |
case 0 { | |
// there was no return data | |
success := 1 | |
} | |
default { | |
// it returned some malformed input | |
success := 0 | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment