Contract Name:
MultiSigWalletProxy
Contract Source Code:
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "./MultiSigWalletProxy.sol";
import "./MultiSigWalletImplementation.sol";
contract MultiSigWalletFactory {
event NewMultiSigWalletCreated(address wallet);
function createMultiSigWallet(
address _implementation,
address[] memory owners,
uint required,
uint256 nonce
) public payable returns (address payable) {
bytes32 salt = keccak256(abi.encodePacked(nonce, owners, required));
bytes memory initCode = abi.encodePacked(
type(MultiSigWalletProxy).creationCode,
abi.encode(address(_implementation), abi.encodeWithSignature("initialize(address[],uint256)", owners, required))
);
address payable wallet;
assembly {
wallet := create2(0, add(initCode, 0x20), mload(initCode), salt)
if iszero(extcodesize(wallet)) {
revert(0, 0)
}
}
emit NewMultiSigWalletCreated(wallet);
return wallet;
}
function calculateMultiSigWalletAddress(
address _implementation,
address[] memory owners,
uint required,
uint256 nonce
) public view returns (address wallet) {
bytes32 salt = keccak256(abi.encodePacked(nonce, owners, required));
bytes memory initCode = abi.encodePacked(
type(MultiSigWalletProxy).creationCode,
abi.encode(address(_implementation), abi.encodeWithSignature("initialize(address[],uint256)", owners, required))
);
bytes32 hash = keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(initCode)
));
return address(uint160(uint(hash)));
}
function createMultiSigWalletWithTransaction(
address _implementation,
address[] memory owners,
uint required,
uint256 nonce,
MultiSigWalletImplementation.Transaction memory transaction,
MultiSigWalletImplementation.Signature[] memory signatures
) public payable returns (address payable, bool) {
address payable wallet = createMultiSigWallet(_implementation, owners, required, nonce);
bool isOk = MultiSigWalletImplementation(wallet).batchSignature(transaction, signatures);
return (wallet, isOk);
}
}
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
/// @title Multisignature wallet - Allows multiple parties to agree on transactions before execution.
/// @author Stefan George - <[email protected]>
contract MultiSigWalletImplementation {
/*
* Events
*/
event Confirmation(address indexed sender, uint indexed transactionId);
event Revocation(address indexed sender, uint indexed transactionId);
event Submission(uint indexed transactionId);
event Execution(uint indexed transactionId);
event ExecutionFailure(uint indexed transactionId);
event Deposit(address indexed sender, uint value);
event OwnerAddition(address indexed owner);
event OwnerRemoval(address indexed owner);
event RequirementChange(uint required);
/*
* Constants
*/
uint public constant MAX_OWNER_COUNT = 50;
/*
* Storage
*/
mapping(uint => Transaction) public transactions;
mapping(uint => mapping(address => bool)) public confirmations;
mapping(address => bool) public isOwner;
mapping(bytes32 => bool) public txNonces;
address[] public owners;
uint public required;
uint public transactionCount;
bytes32 public DOMAIN_SEPARATOR;
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
bytes32 internal constant TRANSACTION_TYPEHASH = keccak256("Transaction(uint nonce,address destination,uint value,bytes data)");
bool initialized;
struct Transaction {
uint nonce;
address destination;
uint value;
bytes data;
bool executed;
}
struct Signature {
address signer;
uint8 v;
bytes32 r;
bytes32 s;
}
/*
* Modifiers
*/
modifier onlyWallet() {
require(msg.sender == address(this));
_;
}
modifier ownerDoesNotExist(address owner) {
require(!isOwner[owner]);
_;
}
modifier ownerExists(address owner) {
require(isOwner[owner]);
_;
}
modifier transactionExists(uint transactionId) {
require(transactions[transactionId].destination != address(0));
_;
}
modifier confirmed(uint transactionId, address owner) {
require(confirmations[transactionId][owner]);
_;
}
modifier notConfirmed(uint transactionId, address owner) {
require(!confirmations[transactionId][owner]);
_;
}
modifier notExecuted(uint transactionId) {
require(!transactions[transactionId].executed);
_;
}
modifier notNull(address _address) {
require(_address != address(0));
_;
}
modifier validRequirement(uint ownerCount, uint _required) {
require(
ownerCount <= MAX_OWNER_COUNT &&
_required <= ownerCount &&
_required != 0 &&
ownerCount != 0
);
_;
}
/// @dev Receive function allows to deposit ether.
receive() external payable {
if (msg.value > 0) emit Deposit(msg.sender, msg.value);
}
/// @dev Fallback function allows to deposit ether.
fallback() external payable {
if (msg.value > 0) emit Deposit(msg.sender, msg.value);
}
/*
* Public functions
*/
/// @dev Contract constructor sets initial owners and required number of confirmations.
constructor() {}
function initialize(
address[] memory _owners,
uint _required
) validRequirement(_owners.length, _required) public {
require(!initialized, "already initialized");
DOMAIN_SEPARATOR = keccak256(
abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256("MultiSigWallet"), // name
keccak256("2"), // version
block.chainid,
address(this)
)
);
for (uint i = 0; i < _owners.length; i++) {
require(!isOwner[_owners[i]] && _owners[i] != address(0));
isOwner[_owners[i]] = true;
}
owners = _owners;
required = _required;
initialized = true;
}
/// @dev Allows to add a new owner. Transaction has to be sent by wallet.
/// @param owner Address of new owner.
function addOwner(
address owner
)
public
onlyWallet
ownerDoesNotExist(owner)
notNull(owner)
validRequirement(owners.length + 1, required)
{
isOwner[owner] = true;
owners.push(owner);
emit OwnerAddition(owner);
}
/// @dev Allows to remove an owner. Transaction has to be sent by wallet.
/// @param owner Address of owner.
function removeOwner(address owner) public onlyWallet ownerExists(owner) {
isOwner[owner] = false;
for (uint i = 0; i < owners.length - 1; i++)
if (owners[i] == owner) {
owners[i] = owners[owners.length - 1];
delete owners[i];
break;
}
if (required > owners.length) changeRequirement(owners.length);
emit OwnerRemoval(owner);
}
/// @dev Allows to replace an owner with a new owner. Transaction has to be sent by wallet.
/// @param owner Address of owner to be replaced.
/// @param newOwner Address of new owner.
function replaceOwner(
address owner,
address newOwner
) public onlyWallet ownerExists(owner) ownerDoesNotExist(newOwner) {
for (uint i = 0; i < owners.length; i++)
if (owners[i] == owner) {
owners[i] = newOwner;
break;
}
isOwner[owner] = false;
isOwner[newOwner] = true;
emit OwnerRemoval(owner);
emit OwnerAddition(newOwner);
}
/// @dev Allows to change the number of required confirmations. Transaction has to be sent by wallet.
/// @param _required Number of required confirmations.
function changeRequirement(
uint _required
) public onlyWallet validRequirement(owners.length, _required) {
required = _required;
emit RequirementChange(_required);
}
/// @dev Allows an owner to submit and confirm a transaction.
/// @param destination Transaction target address.
/// @param value Transaction ether value.
/// @param data Transaction data payload.
/// @return transactionId Returns transaction ID.
function submitTransaction(
address destination,
uint value,
bytes memory data
) public returns (uint transactionId) {
transactionId = addTransaction(destination, value, data);
confirmTransaction(transactionId);
}
/// @dev Allows an owner to confirm a transaction.
/// @param transactionId Transaction ID.
function confirmTransaction(
uint transactionId
)
public
ownerExists(msg.sender)
transactionExists(transactionId)
notConfirmed(transactionId, msg.sender)
{
confirmations[transactionId][msg.sender] = true;
emit Confirmation(msg.sender, transactionId);
executeTransaction(transactionId);
}
/// @dev Allows an owner to revoke a confirmation for a transaction.
/// @param transactionId Transaction ID.
function revokeConfirmation(
uint transactionId
)
public
ownerExists(msg.sender)
confirmed(transactionId, msg.sender)
notExecuted(transactionId)
{
confirmations[transactionId][msg.sender] = false;
emit Revocation(msg.sender, transactionId);
}
/// @dev Allows anyone to execute a confirmed transaction.
/// @param transactionId Transaction ID.
function executeTransaction(
uint transactionId
)
public
ownerExists(msg.sender)
confirmed(transactionId, msg.sender)
notExecuted(transactionId)
{
_executeTransaction(transactionId);
}
function _executeTransaction(
uint transactionId
) internal notExecuted(transactionId) {
if (isConfirmed(transactionId)) {
Transaction storage txn = transactions[transactionId];
txn.executed = true;
if (
external_call(
txn.destination,
txn.value,
txn.data.length,
txn.data
)
) emit Execution(transactionId);
else {
emit ExecutionFailure(transactionId);
txn.executed = false;
}
}
}
// call has been separated into its own function in order to take advantage
// of the Solidity's code generator to produce a loop that copies tx.data into memory.
function external_call(
address destination,
uint value,
uint dataLength,
bytes memory data
) internal returns (bool) {
bool result;
assembly {
let x := mload(0x40) // "Allocate" memory for output (0x40 is where "free memory" pointer is stored by convention)
let d := add(data, 32) // First 32 bytes are the padded length of data, so exclude that
result := call(
sub(gas(), 34710), // 34710 is the value that solidity is currently emitting
// It includes callGas (700) + callVeryLow (3, to pay for SUB) + callValueTransferGas (9000) +
// callNewAccountGas (25000, in case the destination address does not exist and needs creating)
destination,
value,
d,
dataLength, // Size of the input (in bytes) - this is what fixes the padding problem
x,
0 // Output is ignored, therefore the output size is zero
)
}
return result;
}
/// @dev Returns the confirmation status of a transaction.
/// @param transactionId Transaction ID.
/// @return status Confirmation status.
function isConfirmed(uint transactionId) public view returns (bool status) {
uint count = 0;
for (uint i = 0; i < owners.length; i++) {
if (confirmations[transactionId][owners[i]]) count += 1;
if (count == required) return true;
}
}
/*
* Internal functions
*/
/// @dev Adds a new transaction to the transaction mapping, if transaction does not exist yet.
/// @param destination Transaction target address.
/// @param value Transaction ether value.
/// @param data Transaction data payload.
/// @return transactionId Returns transaction ID.
function addTransaction(
address destination,
uint value,
bytes memory data
) internal notNull(destination) returns (uint transactionId) {
if(transactions[transactionId].destination == address(0)) {
transactionId = transactionCount;
transactions[transactionId] = Transaction({
nonce: transactionId,
destination: destination,
value: value,
data: data,
executed: false
});
transactionCount += 1;
emit Submission(transactionId);
} else {
revert("transactionId already exist");
}
}
/*
* Web3 call functions
*/
/// @dev Returns number of confirmations of a transaction.
/// @param transactionId Transaction ID.
/// @return count Number of confirmations.
function getConfirmationCount(
uint transactionId
) public view returns (uint count) {
for (uint i = 0; i < owners.length; i++)
if (confirmations[transactionId][owners[i]]) count += 1;
}
/// @dev Returns total number of transactions after filers are applied.
/// @param pending Include pending transactions.
/// @param executed Include executed transactions.
/// @return count Total number of transactions after filters are applied.
function getTransactionCount(
bool pending,
bool executed
) public view returns (uint count) {
for (uint i = 0; i < transactionCount; i++)
if (
(pending && !transactions[i].executed) ||
(executed && transactions[i].executed)
) count += 1;
}
/// @dev Returns list of owners.
/// @return List of owner addresses.
function getOwners() public view returns (address[] memory) {
return owners;
}
/// @dev Returns array with owner addresses, which confirmed transaction.
/// @param transactionId Transaction ID.
/// @return _confirmations Returns array of owner addresses.
function getConfirmations(
uint transactionId
) public view returns (address[] memory _confirmations) {
address[] memory confirmationsTemp = new address[](owners.length);
uint count = 0;
uint i;
for (i = 0; i < owners.length; i++)
if (confirmations[transactionId][owners[i]]) {
confirmationsTemp[count] = owners[i];
count += 1;
}
_confirmations = new address[](count);
for (i = 0; i < count; i++) _confirmations[i] = confirmationsTemp[i];
}
/// @dev Returns list of transaction IDs in defined range.
/// @param from Index start position of transaction array.
/// @param to Index end position of transaction array.
/// @param pending Include pending transactions.
/// @param executed Include executed transactions.
/// @return _transactionIds Returns array of transaction IDs.
function getTransactionIds(
uint from,
uint to,
bool pending,
bool executed
) public view returns (uint[] memory _transactionIds) {
uint[] memory transactionIdsTemp = new uint[](transactionCount);
uint count = 0;
uint i;
for (i = 0; i < transactionCount; i++)
if (
(pending && !transactions[i].executed) ||
(executed && transactions[i].executed)
) {
transactionIdsTemp[count] = i;
count += 1;
}
_transactionIds = new uint[](to - from);
for (i = from; i < to; i++)
_transactionIds[i - from] = transactionIdsTemp[i];
}
function hashTransaction(
Transaction memory transaction
) public pure returns (bytes32) {
return
keccak256(
abi.encode(
TRANSACTION_TYPEHASH,
transaction.nonce,
transaction.destination,
transaction.value,
keccak256(bytes(transaction.data))
)
);
}
function getTransactionMessage(
Transaction memory transaction
) public view returns (bytes32) {
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hashTransaction(transaction)
)
);
return digest;
}
function batchSignature(Transaction memory txn, Signature[] memory sortedSignatures) public returns (bool isOK) {
require(sortedSignatures.length >= required, "invalid signature data length");
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hashTransaction(txn)
)
);
require(!txNonces[digest], "tx-executed");
uint256 txId = txn.nonce;
address lastOwner = address(0);
for(uint i = 0; i < sortedSignatures.length; i++) {
Signature memory signature = sortedSignatures[i];
address signer = signature.signer;
uint8 v = signature.v;
bytes32 r = signature.r;
bytes32 s = signature.s;
address currentOwner = ecrecover(digest, v, r, s);
// to save gas, must need signature.signer sorted
require(currentOwner > lastOwner && isOwner[currentOwner] && signer == currentOwner, "error-sig");
lastOwner = currentOwner;
emit Confirmation(signer, txId);
}
txn.executed = true;
txNonces[digest] = true;
if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) {
emit Execution(txId);
} else {
emit ExecutionFailure(txId);
txn.executed = false;
}
return txn.executed;
}
struct Call {
address target;
uint value;
bytes data;
}
function multiCall(
Call[] memory calls
) public onlyWallet {
for (uint i = 0; i < calls.length; i++) {
if (external_call(
calls[i].target,
calls[i].value,
calls[i].data.length,
calls[i].data
)) {}
else {
revert("internal call failed");
}
}
}
}
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "./MultiSigWalletImplementation.sol";
contract MultiSigWalletImplementationBeacon {
event MultiSigWalletImplementationDeployed(address indexed implementation);
constructor() {
MultiSigWalletImplementation implementation = new MultiSigWalletImplementation();
address[] memory owners = new address[](1);
owners[0] = msg.sender;
implementation.initialize(owners, 1);
emit MultiSigWalletImplementationDeployed(address(implementation));
}
}
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract MultiSigWalletProxy {
address public implementation;
constructor(address _implementation, bytes memory _data) {
implementation = _implementation;
if(_data.length > 0) {
(bool success,) = _implementation.delegatecall(_data);
require(success, "MultiSigWalletProxy: Initialization failed");
}
}
fallback() external payable {
_delegate(implementation);
}
receive() external payable {
_delegate(implementation);
}
function _delegate(address _implementation) internal {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
} default {
return(0, returndatasize())
}
}
}
}