Skip to content

Instantly share code, notes, and snippets.

@z0r0z
Created December 28, 2021 17:32
Show Gist options
  • Save z0r0z/bc01996303f43701e5200f79cdd4ef25 to your computer and use it in GitHub Desktop.
Save z0r0z/bc01996303f43701e5200f79cdd4ef25 to your computer and use it in GitHub Desktop.
EIP-712-signed multi-signature contract with NFT ids for signers.
// SPDX-License-Identifier: GPL-3.0-or-later
import "https://github.com/Rari-Capital/solmate/blob/audit-fixes/src/tokens/ERC721.sol";
error NoGovParity();
error NoExecParity();
error SigBounds();
error NoSigParity();
error NotSigner();
error SigOutOfOrder();
error ExecuteFailed();
pragma solidity >=0.8.4;
/// @notice EIP-712-signed multi-signature contract with NFT identifiers for signers.
/// @dev This design allows signers to transfer role - consider overriding transfers as alternative.
/// @author Modified from MultiSignatureWallet (https://github.com/SilentCicero/MultiSignatureWallet)
contract MultiSigNFT is ERC721 {
event Execute(address[] targets, uint256[] values, bytes[] payloads);
event Govern(address[] signers, uint256 requiredSignatures);
string public baseURI;
uint256 public nonce;
uint256 public requiredSignatures;
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)');
bytes32 private constant GOV_HASH =
keccak256('Gov(address[] signers,uint256[] ids,uint256 requiredSignatures_,uint256 nonce)');
constructor(
address[] memory signers,
uint256[] memory ids,
uint256 requiredSignatures_,
string memory name_,
string memory symbol_,
string memory baseURI_
) ERC721(name_, symbol_) {
if (signers.length != ids.length) revert NoGovParity();
if (requiredSignatures_ > signers.length) revert SigBounds();
// cannot realistically overflow on human timescales
unchecked {
for (uint256 i = 0; i < signers.length; i++)
_mint(signers[i], ids[i]);
}
baseURI = baseURI_;
requiredSignatures = requiredSignatures_;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator();
}
function tokenURI(uint256) public view override returns (string memory uri) {
uri = baseURI;
}
function DOMAIN_SEPARATOR() private view returns (bytes32 domainSeparator) {
domainSeparator = block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _computeDomainSeparator();
}
function _computeDomainSeparator() private view returns (bytes32 domainSeparator) {
domainSeparator = keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
keccak256(bytes('Multisig')),
keccak256(bytes('1')),
block.chainid,
address(this)
)
);
}
function execute(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata payloads,
uint8[] calldata v,
bytes32[] calldata r,
bytes32[] calldata s
) external {
if (targets.length != values.length || values.length != payloads.length) revert NoExecParity();
if (v.length != r.length || r.length != s.length) revert NoSigParity();
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, v[i], r[i], s[i]);
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 < targets.length; i++) {
(bool success, ) = targets[i].call{value: values[i]}(payloads[i]);
if (!success) revert ExecuteFailed();
}
}
emit Execute(targets, values, payloads);
}
function govern(
address[] calldata signers,
uint256[] calldata ids,
uint256 requiredSignatures_,
uint8[] calldata v,
bytes32[] calldata r,
bytes32[] calldata s
) external {
if (signers.length != ids.length) revert NoGovParity();
if (v.length != r.length || r.length != s.length) revert NoSigParity();
bytes32 digest =
keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
GOV_HASH,
signers,
ids,
requiredSignatures_,
nonce++
)
)
)
);
address previous;
// cannot realistically overflow on human timescales
unchecked {
for (uint256 i = 0; i < requiredSignatures; i++) {
address recoveredAddress = ecrecover(digest, v[i], r[i], s[i]);
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 < signers.length; i++) {
_mint(signers[i], ids[i]);
}
}
if (requiredSignatures_ > totalSupply) revert SigBounds();
requiredSignatures = requiredSignatures_;
emit Govern(signers, requiredSignatures_);
}
receive() external payable {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment