Beware of scams impersonating Jump Trading Group. We only communicate through our official accounts.
- Firedancer
- Thinking
- Connect
MetaSilo = Silo <3 MetaMask
Nikhil Suri
Aug 19 2022 _ 19 min read
TLDR: It is common for crypto users to use MetaMask + Ledger to interact with a variety of DeFi protocols. The two biggest limitations and fears with this solution are the availability of the Ledger (particularly in a hybrid/remote working environment) and the danger of ending up on a phishing site and signing a malicious transaction.
In this article we introduce MetaSilo — our integration of Silo with MetaMask — which we believe may help larger crypto teams scale and sleep better.
—
As previously discussed, Silo[1] is our soon-to-be open source custody solution at Jump Crypto. While Silo covers transferring digital assets from one place to another, crypto teams may also wish to interact with the DeFi ecosystem. They want to be able to quickly use new protocols and arbitrary smart contracts while also upholding the highest security standards.
People primarily interact with Web3 through browser wallets such as MetaMask. These wallets store private keys locally in the browser, which is okay for small amounts of money, but risky for anything beyond that. Hardware wallets, such as Ledger, are the most common solution for additional security. However, there are still significant risks with using Ledgers:
- They are single, physical devices that can be lost, destroyed, or stolen.
- They are difficult to use in a team since only one person can use a Ledger at a time.
- There is no policy checking or multi-approval functionality natively built in.
DeFi can also be an adversarial environment; in the worst case, a commonly used Web3 frontend can be hacked or users can be phished into signing fraudulent transactions. In the current status quo, trusting a Ledger is akin to trusting the UIs of Web3 frontends and MetaMask — transactions are not always totally clear and, more importantly, the security is out of your control.
To mitigate this risk and provide the flexibility needed to dip into funds stored in Silo for all kinds of blockchain transactions, we built MetaSilo — an air-gapped integration for Silo with browser wallets. With MetaSilo, teams can leverage existing browser wallets that the majority of Web3 supports, while plugging in Silo as the secure signing backend. In this article, we will discuss why we chose the air-gapped integration over alternatives, our security model, the technical details of our implementation, and future steps[2].
Air-Gapped Wallets — A Quick Primer
An “air-gapped” crypto wallet stores private keys on a device that is fully isolated — never connected to the internet and never physically connected to another device, and thus “air-gapped”. A clever way to interact with these wallets to sign transactions is through scanning QR codes:
- MetaMask produces a QR code of the unsigned transaction.
- An air-gapped wallet scans the QR code, signs the payload, and produces a QR code of the signature.
- MetaMask scans the QR code from the wallet, adds the signature to the transaction, and submits the signed transaction to the network.
In addition to this “signing” flow, there is also often a “syncing” flow:
- The air-gapped wallet produces a QR code representing its address(es).
- MetaMask scans this QR code and then displays the wallet metadata (balances, wallet name, etc) in its UI.
Technically, the QR codes are serialized as CBOR-encoded BC-UR messages. These are capable of encoding various types of seeds, simple key-pairs, HD wallets, and signature requests and responses.
Alternatives Considered
We considered a couple other alternatives to MetaSilo.
One option was to build integrations with smart contracts into Silo itself. This would involve expanding Silo’s transfer functionality to include direct interactions with common protocols such Wormhole, UniSwap, Aave, etc. While certainly the most secure and likely the easiest approach, ultimately it was a) not scalable enough and b) too slow. Every new kind of smart contract interaction would require a code change — even just adding a new contract address + ABI (for EVM chains) to a config and redeploying is a slowdown that would cause teams to continually rely on engineering intervention. Contract interactions are also not standardized across chains. For example, in Solana each contract defines its own serialization method.
In addition, building an effective interface doesn’t just involve showing contract functions/parameters and allowing execution. Good UIs show rich metadata that is essential for making the right decision. Since each contract has different metadata that is important to show, building contract interactions natively into Silo would require us to handle contracts case-by-case[3]. If we were to go down this route, we’d be reinventing the wheel by building an internal version of the contract interaction functionality that is already provided by Etherscan and Web3 frontends. That being said, we are still exploring generic ways to add cross-chain smart contract support natively into Silo for common operations.
Another compelling option was to use the WalletConnect protocol. WalletConnect is a very promising project that has the potential to standardize QR code communication between Dapps and wallets across different chains. For now, we decided to integrate with MetaMask since it is the most well-supported wallet which covers all Dapps on EVM chains. However, we are keeping a close eye on the WalletConnect project as it evolves and (hopefully) more Dapps integrate with it.
Securing the Solution
Any new feature is a possible attack vector. So, beyond Silo’s native security provisions, we had to make some additional considerations for MetaSilo. The most glaring attack vector that MetaSilo introduces is the arbitrary payload that MetaMask provides to Silo for signing. Silo should not blindly sign payloads; it should first decode the data and then perform proper policy checks. Specifically:
- Only interactions with known or approved contracts should be allowed.
- Only air-gapped addresses should be allowed to sign air-gapped transactions.
- All payloads should be well-known, meaning that they decode into standardized data types. This guards against signing messages that are really fraudulent transactions in disguise.
- We want to make signature requests as clear as possible for the users of MetaSilo. Users should know exactly what they are signing and the Silo UI should be a last line of defense.
Only interactions with known or approved contracts should be allowed: Finding the right balance between requiring approvals for actions and allowing quick transactions is an iterative process. The first version requires no approvals. Looking forward, we plan to require approvals for the first interaction with new contracts. This will enable teams to sanity-check what they are doing by getting a second pair on eyes on transactions and double checking contract addresses using Etherscan or other blockchain explorers. Once a new contract has been approved, it’ll be added to a whitelist and future interactions with the same contract[4] will not require approvals.
Only air-gapped addresses should be allowed to sign air-gapped transactions: MetaSilo is not backwards-compatible with existing Silo addresses. This is a critical design choice that makes MetaSilo safer to roll out and plays nicely with existing policy rules. The current policy engine expresses rules as allowing transfers for specific assets from a source to a destination. It is much more complex (and slow) to write policies for arbitrary smart contracts, since the interactions cannot always be so succinctly expressed. We also want to make sure that there is no way in which air-gapped transactions compromise the integrity of Silo by allowing users to bypass existing rules.
From a risk-management perspective, it is also much safer to start using MetaSilo with freshly generated addresses. Getting funds into these new addresses would then involve writing policies to allow safely sending from existing addresses. This approach reduces unnecessary exposure of existing funds.
Ensuring well-known payloads: The BC-UR spec defines a dataType
parameter to differentiate between different kinds of payloads. MetaMask honors this parameter, which means that Silo can interpret each payload that MetaMask sends as a standardized Ethereum data type. No client will be able to trick Silo into signing messages that are disguised as transactions[5]. Data types that correspond to transactions are decoded into transaction objects and then checked according to policy. Data types that correspond to simple signature requests are signed according to either EIP 191 or EIP 712, which establish that they can never be Ethereum transactions.
Making requests as clear as possible: Ultimately, automated BFT policy-checking can only take teams so far. Humans make mistakes — someone could accidentally approve a fraudulent contract interaction or incorrectly input a transaction parameter. From a security perspective, Web3 UI/UX has a long way to go. To reduce human errors as much as possible, the Silo UI displays the fully decoded transaction and highlights important fields such as the source/destination addresses, amounts, and decoded transaction data (if the ABI is available). It also shows alerts anytime a user interacts with a new contract to make it clear that they are initiating a potentially risky operation.
This is just a start — there are many ways we can expand MetaSilo’s policy checking and UX beyond these considerations. Building effective security is a continuous process and we hope to improve our model by engaging with teams in the broader community.
If you’re still with us and are curious to dive into the technical details of our implementation, read on! Otherwise, feel free to jump to the conclusion for a discussion of our overall learnings and next steps.
Technical Implementation
Most air-gapped wallets today are modeled as Hierarchical Deterministic (HD) Wallets. A quick primer on HD Wallets (reader beware — cryptography ahead!):
HD Wallets have a single secret key from which all other “children” keys are derived. Given a secret key \(s\), its public key \(P = sG\), and an integer \(i >= 0\), one can calculate \(s_i = s + hash(P, i)\), where \(s_i\) is the secret key for the child at “index” \(i\). This is known as “unhardened derivation”, and is very convenient: the secret owner can calculate the child \(s_i\) from just the secret \(s\) and the index \(i\), but also the public key and the index are sufficient to calculate the child public key (with \(P = sG\) and \(P_i = s_iG\), \(P_i = P + hash(P, i) * G\)).
There can be multiple steps of derivation — for example, if you know my parent public key, I can tell you to use child \(3/17/42\) and you will able to calculate this great-grandchild public key of my \(P\) by iteratively applying the derivation calculation (first where \(i = 3\), then \(i = 17\), etc). But, I maintain that only I know the corresponding great-grand child secret of my \(s\).
There is, however, a downside to unhardened derivation: any child secret key, together with the parent public key, allows calculating the parent secret key. Since \(s_i = s + hash(P, i)\), one can simply calculate \(s_i - hash(P, i)\) to obtain \(s\). The solution to this is “hardened” key derivation, where \(s_i = s + hash(s, i)\). Under this construction, leaking \(s_i\) is not sufficient enough to recover the parent secret \(s\). While more secure, hardened derivation is less convenient, since one can no longer derive child public keys from the parent public key. A hardened key-path is denoted using apostrophes after the index — for example, \(3'/17/42\) is one-step hardened, and two-steps unhardened.
In practice, most hardware wallets are compliant with BIP-44. This means that for each chain, a hardened sub-key \(Q = s/44'/coin'\) is calculated, where \(coin'\) corresponds to one of the standardized BIP-44 coin types. \(Q\) and its key-path can then be communicated to a browser wallet such as MetaMask, which can calculate further addresses via unhardened children \(Q/i/j/...\), display those in its UI, and issue signing requests.
Coming back to MetaSilo, frontends such as MetaMask assume that the air-gapped wallet being synced is an HD wallet. This means that MetaMask will take a key and derive children from that key — those children are the addresses MetaMask will allow a user to select for usage. Since MetaMask interprets the key that is given to it as the parent key, one cannot sync a simple key-pair. The underlying implementation for syncing a wallet with MetaMask is:
- Scan the QR code representing the wallet, which contains the parent key (link to encoding).
- Derive the children addresses, and present those on-screen to the user for selection.
- Display the selected child address in the normal MetaMask wallet UI screen.
Then, when MetaMask issues a signing request, the flow is:
- Issue a signing request, which contains the derived child index and the parent public key fingerprint (link to encoding)[6].
- The air-gapped backend needs to use the parent public key and child index to find the child address and then sign the transaction using the child secret key.
Since Silo holds only simple key-pairs, our first challenge was to figure out how to integrate Silo with frontends that only support HD wallets.
There were two solutions we considered:
- Contribute a new keyring backend to MetaMask along with UI changes that support air-gapped simple key-pairs.
- Add key derivation functionality to Silo.
Option #1 was tempting. It would not only require minimal code changes in Silo, but also add what we consider to be very important functionality to MetaMask. Since HD wallets can be tricky to implement, this support would make it much easier for anyone to integrate their own air-gapped backend with MetaMask. If custom signing backends that could leverage popular browser wallets were easier to implement, we think there’d be an explosion of self-custodial solutions that could give people varying degrees of security and flexibility.
However, we decided against contributing to MetaMask for several reasons. As a very popular and high-risk open-source project, development rightfully moves at a slow and steady pace. Any change, especially a large one, needs to be treated with caution. Contributing changes ourselves is also not very scalable. After MetaMask, we’d need to contribute to the browser wallets for all other non-EVM chains that we use, which would involve onboarding onto large new codebases that are potentially written using different languages, frameworks, build systems, etc.
Therefore, we proceeded with option #2.
Implementing Key Derivation in Silo
Silo is not an HD Wallet — Silo custodies multiple wallets that are simple (sk, pk) key-pairs. There is no top-level seed phrase that was used to derive all the keys in Silo. It uses a different method of producing entropy to generate these simple key-pairs.
Technically, one can derive children from any key-pair by following the BIP-32 spec. To execute the key derivation algorithm, all that is needed in addition to the key-pair is a random 256 bits of entropy (the “chain code”). To derive child keys in Silo, we use the static chain code 0x00. For a deeper explanation of chain codes and why we chose this value, please refer to Appendix A.
Since Silo uses thresholded keys, which cannot be hardened[7], we had the following plan to integrate with MetaMask:
- Generate simple (sk, pk) key-pairs that are marked as “air-gapped”, which will act as the root-level keys from which unhardened children can be derived from.
- Communicate one of these keys to MetaMask with the instruction to derive single-step children (MetaMask can only sync with one parent key at a time).
- When MetaMask sends a signing request for one of the child keys, ensure that the child key exists in Silo (deriving it if it does not), and then sign with the child secret key.
This design is similar to having multiple ledgers, except with the additional security and availability guarantees that come with Silo.
Integrating with MetaMask
If only it were so easy.
To sync Silo “parent” keys with MetaMask, we needed to provide a CryptoHDKey BC-UR payload and appropriately fill in these fields[8]:
const hdkey = new CryptoHDKey({
isMaster: false,
key: parentPublicKey,
origin: ?,
children: ?,
chainCode: 0x00,
parentFingerprint: parentPublicKey (first 8 bytes),
name: parentPublicKey name
});
Our challenge was figuring out what exactly to use for the origin
and children
fields to coax MetaMask into deriving single-step children. After much trial and error, we decided to dive into the source code to understand how MetaMask interprets these fields.
The MetaMask keyring backend for air-gapped wallets handles reading HD Wallets, calculating derived children, and managing the signature requests/responses made to/from the actual air-gapped wallet. The keyring backend has two critical functions in the HD Key syncing flow: __readCryptoHDKey
and __addressFromIndex
(source):
private __readCryptoHDKey = (cryptoHDKey: CryptoHDKey) => {
const hdPath = `m/${cryptoHDKey.getOrigin().getPath()}`;
const xfp = cryptoHDKey.getOrigin().getSourceFingerprint()?.toString("hex");
const childrenPath =
cryptoHDKey.getChildren()?.getPath() || DEFAULT_CHILDREN_PATH;
const name = cryptoHDKey.getName();
if (cryptoHDKey.getNote() === KEYRING_ACCOUNT.standard) {
this.keyringAccount = KEYRING_ACCOUNT.standard;
} else if (cryptoHDKey.getNote() === KEYRING_ACCOUNT.ledger_legacy) {
this.keyringAccount = KEYRING_ACCOUNT.ledger_legacy;
}
if (!xfp) {
throw new Error(
"KeystoneError#invalid_data: invalid crypto-hdkey, cannot get source fingerprint"
);
}
const xpub = cryptoHDKey.getBip32Key();
this.xfp = xfp;
this.xpub = xpub;
this.hdPath = hdPath;
this.childrenPath = childrenPath;
if (name !== undefined && name !== "") {
this.name = name;
}
this.initialized = true;
};
__addressFromIndex = async (pb: string, i: number): Promise<string> => {
if (this.keyringMode === KEYRING_MODE.hd) {
this.checkKeyring();
if (!this.hdk) {
// @ts-ignore
this.hdk = HDKey.fromExtendedKey(this.xpub);
}
const childrenPath = this.childrenPath
.replace("*", String(i))
.replace(/\*/g, "0");
const dkey = this.hdk.derive(`${pb}/${childrenPath}`);
const address =
"0x" + publicToAddress(dkey.publicKey, true).toString("hex");
return toChecksumAddress(address);
} else {
const result = Object.keys(this.paths)[i];
if (result) {
return toChecksumAddress(result);
} else {
throw new Error(`KeystoneError#pubkey_account.no_expected_account`);
}
}
};
At a high level, __readCryptoHDKey
performs validations and initializes some required fields which __addressFromIndex
later uses to actually derive the child addresses. To derive a single-step child address, the expression ${pb}/${childrenPath}
in __addressFromIndex
must evaluate to "m/i"
, where i = 0
for the first child, i = 1
for the second, and so on.
Since pb
is a constant that is already hardcoded to m
(stands for “master” key), we needed to make sure that
const childrenPath = this.childrenPath
.replace("*", String(i))
.replace(/\*/g, "0");
evaluates to the string value of i
, which will only happen if this.childrenPath == "*"
. this.childrenPath
is set in __readCryptoHDKey
:
const childrenPath =
cryptoHDKey.getChildren()?.getPath() || DEFAULT_CHILDREN_PATH;
Since DEFAULT_CHILDREN_PATH
is another constant that is hardcoded to "0/*"
, which would provide us with two steps of derivation (the children of the child at index 0, which we did not want), we needed to ensure that cryptoHDKey.getChildren()?.getPath() = "*"
.
From the CryptoKeypath implementation, we see that getPath()
will evaluate to "*"
if there exists a single pathComponent
which is a wildcard
:
public getPath = () => {
if (this.components.length === 0) {
return undefined;
}
const components = this.components.map((component) => {
return `${component.isWildcard() ? '*' : component.getIndex()}${
component.isHardened() ? "'" : ''
}`;
});
return components.join('/');
};
And from the PathComponent implementation, we see that to make the pathComponent
a wildcard, we need to pass an undefined
index value.
constructor(args: { index?: number; hardened: boolean }) {
this.index = args.index;
this.hardened = args.hardened;
if (this.index !== undefined) {
this.wildcard = false;
} else {
this.wildcard = true;
}
if (this.index && (this.index & PathComponent.HARDENED_BIT) !== 0) {
throw new Error(
`#[ur-registry][PathComponent][fn.constructor]: Invalid index ${this.index} - most significant bit cannot be set`,
);
}
}
Therefore, we were able to use the following CryptoHDKey
construction to successfully sync Silo with MetaMask:
const hdkey = new CryptoHDKey({
isMaster: false,
key: Buffer.from(address.pubkey),
origin: new CryptoKeypath([], Buffer.from(fingerprint, "hex")),
children: new CryptoKeypath([
new PathComponent({ index: undefined, hardened: false })
]),
chainCode: Buffer.from("0000000000000000000000000000000000000000000000000000000000000000", "hex"),
parentFingerprint: Buffer.from(fingerprint, "hex"),
name: address.name
});
Once synced, MetaMask displayed the single-step derived children for selection in its UI!
We hope that others can use our implementation above as a model for building their own air-gapped wallets and integrating with MetaMask.
Overall Learnings and Next Steps
Our immediate next steps are to continue iterating on our design and security model for MetaSilo, and to add support for more chains.
Looking to the future, we’re continuing to think about scalable ways to build direct smart contract integrations into Silo. With MetaSilo, developers can monitor what contracts are used most often and prioritize implementing automation around them. However, moving beyond case-by-case automation remains a challenge. While a protocol such as WalletConnect could help standardize cross-chain QR code formats, different chains still use different transaction serialization formats, abstractions, and smart contract primitives. We’d love to see the development of more cross-chain standards or libraries that provide simple interfaces for cross-chain smart contract integrations.
At a high level, working on this project has shown us how important standardization is for the crypto ecosystem. The EIP 191/712 standards allowed us to make MetaSilo more secure, since we could rely on a safe serialization format that is built into the protocol. If we’re lucky, standards can help fight interoperability challenges like the ones we ran into (see Appendix B for a very specific example).
We believe that standard interfaces around wallets are critical to Web3. Air-gapped wallets are a major step forward in this regard. They allow anyone to build on top of the excellent UI applications — like MetaMask — that’ve been developed, while also ensuring that people can implement their own key security backends. We’d love to collaborate with teams building browser wallets or wallet backends to accelerate the development of these standards and prioritize support for air-gapped wallets. We’d especially love to see support for simple key-pair wallets instead of only HD Key wallets.
Finally, if you found any of the above interesting, we’d love to chat! Please reach out to @nsuri_, @nickraystalder, @conorpp, or @0x0ece. We want to make Silo the best open source custody solution and enjoy learning about use-cases and sharing ideas. We’re also hiring — if you’re an interested security / blockchain / full-stack engineer, we’d love to hear from you!
Appendix A
HD Wallet Chain Codes
BIP-32 defines the chain code as a random extra 256 bits of entropy that prevents child derivation from depending solely on the parent public/private keys themselves. It is the same for both the parent public/private keys and used as to create a keyed hash function that computes the child key, denoted simply by $hash(P, i)$ above to simplify the exposition.
The hash function used for BIP-44 is HMAC-SHA512, which produces a 512 bit (64 byte) hash. The child key is computed using the first 32 bytes of the hash, while the last 32 bytes of the hash are meant to be used as the chain code if deriving further children from this child key. Chain codes are, like derived keys, also iterative.
BIP-32 has a spec for generating the top-level master key as well, which involves computing S = 256 bit sequence from a (P)RNG
and HMAC-SHA512(Key = "Bitcoin seed", Data = S)
. The master key is meant to be the first 32 bytes of this hash, while the top-level chain code is meant to be the last 32 bytes of the hash. Since we do not follow this “master” key generation strategy in Silo, we needed to either pick a random chain code to use for all keys, or generate random chain codes along with keys. We chose the former option for its simplicity, which is why we use the chain code 0x00 for all single-step derivations: the keys themselves already have enough entropy.
Appendix B
Where Interoperating Gets Hard
MetaSilo uses the Silo UI to scan the QR code from MetaMask and then send the signing request to the Silo validators running on cosmos. MetaMask and the Silo UI are both written in JavaScript, and use the ethereumjs library. The Silo validators are written in Go, and use go-ethereum (geth).
While developing MetaSilo, we found that payloads do not decode seamlessly between these libraries. Specifically, geth expects signed transactions with r,s,v
signature values set:
type DynamicFeeTx struct {
ChainID *big.Int
Nonce uint64
GasTipCap *big.Int // a.k.a. maxPriorityFeePerGas
GasFeeCap *big.Int // a.k.a. maxFeePerGas
Gas uint64
To *common.Address `rlp:"nil"` // nil means contract creation
Value *big.Int
Data []byte
AccessList AccessList
// Signature values
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
}
However, MetaMask passes an RLP-encoded unsigned transaction, which is missing the r,s,v
signature values. Since the payload is 3 values short of what geth expects, geth cannot successfully decode it into a well-defined transaction data type. This was a serious issue for us; As described in the security section above, we needed to decode this payload into a well-defined type before signing it.
To resolve this issue, we submitted this PR to go-ethereum to allow decoding unsigned transactions in geth. However, it turned out that this change would have too many unsafe implications, so we resorted to a more hacky solution. We fell victim to a common issue in the space: a lack of standards limits the compatibility between libraries, which not only makes it significantly more difficult to build robust software, but also harder for teams with diverse skill sets to start building and collaborating in crypto.
Our fix was to copy/paste the geth transaction types into our own Go structs with the r,s,v
signature values tagged as rlp:"optional"
, and then translate our custom structs into the canonical go-ethereum types. It works, but it’s not scalable. We’d love to see an Ethereum standard that addresses passing unsigned transactions around from one place to another.
https://jumpcrypto.com/custody-solutions-which-option-is-right-for-me/ and https://jumpcrypto.com/custody-bft-policy-checking-threshold-signatures/ ↩︎
It is worth mentioning that both MetaMask and Ledger have enterprise products that help address these issues as well. However, as Silo is an open-source custody solution, we did not want to introduce a dependency on a 3rd party enterprise product. ↩︎
An interesting alternative for adding native smart contract interactions into Silo is to use the Silo UI for execution, while using Web3 UIs to view metadata. The challenge here is that the UX flows are different. When a user clicks a button to perform an operation on a Web3 frontend, it isn’t completely clear to the user what smart contract function is executed. This makes it much more difficult for a user to line up what they want to do in the Web3 UI with what their options are in the Silo UI. ↩︎
There are lots of variations to explore beyond the post-once-whitelist-forever policy. For example, Silo could whitelist contract addresses with an expiration date, so that teams can stay up to date with the contracts that they are using. Using contract ABIs, Silo could also create rules around the contract functions that are called and the parameter values being used. ↩︎
An attacker could try to trick Silo by passing
dataType = 3
and a payload representing an RLP-encoded unsigned transaction.dataType = 3
corresponds to personal_sign messages, which are arbitrary messages meant to be signed but not submitted to the blockchain. Lots of Web3 frontends use personal_sign messages for logins (for example, polygon bridge and dydx). ↩︎The parent key fingerprint is the first 8 bytes of the parent public key. The EthSignRequest also has a field for the child address, but at the time of our development this had not been added to the payload in the MetaMask keyring library. We contributed these changes to provide a fuller SignRequest payload. At the time of writing, the PR to update the keystone-airgaped-base library is still awaiting merge into MetaMask. ↩︎
The security benefit of a threshold signing system depends on never combining the distributed key shares to the full secret \(s\), which would be needed to feed into the hash function. ↩︎
Shoutout to the team at KeystoneHQ for their fantastic implementations of the BC-UR spec and the MetaMask air-gapped keyring backend. ↩︎
Share
Stay up to date with the latest from Jump_
More articles
Disclaimer
The information on this website and on the Brick by Brick podcast or Ship Show Twitter spaces is provided for informational, educational, and entertainment purposes only. This information is not intended to be and does not constitute financial advice, investment advice, trading advice, or any other type of advice. You should not make any decision – financial, investment, trading or otherwise – based on any of the information presented here without undertaking your own due diligence and consulting with a financial adviser. Trading, including that of digital assets or cryptocurrency, has potential rewards as well as potential risks involved. Trading may not be suitable for all individuals. Recordings of podcast episodes or Twitter spaces events may be used in the future.