Contracts
Factory
Smart wallet addresses are typically deterministically generated based on their initial configuration. Keyspace wallets continue that practice, but the initial configuration lives in Keyspace instead. Consider CoinbaseSmartWalletFactory
, which uses the initial owners and a nonce to generate the address. Instead, the owners and nonce will be stored in Keyspace instead, so only the single Keyspace key is needed to generate the address, regardless of how many initial owners there are.
function createAccount(bytes[] calldata owners, uint256 nonce)
function createAccount(bytes calldata owner)
external
payable
virtual
returns (CoinbaseSmartWallet account)
{
if (owners.length == 0) {
revert OwnerRequired();
}
(bool alreadyDeployed, address accountAddress) =
LibClone.createDeterministicERC1967(msg.value, implementation, _getSalt(owners, nonce));
LibClone.createDeterministicERC1967(msg.value, implementation, _getSalt(owner));
account = CoinbaseSmartWallet(payable(accountAddress));
if (!alreadyDeployed) {
account.initialize(owners);
account.initialize(owner);
}
}
Wallet Contract
To send transactions with a Keyspace-enabled wallet, the client needs to provide a signed UserOperation
, the current configuration for the wallet stored in Keyspace (e.g. the public key for a single signature wallet), and stateProof
, a SNARK that proves the wallet configuration in the current state of Keyspace.
function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)
internal override virtual returns (uint256 validationData) {
(bytes memory signature, uint256 publicKeyX, uint256 publicKeyY, bytes memory stateProof) =
abi.decode(userOp.signature, (bytes, uint256, uint256, bytes));
bytes32 hash = userOpHash.toEthSignedMessageHash();
bytes memory publicKeyBytes = abi.encode(publicKeyX, publicKeyY);
address a = address(bytes20(keccak256(publicKeyBytes) << 96));
address b = hash.recover(signature);
if (a != b)
return SIG_VALIDATION_FAILED;
uint256[] memory data = new uint256[](8);
data[0] = publicKeyX;
data[1] = publicKeyY;
uint256[] memory public_inputs = new uint256[](3);
public_inputs[0] = key;
public_inputs[1] = keyStore.root();
public_inputs[2] = uint256(keccak256(abi.encodePacked(data))>>8);
require(stateVerifier.Verify(stateProof, public_inputs), "keystore state proof failed");
return 0;
}
Note that in addition to a typical ecrecover
, we need to ensure that the recovered address matches the public key the user has provided with the transaction. We then verify a proof that the same public key is currently consistent with Keyspace's state root.
Wallet Addresses
To generate deterministic addresses for wallets on each chain, smart wallet factories are typically deployed using tools like the Safe Singleton Factory to get the same factory address on each chain, then use CREATE2 when deploying wallets to get the same wallet address on each chain. This is the best approach available, but there are still some chains, like ZKsync-derived chains, where having different addresses is unavoidable. There are also scenarios where deployment mistakes are made on new chains that result in the desired address being permanently blocked.
With Keyspace integration, there's yet another contract with an address that can vary: the bridged keystore root oracle. Since the wallet and factory contracts must refer to the bridged keystore root oracle address, this address difference will cascade to those contracts as well.
Since identical addresses cannot be guaranteed, Keyspace recommends an ENS-centric user experience for wallets that integrate Keyspace. Wallet addresses would be de-emphasized in favor of ENS names. ENS supports multichain addresses via ENSIP-9 and ENSIP-11. On chains where the user's address differs from the default, wallets should set multichain address records with the chain-specific address.
On the sending side, wallets and exchanges must consistently implement ENSIP-9 and ENSIP-11 for users' expectations to be met. As smart wallets are adopted, we expect using Ethereum addresses as the recipient for funds to fall out of favor as sending to ENS names becomes the default. It must be safe to send to an ENS name even when a chain-specific address has been set.