Skip to content

Commit

Permalink
Quark Nonce Manager [Renaming] (#207)
Browse files Browse the repository at this point in the history
This patch mostly renames `QuarkStateManager` to `QuarkNonceManager`.
Additionally, we start to use the term `submissionToken`, since it can
cover first plays or replays both as "submissions". We choose the term
`FREE` for the 0-token and `EXHAUSTED` for non-replayable tokens.
Overall, this should provide a clearer and more consistent naming
throughout the changeset.
  • Loading branch information
hayesgm committed Sep 13, 2024
1 parent a6cd465 commit 3ad7be5
Show file tree
Hide file tree
Showing 35 changed files with 589 additions and 408 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ _Quark Wallet_ executes _Quark Operations_ containing a transaction script (or a

_Quark Operations_ are either directly executed or authorized by signature, and can include replayable transactions and support callbacks for complex operations like flash-loans. See the [Quark Wallet Features](#quark-wallet-features) section for more details.

### Quark State Manager
### Quark Nonce Manager

_Quark State Manager_ is a contract that manages nonces and ensures isolated storage for each Quark wallet and operation, preventing storage clashes between different wallets and operations.
_Quark Nonce Manager_ is a contract that manages nonces for each Quark wallet and operation, preventing accidental replays of operations. Quark operations can be replayable by generating a secret key and building a hash-chain to allow N replays of a given script.

### Wallet Factory

Expand All @@ -44,7 +44,7 @@ flowchart TB
wallet[Quark Wallet]
jar[Code Jar]
script[Quark Script]
state[Quark State Manager]
state[Quark Nonce Manager]
factory -- 1. createAndExecute --> wallet
wallet -- 2. saveCode --> jar
Expand Down Expand Up @@ -82,7 +82,7 @@ An example use-case for replayable scripts is recurring purchases. If a user wan

#### Same script address, but different calldata

For replayable transactions where the nonce is cleared, _Quark State Manager_ requires future transactions using that nonce to use the same script. This is to ensure that the same nonce is not accidentally used by two different scripts. However, it does not require the `calldata` passed to that script to be the same. This means that a cleared nonce can be executed with the same script but different calldata.
For replayable transactions where the nonce is cleared, _Quark Nonce Manager_ requires future transactions using that nonce to use the same script. This is to ensure that the same nonce is not accidentally used by two different scripts. However, it does not require the `calldata` passed to that script to be the same. This means that a cleared nonce can be executed with the same script but different calldata.

Allowing the calldata to change greatly increases the flexibility of replayable scripts. One can think of a replayable script like a sub-module of a wallet that supports different functionality. In the [example script](./test/lib/RecurringPurchase.sol) for recurring purchases, there is a separate `cancel` function that the user can sign to cancel the nonce, and therefore, cancel all the recurring purchases that use this nonce. The user can also also sign multiple `purchase` calls, each with different purchase configurations. This means that multiple variations of recurring purchases can exist on the same nonce and can all be cancelled together.

Expand All @@ -92,7 +92,7 @@ One danger of flexible `calldata` in replayable scripts is that previously signe

Callbacks are an opt-in feature of Quark scripts that allow for an external contract to call into the Quark script (in the context of the _Quark wallet_) during the same transaction. An example use-case of callbacks is Uniswap flashloans ([example script](./quark-core-scripts/src/UniswapFlashLoan.sol)), where the Uniswap pool will call back into the _Quark wallet_ to make sure that the loan is paid off before ending the transaction.

Callbacks need to be explicitly turned on by Quark scripts. Specifically, this is done by writing the callback target address to the callback storage slot in _Quark State Manager_ (can be done via the `allowCallback` helper function in [`QuarkScript.sol`](./quark-core/src/QuarkScript.sol)).
Callbacks need to be explicitly turned on by Quark scripts. Specifically, this is done by writing the callback target address to the callback storage slot in _Quark Nonce Manager_ (can be done via the `allowCallback` helper function in [`QuarkScript.sol`](./quark-core/src/QuarkScript.sol)).

### EIP-1271 Signatures

Expand Down
4 changes: 2 additions & 2 deletions script/DeployQuarkWalletFactory.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {CodeJar} from "codejar/src/CodeJar.sol";

import {QuarkWallet} from "quark-core/src/QuarkWallet.sol";
import {BatchExecutor} from "quark-core/src/periphery/BatchExecutor.sol";
import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol";
import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol";

import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol";
import {QuarkFactory} from "quark-factory/src/QuarkFactory.sol";
Expand Down Expand Up @@ -49,7 +49,7 @@ contract DeployQuarkWalletFactory is Script {

quarkFactory.deployQuarkContracts();

console.log("Quark State Manager Deployed:", address(quarkFactory.quarkStateManager()));
console.log("Quark Nonce Manager Deployed:", address(quarkFactory.quarkNonceManager()));
console.log("Quark Wallet Implementation Deployed:", address(quarkFactory.quarkWalletImpl()));
console.log("Quark Wallet Proxy Factory Deployed:", address(quarkFactory.quarkWalletProxyFactory()));
console.log("Batch Executor Deployed:", address(quarkFactory.batchExecutor()));
Expand Down
56 changes: 56 additions & 0 deletions src/quark-core/src/QuarkNonceManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;

import {IQuarkWallet} from "quark-core/src/interfaces/IQuarkWallet.sol";

/**
* @title Quark Nonce Manager
* @notice Contract for managing nonces for Quark wallets
* @author Compound Labs, Inc.
*/
contract QuarkNonceManager {
error NonReplayableNonce(address wallet, bytes32 nonce, bytes32 submissionToken);
error InvalidSubmissionToken(address wallet, bytes32 nonce, bytes32 submissionToken);

event NonceSubmitted(address wallet, bytes32 nonce, bytes32 submissionToken);

/// @notice Represents the unclaimed bytes32 value.
bytes32 public constant FREE = bytes32(uint256(0));

/// @notice A token that implies a Quark Operation is no longer replayable.
bytes32 public constant EXHAUSTED = bytes32(type(uint256).max);

/// @notice Mapping from nonces to last used submission token.
mapping(address wallet => mapping(bytes32 nonce => bytes32 lastToken)) public nonceSubmissions;

/**
* @notice Returns the nonce token (last submission token) for a given nonce. For finalized scripts, this will be `uint256(-1)`. For unclaimed nonces, this will be `uint256(0)`. Otherwise, it will be the next value in the replay chain.
* @param wallet The wallet for which to get the nonce token.
* @param nonce The nonce for the given request.
* @return submissionToken The last used submission token, or 0 if unused or -1 if finalized.
*/
function getNonceSubmission(address wallet, bytes32 nonce) external view returns (bytes32 submissionToken) {
return nonceSubmissions[wallet][nonce];
}

/**
* @notice Attempts a first or subsequent submission of a given nonce from a wallet.
* @param nonce The nonce of the chain to submit.
* @param submissionToken The submission token of the submission. For single-use operations, set `submissionToken` to `uint256(-1)`. For first-use replayable operations, set `submissionToken` = `nonce`.
*/
function submitNonceToken(bytes32 nonce, bytes32 submissionToken) external {
bytes32 lastTokenSubmission = nonceSubmissions[msg.sender][nonce];
if (lastTokenSubmission == EXHAUSTED) {
revert NonReplayableNonce(msg.sender, nonce, submissionToken);
}

bool validFirstPlayOrReplay =
lastTokenSubmission == FREE || keccak256(abi.encodePacked(submissionToken)) == lastTokenSubmission;
if (!validFirstPlayOrReplay) {
revert InvalidSubmissionToken(msg.sender, nonce, submissionToken);
}

nonceSubmissions[msg.sender][nonce] = submissionToken;
emit NonceSubmitted(msg.sender, nonce, submissionToken);
}
}
2 changes: 1 addition & 1 deletion src/quark-core/src/QuarkScript.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ abstract contract QuarkScript {
bytes32 internal constant REENTRANCY_FLAG_SLOT =
bytes32(uint256(keccak256("quark.scripts.reentrancy.guard.v1")) - 1);

/// @notice A safer, but gassier reentrancy guard that writes the flag to the QuarkStateManager
/// @notice A safer, but gassier reentrancy guard that writes the flag to the QuarkNonceManager
modifier nonReentrant() {
bytes32 slot = REENTRANCY_FLAG_SLOT;
bytes32 flag;
Expand Down
55 changes: 0 additions & 55 deletions src/quark-core/src/QuarkStateManager.sol

This file was deleted.

26 changes: 13 additions & 13 deletions src/quark-core/src/QuarkWallet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {IERC1271} from "openzeppelin/interfaces/IERC1271.sol";

import {CodeJar} from "codejar/src/CodeJar.sol";

import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol";
import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol";
import {IHasSignerExecutor} from "quark-core/src/interfaces/IHasSignerExecutor.sol";

/**
Expand Down Expand Up @@ -71,8 +71,8 @@ contract QuarkWallet is IERC1271 {
/// @notice Address of CodeJar contract used to deploy transaction script source code
CodeJar public immutable codeJar;

/// @notice Address of QuarkStateManager contract that manages nonces and nonce-namespaced transaction script storage
QuarkStateManager public immutable stateManager;
/// @notice Address of QuarkNonceManager contract that manages nonces and nonce-namespaced transaction script storage
QuarkNonceManager public immutable nonceManager;

/// @notice Name of contract
string public constant NAME = QuarkWalletMetadata.NAME;
Expand Down Expand Up @@ -113,8 +113,8 @@ contract QuarkWallet is IERC1271 {
/// @notice Well-known storage slot for the currently executing script's address (if any)
bytes32 public constant ACTIVE_SCRIPT_SLOT = bytes32(uint256(keccak256("quark.v1.active.script")) - 1);

/// @notice A token that implies a Quark Operation is no longer replayable.
bytes32 public constant NO_REPLAY_TOKEN = bytes32(type(uint).max);
/// @notice A nonce submission token that implies a Quark Operation is no longer replayable.
bytes32 public constant EXHAUSTED_TOKEN = bytes32(type(uint256).max);

/// @notice The magic value to return for valid ERC1271 signature
bytes4 internal constant EIP_1271_MAGIC_VALUE = 0x1626ba7e;
Expand All @@ -136,11 +136,11 @@ contract QuarkWallet is IERC1271 {
/**
* @notice Construct a new QuarkWalletImplementation
* @param codeJar_ The CodeJar contract used to deploy scripts
* @param stateManager_ The QuarkStateManager contract used to write/read nonces and storage for this wallet
* @param nonceManager_ The QuarkNonceManager contract used to write/read nonces and storage for this wallet
*/
constructor(CodeJar codeJar_, QuarkStateManager stateManager_) {
constructor(CodeJar codeJar_, QuarkNonceManager nonceManager_) {
codeJar = codeJar_;
stateManager = stateManager_;
nonceManager = nonceManager_;
}

/**
Expand Down Expand Up @@ -223,7 +223,7 @@ contract QuarkWallet is IERC1271 {
codeJar.saveCode(op.scriptSources[i]);
}

stateManager.submitNonceToken(op.nonce, NO_REPLAY_TOKEN);
nonceManager.submitNonceToken(op.nonce, EXHAUSTED_TOKEN);

emit ExecuteQuarkScript(msg.sender, op.scriptAddress, op.nonce, ExecutionType.Signature);

Expand All @@ -233,7 +233,7 @@ contract QuarkWallet is IERC1271 {
/**
* @notice Verify a signature and execute a replayable QuarkOperation
* @param op A QuarkOperation struct
* @param replayToken The replay token for the replayable quark operation. For the first submission, this is generally the `rootHash`.
* @param submissionToken The submission token for the replayable quark operation for QuarkNonceManager. For the first submission, this is generally the `rootHash` of a chain.
* @param digest A EIP-712 digest for either a QuarkOperation or MultiQuarkOperation to verify the signature against
* @param v EIP-712 signature v value
* @param r EIP-712 signature r value
Expand All @@ -242,7 +242,7 @@ contract QuarkWallet is IERC1271 {
*/
function verifySigAndExecuteReplayableQuarkOperation(
QuarkOperation calldata op,
bytes32 replayToken,
bytes32 submissionToken,
bytes32 digest,
uint8 v,
bytes32 r,
Expand All @@ -260,7 +260,7 @@ contract QuarkWallet is IERC1271 {
codeJar.saveCode(op.scriptSources[i]);
}

stateManager.submitNonceToken(op.nonce, replayToken);
nonceManager.submitNonceToken(op.nonce, submissionToken);

emit ExecuteQuarkScript(msg.sender, op.scriptAddress, op.nonce, ExecutionType.Signature);

Expand Down Expand Up @@ -292,7 +292,7 @@ contract QuarkWallet is IERC1271 {
codeJar.saveCode(scriptSources[i]);
}

stateManager.submitNonceToken(nonce, NO_REPLAY_TOKEN);
nonceManager.submitNonceToken(nonce, EXHAUSTED_TOKEN);

emit ExecuteQuarkScript(msg.sender, scriptAddress, nonce, ExecutionType.Direct);

Expand Down
8 changes: 4 additions & 4 deletions src/quark-core/src/QuarkWalletStandalone.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity 0.8.23;

import {CodeJar} from "codejar/src/CodeJar.sol";

import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol";
import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol";
import {QuarkWallet, IHasSignerExecutor} from "quark-core/src/QuarkWallet.sol";

/**
Expand All @@ -23,10 +23,10 @@ contract QuarkWalletStandalone is QuarkWallet, IHasSignerExecutor {
* @param signer_ The address that is allowed to sign QuarkOperations for this wallet
* @param executor_ The address that is allowed to directly execute Quark scripts for this wallet
* @param codeJar_ The CodeJar contract used to deploy scripts
* @param stateManager_ The QuarkStateManager contract used to write/read nonces and storage for this wallet
* @param nonceManager_ The QuarkNonceManager contract used to write/read nonces and storage for this wallet
*/
constructor(address signer_, address executor_, CodeJar codeJar_, QuarkStateManager stateManager_)
QuarkWallet(codeJar_, stateManager_)
constructor(address signer_, address executor_, CodeJar codeJar_, QuarkNonceManager nonceManager_)
QuarkWallet(codeJar_, nonceManager_)
{
signer = signer_;
executor = executor_;
Expand Down
10 changes: 5 additions & 5 deletions src/quark-factory/src/QuarkFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity 0.8.23;

import {CodeJar} from "codejar/src/CodeJar.sol";
import {QuarkWallet} from "quark-core/src/QuarkWallet.sol";
import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol";
import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol";
import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol";
import {BatchExecutor} from "quark-core/src/periphery/BatchExecutor.sol";

Expand All @@ -16,20 +16,20 @@ contract QuarkFactory {
CodeJar public immutable codeJar;
QuarkWallet public quarkWalletImpl;
QuarkWalletProxyFactory public quarkWalletProxyFactory;
QuarkStateManager public quarkStateManager;
QuarkNonceManager public quarkNonceManager;
BatchExecutor public batchExecutor;

constructor(CodeJar codeJar_) {
codeJar = codeJar_;
}

function deployQuarkContracts() external {
quarkStateManager =
QuarkStateManager(payable(codeJar.saveCode(abi.encodePacked(type(QuarkStateManager).creationCode))));
quarkNonceManager =
QuarkNonceManager(payable(codeJar.saveCode(abi.encodePacked(type(QuarkNonceManager).creationCode))));
quarkWalletImpl = QuarkWallet(
payable(
codeJar.saveCode(
abi.encodePacked(type(QuarkWallet).creationCode, abi.encode(codeJar, quarkStateManager))
abi.encodePacked(type(QuarkWallet).creationCode, abi.encode(codeJar, quarkNonceManager))
)
)
);
Expand Down
Loading

0 comments on commit 3ad7be5

Please sign in to comment.