Wallet Configuration
In the context of Sequence Wallets, the "Wallet Configuration" is a set of parameters that defines the behavior of the wallet, primarily used to define the access control of the wallet, meaning who can sign transactions and how many signatures are needed.
Top level properties
Sequence v2 configurations contain the following 3 properties:
threshold
- The required "weight sum" needed for a signature to be considered valid.checkpoint
- Used as a salt and ordering mechanism for wallet updates.tree
- Determines the signers and their weights for the wallet.
Threshold
The threshold
is a uint16
; it can have any value between 0 and 65535. Signatures are only considered valid or invalid if the sum of the weights of the signers that signed the transaction is greater or equal to the threshold.
Checkpoint
The checkpoint
is a uint32
. During wallet creation, a semi-random value can be provided to generate independent wallets with the same initial configuration. Then, during normal operation, the checkpoint
is used by Light State Sync to ensure that wallet updates are applied in the correct order.
Tree
The tree
is an unbalanced binary Merkle tree, where each leaf may contain a signer, a static signature, or a subtree. The tree can represent any combination of signers and weights and can be used to create complex multi-signature wallets.
The possible leaf types are:
Signer
Signers are represented by a signer address
and a uint8
weight. The weight is how much the signer contributes to the threshold.
The address can belong to either an ERC1271
compliant contract or an EOA
wallet.
The leaf hash is calculated as follows:
bytes32(uint256(weight) << 160 | uint256(uint160(addr)))
Subdigest
This represents a static subdigest for which any signature is valid. If a signature for this subdigest is provided, the total weight of the signature is automatically set to Infinity
.
Notice static subdigests that exist within nested trees will have their "Infinity" weight reduced to the weight of the nested tree.
The leaf hash is calculated as follows:
keccak256(abi.encodePacked('Sequence static digest:\n', subdigest));
Subtree (nested configuration)
This represents a whole new wallet configuration, this "nested configuration" has its own:
- External
weight
(uint8
) - Internal
threshold
(uint16
) - Internal
tree
The way it works is that if a signature reaches the internal threshold
within the subtree it is considered valid, and the external weight
is added to the parent tree. Any number of nested configurations can be created, and it is possible to create multiple nesting levels.
This pattern can be used, among other things, to express the following scenarios:
- Non-linear weight distribution, A and B signers can provide 1 weight each, but together they can provide 3 weight.
- Limited total weight contribution, A, B, and C signers can provide 1 weight each, but together they can only provide 2 weight.
- "Department configurations", N departments are required to sign, and each department has its own inner configuration.
The leaf hash is calculated as follows:
keccak256(abi.encodePacked(
'Sequence nested config:\n',
imageHash(tree),
threshold,
weight
))
ImageHash
The configuration
is never stored as a whole; instead, the Merkle tree is hashed into a single bytes32
value, this is internally called the imageHash
of the configuration.
The imageHash
is calculated as follows:
imageHash := keccak256(abi.encode(
keccak256(abi.encode(
hashTree(tree),
threshold
)),
checkpoint
))
The hashTree
function is a recursive function that hashes the tree into a single bytes32
value, pseudo code for the hashTree
function is as follows:
export function hashTree(node: Node | Leaf): string {
if (isSignerLeaf(node)) {
return ethers.solidityPackedKeccak256(
['uint96', 'address'],
[node.weight, node.address]
)
}
if (isSubdigestLeaf(node)) {
return ethers.solidityPackedKeccak256(
['string', 'bytes32'],
['Sequence static digest:\n', node.subdigest]
)
}
if (isNestedLeaf(node)) {
const nested = hashTree(node.tree)
return ethers.solidityPackedKeccak256(
['string', 'bytes32', 'uint256', 'uint256'],
['Sequence nested config:\n', nested, node.threshold, node.weight]
)
}
return ethers.solidityPackedKeccak256(
['bytes32', 'bytes32'],
[hashTree(node.left), hashTree(node.right)]
)
}
Initial Configuration
All Sequence Wallets have an "initial configuration", implemented by using the imageHash
of the initial configuration as the SALT during the CREATE2
deployment of the wallet.
Wallets are deployed by calling the deploy
function of the Factory
contract, which takes the following parameters:
mainModule
: The address of the initial code implementation of the wallet.salt
: TheimageHash
of the initial configuration.