Skip to content

Latest commit

 

History

History
235 lines (150 loc) · 14.7 KB

README.md

File metadata and controls

235 lines (150 loc) · 14.7 KB

Quark

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 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

Code Jar

Code Jar maps callable contract code to addresses which can then be delegate-called to. Specifically, Code Jar uses CREATE2 to find or create a contract address whose creation code matches some given input code (EVM opcodes encoded as data). The calling contract (e.g. a wallet) may call Code Jar's saveCode function and then run delegatecall on the resulting address, which effectively executes arbitrary code.

Quark Wallet

Quark Wallet is a scriptable wallet located at a counterfactual address derived from an owner EOA. The same EOA will have the same Quark Wallet address across all chains if deployed from the same Quark Wallet Factory.

Quark Wallet executes Quark Operations containing a transaction script (or address pointing to a transaction script) and calldata representing an encoded function call into that script.

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 section for more details.

Quark Nonce Manager

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

The Quark Wallet Factory is the central contract for deploying new Quark Wallets at pre-determined addresses. It is generally deployed with peer contracts via Code Jar deployments.

Quark Script

Quark Script is an extensible contract that exposes helper functions for other Quark scripts to inherit from. The helper functions include those for enabling callbacks, allowing replay of Quark Operations, and reading from and writing to a key in the Quark State Manager.

Core Scripts

Core scripts are a set of important scripts that should be deployed via Code Jar to cover essential operations that will likely be used by a large number of Quark Operations. Examples of Core Scripts include Multicall, Ethcall, Paycall and flashloans with callbacks.

System Diagrams

Happy path for wallet creation and execution of Quark Operation

flowchart TB
    factory[Quark Wallet Factory]
    wallet[Quark Wallet]
    jar[Code Jar]
    script[Quark Script]
    state[Quark Nonce Manager]

    factory -- 1. createAndExecute --> wallet
    wallet -- 2. saveCode --> jar
    jar -- 3. CREATE2  --> script
    wallet -- 4. Executes script\nusing delegatecall --> script
Loading

Quark Wallet Features

Multi Quark Operations

Multi Quark operations are a batch of Quark operation digests signed via a single EIP-712 signature. The Multi Quark operation EIP-712 signature is not scoped to a specific address and chain id, allowing a single signature to flexibly execute operations on multiple Quark wallets owned by the signer on any number of chains. The Quark operation EIP-712 digests in the Multi Quark operation are still scoped to a specific address and chain id, so they are protected against replay attacks.

One common use-case for this feature is cross-chain Quark operations. For example, a single signature can be used to 1) Bridge USDC from mainnet to Base and 2) Supply USDC to some protocol on Base.

Separation of Signer and Executor

The signer and executor roles are separate roles in the Quark Wallet. The signer is able to sign Quark operations that can be executed by the Quark Wallet. The executor is able to directly execute scripts on the Quark Wallet. Theoretically, the same address can be both the signer and executor of a Quark Wallet. Similarly, the signer and/or executor can be set to the null (zero) address to effectively remove that role from the wallet.

The separation of these two roles allows for a sub-wallet system, where a wallet can be the executor for another wallet but both wallets share the same signer. This is discussed in more detail in the Sub-wallets section.

Sub-wallets

Sub-wallets are Quark wallets controlled by another Quark wallet. Specifically, the sub-wallet's executor is another Quark wallet (dubbed the "main wallet"), meaning the main wallet can directly execute scripts on the sub-wallet. This allows for complex interactions that span multiple Quark wallets to be executed via a single signature.

For example, let Wallet A be the executor of Wallet B. Alice is the signer for Wallet A. If Alice wants to borrow USDC from Comet in Wallet A, transfer the USDC to Wallet B, and then supply the USDC to Comet from Wallet B, she can accomplish this with a single signature of a Quark operation. The final action of "supply USDC to Comet in Wallet B" can be invoked by a direct execution call from Wallet A.

Replayable Scripts

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).

Nonce hash chain:

Final replay =        "nonceSecret"
  N-1 replay = hash  ("nonceSecret")
  N-2 replay = hash^2("nonceSecret")
  ...
  First play = hash^n("nonceSecret") = operation.nonce

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). 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), 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 Nonce Manager (can be done via the allowCallback helper function in QuarkScript.sol).

EIP-1271 Signatures

Quark wallets come with built-in support for EIP-1271 signatures. This means that Quark wallets can be owned by smart contracts. The owner smart contract verifies any signatures signed for the Quark wallet, allowing for different types of signature verification schemes to be used (e.g. multisig, passkey).

Adding Library Dependencies

Dependencies should be added using forge install from the root directory. Whenever a new dependency is added, a remapping for it must be defined in both remappings.txt and remappings-relative.txt. Please see Project Structure and Sub-projects and Dependencies.

As of yet, there is no way to automatically keep these in sync. We may explore options for this in the future -- or a linting step that checks for missing remappings.

Project Structure and Sub-projects

Quark is not quite a typical foundry project. While there is a root foundry.toml that combines all the contracts into a single project, related source files are split across sub-directories containing their own, smaller foundry.toml projects. We call these "sub-projects." These sub-projects all live in src.

