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