Skip to content

Instantly share code, notes, and snippets.

@saintsal
Forked from z0r0z/ClubSig.sol
Created January 24, 2022 09:16
Show Gist options
  • Save saintsal/57a965749acfafe1b0fc03ed63f32283 to your computer and use it in GitHub Desktop.
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
// 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