Skip to content

Instantly share code, notes, and snippets.

@z0r0z
Last active September 24, 2022 19:47
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save z0r0z/3a164f00ebbaf5adc490af25cfaeadc3 to your computer and use it in GitHub Desktop.
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
// 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