Blog

Warping Time for Solana DeFi: Reconciling Expiring Nonces and Institutional Policy Controls

Written by Dima Kogan | Jul 6, 2023 5:59:07 PM

Supporting Solana DeFi

To securely participate in the DeFi ecosystem, institutions require a transaction workflow that facilitates institutional-grade policy controls. Such policy controls often require multiple people within the organization to approve the transaction, which can take minutes or even hours. However, Solana DeFi transactions do not readily admit such a lengthy approval process. The reason is that, on the one hand, transactions have a short expiration time by default, which stems from the way that Solana defends against replay attacks. On the other hand, wallets cannot easily extend that expiration time by modifying the transaction, as any modification invalidates any partial signatures that the transaction already contains by the time the wallet receives it.

In this blog post, we'll discuss how we have tackled this problem and built a solution that brings institutional transaction workflows to Solana DeFi transactions. Many people on the Fordefi team have contributed to this solution, but a special shoutout goes to Yogev Bar-On, Ron Cohen, Rony Fragin, and David Tsah.

The Recent Blockhash Mechanism

To understand the challenge in supporting Solana DeFi in an institutional wallet, we first need to understand a mechanism called Recent Blockhash, which Solana uses to protect against replay attacks. Recall that every blockchain must guarantee that each signed transaction is executed at most once. Otherwise, if Alice signs a “Transfer 1 SOL to Bob” transaction, Bob could send 100 copies of the transaction to the blockchain, triggering a transfer of 100 SOL from Alice to Bob.

Solana protects against replay attacks by disallowing inclusion of any transaction that is identical to a transaction that has already been included in the blockchain. The way this is implemented is that every transaction must include a hash of a recent block, where a block is considered recent if it is at most 151 slots old, which corresponds to 1–2 minutes. Each validator maintains a list of all the recent transactions, and when it receives a new transaction, it checks that: (i) the transaction includes a recent blockhash; and (ii) the transaction is not identical to any of the recent transactions. The validator only includes the transaction if it satisfies both conditions.

An artifact of this mechanism is that once a transaction is signed with a particular recent blockhash, the transaction has an expiration time of 1–2 minutes, and it must be sent to the blockchain and included in a block within this time period.

On the top: a transaction from block #225192220. Its previous block hash field contains the hash of block #225192178 (shown on the bottom), which has been included in the blockchain 16 seconds earlier.

The Challenge for Institutional Wallets

Institutional wallets serve organizations rather than individuals. They allow a group of people to collectively use the same keys and addresses. A key feature of institutional wallets is the ability to enforce an organizational transaction policy. The policy allows the administrators of the organization to require certain transactions to be approved by multiple people within the organization. When a user submits a transaction to the wallet, the wallet determines the required approvers for the transaction based on the policy. The wallet then notifies the approvers to request their approval and holds the transaction until it receives the necessary approvals. Once the transaction is approved, the wallet signs the transaction (in the case of an MPC wallet, the signature is produced by executing an MPC protocol) and sends it to the blockchain.

The approval process introduces a possible delay between the time a transaction is created and the time it is signed and sent to the blockchain. When the wallet creates a transaction, which is the case for transfers that the user initiates from the wallet UI, the wallet can defer the choice of recent blockhash and only set it after the transaction has been approved and is about to be signed. This guarantees that the blockhash is recent enough and the transaction is valid. The problem is with DeFi transactions, which are created by DApps and passed to the wallet when they already contain a recent blockhash. If left unchanged, the blockhash is likely to expire by the time the transaction is approved and signed, which invalidates the transaction.

Attempts at a Solution

Our initial attempt was to update the recent blockhash in the transaction right before signing, overriding the stale hash with a fresh one. However, this approach fails. The reason this fails is due to the fact that DeFi transactions are often partially signed by the DApp by the time the wallet receives the transaction from the DApp. Modifying the recent blockhash within the transaction thus invalidates the partial signatures and therefore the transaction itself. We therefore seem to be stuck with the original recent blockhash and its short lifetime.

