Awesome
Proxy hot wallet demo for Substrate (Polkadot & Kusama)
Demo for a safe and effective custodial hot wallet architecture using features innovated by Substrate FRAME pallets and featured in production chains such Polkadot and Kusama.
By @joepetrowski & @emostov
N.B. This demo needs some updates to reflect best practices. Primarily, there should be NonTransfer proxies that have the ability to remove the Transfer proxies if a questionable proxy announcement is made. Please file an issue if would like clarification and/or see the updates reflected in the demo.
Disclaimer
This repo is for demonstration purposes only.
Table of contents
Background
When managing significant sums of funds on behalf of other entities, a major challenge is moving around funds without compromising the private key of the deposit addresses. In traditional block chains the private key must be "hot" (on a device exposed to the internet) in order to efficiently and programmatically move funds from the account (i.e. accounts that a user might deposit funds to). The moment this "hot" key is comprised the attacker has total control of funds.
In this repo we demonstrate an architecture pattern enabled by the Substrate FRAME proxy
, multisig
and utility
(see pseudonymal dispatch) pallets, that minimizes attack vectors associated with operating a hot wallet as a custodian.
The "hot" account is a multisig composite address that adds a proxy which announces transactions that can be executed after some delay. Pseudonymal accounts are derived from the multisig address and can be generated for every new deposit by a user to keep accounting clear. The proxy account can regularly transfer funds from the derivative accounts to a cold storage location(s). If the system detects a announcement by the proxy for a transfer to a non-certified address, then the multisig accounts can broadcast transactions to revoke the proxies privileges within the announcement period and prevent any of the proxies announced transactions from being executed.
Technologies
- Parity Polkadot node implementation
- @substrate/txwrapper: offline transaction construction lib for Substrate
- @substrate/api-sidecar: RESTful api microservice for Substrate nodes
- @polkadot/util-crypto: Substrate cryptography utility lib
Architecture
Setup
- Create m-of-n multisig MS from K = {k} keys, held by trusted parties (e.g. founders).
- Set a proxy H with time delay T for MS.
- Create derivative addresses D = {d} for user deposits.
- Create cold storage S (not discussed here).
Simple use
- Proxy H announces call hash on-chain.
- H sends actual call to backend system.
- Backend ensures that call hash matches and parses call.
- Applies internal rule (e.g. to whitelisted address).
- If alerted, m-of-n K can reject the transaction.
- If timeout, H broadcasts the actual call.
Normal transaction flow
-
User i sends a deposit of v tokens to their addresses, di
-
Listener observes balances.Transfer event to address di
-
A machine** constructs the following call C***:
C = utility.as_derivative( index: i, call: balances.transfer( dest: S_pub, value: v, ) )
-
Key H signs and broadcasts the transaction:
proxy.announce(real: MS_pub, call_hash: hash(C))
-
Listener observes announce transaction on-chain and asks for the call C. It verifies two things:
- Its hash matches the announcement, and
- Any internal rules, e.g. the transfer is to a whitelisted S.
-
Verifications pass, no alarm.
-
After time delay T, any account can broadcast the transaction:
proxy.proxy_announced( delegate: H_pub, real: MS_pub, force_proxy_type: Any, call: C, )
-
Listener verifies that the transfer was successful when it sees a
balances.Transfer
event to address MS.
** Can be any machine, even without access to key H. *** Note that it can transfer the full value v as the address H will pay the transaction fees.
Call rejection
-
Verification does not pass.
-
Owners of K are alerted and have time T to react.
-
Construct the following call R**:
R = proxy.reject_announcement( delegate: H_pub, call_hash: hash(C), )
-
Owners of K broadcast the call R.
-
Fallback: owners cannot either override the flag or broadcast their multisig transactions in time.
-
System hold immortal transaction ready to remove H as a proxy for MS (
proxy.remove_proxies()
). The system can submit this at any point. Note: only the first multisig transaction can be immortal and stored; the remaining need theTimePoint
of the first multisig transaction so they need to be constructed in real time.
** In the event of a malicious announcement it is always recommended to call proxy.remove_proxies()
ASAP.
Misc. optimizations
-
Use batch transactions, e.g.:
utility.batch( utility.as_derivative(index: 0, call: C0), utility.as_derivative(index: 1, call: C1), utility.as_derivative(index: 2, call: C2), … )
-
Reduce T and only use the fallback for faster settlement.
-
Use anonymous proxies for MS to allow member change, in case some K<sub>i</sub> is compromised.
Demo Outline
- Generate 6 keyrings and make sure accounts have funds
- 3 for multisig
- 1 for proxy
- 1 for stash
- 1 for depositor
- Generate a 2-of-3 multisig address and log it to console
- Transfer funds from Alice to the multisig
balances.transfer()
- Set the 4th key as a proxy for the multisig
Call = proxy.addProxy(key4, Any, 10)
// 10 blocks = 1 minmultisig.approve_as_multi(hash(Call))
multisig.as_multi(Call)
- Create derivative accounts from multisig address for depositor(s) to send tokens to
- Transfer funds
v
from depositor to two derivative accountsbalances.transfer()
- Create two calls: one good and one bad
C0 = utility.as_derivative(0, balances.transfer(stash, v))
C1 = utility.as_derivative(1, balances.transfer(attacker, v))
- Demonstrate the happy path
key4
broadcastsproxy.announce(hash(C0))
and sendsC0
to another worker- safety worker decodes
C0
and ensures that destination address isstash
- after 10 blocks,
key4
broadcastsproxy.proxy_announced(C0)
- Demonstrate adversarial path
key4
broadcastsproxy.announce(hash(C1))
and sendsC1
to safety worker- safety worker decodes
C1
and sees that destination address is notstash
multisig.approve_as_multi(proxy.remove_proxies(hash(C1)))
multisig.as_multi(proxy.reject_announcement(hash(C1)))
Run
- This demo relies on using a parity polkadot development node; you can download the source here. Follow the instructions to download and compile the code.
- Make sure the node's database is empty by running:
./target/release/polkadot purge-chain --dev
(N.B. the nodes DB must be purged before every run of the demo script) - Start the node by running
./target/release/polkadot --dev
- In another terminal session change directories to this project and install dependencies by running
yarn
- Start up Sidecar by running
yarn sidecar
- In another terminal session, make sure you are in this project directory and start the demo by running
yarn start
Note: this script assumes the polkadot node and Sidecar are using the defualt development ports.