You can still forge build and forge test from the root directory and expect things to work normally. However, sub-projects can also be developed, built, and tested independently. These sub-projects are:

  • src/codejar
  • src/quark-core
  • src/quark-core-scripts
  • src/quark-factory
  • src/quark-proxy

By separating contracts into these sub-projects, it is possible to use per-project compilation settings to optimize and deploy different sets of contracts with different configurations. Moreover, builds and test runs can be faster for individual sub-projects: from a sub-project directory, forge build builds only that sub-project's contracts, and forge test runs only the tests that pertain to it. For example:

# compiles and tests just the quark-core contracts
$ cd quark-core && forge test

Sub-projects still share a root-level lib and test directory, which makes it easier to define the root-level project that compiles and tests the contracts all at once. This also helps to make builds faster: only library contracts actually imported by a sub-project source file are compiled when the sub-project is built, since the lib folder is one level above the project root.

Builds are also faster because build caches are separate per-project, so independent builds are more often cached.

Test suites are faster for similar reasons: test suite builds can also utilize isolated per-project caches, and all tests have a shared test/lib library one level above the test root, so only testing helper contracts that are actually imported by a test suite will be compiled.

Dependencies

Please note that project dependencies must be installed in the root directory (not in sub-project directories), and any new entries added to remappings.txt must also be added to a remappings-relative.txt with a .. prefix in order for sub-projects to be able to import them.

In other words, if an entry is added to remappings.txt like:

v3-core=/lib/v3-core

Then a corresponding entry must be added to remappings-relative.txt so that sub-projects can properly resolve the path:

v3-core=../lib/v3-core

See Sub-project Remappings for more details.

Sub-project Remappings

As a consequence of the sub-project structure, source files can no longer use relative imports. Instead, a remapping to each sub-project directory is defined, and all imports are sub-project namespaced, even within the same sub-project: instead of import "./QuarkWallet.sol";, it must be written import "quark-core/src/QuarkWallet.sol";.

In the root-level remappings.txt file, these remappings look ordinary, like quark-core=./quark-core/. However, each sub-project needs its own remappings in order to build, and these need to be relative to the root directory even though the project has its own subdirectory; as a result, in each subproject, a remappings.txt symlink is created to the remappings-relative.txt file in the root directory. This remappings-relative.txt file adjusts all of the remappings in the regular root-level remappings.txt to be prefixed with a .. so that they will resolve relative to the root directory, and not the sub-project's directory.

Whenever a new remapping is added to remappings.txt, a corresponding entry must be added to remappings-relative.txt that prefixes the remapping path with .. to maintain sub-project compiles; this is covered with an example in the Dependencies section.

Fork tests and MAINNET_RPC_URL

Some tests require forking mainnet, e.g. to exercise use-cases like supplying and borrowing in a comet market.

The "fork url" is specified using the environment variable MAINNET_RPC_URL. It can be any node provider for Ethereum mainnet, such as Infura or Alchemy.

The environment variable can be set when running tests, like so:

$ MAINNET_RPC_URL=... forge test

Updating gas snapshots

In CI we compare gas snapshots against a committed baseline (stored in .gas-snapshot), and the job fails if any diff in the snapshot exceeds a set threshold from the baseline.

You can accept the diff and update the baseline if the increased gas usage is intentional. Just run the following command:

$ MAINNET_RPC_URL=... ./script/update-snapshot.sh

Then commit the updated snapshot file:

$ git add .gas-snapshot && git commit -m "commit new baseline gas snapshot"

Deploy

To run the deploy, first, find the Code Jar address, or deploy Code Jar via:

./script/deploy-code-jar.sh

Then deploy Quark via:

CODE_JAR=... ./script/deploy-quark.sh

To actually deploy contracts on-chain, the following env variables need to be set:

# Required
RPC_URL=
DEPLOYER_PK=
# Optional for verifying deployed contracts
ETHERSCAN_KEY=
CODE_JAR=

Once the env variables are defined, run the following command:

set -a && source .env && ./script/deploy-quark.sh --broadcast

CodeJar Deployments

Using artifacts from release-v2024-03-27+2249648.

Network CodeJar Address
Mainnet 0x2b68764bCfE9fCD8d5a30a281F141f69b69Ae3C8
Base 0x2b68764bCfE9fCD8d5a30a281F141f69b69Ae3C8
Sepolia 0x2b68764bCfE9fCD8d5a30a281F141f69b69Ae3C8
Arbitrum 0x2b68764bCfE9fCD8d5a30a281F141f69b69Ae3C8
Optimism 0x2b68764bCfE9fCD8d5a30a281F141f69b69Ae3C8
Polygon Pending
Scroll Pending
Base Sepolia 0x2b68764bCfE9fCD8d5a30a281F141f69b69Ae3C8
Arbitrum Sepolia 0x2b68764bCfE9fCD8d5a30a281F141f69b69Ae3C8
Optimism Sepolia 0x2b68764bCfE9fCD8d5a30a281F141f69b69Ae3C8