The problem of short-lived blockhashes has not gone unnoticed by the Solana developers. To address this problem, Solana supports an alternative mechanism to the recent blockhash called Durable Nonces. Durable nonces build on a special type of account, called a nonce account, which stores a nonce value. A transaction can use a durable nonce instead of a recent blockhash by referring to a nonce account and using the nonce value from the account instead of the recent blockhash. The transaction must also update the nonce in the nonce account to a new value. As a result, each value in the nonce account can only be used by one transaction, which prevents the replay attack. Moreover, there is no time-based expiration to the nonce value, which allows for a lengthy approval & signing process. (For more info, see this great exposition on Durable Nonces.)

However, durable nonces cannot be readily used by institutional wallets to support DeFi transactions. The reason is that DApps do not use durable nonces in the transactions they create. In fact, there does not exist a standard interface through which DApps can ask a wallet whether there is a nonce account they should use for the transaction they create. Therefore, if a wallet wants to replace the recent blockhash in the transaction with a durable nonce, it must modify the transaction it receives from the DApp. But this brings us back to the problem of the wallet being unable to modify transactions, due to transactions having partial signatures by the time the wallet receives them.

To recap, when the wallet receives a transaction from the DApp, the transaction contains (i) a short-lived recent blockhash; and (ii) partial signatures that prevent the wallet from replacing the short-lived recent blockhash with a more fresh value or with a durable nonce.

Partial Signatures

Let us take a closer look at the source of the partial signatures on Solana DeFi transactions. Recall that each Solana transaction may reference multiple accounts, and it must declare upfront the list of accounts it references. In addition, if a transaction needs to modify an account, it must also include a signature with the private key of the owner of that account. As a result, transactions that modify multiple accounts include multiple signatures.

It turns out that Solana DeFi transactions often create temporary accounts. To create a temporary account, the DApp generates an ephemeral key pair, sets the public key as the initial owner of the account, and uses the private key to sign the transaction. In fact, in all of the cases we examined, the partial signatures on the transactions corresponded to new temporary accounts created as part of the transaction.

There are multiple reasons DApps use temporary accounts. One use case is to facilitate swaps. The Solana documentation recommends:

The best practice is to spl_token::instruction::approve a precise amount to a new throwaway Keypair, and then have that new Keypair sign the swap transaction. This limits the amount of tokens that can be taken from the user's account by the program.

For example, the decentralized exchange Orca follows this pattern.

Transactions that handle Wrapped SOL also often use temporary token accounts. Such transactions would often credit the user with Wrapped SOL in the temporary token account they create and then close the account, burning the Wrapped SOL, and crediting the user’s regular account with native SOL. See, for example, the implementation in the Solana Token Program and Raydium.

Finally, when users provide liquidity to pools, their position is often represented with LP (Liquidity Provider) tokens. In many cases, the LP token mint account is created as part of the transaction that deposits the liquidity. Like in the previous examples, to create a mint account, the DApp generates an ephemeral key pair with which it signs the transaction. See for example the [createMint] function in the Solana Token Program.

Example of a transaction that includes a signature by a temporary account that gets created as part of the transaction. Source: Solscan.io

The Solution

In all the above examples, the DApp generates the ephemeral key pair for the temporary account on the client-side in the user’s browser. This suggests a pathway to solving the partial signing problem:

1. Extract from the DApp the private ephemeral keys of all the temporary accounts that the DApp creates. This can be done if the wallet has a browser extension, which has access to the client-side code running in the browser.

2. Modify the transaction, replacing the stale recent blockhash with a fresh one (or with a durable nonce).

3. Sign the transaction with the wallet’s private key.

4. Fix the now-invalid partial signatures on the transaction by re-signing the transaction with the extracted ephemeral keys.

Extracting the Ephemeral Private Keys

