Created
December 28, 2021 17:32
-
-
Save z0r0z/bc01996303f43701e5200f79cdd4ef25 to your computer and use it in GitHub Desktop.
EIP-712-signed multi-signature contract with NFT ids for signers.
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/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