This documentation is for version 0.0.24, part of it may be out of date.
As most people involved in the development of blockchain projects, I realized UX is a major issue right now. This led me to follow the existing effort on identity smart contracts and meta-transactions. I realized that there is a lot of effort duplication which is leading to incompatible solutions. It is also clear that no solution is future proof.
Rather than coming up with a new competing solution, I decided to experiment with my knowledge of contract upgradability to build a solution that is simple enough to be adopted straight away but with the ability to upgrade to any existing or future standard.
To me, an upgradeable identity smart contract is like a mythical creature that would watch over your assets while being able to change shape so it would always friendly. I am a fan of Asian (and more particularly Japanese) culture, so the reference to the mystical shapeshifting fox was obvious.
Kitsune wallet can be significant in many ways. First, it is designed to help wallet developers use proxies more easily. This will improve memory usage on the blockchain. In addition, Kistune makes these proxy upgradeable, so if the wallet logic as an error, or is missing a feature, you can upgrade in a single transaction that preserves your address (along with all the claims attached to it). Last but not least, being able to upgrade your proxy also means you are able to completely change the interface, so a user is not locked with the project that deployed the proxy in the first place.
My hope is that a Kitsune wallet proxy, can be your sole on-chain identity, that you will keep for the rest of your life, and transmit to your next of kin.
Technical architecture can quickly be very complex, particularly when talking of upgradeable smart contracts. The thing is, ethereum smart contracts have their code (the logic they are going to execute) and their memory (the data they hold). Contracts also have the ability to call another contract or to perform a delegatecall. Calls are simple as they move the context to another contract, asking it to perform some operation using its own code and memory. Delegate calls are different in the sense that they are executing the targeted contract’s code but using the memory of the caller. This is how libraries work. Using the same pattern Kitsune wallet deployed proxy that contains a minimum of code and uses delegate calls to a master for all the complex wallet logic. This means a single master can serve millions of users, each one of them only needs a lightweight proxy. By simply changing the master a proxy is using, you completely reshape the proxy capabilities. This is how most upgradeable contracts work.
The added value of Kitsune is the way masters are structured, and the way they deal with memory. Kitsune wallet prevents the proxy from linking to dangerous masters and requires the masters to include specific methods for memory cleanup and replay protection through upgrades.
WalletOwnable
is a very simple master that provide simple ownership. There is no multisig feature here, as a single address controles the wallet. Using this master turns the kitsune-wallet proxy into a simple ERC725 and ERC1271 compatible proxy (with the added upgradability mechanism).
Methods includes:
Function name | arguments | returns | view | Comment |
---|---|---|---|---|
master |
() | (address) | Yes | KitsuneWallet: get master address |
updateMaster |
(address,bytes calldata,bool) | No | KitsuneWallet: update master | |
transferOwnership |
(address) | No | Wallet specific: change ownership of the contract | |
renounceOwnership |
() | No | Wallet specific: remove owner /!\ Will lock the proxy forever | |
execute |
(uint256, address, uint256, bytes memory) | No | Wallet specific: execute transaction | |
owner |
() | (address) | Yes | (ERC725v2 compatibility & Wallet): owner |
setData |
(bytes32, bytes calldata) | No | (ERC725v2 compatibility) | |
getData |
(bytes32) | (bytes memory) | Yes | (ERC725v2 compatibility) |
isValidSignature |
(bytes32, bytes memory) | (bool) | Yes | (ERC1271 compatibility) |
WalletMultisig
is a multisig master that provide complexe ownership pattern. It relies on a key-value store to record the purpose of the various key. A key with purpose 0x0
has no right. Purposes are encoded as a bit-mask, meaning up to 256 purposes can individually be enabled/disabled for each key.
- Purpose
0x1
is management right (key can participate to actions updating the proxy, such as adding or removing keys) - Purpose
0x2
is action right (key can participate to actions on external contract, such as ether & token transfers, contract creations, ...) - Purpose
0x4
is signature right (key can sign messages that will be recognised as valid by the ERC1271 methodisValidSignature
)
Calls to the execute method can be perform by anyone, but the subsequent calls will only be performed if the meta-transaction is signed by authorized accounts. Anyone sending signed meta-transaction is refered to as a relayer.
Methods includes:
Function name | arguments | returns | view | Comment |
---|---|---|---|---|
master |
() | (address) | Yes | KitsuneWallet: get master address |
updateMaster |
(address,bytes calldata,bool) | No | KitsuneWallet: update master | |
addrToKey |
(address) | (bytes32) | Yes | Wallet specific: convert address to key |
nonce |
() | (uint256) | Yes | Wallet specific: meta nonce for replay protection |
getActiveKeys |
() | (bytes32[] memory) | Yes | Wallet specific: list of all the keys with any purpose |
getKey |
(bytes32) | (bytes32) | Yes | Wallet specific: Get a key entire purpose |
getKey |
(address) | (bytes32) | Yes | Wallet specific: Get a key entire purpose |
keyHasPurpose |
(bytes32, bytes32) | (bool) | Yes | Wallet specific: Check if a key as the required purposes |
keyHasPurpose |
(bytes32, uint256) | (bool) | Yes | Wallet specific: Check if a key as the required purposes |
keyHasPurpose |
(address, bytes32) | (bool) | Yes | Wallet specific: Check if a key as the required purposes |
keyHasPurpose |
(address, uint256) | (bool) | Yes | Wallet specific: Check if a key as the required purposes |
setKey |
(bytes32, bytes32) | No | Wallet specific: Change the purpose associated with a key | |
setKey |
(bytes32, uint256) | No | Wallet specific: Change the purpose associated with a key | |
setKey |
(address, bytes32) | No | Wallet specific: Change the purpose associated with a key | |
setKey |
(address, uint256) | No | Wallet specific: Change the purpose associated with a key | |
execute |
(uint256, address, uint256, bytes memory, uint256, bytes[] memory) | No | Wallet specific: Execute a transaction (must be signed with authorized keys) | |
managementKeyCount |
() | (uint256) | Yes | Wallet specific: Number of keys with management purpose |
getActionThreshold |
() | (uint256) | Yes | Wallet specific: Number of keys required to perform an action |
getManagementThreshold |
() | (uint256) | Yes | Wallet specific: Number of keys required to perform management |
setActionThreshold |
(uint256) | No | Wallet specific: CHange the action threshold | |
setManagementThreshold |
(uint256) | No | Wallet specific: CHange the management threshold | |
owner |
() | (address) | Yes | (ERC725v2 compatibility) proxy is owned by itself |
setData |
(bytes32, bytes calldata) | No | (ERC725v2 compatibility) | |
getData |
(bytes32) | (bytes memory) | Yes | (ERC725v2 compatibility) |
isValidSignature |
(bytes32, bytes memory) | (bool) | Yes | (ERC1271 compatibility) |
WalletMultisigRefund
is a extention to WalletMultisig
that includes a feature to refund the relayer to cover the gas cost. The refund can be done in ether or using an ERC20 token.
Methods are the same as WalletMultisig
except for the execute
method that supports the modification in the meta-transaction.
Function name | arguments | returns | view | Comment |
---|---|---|---|---|
execute |
(uint256, address, uint256, bytes memory, uint256, address, uint256, bytes[] memory) | No | Wallet specific: Execute a transaction (must be signed with authorized keys) |
WalletMultisigRefundOutOfOrder
is a extention to WalletMultisigRefund
that includes a feature to perform transaction out-of-order. In this case the meta-nonce is replaced by a salt to perform replay protection.
Methods are the same as WalletMultisig
and WalletMultisigRefund
except for the execute
method that supports the modification in the meta-transaction.
Function name | arguments | returns | view | Comment |
---|---|---|---|---|
execute |
(uint256, address, uint256, bytes memory, uint256, bytes32, address, uint256, bytes[] memory) | No | Wallet specific: Execute a transaction (must be signed with authorized keys) |
Meta-transaction used by the WalletMultisig
, WalletMultisigRefund
and WalletMultisigRefundOutOfOrder
follow a common pattern:
Name | Type | Used by WM |
Used by WMR |
Used by WMROOO |
Comment |
---|---|---|---|---|---|
operationType | uint256 | x | x | x | 0 call, 1 create contract |
to | address | x | x | x | Destination of the call |
value | uint256 | x | x | x | Value of the call (wei transfered) |
data | bytes | x | x | x | Data of the call |
nonce | uint256 | x | x | x | Meta-nonce (replay protection) |
salt | bytes32 | x | Salt for replay protection of out-of-order meta-transaction | ||
gasToken | address | x | x | Address of the ERC20 token to use for gas refund (or 0 for refund in ether) | |
gasPrice | uint256 | x | x | Gas price for the refund (in ERC20 token or ether) | |
sigs | bytes[] | x | x | x | Signatures of the meta-transaction by authorized keys |
- Use
nonce = 0
for out-of-order transactions protected by salt (WalletMultisigRefundOutOfOrder
only)
If multiple signatures must be required for an action, the different signatures must be ordered following the increassing order of the signing addresses. For more details about meta-transaction hashing and signature, please refer to utils/utils.js
and to the different tests.
In order to be a Kitsune compatible master, your contract must follow some rules:
- Inherit from
contracts/masters/MasterBase.sol
. - Implement the
_controller()
internal function that defines who can update the proxy. This is generally theaddress(this)
for multisig contracts without priviledge access. - Implement an initialization function that will be called as part of the update process. This function should be protected by the
initializer()
modifier. - Overload the
_cleanup()
internal function that must cleanup the memory during the upgrade process.
WalletOwnable
provides a simple example. UniversalLogin also provides an exemple in its WalletMaster contract.
Example of code used to deploy a proxy base on the WalletMultisig
master controlled by two keys.
const ethers = require('ethers');
const proxy = require('./build/Proxy')
const master = require('./build/WalletMultisig');
initializationTX = new ethers.utils.Interface(master.abi).functions.initialize.encode([
[
ethers.utils.hexZeroPad(<managment_key_1>, 32).toString().toLowerCase(), // addrToKey(<managment_key_1>)
ethers.utils.hexZeroPad(<managment_key_2>, 32).toString().toLowerCase(), // addrToKey(<managment_key_2>)
],
[
"0x0000000000000000000000000000000000000000000000000000000000000003", // purpose: management & action
"0x0000000000000000000000000000000000000000000000000000000000000003", // purpose: management & action
],
1, // Only one signature needed for management
1, // Only one signature needed for action
]);
new ethers.ContractFactory(proxy.abi, proxy.bytecode).getDeployTransaction(master.networks['42'].address, initializationTX);
To use a proxy, just instanciate a example of the master it uses (can be verified using the master()
view method) at the address of the proxy. The proxy will transparently redirect all calls and results.
To upgrade a proxy to a new master, call the updateMaster(address,bytes calldata,bool)
method implemented by the master. If both master use the same memory pattern you could eventually disregard the initialization step.
Example of proxy deployment and upgrade are visible in examples/000_deploy-and-upgrade.ts