To extract the ephemeral private keys that the DApp creates in the user’s browser, we need to somehow add a hook in the key generation process, which we can then use to learn the private keys.

The standard way to generate a fresh key pair is using the Keypair.generate() function from the solana-web3 library. Hooking this function would allow us to learn each private key that the DApp generates from the return value of the function. However, there is no easy way to hook this library function, as most of the DApps are minified.

Looking a little bit deeper, we see that Keypair.generate() internally calls ed25519.utils.randomPrivateKey() from the noble-ed25519 library. In its turn, this function calls crypto.getRandomValues function from the Web Crypto API. This turns out to be an excellent hook point, since the Web Crypto API is a native API provided by the browser, and its methods are global and not minified. Our browser extension can, therefore, hook this function, inserting our code whenever it is called.

In our hook, we can learn the random bytes returned by the browser’s native interface, or even return random values that we control from our source of randomness. Either way, our code can learn the random bytes that form the ed25519 random private key of the temporary account!

The overall flow is then as follows. Whenever the user connects our wallet to a Solana DApp, our wallet injects the hook code into the DApp, hooking the crypto.getRandomValues. Whenever this function is called, we learn the ed25519 keypair obtained from those random values. When our wallet then receives a partially-signed transaction from the DApp, it waits for the transaction to receive all the necessary approvals, based on the policy. Once the transaction is approved and is ready to be signed, the wallet updates the recent blockhash in the transaction. The wallet then produces a signature on behalf of the wallet’s account (by executing the MPC protocol). Finally, the wallet then fixes the DApp’s partial signatures on the transaction. To this end, for each account that the transaction specifies as a signer, the wallet looks, among all ephemeral key pairs it has learned (in the same browser window), for the key pair whose public key matches that signing account. The wallet then signs the transaction with the learned private key and attaches the updated signature to the transaction. The wallet then sends the fully-signed transaction to the blockchain.

Frontend Fix-ups

As described, the solution above prolongs the lifetime of the transaction, but it has one shortcoming. The DApp’s frontend still assumes the transaction has the original expiration time, and the DApps UI would often show an error if the transaction is not signed within the original expiration time. Although this does not affect the successful completion of the transaction on-chain, it results in a confusing user experience.

Example of the Orca Frontend timing out while waiting for the transaction to be signed. (Speed 4x).

The DApp’s frontend (or more precisely, the solana-web3 library) implements this expiration check by monitoring the chain’s block height by querying the RPC node and comparing the chain’s current block height with the expiration block height of the transaction (which, as mentioned above, is h + 151, where h is the height of the block whose hash is used as the recent block hash). When the chain reaches the expiration block height, the DApp shows an error to the user.

To solve this problem, our extension adds another hook in the DApp’s webpage. Specifically, the extension hooks all calls to the RPC node that query the recent block height (such as getBlockHeight and getEpochInfo). It then “freezes” the chain’s block height in the RPC responses, until the wallet finishes processing the transaction. From the DApp’s perspective, the block height stands still, and the transaction remains valid. The result is a cleaner user experience without the DApp showing any false-positive errors to the user.

Demo

Putting all the pieces together, we obtain the result we wanted: a Solana DeFi flow which is not subject to short expiration and that allows an institutional-grade approval process! See below a demo video of the end-to-end solution:

Conclusion

In this blog post, we described the solution we have developed to extend the short lifetime of Solana DeFi transactions. Our solution requires some low-level hackery, and it would be desirable to find a more principled solution. Our feeling is that such a solution is likely to require extending the standard wallet interface. A new interface could, for example, allow the wallet to instruct the DApp (or the solana-web3 library) to use durable nonces instead of recent blockhashes. Alternatively, a new interface could allow the DApp (or the solana-web3 library) to pass all ephemeral private keys it has generated to the wallet. We look forward to engaging with the Solana community to further improve the support for institutional wallets in Solana and advance institutional adoption of Solana in general.