Skip to content

Commit

Permalink
Quark v2 (#216)
Browse files Browse the repository at this point in the history
This is a moderately-sized redesign of Quark v1. The main goal of this
redesign is to improve the security of replayable operations by making
them only submittable by submitters that have access to a secret
submission token. A secondary goal of this redesign is to simplify a
bunch of the flows to reduce gas overhead and code complexity.

Changes:
- Introduce new replay mechanism that requires a submitter to provide a
`submissionToken`. Previously, replayable operations were submittable by
any address
- Remove isolated storage in the `QuarkStateManager`, which allows us to
remove much of the complicated codepaths related to
`setActiveNonceAndCallback`, `activeNonceScript`, `nonceScriptAddress`,
and `walletStorage`
- Rename `QuarkStateManager` to `QuarkNonceManager` since its job is now
to only manage nonces
- Adjust `QuarkOperation` struct to have a `isReplayable` field
- Bump up Solidity version to 0.8.27 and EVM version to `cancun`
- Use `TSTORE/TLOAD` to save gas

---------

Co-authored-by: Geoff Hayes <[email protected]>
  • Loading branch information
kevincheng96 and hayesgm authored Sep 30, 2024
1 parent da0cfcd commit 1afeb4e
Show file tree
Hide file tree
Showing 100 changed files with 3,079 additions and 1,185 deletions.
384 changes: 204 additions & 180 deletions .gas-snapshot

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Run Forge build
run: |
forge --version
forge build --sizes
forge build --via-ir --sizes
id: build

- name: Run Forge Format
Expand Down
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

Quark is an Ethereum smart contract wallet system, designed to run custom code — termed Quark Operations — with each transaction. This functionality is achieved through Quark wallet's capability to execute code from a separate contract via a `callcode` or `delegatecall` operation. The system leverages _Code Jar_, using `CREATE2` to deploy EVM bytecode for efficient code re-use. Additionally, the _Quark State Manager_ contract plays a pivotal role in managing nonces and ensuring isolated storage per operation, thus preventing storage conflicts. The system also includes a wallet factory for deterministic wallet creation and a suite of Core Scripts — audited, versatile contracts that form the foundation for complex Quark Operations such as multicalls and flash-loans.
Quark is an Ethereum smart contract wallet system, designed to run custom code — termed Quark Operations — with each transaction. This functionality is achieved through Quark wallet's capability to execute code from a separate contract via a `callcode` or `delegatecall` operation. The system leverages _Code Jar_, using `CREATE2` to deploy EVM bytecode for efficient code re-use. Additionally, the _Quark Nonce Manager_ contract plays a pivotal role in managing nonces for each wallet operation. The system also includes a wallet factory for deterministic wallet creation and a suite of Core Scripts — audited, versatile contracts that form the foundation for complex Quark Operations such as multicalls and flash-loans.

## Contracts

Expand All @@ -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,14 +44,12 @@ 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
jar -- 3. CREATE2 --> script
wallet -- 4. setActiveNonceAndCallback --> state
state -- 5. executeScriptWithNonceLock --> wallet
wallet -- 6. Executes script\nusing callcode --> script
wallet -- 4. Executes script\nusing callcode --> script
```

## Quark Wallet Features
Expand All @@ -76,23 +74,25 @@ For example, let _Wallet A_ be the `executor` of _Wallet B_. Alice is the `signe

### Replayable Scripts

Replayable scripts are Quark scripts that can re-executed multiple times using the same signature of a _Quark operation_. More specifically, replayable scripts explicitly clear the nonce used by the transaction (can be done via the `allowReplay` helper function in [`QuarkScript.sol`](./quark-core/src/QuarkScript.sol)) to allow for the same nonce to be re-used with the same script address.
Replayable scripts are Quark scripts that can be re-executed N times using the same signature of a _Quark operation_. More specifically, replayable scripts generate a nonce chain where the original signer knows a secret and hashes that secret N times. The signer can reveal a single "submission token" to replay the script which is easily verified on-chain. When the signer reveals the last submission token (the original secret) and submits it on-chain, no more replays are allowed (assuming the secret was chosen as a strong random). The signer can always cancel replays by submitting a nop non-replayable script on-chain or simply forgetting the secret. Note: the chain can be arbitrarily long and does not expend any additional gas on-chain for being longer (except if a script wants to know its position in the chain).

An example use-case for replayable scripts is recurring purchases. If a user wanted to buy X WETH using 1,000 USDC every Wednesday until 10,000 USDC is spent, they can achieve this by signing a single _Quark operation_ of a replayable script ([example](./test/lib/RecurringPurchase.sol)). A submitter can then submit this same signed _Quark operation_ every Wednesday to execute the recurring purchase. The replayable script should have checks to ensure conditions are met before purchasing the WETH.

#### 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.
```
Nonce hash chain:
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.
Final replay = "nonceSecret"
N-1 replay = hash ("nonceSecret")
N-2 replay = hash^2("nonceSecret")
...
First play = hash^n("nonceSecret") = operation.nonce
```

One danger of flexible `calldata` in replayable scripts is that previously signed `calldata` can always be re-executed. The Quark system does not disallow previously used calldata when a new calldata is executed. This means that scripts may need to implement their own method of invalidating previously-used `calldata`.
An example use-case for replayable scripts is recurring purchases. If a user wanted to buy X WETH using 1,000 USDC every Wednesday until 10,000 USDC is spent, they can achieve this by signing a single _Quark operation_ of a replayable script ([example](./test/lib/RecurringPurchase.sol)). A submitter can then submit this same signed _Quark operation_ every Wednesday to execute the recurring purchase. The replayable script should have checks to ensure conditions are met before purchasing the WETH.

### Callbacks

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 foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[profile.default]
solc = "0.8.23"
evm_version = "paris"
solc = "0.8.27"
evm_version = "cancun"

libs = [ "./lib" ]

Expand Down
2 changes: 1 addition & 1 deletion script/DeployCodeJarFactory.s.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

import "forge-std/Script.sol";
import "forge-std/console.sol";
Expand Down
6 changes: 3 additions & 3 deletions script/DeployQuarkWalletFactory.s.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

import "forge-std/Script.sol";
import "forge-std/console.sol";
Expand All @@ -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
4 changes: 2 additions & 2 deletions src/codejar/foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[profile.default]
solc = "0.8.23"
evm_version = "paris"
solc = "0.8.27"
evm_version = "cancun"

libs = [ "../../lib" ]

Expand Down
2 changes: 1 addition & 1 deletion src/codejar/src/CodeJar.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

/**
* @title Code Jar
Expand Down
2 changes: 1 addition & 1 deletion src/codejar/src/CodeJarFactory.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

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

Expand Down
4 changes: 2 additions & 2 deletions src/quark-core-scripts/foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[profile.default]
solc = "0.8.23"
evm_version = "paris"
solc = "0.8.27"
evm_version = "cancun"

libs = [ "../../lib" ]

Expand Down
41 changes: 41 additions & 0 deletions src/quark-core-scripts/src/Cancel.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.27;

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

/**
* @title Cancel Core Script
* @notice Core transaction script that can be used to cancel quark operations.
* @author Legend Labs, Inc.
*/
contract Cancel {
/**
* @notice May cancel a script by being run as a no-op (no operation).
*/
function nop() external pure {}

/**
* @notice Cancels a script by calling into nonce manager to cancel the script's nonce.
* @param nonce The nonce of the quark operation to cancel (exhaust)
*/
function cancel(bytes32 nonce) external {
nonceManager().cancel(nonce);
}

/**
* @notice Cancels many scripts by calling into nonce manager to cancel each script's nonce.
* @param nonces A list of nonces of the quark operations to cancel (exhaust)
*/
function cancelMany(bytes32[] calldata nonces) external {
QuarkNonceManager manager = nonceManager();
for (uint256 i = 0; i < nonces.length; ++i) {
bytes32 nonce = nonces[i];
manager.cancel(nonce);
}
}

function nonceManager() internal view returns (QuarkNonceManager) {
return QuarkNonceManager(IQuarkWallet(address(this)).nonceManager());
}
}
2 changes: 1 addition & 1 deletion src/quark-core-scripts/src/ConditionalMulticall.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

import "quark-core-scripts/src/lib/ConditionalChecker.sol";

Expand Down
2 changes: 1 addition & 1 deletion src/quark-core-scripts/src/Ethcall.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

/**
* @title Ethcall Core Script
Expand Down
2 changes: 1 addition & 1 deletion src/quark-core-scripts/src/Multicall.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

/**
* @title Multicall Core Script
Expand Down
2 changes: 1 addition & 1 deletion src/quark-core-scripts/src/Paycall.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

import "quark-core-scripts/src/vendor/chainlink/AggregatorV3Interface.sol";
import "openzeppelin/token/ERC20/utils/SafeERC20.sol";
Expand Down
2 changes: 1 addition & 1 deletion src/quark-core-scripts/src/Quotecall.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

import "quark-core-scripts/src/vendor/chainlink/AggregatorV3Interface.sol";
import "openzeppelin/token/ERC20/utils/SafeERC20.sol";
Expand Down
2 changes: 1 addition & 1 deletion src/quark-core-scripts/src/UniswapFlashLoan.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

import "openzeppelin/token/ERC20/utils/SafeERC20.sol";
import "v3-core/contracts/interfaces/IUniswapV3Pool.sol";
Expand Down
2 changes: 1 addition & 1 deletion src/quark-core-scripts/src/UniswapFlashSwapExactOut.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

import "openzeppelin/token/ERC20/utils/SafeERC20.sol";
import "v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol";
Expand Down
2 changes: 1 addition & 1 deletion src/quark-core-scripts/src/lib/ConditionalChecker.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

library ConditionalChecker {
enum CheckType {
Expand Down
2 changes: 1 addition & 1 deletion src/quark-core-scripts/src/lib/UniswapFactoryAddress.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.23;
pragma solidity 0.8.27;

library UniswapFactoryAddress {
// Reference: https://docs.uniswap.org/contracts/v3/reference/deployments
Expand Down
2 changes: 1 addition & 1 deletion src/quark-core-scripts/src/vendor/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"newLines": 6,
"lines": [
" // SPDX-License-Identifier: GPL-2.0-or-later",
"-pragma solidity 0.8.23;",
"-pragma solidity 0.8.27;",
"+pragma solidity >=0.5.0;",
" ",
" /// @title Provides functions for deriving a pool address from the factory, tokens, and the fee",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.23;
pragma solidity 0.8.27;

/// @title Provides functions for deriving a pool address from the factory, tokens, and the fee
library PoolAddress {
Expand Down
4 changes: 2 additions & 2 deletions src/quark-core/foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[profile.default]
solc = "0.8.23"
evm_version = "paris"
solc = "0.8.27"
evm_version = "cancun"

libs = [ "../../lib" ]

Expand Down
78 changes: 78 additions & 0 deletions src/quark-core/src/QuarkNonceManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.27;

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

library QuarkNonceManagerMetadata {
/// @notice Represents the unclaimed bytes32 value.
bytes32 internal constant FREE = bytes32(uint256(0));

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

/**
* @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 InvalidNonce(address wallet, bytes32 nonce);
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 = QuarkNonceManagerMetadata.FREE;

/// @notice A token that implies a Quark Operation is no longer replayable.
bytes32 public constant EXHAUSTED = QuarkNonceManagerMetadata.EXHAUSTED;

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

/**
* @notice Ensures a given nonce is canceled for sender. An un-used nonce will not be usable in the future, and a replayable nonce will no longer be replayable. This is a no-op for already canceled operations.
* @param nonce The nonce of the chain to cancel.
*/
function cancel(bytes32 nonce) external {
submissions[msg.sender][nonce] = EXHAUSTED;
emit NonceSubmitted(msg.sender, nonce, EXHAUSTED);
}

/**
* @notice Attempts a first or subsequent submission of a given nonce from a wallet.
* @param nonce The nonce of the chain to submit.
* @param isReplayable True only if the operation has been marked as replayable. Otherwise, submission token must be the EXHAUSTED value.
* @param submissionToken The token for this submission. For single-use operations, set `submissionToken` to `uint256(-1)`. For first-use replayable operations, set `submissionToken` = `nonce`. Otherwise, the next submission token from the nonce-chain.
*/
function submit(bytes32 nonce, bool isReplayable, bytes32 submissionToken) external {
bytes32 lastTokenSubmission = submissions[msg.sender][nonce];
if (lastTokenSubmission == EXHAUSTED) {
revert NonReplayableNonce(msg.sender, nonce, submissionToken);
}
// Defense-in-depth check for `nonce != FREE` and `nonce != EXHAUSTED`
if (nonce == FREE || nonce == EXHAUSTED) {
revert InvalidNonce(msg.sender, nonce);
}
// Defense-in-depth check for `submissionToken != FREE` and `submissionToken != EXHAUSTED`
if (submissionToken == FREE || submissionToken == EXHAUSTED) {
revert InvalidSubmissionToken(msg.sender, nonce, submissionToken);
}

bool validFirstPlay = lastTokenSubmission == FREE && submissionToken == nonce;

/* let validFirstPlayOrReplay = validFirstPlay or validReplay [with short-circuiting] */
bool validFirstPlayOrReplay =
validFirstPlay || keccak256(abi.encodePacked(submissionToken)) == lastTokenSubmission;

if (!validFirstPlayOrReplay) {
revert InvalidSubmissionToken(msg.sender, nonce, submissionToken);
}

// Note: even with a valid submission token, we always set non-replayables to exhausted (e.g. for cancellations)
submissions[msg.sender][nonce] = isReplayable ? submissionToken : EXHAUSTED;
emit NonceSubmitted(msg.sender, nonce, submissionToken);
}
}
Loading

0 comments on commit 1afeb4e

Please sign in to comment.