Create the multi-signature wallet contract
The multi-signature wallet needs to do the following:
- Parse multi-signatures sent to it and validate them
- Store information about valid signers for validation
- Handle external messages
- Transfer default tokens and/or custom currencies
To validate signatures, the multi-signature wallet needs to use the Nil.validateSignature(pubkey, hash, authData)
function. The function can only accept authData
that is 256 bytes or less. As a signatures takes up 65 bytes, the wallet can only support three valid signers at maximum.
Contract definition
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.9;
import "@nilfoundation/smart-contracts/contracts/Wallet.sol";
import "@nilfoundation/smart-contracts/contracts/Nil.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
/**
* @title MultiSigWallet
* @author =nil; Foundation
* @notice This contract provides a canonical example of a multi-signature wallet on =nil;.
*/
contract MultiSigWallet is NilBase {}
Constructor and properties
bytes[] pubkeys;
/**
* The contract constructor takes an array of public keys.
* The length of the array cannot exceeed three signatures.
* @param _publicKeys The array of public keys of the wallet signers.
*/
constructor(bytes[] memory _publicKeys) payable {
uint publicKeyLen = _publicKeys.length;
require(publicKeyLen <= 3, "MultiSigWallet: too many public keys");
require(publicKeyLen > 1, "MultiSigWallet: too few public keys");
pubkeys = _publicKeys;
}
The contract stores the signers' public keys inside the pubkeys
array. The constructor checks for the length of the passed _publicKeys
array and assigns pubkeys
to this array as long as its length is correct.
Parsing multi-signatures and validating them
To handle external messages, the contract uses the verifyExternal()
function:
/**
* @notice This function verifies external signatures and makes it possible to call the wallet externally.
* @param hash The hash of the external message.
* @param signature The signature of the external message.
*/
function verifyExternal(
uint256 hash,
bytes calldata signature
) external view returns (bool) {
uint32 offset = 0;
uint8 pubkeysLen = uint8(pubkeys.length);
for (uint i = 0; i < pubkeysLen; i++) {
bytes memory userSignature = parseSignature(signature, offset);
offset += 1;
require(
Nil.validateSignature(pubkeys[i], hash, userSignature),
"Invalid signature"
);
}
return true;
}
The function iterates over all signatures passed in the signature
calldata using the offset
counter. Each iteration calls the parseSignature()
function:
/**
* @notice This function parses a signature and defines its r, s, and v components.
* @param _signatures The signatures from which the function should extract the required signature.
* @param _pos The starting point from which the analyzed signature starts.
*/
function parseSignature(
bytes memory _signatures,
uint _pos
) public pure returns (bytes memory signature) {
uint offset = _pos * 65;
bytes32 r;
bytes32 s;
uint8 v;
// The signature format is a compact form of:
// {bytes32 r}{bytes32 s}{uint8 v}
// In this compact form, uint8 is not padded to 32 bytes.
assembly {
// solium-disable-line security/no-inline-assembly
r := mload(add(_signatures, add(32, offset)))
s := mload(add(_signatures, add(64, offset)))
// The function loads the last 32 bytes, including 31 bytes
// of 's'. There is no 'mload8' to do this.
//
// 'byte' is not applicable here due to the Solidity parser,
// so the function uses the second best option, 'and'
v := and(mload(add(_signatures, add(65, offset))), 0xff)
}
if (v < 27) v += 27;
require(v == 27 || v == 28);
signature = new bytes(65);
assembly {
// solium-disable-line security/no-inline-assembly
mstore(add(signature, 0x20), r)
mstore(add(signature, 0x40), s)
mstore8(add(signature, 0x60), v)
}
return signature;
}
The function reconstructs an individual signature when given 65 bytes from the given offset (_pos
). It does so by extracting r, s, and v components and using mstore
and mstore8
.
Handling cross-shard transfers
The wallet provides a simple 'wrapper' function for handling async transfers of default tokens and custom currencies:
/**
* @notice This function acts as a 'wrapper' function for Nil.asyncCallWithTokens().
* @dev Makes an asynchronous call.
* @param dst The destination address.
* @param refundTo The address where to send refund message.
* @param bounceTo The address where to send bounce message.
* @param feeCredit The amount of tokens available to pay all fees during message processing.
* @param tokens The multi-currency tokens to send.
* @param value The value to send.
* @param callData The call data of the called method.
*/
function asyncCall(
address dst,
address refundTo,
address bounceTo,
uint feeCredit,
Nil.Token[] memory tokens,
uint value,
bytes calldata callData
) public onlyExternal {
Nil.asyncCallWithTokens(
dst,
refundTo,
bounceTo,
feeCredit,
Nil.FORWARD_NONE,
value,
tokens,
callData
);
}
Full code
Here is the full code of the multi-signature wallet contract:
pragma solidity ^0.8.9;
import "@nilfoundation/smart-contracts/contracts/Wallet.sol";
import "@nilfoundation/smart-contracts/contracts/Nil.sol";
/**
* @title MultiSigWallet
* @author =nil; Foundation
* @notice This contract provides a canonical example of a multi-signature wallet on =nil;.
*/
contract MultiSigWallet is NilBase {
bytes[] pubkeys;
/**
* The contract constructor takes an array of public keys.
* The length of the array cannot exceeed three signatures.
* @param _publicKeys The array of public keys of the wallet signers.
*/
constructor(bytes[] memory _publicKeys) payable {
uint publicKeyLen = _publicKeys.length;
if (publicKeyLen > 3) {
revert("MultiSigWallet: too many public keys");
}
if (publicKeyLen < 1) {
revert("MultiSigWallet: too few public keys");
}
pubkeys = _publicKeys;
}
/**
* @notice This function parses a signature and defines its r, s, and v components.
* @param _signatures The signatures from which the function should extract the required signature.
* @param _pos The starting point from which the analyzed signature starts.
*/
function parseSignature(
bytes memory _signatures,
uint _pos
) public pure returns (bytes memory signature) {
uint offset = _pos * 65;
bytes32 r;
bytes32 s;
uint8 v;
// The signature format is a compact form of:
// {bytes32 r}{bytes32 s}{uint8 v}
// In this compact form, uint8 is not padded to 32 bytes.
assembly {
// solium-disable-line security/no-inline-assembly
r := mload(add(_signatures, add(32, offset)))
s := mload(add(_signatures, add(64, offset)))
// The function loads the last 32 bytes, including 31 bytes
// of 's'. There is no 'mload8' to do this.
//
// 'byte' is not applicable here due to the Solidity parser,
// so the function uses the second best option, 'and'
v := and(mload(add(_signatures, add(65, offset))), 0xff)
}
if (v < 27) v += 27;
require(v == 27 || v == 28);
signature = new bytes(65);
assembly {
// solium-disable-line security/no-inline-assembly
mstore(add(signature, 0x20), r)
mstore(add(signature, 0x40), s)
mstore8(add(signature, 0x60), v)
}
return signature;
}
/**
* @notice This function acts as a 'wrapper' function for Nil.asyncCallWithTokens().
* @dev Makes an asynchronous call.
* @param dst The destination address.
* @param refundTo The address where to send refund message.
* @param bounceTo The address where to send bounce message.
* @param feeCredit The amount of tokens available to pay all fees during message processing.
* @param tokens The multi-currency tokens to send.
* @param value The value to send.
* @param callData The call data of the called method.
*/
function asyncCall(
address dst,
address refundTo,
address bounceTo,
uint feeCredit,
Nil.Token[] memory tokens,
uint value,
bytes calldata callData
) public onlyExternal {
Nil.asyncCallWithTokens(
dst,
refundTo,
bounceTo,
feeCredit,
Nil.FORWARD_NONE,
value,
tokens,
callData
);
}
/**
* @notice This function verifies external signatures and makes it possible to call the wallet externally.
* @param hash The hash of the external message.
* @param signature The signature of the external message.
*/
function verifyExternal(
uint256 hash,
bytes calldata signature
) external view returns (bool) {
uint32 offset = 0;
uint8 pubkeysLen = uint8(pubkeys.length);
for (uint i = 0; i < pubkeysLen; i++) {
bytes memory userSignature = parseSignature(signature, offset);
offset += 1;
require(
Nil.validateSignature(pubkeys[i], hash, userSignature),
"Invalid signature"
);
}
return true;
}
}