diff --git a/README.md b/README.md index b4c8bc2d..e3ca8ef7 100644 --- a/README.md +++ b/README.md @@ -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 @@ -49,9 +49,7 @@ flowchart TB 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 @@ -76,17 +74,19 @@ 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 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. +``` +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 diff --git a/foundry.toml b/foundry.toml index b18fae7d..9cd9f8aa 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,6 @@ [profile.default] -solc = "0.8.23" -evm_version = "paris" +solc = "0.8.27" +evm_version = "cancun" libs = [ "./lib" ] diff --git a/script/DeployCodeJarFactory.s.sol b/script/DeployCodeJarFactory.s.sol index 8059e462..0b6c9cdf 100644 --- a/script/DeployCodeJarFactory.s.sol +++ b/script/DeployCodeJarFactory.s.sol @@ -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"; diff --git a/script/DeployQuarkWalletFactory.s.sol b/script/DeployQuarkWalletFactory.s.sol index a8769733..56f3f342 100644 --- a/script/DeployQuarkWalletFactory.s.sol +++ b/script/DeployQuarkWalletFactory.s.sol @@ -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"; diff --git a/src/codejar/foundry.toml b/src/codejar/foundry.toml index 663d458c..04443a55 100644 --- a/src/codejar/foundry.toml +++ b/src/codejar/foundry.toml @@ -1,6 +1,6 @@ [profile.default] -solc = "0.8.23" -evm_version = "paris" +solc = "0.8.27" +evm_version = "cancun" libs = [ "../../lib" ] diff --git a/src/codejar/src/CodeJar.sol b/src/codejar/src/CodeJar.sol index 6866311e..fb8441c1 100644 --- a/src/codejar/src/CodeJar.sol +++ b/src/codejar/src/CodeJar.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; /** * @title Code Jar diff --git a/src/codejar/src/CodeJarFactory.sol b/src/codejar/src/CodeJarFactory.sol index cedd54f8..91abf544 100644 --- a/src/codejar/src/CodeJarFactory.sol +++ b/src/codejar/src/CodeJarFactory.sol @@ -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"; diff --git a/src/quark-core-scripts/foundry.toml b/src/quark-core-scripts/foundry.toml index 455503d4..5d8adf3f 100644 --- a/src/quark-core-scripts/foundry.toml +++ b/src/quark-core-scripts/foundry.toml @@ -1,6 +1,6 @@ [profile.default] -solc = "0.8.23" -evm_version = "paris" +solc = "0.8.27" +evm_version = "cancun" libs = [ "../../lib" ] diff --git a/src/quark-core-scripts/src/Cancel.sol b/src/quark-core-scripts/src/Cancel.sol new file mode 100644 index 00000000..c37f653b --- /dev/null +++ b/src/quark-core-scripts/src/Cancel.sol @@ -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()); + } +} diff --git a/src/quark-core-scripts/src/ConditionalMulticall.sol b/src/quark-core-scripts/src/ConditionalMulticall.sol index fa8ab848..612e6062 100644 --- a/src/quark-core-scripts/src/ConditionalMulticall.sol +++ b/src/quark-core-scripts/src/ConditionalMulticall.sol @@ -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"; diff --git a/src/quark-core-scripts/src/Ethcall.sol b/src/quark-core-scripts/src/Ethcall.sol index 77402537..017e7a68 100644 --- a/src/quark-core-scripts/src/Ethcall.sol +++ b/src/quark-core-scripts/src/Ethcall.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; /** * @title Ethcall Core Script diff --git a/src/quark-core-scripts/src/Multicall.sol b/src/quark-core-scripts/src/Multicall.sol index b7759381..479ef7ae 100644 --- a/src/quark-core-scripts/src/Multicall.sol +++ b/src/quark-core-scripts/src/Multicall.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; /** * @title Multicall Core Script diff --git a/src/quark-core-scripts/src/Paycall.sol b/src/quark-core-scripts/src/Paycall.sol index aebd4ff0..01112d09 100644 --- a/src/quark-core-scripts/src/Paycall.sol +++ b/src/quark-core-scripts/src/Paycall.sol @@ -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"; diff --git a/src/quark-core-scripts/src/Quotecall.sol b/src/quark-core-scripts/src/Quotecall.sol index b980b882..ce292ddb 100644 --- a/src/quark-core-scripts/src/Quotecall.sol +++ b/src/quark-core-scripts/src/Quotecall.sol @@ -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"; diff --git a/src/quark-core-scripts/src/UniswapFlashLoan.sol b/src/quark-core-scripts/src/UniswapFlashLoan.sol index 5025d7a8..817b9281 100644 --- a/src/quark-core-scripts/src/UniswapFlashLoan.sol +++ b/src/quark-core-scripts/src/UniswapFlashLoan.sol @@ -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"; diff --git a/src/quark-core-scripts/src/UniswapFlashSwapExactOut.sol b/src/quark-core-scripts/src/UniswapFlashSwapExactOut.sol index b65aaf7d..2c256a97 100644 --- a/src/quark-core-scripts/src/UniswapFlashSwapExactOut.sol +++ b/src/quark-core-scripts/src/UniswapFlashSwapExactOut.sol @@ -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"; diff --git a/src/quark-core-scripts/src/lib/ConditionalChecker.sol b/src/quark-core-scripts/src/lib/ConditionalChecker.sol index b8d92b36..1a0ceee0 100644 --- a/src/quark-core-scripts/src/lib/ConditionalChecker.sol +++ b/src/quark-core-scripts/src/lib/ConditionalChecker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; library ConditionalChecker { enum CheckType { diff --git a/src/quark-core-scripts/src/lib/UniswapFactoryAddress.sol b/src/quark-core-scripts/src/lib/UniswapFactoryAddress.sol index 82d6b7b2..d154a4b7 100644 --- a/src/quark-core-scripts/src/lib/UniswapFactoryAddress.sol +++ b/src/quark-core-scripts/src/lib/UniswapFactoryAddress.sol @@ -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 diff --git a/src/quark-core-scripts/src/vendor/manifest.json b/src/quark-core-scripts/src/vendor/manifest.json index 40cc65e2..d32806d1 100644 --- a/src/quark-core-scripts/src/vendor/manifest.json +++ b/src/quark-core-scripts/src/vendor/manifest.json @@ -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", diff --git a/src/quark-core-scripts/src/vendor/uniswap_v3_periphery/PoolAddress.sol b/src/quark-core-scripts/src/vendor/uniswap_v3_periphery/PoolAddress.sol index 73eb80aa..1e3435af 100644 --- a/src/quark-core-scripts/src/vendor/uniswap_v3_periphery/PoolAddress.sol +++ b/src/quark-core-scripts/src/vendor/uniswap_v3_periphery/PoolAddress.sol @@ -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 { diff --git a/src/quark-core/foundry.toml b/src/quark-core/foundry.toml index 7fd17720..09a39c63 100644 --- a/src/quark-core/foundry.toml +++ b/src/quark-core/foundry.toml @@ -1,6 +1,6 @@ [profile.default] -solc = "0.8.23" -evm_version = "paris" +solc = "0.8.27" +evm_version = "cancun" libs = [ "../../lib" ] diff --git a/src/quark-core/src/QuarkNonceManager.sol b/src/quark-core/src/QuarkNonceManager.sol index 357e194c..5f4d938d 100644 --- a/src/quark-core/src/QuarkNonceManager.sol +++ b/src/quark-core/src/QuarkNonceManager.sol @@ -1,56 +1,79 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +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 NonReplayableNonce(address wallet, bytes32 nonce, bytes32 submissionToken, bool exhausted); + error InvalidNonce(address wallet, bytes32 nonce); error InvalidSubmissionToken(address wallet, bytes32 nonce, bytes32 submissionToken); event NonceSubmitted(address wallet, bytes32 nonce, bytes32 submissionToken); + event NonceCanceled(address wallet, bytes32 nonce); /// @notice Represents the unclaimed bytes32 value. - bytes32 public constant FREE = bytes32(uint256(0)); + bytes32 public constant FREE = QuarkNonceManagerMetadata.FREE; /// @notice A token that implies a Quark Operation is no longer replayable. - bytes32 public constant EXHAUSTED = bytes32(type(uint256).max); + bytes32 public constant EXHAUSTED = QuarkNonceManagerMetadata.EXHAUSTED; /// @notice Mapping from nonces to last used submission token. - mapping(address wallet => mapping(bytes32 nonce => bytes32 lastToken)) public nonceSubmissions; + mapping(address wallet => mapping(bytes32 nonce => bytes32 lastToken)) public submissions; /** - * @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. + * @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 getNonceSubmission(address wallet, bytes32 nonce) external view returns (bytes32 submissionToken) { - return nonceSubmissions[wallet][nonce]; + function cancel(bytes32 nonce) external { + submissions[msg.sender][nonce] = EXHAUSTED; + emit NonceCanceled(msg.sender, 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`. + * @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 submitNonceToken(bytes32 nonce, bytes32 submissionToken) external { - bytes32 lastTokenSubmission = nonceSubmissions[msg.sender][nonce]; + function submit(bytes32 nonce, bool isReplayable, bytes32 submissionToken) external { + bytes32 lastTokenSubmission = submissions[msg.sender][nonce]; if (lastTokenSubmission == EXHAUSTED) { - revert NonReplayableNonce(msg.sender, nonce, submissionToken); + revert NonReplayableNonce(msg.sender, nonce, submissionToken, true); + } + // 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 = - lastTokenSubmission == FREE || keccak256(abi.encodePacked(submissionToken)) == lastTokenSubmission; + validFirstPlay || keccak256(abi.encodePacked(submissionToken)) == lastTokenSubmission; + if (!validFirstPlayOrReplay) { revert InvalidSubmissionToken(msg.sender, nonce, submissionToken); } - nonceSubmissions[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); } } diff --git a/src/quark-core/src/QuarkScript.sol b/src/quark-core/src/QuarkScript.sol index 01626e9d..772272eb 100644 --- a/src/quark-core/src/QuarkScript.sol +++ b/src/quark-core/src/QuarkScript.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; -import {QuarkWallet, IHasSignerExecutor} from "quark-core/src/QuarkWallet.sol"; +import {QuarkWallet, QuarkWalletMetadata, IHasSignerExecutor, IQuarkWallet} from "quark-core/src/QuarkWallet.sol"; +import {QuarkNonceManager, QuarkNonceManagerMetadata} from "quark-core/src/QuarkNonceManager.sol"; /** * @title Quark Script @@ -10,6 +11,9 @@ import {QuarkWallet, IHasSignerExecutor} from "quark-core/src/QuarkWallet.sol"; */ abstract contract QuarkScript { error ReentrantCall(); + error InvalidActiveNonce(); + error InvalidActiveSubmissionToken(); + error NoActiveNonce(); /// @notice Storage location for the re-entrancy guard bytes32 internal constant REENTRANCY_FLAG_SLOT = @@ -19,21 +23,20 @@ abstract contract QuarkScript { modifier nonReentrant() { bytes32 slot = REENTRANCY_FLAG_SLOT; bytes32 flag; - // TODO: Move to TSTORE after updating Solidity version to >=0.8.24 assembly { - flag := sload(slot) + flag := tload(slot) } if (flag == bytes32(uint256(1))) { revert ReentrantCall(); } assembly { - sstore(slot, 1) + tstore(slot, 1) } _; assembly { - sstore(slot, 0) + tstore(slot, 0) } } @@ -66,24 +69,23 @@ abstract contract QuarkScript { return IHasSignerExecutor(address(this)).executor(); } + function nonceManager() internal view returns (QuarkNonceManager) { + return QuarkNonceManager(IQuarkWallet(address(this)).nonceManager()); + } + function allowCallback() internal { - QuarkWallet self = QuarkWallet(payable(address(this))); - // TODO: Can save gas by just having the constant in QuarkScript - bytes32 callbackSlot = self.CALLBACK_SLOT(); - bytes32 activeScriptSlot = self.ACTIVE_SCRIPT_SLOT(); + bytes32 callbackSlot = QuarkWalletMetadata.CALLBACK_SLOT; + bytes32 activeScriptSlot = QuarkWalletMetadata.ACTIVE_SCRIPT_SLOT; assembly { - // TODO: Move to TLOAD/TSTORE after updating Solidity version to >=0.8.24 - let activeScript := sload(activeScriptSlot) - sstore(callbackSlot, activeScript) + let activeScript := tload(activeScriptSlot) + tstore(callbackSlot, activeScript) } } function clearCallback() internal { - QuarkWallet self = QuarkWallet(payable(address(this))); - bytes32 callbackSlot = self.CALLBACK_SLOT(); + bytes32 callbackSlot = QuarkWalletMetadata.CALLBACK_SLOT; assembly { - // TODO: Move to TSTORE after updating Solidity version to >=0.8.24 - sstore(callbackSlot, 0) + tstore(callbackSlot, 0) } } @@ -97,8 +99,9 @@ abstract contract QuarkScript { function read(bytes32 key) internal view returns (bytes32) { bytes32 value; + bytes32 isolatedKey = getNonceIsolatedKey(key); assembly { - value := sload(key) + value := sload(isolatedKey) } return value; } @@ -111,11 +114,60 @@ abstract contract QuarkScript { return write(keccak256(bytes(key)), value); } - // TODO: Consider adding nonce-based scoping by TLOAD'ing the nonce and using - // that to hash the key. function write(bytes32 key, bytes32 value) internal { + bytes32 isolatedKey = getNonceIsolatedKey(key); assembly { - sstore(key, value) + sstore(isolatedKey, value) } } + + // Returns a key isolated to the active nonce of a script + // This provide cooperative isolation of storage between scripts. + function getNonceIsolatedKey(bytes32 key) internal view returns (bytes32) { + bytes32 nonce = getActiveNonce(); + if (nonce == bytes32(0)) { + revert NoActiveNonce(); + } + return keccak256(abi.encodePacked(nonce, key)); + } + + // Note: this may not be accurate after any nested calls from a script + function getActiveNonce() internal view returns (bytes32) { + bytes32 activeNonceSlot = QuarkWalletMetadata.ACTIVE_NONCE_SLOT; + bytes32 value; + assembly { + value := tload(activeNonceSlot) + } + + return value; + } + + // Note: this may not be accurate after any nested calls from a script + function getActiveSubmissionToken() internal view returns (bytes32) { + bytes32 activeSubmissionTokenSlot = QuarkWalletMetadata.ACTIVE_SUBMISSION_TOKEN_SLOT; + bytes32 value; + assembly { + value := tload(activeSubmissionTokenSlot) + } + return value; + } + + // Note: this may not be accurate after any nested calls from a script + // Returns the active replay count of this script. Thus, the first submission should return 0, + // the second submission 1, and so on. This must be called before the script makes any external calls. + function getActiveReplayCount() internal view returns (uint256) { + bytes32 nonce = getActiveNonce(); + bytes32 submissionToken = getActiveSubmissionToken(); + uint256 n; + + if (submissionToken == QuarkNonceManagerMetadata.EXHAUSTED) { + return 0; + } + + for (n = 0; submissionToken != nonce; n++) { + submissionToken = keccak256(abi.encodePacked(submissionToken)); + } + + return n; + } } diff --git a/src/quark-core/src/QuarkWallet.sol b/src/quark-core/src/QuarkWallet.sol index bb887fe3..e67162ee 100644 --- a/src/quark-core/src/QuarkWallet.sol +++ b/src/quark-core/src/QuarkWallet.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import {ECDSA} from "openzeppelin/utils/cryptography/ECDSA.sol"; import {IERC1271} from "openzeppelin/interfaces/IERC1271.sol"; @@ -8,6 +8,7 @@ import {CodeJar} from "codejar/src/CodeJar.sol"; import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {IHasSignerExecutor} from "quark-core/src/interfaces/IHasSignerExecutor.sol"; +import {IQuarkWallet} from "quark-core/src/interfaces/IQuarkWallet.sol"; /** * @title Quark Wallet Metadata @@ -23,7 +24,7 @@ library QuarkWalletMetadata { /// @notice The EIP-712 typehash for authorizing an operation for this version of QuarkWallet bytes32 internal constant QUARK_OPERATION_TYPEHASH = keccak256( - "QuarkOperation(bytes32 nonce,address scriptAddress,bytes[] scriptSources,bytes scriptCalldata,uint256 expiry)" + "QuarkOperation(bytes32 nonce,bool isReplayable,address scriptAddress,bytes[] scriptSources,bytes scriptCalldata,uint256 expiry)" ); /// @notice The EIP-712 typehash for authorizing a MultiQuarkOperation for this version of QuarkWallet @@ -39,6 +40,19 @@ library QuarkWalletMetadata { /// @notice The EIP-712 domain typehash used for MultiQuarkOperations for this version of QuarkWallet bytes32 internal constant MULTI_QUARK_OPERATION_DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version)"); + + /// @notice Well-known storage slot for the currently executing script's callback address (if any) + bytes32 internal constant CALLBACK_SLOT = bytes32(uint256(keccak256("quark.v1.callback")) - 1); + + /// @notice Well-known storage slot for the currently executing script's address (if any) + bytes32 internal constant ACTIVE_SCRIPT_SLOT = bytes32(uint256(keccak256("quark.v1.active.script")) - 1); + + /// @notice Well-known storage slot for the nonce of the script that's currently executing. + bytes32 internal constant ACTIVE_NONCE_SLOT = bytes32(uint256(keccak256("quark.v1.active.nonce")) - 1); + + /// @notice Well-known storage slot for the submission token of the script that's currently executing. + bytes32 internal constant ACTIVE_SUBMISSION_TOKEN_SLOT = + bytes32(uint256(keccak256("quark.v1.active.submissionToken")) - 1); } /** @@ -64,14 +78,19 @@ contract QuarkWallet is IERC1271 { } /// @notice Event emitted when a Quark script is executed by this Quark wallet - event ExecuteQuarkScript( - address indexed executor, address indexed scriptAddress, bytes32 indexed nonce, ExecutionType executionType + event QuarkExecution( + address indexed executor, + address indexed scriptAddress, + bytes32 indexed nonce, + bytes32 submissionToken, + bool isReplayable, + ExecutionType executionType ); /// @notice Address of CodeJar contract used to deploy transaction script source code CodeJar public immutable codeJar; - /// @notice Address of QuarkNonceManager contract that manages nonces and nonce-namespaced transaction script storage + /// @notice Address of QuarkNonceManager contract that manages nonces for this quark wallet QuarkNonceManager public immutable nonceManager; /// @notice Name of contract @@ -108,13 +127,16 @@ contract QuarkWallet is IERC1271 { ); /// @notice Well-known storage slot for the currently executing script's callback address (if any) - bytes32 public constant CALLBACK_SLOT = bytes32(uint256(keccak256("quark.v1.callback")) - 1); + bytes32 public constant CALLBACK_SLOT = QuarkWalletMetadata.CALLBACK_SLOT; /// @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); + bytes32 public constant ACTIVE_SCRIPT_SLOT = QuarkWalletMetadata.ACTIVE_SCRIPT_SLOT; + + /// @notice Well-known storage slot for the nonce of the script that's currently executing. + bytes32 public constant ACTIVE_NONCE_SLOT = QuarkWalletMetadata.ACTIVE_NONCE_SLOT; - /// @notice A nonce submission token that implies a Quark Operation is no longer replayable. - bytes32 public constant EXHAUSTED_TOKEN = bytes32(type(uint256).max); + /// @notice Well-known storage slot for the submission token of the script that's currently executing. + bytes32 public constant ACTIVE_SUBMISSION_TOKEN_SLOT = QuarkWalletMetadata.ACTIVE_SUBMISSION_TOKEN_SLOT; /// @notice The magic value to return for valid ERC1271 signature bytes4 internal constant EIP_1271_MAGIC_VALUE = 0x1626ba7e; @@ -123,6 +145,8 @@ contract QuarkWallet is IERC1271 { struct QuarkOperation { /// @notice Nonce identifier for the operation bytes32 nonce; + /// @notice Whether this script is replayable or not. + bool isReplayable; /// @notice The address of the transaction script to run address scriptAddress; /// @notice Creation codes Quark must ensure are deployed before executing this operation @@ -136,7 +160,7 @@ contract QuarkWallet is IERC1271 { /** * @notice Construct a new QuarkWalletImplementation * @param codeJar_ The CodeJar contract used to deploy scripts - * @param nonceManager_ The QuarkNonceManager contract used to write/read nonces and storage for this wallet + * @param nonceManager_ The QuarkNonceManager contract used to write/read nonces for this wallet */ constructor(CodeJar codeJar_, QuarkNonceManager nonceManager_) { codeJar = codeJar_; @@ -156,9 +180,29 @@ contract QuarkWallet is IERC1271 { external returns (bytes memory) { + return executeQuarkOperationWithSubmissionToken(op, op.nonce, v, r, s); + } + + /** + * @notice Executes a first play or a replay of a QuarkOperation via signature + * @dev Can only be called with signatures from the wallet's signer + * @param op A QuarkOperation struct + * @param submissionToken A submission token. For replayable operations, initial value should be `submissionToken = op.nonce`, for non-replayable operations, `submissionToken = bytes32(type(uint256).max)`. + * @param v EIP-712 signature v value + * @param r EIP-712 signature r value + * @param s EIP-712 signature s value + * @return Return value from the executed operation + */ + function executeQuarkOperationWithSubmissionToken( + QuarkOperation calldata op, + bytes32 submissionToken, + uint8 v, + bytes32 r, + bytes32 s + ) public returns (bytes memory) { bytes32 opDigest = getDigestForQuarkOperation(op); - return verifySigAndExecuteQuarkOperation(op, opDigest, v, r, s); + return verifySigAndExecuteQuarkOperation(op, submissionToken, opDigest, v, r, s); } /** @@ -178,69 +222,56 @@ contract QuarkWallet is IERC1271 { bytes32 r, bytes32 s ) public returns (bytes memory) { - bytes32 opDigest = getDigestForQuarkOperation(op); - - bool isValidOp = false; - for (uint256 i = 0; i < opDigests.length; ++i) { - if (opDigest == opDigests[i]) { - isValidOp = true; - break; - } - } - if (!isValidOp) { - revert InvalidMultiQuarkOperation(); - } - bytes32 multiOpDigest = getDigestForMultiQuarkOperation(opDigests); - - return verifySigAndExecuteQuarkOperation(op, multiOpDigest, v, r, s); + return executeMultiQuarkOperationWithSubmissionToken(op, op.nonce, opDigests, v, r, s); } /** - * @notice Verify a signature and execute a single-use QuarkOperation + * @notice Executes a first play or a replay of a QuarkOperation that is part of a MultiQuarkOperation via signature + * @dev Can only be called with signatures from the wallet's signer * @param op A QuarkOperation struct - * @param digest A EIP-712 digest for either a QuarkOperation or MultiQuarkOperation to verify the signature against + * @param submissionToken The submission token for the replayable quark operation for QuarkNonceManager. This is initially the `op.nonce`, and for replayable operations, it is the next token in the nonce chain. + * @param opDigests A list of EIP-712 digests for the operations in a MultiQuarkOperation * @param v EIP-712 signature v value * @param r EIP-712 signature r value * @param s EIP-712 signature s value * @return Return value from the executed operation */ - function verifySigAndExecuteQuarkOperation( + function executeMultiQuarkOperationWithSubmissionToken( QuarkOperation calldata op, - bytes32 digest, + bytes32 submissionToken, + bytes32[] memory opDigests, uint8 v, bytes32 r, bytes32 s - ) internal returns (bytes memory) { - if (block.timestamp >= op.expiry) { - revert SignatureExpired(); - } - - // if the signature check does not revert, the signature is valid - checkValidSignatureInternal(IHasSignerExecutor(address(this)).signer(), digest, v, r, s); + ) public returns (bytes memory) { + bytes32 opDigest = getDigestForQuarkOperation(op); - // guarantee every script in scriptSources is deployed - for (uint256 i = 0; i < op.scriptSources.length; ++i) { - codeJar.saveCode(op.scriptSources[i]); + bool isValidOp = false; + for (uint256 i = 0; i < opDigests.length; ++i) { + if (opDigest == opDigests[i]) { + isValidOp = true; + break; + } } + if (!isValidOp) { + revert InvalidMultiQuarkOperation(); + } + bytes32 multiOpDigest = getDigestForMultiQuarkOperation(opDigests); - nonceManager.submitNonceToken(op.nonce, EXHAUSTED_TOKEN); - - emit ExecuteQuarkScript(msg.sender, op.scriptAddress, op.nonce, ExecutionType.Signature); - - return executeScriptInternal(op.scriptAddress, op.scriptCalldata); + return verifySigAndExecuteQuarkOperation(op, submissionToken, multiOpDigest, v, r, s); } /** * @notice Verify a signature and execute a replayable QuarkOperation * @param op A QuarkOperation struct - * @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 submissionToken The submission token for the replayable quark operation for QuarkNonceManager. This is initially the `op.nonce`, and for replayable operations, it is the next token in the nonce 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 * @param s EIP-712 signature s value * @return Return value from the executed operation */ - function verifySigAndExecuteReplayableQuarkOperation( + function verifySigAndExecuteQuarkOperation( QuarkOperation calldata op, bytes32 submissionToken, bytes32 digest, @@ -260,11 +291,13 @@ contract QuarkWallet is IERC1271 { codeJar.saveCode(op.scriptSources[i]); } - nonceManager.submitNonceToken(op.nonce, submissionToken); + nonceManager.submit(op.nonce, op.isReplayable, submissionToken); - emit ExecuteQuarkScript(msg.sender, op.scriptAddress, op.nonce, ExecutionType.Signature); + emit QuarkExecution( + msg.sender, op.scriptAddress, op.nonce, submissionToken, op.isReplayable, ExecutionType.Signature + ); - return executeScriptInternal(op.scriptAddress, op.scriptCalldata); + return executeScriptInternal(op.scriptAddress, op.scriptCalldata, op.nonce, submissionToken); } /** @@ -292,11 +325,11 @@ contract QuarkWallet is IERC1271 { codeJar.saveCode(scriptSources[i]); } - nonceManager.submitNonceToken(nonce, EXHAUSTED_TOKEN); + nonceManager.submit(nonce, false, nonce); - emit ExecuteQuarkScript(msg.sender, scriptAddress, nonce, ExecutionType.Direct); + emit QuarkExecution(msg.sender, scriptAddress, nonce, nonce, false, ExecutionType.Direct); - return executeScriptInternal(scriptAddress, scriptCalldata); + return executeScriptInternal(scriptAddress, scriptCalldata, nonce, nonce); } /** @@ -324,6 +357,7 @@ contract QuarkWallet is IERC1271 { abi.encode( QUARK_OPERATION_TYPEHASH, op.nonce, + op.isReplayable, op.scriptAddress, keccak256(encodedScriptSources), keccak256(op.scriptCalldata), @@ -428,12 +462,16 @@ contract QuarkWallet is IERC1271 { * @notice Execute a script using the given calldata * @param scriptAddress Address of script to execute * @param scriptCalldata Encoded calldata for the call to execute on the scriptAddress + * @param nonce The nonce of the quark operation for this execution + * @param submissionToken The submission token for this quark execution * @return Result of executing the script, encoded as bytes */ - function executeScriptInternal(address scriptAddress, bytes memory scriptCalldata) - internal - returns (bytes memory) - { + function executeScriptInternal( + address scriptAddress, + bytes memory scriptCalldata, + bytes32 nonce, + bytes32 submissionToken + ) internal returns (bytes memory) { if (scriptAddress.code.length == 0) { revert EmptyCode(); } @@ -442,20 +480,33 @@ contract QuarkWallet is IERC1271 { uint256 returnSize; uint256 scriptCalldataLen = scriptCalldata.length; bytes32 activeScriptSlot = ACTIVE_SCRIPT_SLOT; + bytes32 activeNonceSlot = ACTIVE_NONCE_SLOT; + bytes32 activeSubmissionTokenSlot = ACTIVE_SUBMISSION_TOKEN_SLOT; assembly { // TODO: TSTORE the callback slot to 0 - // Store the active script - // TODO: Move to TSTORE after updating Solidity version to >=0.8.24 - sstore(activeScriptSlot, scriptAddress) + // Transiently store the active script + tstore(activeScriptSlot, scriptAddress) + + // Transiently store the active nonce + tstore(activeNonceSlot, nonce) + + // Transiently store the active submission token + tstore(activeSubmissionTokenSlot, submissionToken) // Note: CALLCODE is used to set the QuarkWallet as the `msg.sender` success := callcode(gas(), scriptAddress, /* value */ 0, add(scriptCalldata, 0x20), scriptCalldataLen, 0x0, 0) returnSize := returndatasize() - // TODO: Move to TSTORE after updating Solidity version to >=0.8.24 - sstore(activeScriptSlot, 0) + // Transiently clear the active script + tstore(activeScriptSlot, 0) + + // Transiently clear the active nonce + tstore(activeNonceSlot, 0) + + // Transiently clear the active submission token + tstore(activeSubmissionTokenSlot, 0) } bytes memory returnData = new bytes(returnSize); @@ -480,8 +531,7 @@ contract QuarkWallet is IERC1271 { bytes32 callbackSlot = CALLBACK_SLOT; address callback; assembly { - // TODO: Move to TLOAD after updating Solidity version to >=0.8.24 - callback := sload(callbackSlot) + callback := tload(callbackSlot) } if (callback != address(0)) { (bool success, bytes memory result) = callback.delegatecall(data); diff --git a/src/quark-core/src/QuarkWalletStandalone.sol b/src/quark-core/src/QuarkWalletStandalone.sol index 2d7ff963..e48bdfd1 100644 --- a/src/quark-core/src/QuarkWalletStandalone.sol +++ b/src/quark-core/src/QuarkWalletStandalone.sol @@ -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"; @@ -23,7 +23,7 @@ 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 nonceManager_ The QuarkNonceManager contract used to write/read nonces and storage for this wallet + * @param nonceManager_ The QuarkNonceManager contract used to write/read nonces for this wallet */ constructor(address signer_, address executor_, CodeJar codeJar_, QuarkNonceManager nonceManager_) QuarkWallet(codeJar_, nonceManager_) diff --git a/src/quark-core/src/interfaces/IHasSignerExecutor.sol b/src/quark-core/src/interfaces/IHasSignerExecutor.sol index 0b3fbc23..c4e6165d 100644 --- a/src/quark-core/src/interfaces/IHasSignerExecutor.sol +++ b/src/quark-core/src/interfaces/IHasSignerExecutor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; /** * @title Has Signer and Executor interface diff --git a/src/quark-core/src/interfaces/IQuarkWallet.sol b/src/quark-core/src/interfaces/IQuarkWallet.sol index 83bfa536..7758f951 100644 --- a/src/quark-core/src/interfaces/IQuarkWallet.sol +++ b/src/quark-core/src/interfaces/IQuarkWallet.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; /** * @title Quark Wallet interface @@ -21,6 +21,7 @@ interface IQuarkWallet { uint256 expiry; } + function nonceManager() external view returns (address); function executeQuarkOperation(QuarkOperation calldata op, uint8 v, bytes32 r, bytes32 s) external returns (bytes memory); diff --git a/src/quark-core/src/periphery/BatchExecutor.sol b/src/quark-core/src/periphery/BatchExecutor.sol index 78d1de5c..07a5ceb9 100644 --- a/src/quark-core/src/periphery/BatchExecutor.sol +++ b/src/quark-core/src/periphery/BatchExecutor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; diff --git a/src/quark-factory/foundry.toml b/src/quark-factory/foundry.toml index f64bc2f7..a9abd330 100644 --- a/src/quark-factory/foundry.toml +++ b/src/quark-factory/foundry.toml @@ -1,6 +1,6 @@ [profile.default] -solc = "0.8.23" -evm_version = "paris" +solc = "0.8.27" +evm_version = "cancun" libs = [ "../../lib" ] diff --git a/src/quark-factory/src/QuarkFactory.sol b/src/quark-factory/src/QuarkFactory.sol index 257f5539..9eefcbb3 100644 --- a/src/quark-factory/src/QuarkFactory.sol +++ b/src/quark-factory/src/QuarkFactory.sol @@ -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"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; diff --git a/src/quark-proxy/foundry.toml b/src/quark-proxy/foundry.toml index 5317ab6a..15c1aff9 100644 --- a/src/quark-proxy/foundry.toml +++ b/src/quark-proxy/foundry.toml @@ -1,6 +1,6 @@ [profile.default] -solc = "0.8.23" -evm_version = "paris" +solc = "0.8.27" +evm_version = "cancun" libs = [ "../../lib" ] diff --git a/src/quark-proxy/src/QuarkMinimalProxy.sol b/src/quark-proxy/src/QuarkMinimalProxy.sol index 7371a407..e1e89888 100644 --- a/src/quark-proxy/src/QuarkMinimalProxy.sol +++ b/src/quark-proxy/src/QuarkMinimalProxy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import {IHasSignerExecutor} from "quark-core/src/interfaces/IHasSignerExecutor.sol"; diff --git a/src/quark-proxy/src/QuarkWalletProxyFactory.sol b/src/quark-proxy/src/QuarkWalletProxyFactory.sol index 66eadcf4..415da743 100644 --- a/src/quark-proxy/src/QuarkWalletProxyFactory.sol +++ b/src/quark-proxy/src/QuarkWalletProxyFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import {QuarkWallet, QuarkWalletMetadata} from "quark-core/src/QuarkWallet.sol"; diff --git a/test/ReplayableTransactions.t.sol b/test/ReplayableTransactions.t.sol index fedca319..450919dd 100644 --- a/test/ReplayableTransactions.t.sol +++ b/test/ReplayableTransactions.t.sol @@ -1,420 +1,446 @@ -// TODO: Uncomment when replay tokens are supported -// // SPDX-License-Identifier: BSD-3-Clause -// pragma solidity 0.8.23; - -// import "forge-std/Test.sol"; -// import "forge-std/console.sol"; - -// import {CodeJar} from "codejar/src/CodeJar.sol"; - -// import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; -// import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; - -// import {QuarkMinimalProxy} from "quark-proxy/src/QuarkMinimalProxy.sol"; - -// import {RecurringPurchase} from "test/lib/RecurringPurchase.sol"; - -// import {YulHelper} from "test/lib/YulHelper.sol"; -// import {SignatureHelper} from "test/lib/SignatureHelper.sol"; -// import {QuarkOperationHelper, ScriptType} from "test/lib/QuarkOperationHelper.sol"; - -// import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; - -// // TODO: Limit orders -// // TODO: Liquidation protection -// contract ReplayableTransactionsTest is Test { -// event Ping(uint256); -// event ClearNonce(address indexed wallet, uint96 nonce); - -// CodeJar public codeJar; -// QuarkNonceManager public nonceManager; -// QuarkWallet public walletImplementation; - -// uint256 alicePrivateKey = 0x8675309; -// address aliceAccount = vm.addr(alicePrivateKey); -// QuarkWallet aliceWallet; // see constructor() - -// bytes recurringPurchase = new YulHelper().getCode("RecurringPurchase.sol/RecurringPurchase.json"); - -// // Contracts address on mainnet -// address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; -// address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; -// address constant COMP = 0xc00e94Cb662C3520282E6f5717214004A7f26888; -// // Uniswap router info on mainnet -// address constant uniswapRouter = 0xE592427A0AEce92De3Edee1F18E0157C05861564; - -// constructor() { -// // Fork setup -// vm.createSelectFork( -// string.concat( -// "https://node-provider.compound.finance/ethereum-mainnet/", vm.envString("NODE_PROVIDER_BYPASS_KEY") -// ), -// 18429607 // 2023-10-25 13:24:00 PST -// ); - -// codeJar = new CodeJar(); -// console.log("CodeJar deployed to: %s", address(codeJar)); - -// nonceManager = new QuarkNonceManager(); -// console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); - -// walletImplementation = new QuarkWallet(codeJar, nonceManager); -// console.log("QuarkWallet implementation: %s", address(walletImplementation)); - -// aliceWallet = -// QuarkWallet(payable(new QuarkMinimalProxy(address(walletImplementation), aliceAccount, address(0)))); -// console.log("Alice signer: %s", aliceAccount); -// console.log("Alice wallet at: %s", address(aliceWallet)); -// } - -// /* ===== recurring purchase tests ===== */ - -// function createPurchaseConfig(uint40 purchaseInterval, uint256 timesToPurchase, uint216 totalAmountToPurchase) -// internal -// view -// returns (RecurringPurchase.PurchaseConfig memory) -// { -// uint256 deadline = block.timestamp + purchaseInterval * (timesToPurchase - 1) + 1; -// RecurringPurchase.SwapParamsExactOut memory swapParams = RecurringPurchase.SwapParamsExactOut({ -// uniswapRouter: uniswapRouter, -// recipient: address(aliceWallet), -// tokenFrom: USDC, -// amount: uint256(totalAmountToPurchase) / timesToPurchase, -// amountInMaximum: 30_000e6, -// deadline: deadline, -// path: abi.encodePacked(WETH, uint24(500), USDC) // Path: WETH - 0.05% -> USDC -// }); -// RecurringPurchase.PurchaseConfig memory purchaseConfig = RecurringPurchase.PurchaseConfig({ -// interval: purchaseInterval, -// totalAmountToPurchase: totalAmountToPurchase, -// swapParams: swapParams -// }); -// return purchaseConfig; -// } - -// // Executes the script once for gas measurement purchases -// function testRecurringPurchaseHappyPath() public { -// // gas: disable gas metering except while executing operations -// vm.pauseGasMetering(); - -// deal(USDC, address(aliceWallet), 100_000e6); -// uint40 purchaseInterval = 86_400; // 1 day interval -// uint256 timesToPurchase = 1; -// uint216 totalAmountToPurchase = 10 ether; -// RecurringPurchase.PurchaseConfig memory purchaseConfig = -// createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); -// QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( -// aliceWallet, -// recurringPurchase, -// abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), -// ScriptType.ScriptAddress -// ); -// op.expiry = purchaseConfig.swapParams.deadline; -// (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); - -// // gas: meter execute -// vm.resumeGasMetering(); -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), totalAmountToPurchase); -// } - -// function testRecurringPurchaseMultiplePurchases() public { -// // gas: disable gas metering except while executing operations -// vm.pauseGasMetering(); - -// deal(USDC, address(aliceWallet), 100_000e6); -// uint40 purchaseInterval = 86_400; // 1 day interval -// uint256 timesToPurchase = 2; -// uint216 totalAmountToPurchase = 20 ether; -// RecurringPurchase.PurchaseConfig memory purchaseConfig = -// createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); -// QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( -// aliceWallet, -// recurringPurchase, -// abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), -// ScriptType.ScriptAddress -// ); -// op.expiry = purchaseConfig.swapParams.deadline; -// (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); - -// // gas: meter execute -// vm.resumeGasMetering(); -// // 1. Execute recurring purchase for the first time -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); - -// // 2a. Cannot buy again unless time interval has passed -// vm.expectRevert(RecurringPurchase.PurchaseConditionNotMet.selector); -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); - -// // 2b. Execute recurring purchase a second time after warping 1 day -// vm.warp(block.timestamp + purchaseInterval); -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 20 ether); -// } - -// function testCancelRecurringPurchase() public { -// // gas: disable gas metering except while executing operations -// vm.pauseGasMetering(); - -// deal(USDC, address(aliceWallet), 100_000e6); -// uint40 purchaseInterval = 86_400; // 1 day interval -// uint256 timesToPurchase = 2; -// uint216 totalAmountToPurchase = 20 ether; -// RecurringPurchase.PurchaseConfig memory purchaseConfig = -// createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); -// QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( -// aliceWallet, -// recurringPurchase, -// abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), -// ScriptType.ScriptAddress -// ); -// op.expiry = purchaseConfig.swapParams.deadline; -// (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); - -// QuarkWallet.QuarkOperation memory cancelOp = new QuarkOperationHelper().newBasicOpWithCalldata( -// aliceWallet, -// recurringPurchase, -// abi.encodeWithSelector(RecurringPurchase.cancel.selector), -// ScriptType.ScriptAddress -// ); -// (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, cancelOp); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); - -// // gas: meter execute -// vm.resumeGasMetering(); -// // 1. Execute recurring purchase for the first time -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); - -// // 2. Cancel replayable transaction -// aliceWallet.executeQuarkOperation(cancelOp, v2, r2, s2); - -// // 3. Replayable transaction can no longer be executed -// vm.warp(block.timestamp + purchaseInterval); -// vm.expectRevert(QuarkNonceManager.NonceAlreadySet.selector); -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); -// } - -// function testRecurringPurchaseWithDifferentCalldata() public { -// // gas: disable gas metering except while executing operations -// vm.pauseGasMetering(); - -// deal(USDC, address(aliceWallet), 100_000e6); -// uint40 purchaseInterval = 86_400; // 1 day interval -// QuarkWallet.QuarkOperation memory op1; -// QuarkWallet.QuarkOperation memory op2; -// QuarkWallet.QuarkOperation memory cancelOp; -// // Local scope to avoid stack too deep -// { -// uint256 timesToPurchase = 3; -// uint216 totalAmountToPurchase1 = 30 ether; // 10 ETH / day -// uint216 totalAmountToPurchase2 = 15 ether; // 5 ETH / day -// // Two purchase configs using the same nonce: one to purchase 10 ETH and the other to purchase 5 ETH -// RecurringPurchase.PurchaseConfig memory purchaseConfig1 = -// createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase1); -// op1 = new QuarkOperationHelper().newBasicOpWithCalldata( -// aliceWallet, -// recurringPurchase, -// abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig1), -// ScriptType.ScriptAddress -// ); -// op1.expiry = purchaseConfig1.swapParams.deadline; -// RecurringPurchase.PurchaseConfig memory purchaseConfig2 = -// createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase2); -// op2 = new QuarkOperationHelper().newBasicOpWithCalldata( -// aliceWallet, -// recurringPurchase, -// abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig2), -// ScriptType.ScriptAddress -// ); -// op2.expiry = purchaseConfig2.swapParams.deadline; -// cancelOp = new QuarkOperationHelper().newBasicOpWithCalldata( -// aliceWallet, -// recurringPurchase, -// abi.encodeWithSelector(RecurringPurchase.cancel.selector), -// ScriptType.ScriptAddress -// ); -// cancelOp.expiry = op2.expiry; -// } -// (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); -// (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op2); -// (uint8 v3, bytes32 r3, bytes32 s3) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, cancelOp); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); - -// // gas: meter execute -// vm.resumeGasMetering(); -// // 1a. Execute recurring purchase order #1 -// aliceWallet.executeQuarkOperation(op1, v1, r1, s1); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); - -// // 1b. Execute recurring purchase order #2 -// aliceWallet.executeQuarkOperation(op2, v2, r2, s2); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 15 ether); - -// // 2. Warp until next purchase period -// vm.warp(block.timestamp + purchaseInterval); - -// // 3a. Execute recurring purchase order #1 -// aliceWallet.executeQuarkOperation(op1, v1, r1, s1); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 25 ether); - -// // 3b. Execute recurring purchase order #2 -// aliceWallet.executeQuarkOperation(op2, v2, r2, s2); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 30 ether); - -// // 4. Cancel replayable transaction -// aliceWallet.executeQuarkOperation(cancelOp, v3, r3, s3); - -// // 5. Warp until next purchase period -// vm.warp(block.timestamp + purchaseInterval); - -// // 6. Both recurring purchase orders can no longer be executed -// vm.expectRevert(QuarkNonceManager.NonceAlreadySet.selector); -// aliceWallet.executeQuarkOperation(op1, v1, r1, s1); -// vm.expectRevert(QuarkNonceManager.NonceAlreadySet.selector); -// aliceWallet.executeQuarkOperation(op2, v2, r2, s2); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 30 ether); -// } - -// function testRevertsForPurchaseBeforeNextPurchasePeriod() public { -// // gas: disable gas metering except while executing operations -// vm.pauseGasMetering(); - -// deal(USDC, address(aliceWallet), 100_000e6); -// uint40 purchaseInterval = 86_400; // 1 day interval -// uint256 timesToPurchase = 2; -// uint216 totalAmountToPurchase = 20 ether; -// RecurringPurchase.PurchaseConfig memory purchaseConfig = -// createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); -// QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( -// aliceWallet, -// recurringPurchase, -// abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), -// ScriptType.ScriptAddress -// ); -// op.expiry = purchaseConfig.swapParams.deadline; -// (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); - -// // gas: meter execute -// vm.resumeGasMetering(); -// // 1. Execute recurring purchase for the first time -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); - -// // 2. Cannot buy again unless time interval has passed -// vm.expectRevert(RecurringPurchase.PurchaseConditionNotMet.selector); -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); -// } - -// function testRevertsForExpiredQuarkOperation() public { -// // gas: disable gas metering except while executing operations -// vm.pauseGasMetering(); - -// deal(USDC, address(aliceWallet), 100_000e6); -// uint40 purchaseInterval = 86_400; // 1 day interval -// uint256 timesToPurchase = 1; -// uint216 totalAmountToPurchase = 10 ether; -// RecurringPurchase.PurchaseConfig memory purchaseConfig = -// createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); -// QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( -// aliceWallet, -// recurringPurchase, -// abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), -// ScriptType.ScriptAddress -// ); -// op.expiry = block.timestamp - 1; // Set Quark operation expiry to always expire -// (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); - -// // gas: meter execute -// vm.resumeGasMetering(); -// vm.expectRevert(QuarkWallet.SignatureExpired.selector); -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); -// } - -// function testRevertsForExpiredUniswapParams() public { -// // gas: disable gas metering except while executing operations -// vm.pauseGasMetering(); - -// deal(USDC, address(aliceWallet), 100_000e6); -// uint40 purchaseInterval = 86_400; // 1 day interval -// uint256 timesToPurchase = 1; -// uint216 totalAmountToPurchase = 10 ether; -// RecurringPurchase.PurchaseConfig memory purchaseConfig = -// createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); -// purchaseConfig.swapParams.deadline = block.timestamp - 1; // Set Uniswap deadline to always expire -// QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( -// aliceWallet, -// recurringPurchase, -// abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), -// ScriptType.ScriptAddress -// ); -// (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); - -// // gas: meter execute -// vm.resumeGasMetering(); -// vm.expectRevert(bytes("Transaction too old")); -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); -// } - -// function testRevertsForPurchasingOverTheLimit() public { -// // gas: disable gas metering except while executing operations -// vm.pauseGasMetering(); - -// deal(USDC, address(aliceWallet), 100_000e6); -// uint40 purchaseInterval = 86_400; // 1 day interval -// uint256 timesToPurchase = 2; -// uint216 totalAmountToPurchase = 20 ether; // 10 ETH / day -// RecurringPurchase.PurchaseConfig memory purchaseConfig = -// createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); -// purchaseConfig.totalAmountToPurchase = 10 ether; // Will only be able to purchase once -// QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( -// aliceWallet, -// recurringPurchase, -// abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), -// ScriptType.ScriptAddress -// ); -// op.expiry = purchaseConfig.swapParams.deadline; -// (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); - -// // gas: meter execute -// vm.resumeGasMetering(); -// // 1. Execute recurring purchase -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); - -// // 2. Warp until next purchase period -// vm.warp(block.timestamp + purchaseInterval); - -// // 3. Purchasing again will go over the `totalAmountToPurchase` cap -// vm.expectRevert(RecurringPurchase.PurchaseConditionNotMet.selector); -// aliceWallet.executeQuarkOperation(op, v1, r1, s1); - -// assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); -// } -// } +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import {CodeJar} from "codejar/src/CodeJar.sol"; + +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; +import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; + +import {QuarkMinimalProxy} from "quark-proxy/src/QuarkMinimalProxy.sol"; + +import {RecurringPurchase} from "test/lib/RecurringPurchase.sol"; + +import {YulHelper} from "test/lib/YulHelper.sol"; +import {SignatureHelper} from "test/lib/SignatureHelper.sol"; +import {QuarkOperationHelper, ScriptType} from "test/lib/QuarkOperationHelper.sol"; + +import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; + +// TODO: Limit orders +// TODO: Liquidation protection +contract ReplayableTransactionsTest is Test { + event Ping(uint256); + event ClearNonce(address indexed wallet, uint96 nonce); + + CodeJar public codeJar; + QuarkNonceManager public nonceManager; + QuarkWallet public walletImplementation; + + uint256 alicePrivateKey = 0x8675309; + address aliceAccount = vm.addr(alicePrivateKey); + QuarkWallet aliceWallet; // see constructor() + + bytes recurringPurchase = new YulHelper().getCode("RecurringPurchase.sol/RecurringPurchase.json"); + + // Contracts address on mainnet + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant COMP = 0xc00e94Cb662C3520282E6f5717214004A7f26888; + // Uniswap router info on mainnet + address constant uniswapRouter = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + + constructor() { + // Fork setup + vm.createSelectFork( + string.concat( + "https://node-provider.compound.finance/ethereum-mainnet/", vm.envString("NODE_PROVIDER_BYPASS_KEY") + ), + 18429607 // 2023-10-25 13:24:00 PST + ); + + codeJar = new CodeJar(); + console.log("CodeJar deployed to: %s", address(codeJar)); + + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); + + walletImplementation = new QuarkWallet(codeJar, nonceManager); + console.log("QuarkWallet implementation: %s", address(walletImplementation)); + + aliceWallet = + QuarkWallet(payable(new QuarkMinimalProxy(address(walletImplementation), aliceAccount, address(0)))); + console.log("Alice signer: %s", aliceAccount); + console.log("Alice wallet at: %s", address(aliceWallet)); + } + + /* ===== recurring purchase tests ===== */ + + function createPurchaseConfig(uint40 purchaseInterval, uint256 timesToPurchase, uint216 totalAmountToPurchase) + internal + view + returns (RecurringPurchase.PurchaseConfig memory) + { + uint256 deadline = block.timestamp + purchaseInterval * (timesToPurchase - 1) + 1; + RecurringPurchase.SwapParamsExactOut memory swapParams = RecurringPurchase.SwapParamsExactOut({ + uniswapRouter: uniswapRouter, + recipient: address(aliceWallet), + tokenFrom: USDC, + amount: uint256(totalAmountToPurchase) / timesToPurchase, + amountInMaximum: 30_000e6, + deadline: deadline, + path: abi.encodePacked(WETH, uint24(500), USDC) // Path: WETH - 0.05% -> USDC + }); + RecurringPurchase.PurchaseConfig memory purchaseConfig = RecurringPurchase.PurchaseConfig({ + interval: purchaseInterval, + totalAmountToPurchase: totalAmountToPurchase, + swapParams: swapParams + }); + return purchaseConfig; + } + + // Executes the script once for gas measurement purchases + function testRecurringPurchaseHappyPath() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + + deal(USDC, address(aliceWallet), 100_000e6); + uint40 purchaseInterval = 86_400; // 1 day interval + uint256 timesToPurchase = 1; + uint216 totalAmountToPurchase = 10 ether; + RecurringPurchase.PurchaseConfig memory purchaseConfig = + createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + recurringPurchase, + abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), + ScriptType.ScriptAddress + ); + op.expiry = purchaseConfig.swapParams.deadline; + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); + + // gas: meter execute + vm.resumeGasMetering(); + aliceWallet.executeQuarkOperation(op, v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), totalAmountToPurchase); + } + + function testRecurringPurchaseMultiplePurchases() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + + deal(USDC, address(aliceWallet), 100_000e6); + uint40 purchaseInterval = 86_400; // 1 day interval + uint256 timesToPurchase = 2; + uint216 totalAmountToPurchase = 20 ether; + RecurringPurchase.PurchaseConfig memory purchaseConfig = + createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + recurringPurchase, + abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), + ScriptType.ScriptAddress, + 1 + ); + op.expiry = purchaseConfig.swapParams.deadline; + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); + + // gas: meter execute + vm.resumeGasMetering(); + // 1. Execute recurring purchase for the first time + aliceWallet.executeQuarkOperation(op, v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); + + // 2a. Cannot buy again unless time interval has passed + vm.expectRevert(RecurringPurchase.PurchaseConditionNotMet.selector); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); + + // 2b. Execute recurring purchase a second time after warping 1 day + vm.warp(block.timestamp + purchaseInterval); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 20 ether); + } + + function testCancelRecurringPurchase() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + + deal(USDC, address(aliceWallet), 100_000e6); + uint40 purchaseInterval = 86_400; // 1 day interval + uint256 timesToPurchase = 2; + uint216 totalAmountToPurchase = 20 ether; + RecurringPurchase.PurchaseConfig memory purchaseConfig = + createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + recurringPurchase, + abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), + ScriptType.ScriptAddress, + 2 + ); + op.expiry = purchaseConfig.swapParams.deadline; + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + QuarkWallet.QuarkOperation memory cancelOp = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + recurringPurchase, + abi.encodeWithSelector(RecurringPurchase.nop.selector), + ScriptType.ScriptAddress + ); + cancelOp.nonce = op.nonce; + (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, cancelOp); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); + + // gas: meter execute + vm.resumeGasMetering(); + // 1. Execute recurring purchase for the first time + aliceWallet.executeQuarkOperation(op, v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); + + // 2. Cancel replayable transaction + aliceWallet.executeQuarkOperationWithSubmissionToken(cancelOp, submissionTokens[1], v2, r2, s2); + + // 3. Replayable transaction can no longer be executed + vm.warp(block.timestamp + purchaseInterval); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(aliceWallet), op.nonce, submissionTokens[1], true + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); + + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(aliceWallet), op.nonce, submissionTokens[2], true + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[2], v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); + } + + function testRecurringPurchaseWithDifferentCalldata() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + + deal(USDC, address(aliceWallet), 100_000e6); + uint40 purchaseInterval = 86_400; // 1 day interval + QuarkWallet.QuarkOperation memory op1; + bytes32[] memory submissionTokens; + QuarkWallet.QuarkOperation memory op2; + QuarkWallet.QuarkOperation memory cancelOp; + // Local scope to avoid stack too deep + { + uint256 timesToPurchase = 3; + uint216 totalAmountToPurchase1 = 30 ether; // 10 ETH / day + uint216 totalAmountToPurchase2 = 15 ether; // 5 ETH / day + // Two purchase configs using the same nonce: one to purchase 10 ETH and the other to purchase 5 ETH + RecurringPurchase.PurchaseConfig memory purchaseConfig1 = + createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase1); + (op1, submissionTokens) = new QuarkOperationHelper().newReplayableOpWithCalldata( + aliceWallet, + recurringPurchase, + abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig1), + ScriptType.ScriptAddress, + 5 + ); + op1.expiry = purchaseConfig1.swapParams.deadline; + RecurringPurchase.PurchaseConfig memory purchaseConfig2 = + createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase2); + op2 = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + recurringPurchase, + abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig2), + ScriptType.ScriptAddress + ); + op2.expiry = purchaseConfig2.swapParams.deadline; + op2.nonce = op1.nonce; + op2.isReplayable = true; + cancelOp = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + recurringPurchase, + abi.encodeWithSelector(RecurringPurchase.nop.selector), + ScriptType.ScriptAddress + ); + cancelOp.expiry = op2.expiry; + cancelOp.nonce = op1.nonce; + } + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); + (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op2); + (uint8 v3, bytes32 r3, bytes32 s3) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, cancelOp); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); + + // gas: meter execute + vm.resumeGasMetering(); + // 1a. Execute recurring purchase order #1 + aliceWallet.executeQuarkOperation(op1, v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); + + // 1b. Execute recurring purchase order #2 + aliceWallet.executeQuarkOperationWithSubmissionToken(op2, submissionTokens[1], v2, r2, s2); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 15 ether); + + // 2. Warp until next purchase period + vm.warp(block.timestamp + purchaseInterval); + + // 3a. Execute recurring purchase order #1 + aliceWallet.executeQuarkOperationWithSubmissionToken(op1, submissionTokens[2], v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 25 ether); + + // 3b. Execute recurring purchase order #2 + aliceWallet.executeQuarkOperationWithSubmissionToken(op2, submissionTokens[3], v2, r2, s2); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 30 ether); + + // 4. Cancel replayable transaction + aliceWallet.executeQuarkOperationWithSubmissionToken(cancelOp, submissionTokens[4], v3, r3, s3); + + // 5. Warp until next purchase period + vm.warp(block.timestamp + purchaseInterval); + + // 6. Both recurring purchase orders can no longer be executed + vm.expectRevert(); + aliceWallet.executeQuarkOperationWithSubmissionToken(op1, submissionTokens[4], v1, r1, s1); + vm.expectRevert(); + aliceWallet.executeQuarkOperationWithSubmissionToken(op2, submissionTokens[5], v2, r2, s2); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 30 ether); + } + + function testRevertsForPurchaseBeforeNextPurchasePeriod() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + + deal(USDC, address(aliceWallet), 100_000e6); + uint40 purchaseInterval = 86_400; // 1 day interval + uint256 timesToPurchase = 2; + uint216 totalAmountToPurchase = 20 ether; + RecurringPurchase.PurchaseConfig memory purchaseConfig = + createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + recurringPurchase, + abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), + ScriptType.ScriptAddress, + 1 + ); + op.expiry = purchaseConfig.swapParams.deadline; + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); + + // gas: meter execute + vm.resumeGasMetering(); + // 1. Execute recurring purchase for the first time + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[0], v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); + + // 2. Cannot buy again unless time interval has passed + vm.expectRevert(RecurringPurchase.PurchaseConditionNotMet.selector); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); + } + + function testRevertsForExpiredQuarkOperation() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + + deal(USDC, address(aliceWallet), 100_000e6); + uint40 purchaseInterval = 86_400; // 1 day interval + uint256 timesToPurchase = 1; + uint216 totalAmountToPurchase = 10 ether; + RecurringPurchase.PurchaseConfig memory purchaseConfig = + createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); + (QuarkWallet.QuarkOperation memory op,) = new QuarkOperationHelper().newReplayableOpWithCalldata( + aliceWallet, + recurringPurchase, + abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), + ScriptType.ScriptAddress, + 0 + ); + op.expiry = block.timestamp - 1; // Set Quark operation expiry to always expire + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + vm.expectRevert(QuarkWallet.SignatureExpired.selector); + aliceWallet.executeQuarkOperation(op, v1, r1, s1); + } + + function testRevertsForExpiredUniswapParams() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + + deal(USDC, address(aliceWallet), 100_000e6); + uint40 purchaseInterval = 86_400; // 1 day interval + uint256 timesToPurchase = 1; + uint216 totalAmountToPurchase = 10 ether; + RecurringPurchase.PurchaseConfig memory purchaseConfig = + createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); + purchaseConfig.swapParams.deadline = block.timestamp - 1; // Set Uniswap deadline to always expire + (QuarkWallet.QuarkOperation memory op,) = new QuarkOperationHelper().newReplayableOpWithCalldata( + aliceWallet, + recurringPurchase, + abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), + ScriptType.ScriptAddress, + 0 + ); + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + vm.expectRevert(bytes("Transaction too old")); + aliceWallet.executeQuarkOperation(op, v1, r1, s1); + } + + function testRevertsForPurchasingOverTheLimit() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + + deal(USDC, address(aliceWallet), 100_000e6); + uint40 purchaseInterval = 86_400; // 1 day interval + uint256 timesToPurchase = 2; + uint216 totalAmountToPurchase = 20 ether; // 10 ETH / day + RecurringPurchase.PurchaseConfig memory purchaseConfig = + createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); + purchaseConfig.totalAmountToPurchase = 10 ether; // Will only be able to purchase once + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + recurringPurchase, + abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), + ScriptType.ScriptAddress, + 1 + ); + op.expiry = purchaseConfig.swapParams.deadline; + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); + + // gas: meter execute + vm.resumeGasMetering(); + // 1. Execute recurring purchase + aliceWallet.executeQuarkOperation(op, v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); + + // 2. Warp until next purchase period + vm.warp(block.timestamp + purchaseInterval); + + // 3. Purchasing again will go over the `totalAmountToPurchase` cap + vm.expectRevert(RecurringPurchase.PurchaseConditionNotMet.selector); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); + } +} diff --git a/test/codejar/CodeJar.t.sol b/test/codejar/CodeJar.t.sol index 802dc91b..486dc9a6 100644 --- a/test/codejar/CodeJar.t.sol +++ b/test/codejar/CodeJar.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; diff --git a/test/lib/AllowCallbacks.sol b/test/lib/AllowCallbacks.sol index 8593e8c4..50a707b4 100644 --- a/test/lib/AllowCallbacks.sol +++ b/test/lib/AllowCallbacks.sol @@ -1,23 +1,36 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "quark-core/src/QuarkScript.sol"; import "quark-core/src/QuarkWallet.sol"; -contract AllowCallbacks is QuarkScript { - // TODO: Uncomment when replay tokens are supported - // function run(address callbackAddress) public { - // QuarkWallet self = QuarkWallet(payable(address(this))); - // self.nonceManager().write(self.CALLBACK_KEY(), bytes32(uint256(uint160(callbackAddress)))); - // } +interface IComeback { + function request() external returns (uint256); +} + +contract Comebacker { + function comeback() public returns (uint256) { + return IComeback(msg.sender).request() + 1; + } +} - function allowCallbackAndReplay() public { +contract AllowCallbacks is QuarkScript { + function run() public returns (uint256) { allowCallback(); - // TODO: Uncomment when replay tokens are supported - // allowReplay(); + return new Comebacker().comeback() * 2; } - function clear() public { + function runAllowThenClear() public returns (uint256) { + allowCallback(); clearCallback(); + return new Comebacker().comeback() * 2; + } + + function runWithoutAllow() public returns (uint256) { + return new Comebacker().comeback() * 2; + } + + function request() external view returns (uint256) { + return 100 + getActiveReplayCount(); } } diff --git a/test/lib/CallbackFromCounter.sol b/test/lib/CallbackFromCounter.sol index 731bfdac..b81275ed 100644 --- a/test/lib/CallbackFromCounter.sol +++ b/test/lib/CallbackFromCounter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "test/lib/Counter.sol"; import "quark-core/src/QuarkScript.sol"; diff --git a/test/lib/CallcodeReentrancy.sol b/test/lib/CallcodeReentrancy.sol index 594718c3..93468649 100644 --- a/test/lib/CallcodeReentrancy.sol +++ b/test/lib/CallcodeReentrancy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/console.sol"; diff --git a/test/lib/CancelOtherScript.sol b/test/lib/CancelOtherScript.sol deleted file mode 100644 index 63576aac..00000000 --- a/test/lib/CancelOtherScript.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; - -import "quark-core/src/QuarkWallet.sol"; - -contract CancelOtherScript { - function run(bytes32 nonce) public { - return QuarkWallet(payable(address(this))).nonceManager().submitNonceToken(nonce, bytes32(type(uint256).max)); - } -} diff --git a/test/lib/CheckNonceScript.sol b/test/lib/CheckNonceScript.sol new file mode 100644 index 00000000..f7b1919a --- /dev/null +++ b/test/lib/CheckNonceScript.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.27; + +import "quark-core/src/QuarkWallet.sol"; +import "quark-core/src/QuarkScript.sol"; + +contract CheckNonceScript is QuarkScript { + function checkNonce() public view returns (bytes32) { + return getActiveNonce(); + } + + function checkSubmissionToken() public view returns (bytes32) { + return getActiveSubmissionToken(); + } + + function checkReplayCount() public view returns (uint256) { + return getActiveReplayCount(); + } +} diff --git a/test/lib/ConstructorReverter.sol b/test/lib/ConstructorReverter.sol index 9ed871b2..651856a8 100644 --- a/test/lib/ConstructorReverter.sol +++ b/test/lib/ConstructorReverter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; contract ConstructorReverter { error Test(uint256); diff --git a/test/lib/Counter.sol b/test/lib/Counter.sol index 65a47172..d9b02cb9 100644 --- a/test/lib/Counter.sol +++ b/test/lib/Counter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; interface HasCallback { function callback() external payable; diff --git a/test/lib/CounterScript.sol b/test/lib/CounterScript.sol index 773b1433..deca9d4f 100644 --- a/test/lib/CounterScript.sol +++ b/test/lib/CounterScript.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "test/lib/Counter.sol"; diff --git a/test/lib/DeFiScripts.sol b/test/lib/DeFiScripts.sol index 3e1fd1e6..6ef396d9 100644 --- a/test/lib/DeFiScripts.sol +++ b/test/lib/DeFiScripts.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; diff --git a/test/lib/EIP1271Signer.sol b/test/lib/EIP1271Signer.sol index c8290e58..d416d5ce 100644 --- a/test/lib/EIP1271Signer.sol +++ b/test/lib/EIP1271Signer.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.23; +pragma solidity 0.8.27; contract EIP1271Signer { bytes4 internal constant EIP_1271_MAGIC_VALUE = 0x1626ba7e; diff --git a/test/lib/EmptyCode.sol b/test/lib/EmptyCode.sol index c27562ca..c84624c0 100644 --- a/test/lib/EmptyCode.sol +++ b/test/lib/EmptyCode.sol @@ -1,4 +1,4 @@ -pragma solidity "0.8.23"; +pragma solidity "0.8.27"; contract EmptyCode { // NOTE: force the solidity compiler to produce empty code when this is deployed diff --git a/test/lib/EvilReceiver.sol b/test/lib/EvilReceiver.sol index 65b488e1..24d4cc1b 100644 --- a/test/lib/EvilReceiver.sol +++ b/test/lib/EvilReceiver.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "quark-core/src/QuarkWallet.sol"; import "quark-core/src/QuarkScript.sol"; diff --git a/test/lib/ExecuteOnBehalf.sol b/test/lib/ExecuteOnBehalf.sol index bfec2cc7..1b1baa61 100644 --- a/test/lib/ExecuteOnBehalf.sol +++ b/test/lib/ExecuteOnBehalf.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "quark-core/src/QuarkWallet.sol"; diff --git a/test/lib/ExecuteOtherOperation.sol b/test/lib/ExecuteOtherOperation.sol index 62fcf96c..6e853230 100644 --- a/test/lib/ExecuteOtherOperation.sol +++ b/test/lib/ExecuteOtherOperation.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "quark-core/src/QuarkWallet.sol"; import "quark-core/src/QuarkScript.sol"; diff --git a/test/lib/ExecuteWithRequirements.sol b/test/lib/ExecuteWithRequirements.sol index b39d1951..ed5465f9 100644 --- a/test/lib/ExecuteWithRequirements.sol +++ b/test/lib/ExecuteWithRequirements.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "quark-core/src/QuarkWallet.sol"; import "quark-core/src/QuarkNonceManager.sol"; @@ -14,7 +14,7 @@ contract ExecuteWithRequirements { QuarkWallet wallet = QuarkWallet(payable(address(this))); QuarkNonceManager nonceManager = wallet.nonceManager(); for (uint256 i = 0; i < requirements.length; i++) { - if (nonceManager.getNonceSubmission(address(wallet), requirements[i]) == bytes32(uint256(0))) { + if (nonceManager.submissions(address(wallet), requirements[i]) == bytes32(uint256(0))) { revert RequirementNotMet(requirements[i]); } } diff --git a/test/lib/GetMessageDetails.sol b/test/lib/GetMessageDetails.sol index baf483ba..10412d26 100644 --- a/test/lib/GetMessageDetails.sol +++ b/test/lib/GetMessageDetails.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; contract GetMessageDetails { function getMsgSenderAndValue() external payable returns (address, uint256) { diff --git a/test/lib/Incrementer.sol b/test/lib/Incrementer.sol index be0334e8..7bd62340 100644 --- a/test/lib/Incrementer.sol +++ b/test/lib/Incrementer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "test/lib/Counter.sol"; @@ -12,11 +12,12 @@ contract Incrementer { Counter(counter).increment(); } - // TODO: Uncomment when replay tokens are supported - // function incrementCounterReplayable(Counter counter) public { - // incrementCounter(counter); - // QuarkWallet(payable(address(this))).nonceManager().clearNonce(); - // } + function incrementCounter2(Counter counter) public { + Counter(counter).increment(); + Counter(counter).increment(); + Counter(counter).increment(); + Counter(counter).increment(); + } fallback() external { // Counter @@ -24,3 +25,14 @@ contract Incrementer { incrementCounter(Counter(counter)); } } + +contract IncrementerBySix { + function incrementCounter(Counter counter) public { + Counter(counter).increment(); + Counter(counter).increment(); + Counter(counter).increment(); + Counter(counter).increment(); + Counter(counter).increment(); + Counter(counter).increment(); + } +} diff --git a/test/lib/Logger.sol b/test/lib/Logger.sol index 7522ec48..02f8cb90 100644 --- a/test/lib/Logger.sol +++ b/test/lib/Logger.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; contract Logger { event Ping(uint256); diff --git a/test/lib/MaxCounterScript.sol b/test/lib/MaxCounterScript.sol index 34b731b2..ff31ae3d 100644 --- a/test/lib/MaxCounterScript.sol +++ b/test/lib/MaxCounterScript.sol @@ -1,24 +1,25 @@ -// TODO: Uncomment when replay tokens are supported -// // SPDX-License-Identifier: BSD-3-Clause -// pragma solidity 0.8.23; +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.27; -// import "quark-core/src/QuarkScript.sol"; -// import "test/lib/Counter.sol"; +import "quark-core/src/QuarkScript.sol"; +import "test/lib/Counter.sol"; -// contract MaxCounterScript is QuarkScript { -// error EnoughAlready(); +contract MaxCounterScript is QuarkScript { + error EnoughAlready(); -// function run(Counter c) external returns (bytes memory) { -// c.increment(); -// uint256 count = readU256("count"); + event Count(uint256 c); -// if (count >= 3) { -// revert EnoughAlready(); -// } + function run(Counter c) external returns (bytes memory) { + c.increment(); + uint256 count = readU256("count"); -// writeU256("count", count + 1); -// allowReplay(); + if (count >= 3) { + revert EnoughAlready(); + } -// return hex""; -// } -// } + writeU256("count", count + 1); + emit Count(count + 1); + + return hex""; + } +} diff --git a/test/lib/Mememe.sol b/test/lib/Mememe.sol index 50afd81a..a42e9c7f 100644 --- a/test/lib/Mememe.sol +++ b/test/lib/Mememe.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; contract Mememe { address public immutable me; diff --git a/test/lib/Noncer.sol b/test/lib/Noncer.sol new file mode 100644 index 00000000..fb8c1feb --- /dev/null +++ b/test/lib/Noncer.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.27; + +import {QuarkScript} from "quark-core/src/QuarkScript.sol"; +import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; + +contract Stow { + bytes nestedOperation; + + function getNestedOperation() + public + view + returns (QuarkWallet.QuarkOperation memory op, bytes32 submissionToken, uint8 v, bytes32 r, bytes32 s) + { + (op, submissionToken, v, r, s) = + abi.decode(nestedOperation, (QuarkWallet.QuarkOperation, bytes32, uint8, bytes32, bytes32)); + } + + function setNestedOperation( + QuarkWallet.QuarkOperation memory op, + bytes32 submissionToken, + uint8 v, + bytes32 r, + bytes32 s + ) public { + nestedOperation = abi.encode(op, submissionToken, v, r, s); + } +} + +contract Noncer is QuarkScript { + function checkNonce() public view returns (bytes32) { + return getActiveNonce(); + } + + function checkSubmissionToken() public view returns (bytes32) { + return getActiveSubmissionToken(); + } + + function checkReplayCount() public view returns (uint256) { + return getActiveReplayCount(); + } + + // TODO: Test nesting with same nonce + function nestedNonce(QuarkWallet.QuarkOperation memory op, uint8 v, bytes32 r, bytes32 s) + public + returns (bytes32 pre, bytes32 post, bytes memory result) + { + pre = getActiveNonce(); + result = QuarkWallet(payable(address(this))).executeQuarkOperation(op, v, r, s); + post = getActiveNonce(); + + return (pre, post, result); + } + + function nestedSubmissionToken(QuarkWallet.QuarkOperation memory op, uint8 v, bytes32 r, bytes32 s) + public + returns (bytes32 pre, bytes32 post, bytes memory result) + { + pre = getActiveSubmissionToken(); + result = QuarkWallet(payable(address(this))).executeQuarkOperation(op, v, r, s); + post = getActiveSubmissionToken(); + + return (pre, post, result); + } + + function nestedReplayCount(QuarkWallet.QuarkOperation memory op, uint8 v, bytes32 r, bytes32 s) + public + returns (uint256 pre, uint256 post, bytes memory result) + { + pre = getActiveReplayCount(); + result = QuarkWallet(payable(address(this))).executeQuarkOperation(op, v, r, s); + post = getActiveReplayCount(); + + return (pre, post, result); + } + + function postNestRead(QuarkWallet.QuarkOperation memory op, uint8 v, bytes32 r, bytes32 s) + public + returns (uint256) + { + QuarkWallet(payable(address(this))).executeQuarkOperation(op, v, r, s); + return readU256("count"); + } + + function nestedPlay(Stow stow) public returns (uint256) { + uint256 n = getActiveReplayCount(); + if (n == 0) { + (QuarkWallet.QuarkOperation memory op, bytes32 submissionToken, uint8 v, bytes32 r, bytes32 s) = + stow.getNestedOperation(); + bytes memory result = QuarkWallet(payable(address(this))).executeQuarkOperationWithSubmissionToken( + op, submissionToken, v, r, s + ); + (uint256 y) = abi.decode(result, (uint256)); + return y + 10; + } else { + return getActiveReplayCount() + 50; + } + } +} diff --git a/test/lib/Permit2Helper.sol b/test/lib/Permit2Helper.sol index ac3f88d2..9fb952de 100644 --- a/test/lib/Permit2Helper.sol +++ b/test/lib/Permit2Helper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; interface Permit2 { function permit(address owner, Permit2Helper.PermitSingle memory permitSingle, bytes calldata signature) external; diff --git a/test/lib/PrecompileCaller.sol b/test/lib/PrecompileCaller.sol index d386ef37..7b755c1c 100644 --- a/test/lib/PrecompileCaller.sol +++ b/test/lib/PrecompileCaller.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; contract PrecompileCaller { // 0x01 diff --git a/test/lib/QuarkOperationHelper.sol b/test/lib/QuarkOperationHelper.sol index 6bc92b21..38babb2b 100644 --- a/test/lib/QuarkOperationHelper.sol +++ b/test/lib/QuarkOperationHelper.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "quark-core/src/QuarkWallet.sol"; +import {YulHelper} from "test/lib/YulHelper.sol"; enum ScriptType { ScriptAddress, @@ -37,6 +38,29 @@ contract QuarkOperationHelper is Test { bytes memory scriptCalldata, bytes[] memory ensureScripts, ScriptType scriptType + ) public returns (QuarkWallet.QuarkOperation memory) { + return newBasicOpWithCalldata( + wallet, scriptSource, scriptCalldata, ensureScripts, scriptType, semiRandomNonce(wallet) + ); + } + + function newBasicOpWithCalldata( + QuarkWallet wallet, + bytes memory scriptSource, + bytes memory scriptCalldata, + ScriptType scriptType, + bytes32 nonce + ) public returns (QuarkWallet.QuarkOperation memory) { + return newBasicOpWithCalldata(wallet, scriptSource, scriptCalldata, new bytes[](0), scriptType, nonce); + } + + function newBasicOpWithCalldata( + QuarkWallet wallet, + bytes memory scriptSource, + bytes memory scriptCalldata, + bytes[] memory ensureScripts, + ScriptType scriptType, + bytes32 nonce ) public returns (QuarkWallet.QuarkOperation memory) { address scriptAddress = wallet.codeJar().saveCode(scriptSource); if (scriptType == ScriptType.ScriptAddress) { @@ -44,7 +68,8 @@ contract QuarkOperationHelper is Test { scriptAddress: scriptAddress, scriptSources: ensureScripts, scriptCalldata: scriptCalldata, - nonce: semiRandomNonce(wallet), + nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); } else { @@ -52,12 +77,105 @@ contract QuarkOperationHelper is Test { scriptAddress: scriptAddress, scriptSources: ensureScripts, scriptCalldata: scriptCalldata, - nonce: semiRandomNonce(wallet), + nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); } } + function newReplayableOpWithCalldata( + QuarkWallet wallet, + bytes memory scriptSource, + bytes memory scriptCalldata, + ScriptType scriptType, + uint256 replays + ) public returns (QuarkWallet.QuarkOperation memory, bytes32[] memory submissionTokens) { + return newReplayableOpWithCalldata(wallet, scriptSource, scriptCalldata, new bytes[](0), scriptType, replays); + } + + function newReplayableOpWithCalldata( + QuarkWallet wallet, + bytes memory scriptSource, + bytes memory scriptCalldata, + ScriptType scriptType, + uint256 replays, + bytes32 nonce + ) public returns (QuarkWallet.QuarkOperation memory, bytes32[] memory submissionTokens) { + return newReplayableOpWithCalldata( + wallet, scriptSource, scriptCalldata, new bytes[](0), scriptType, replays, nonce + ); + } + + function newReplayableOpWithCalldata( + QuarkWallet wallet, + bytes memory scriptSource, + bytes memory scriptCalldata, + bytes[] memory ensureScripts, + ScriptType scriptType, + uint256 replays + ) public returns (QuarkWallet.QuarkOperation memory, bytes32[] memory submissionTokens) { + return newReplayableOpWithCalldata( + wallet, scriptSource, scriptCalldata, ensureScripts, scriptType, replays, semiRandomNonce(wallet) + ); + } + + function newReplayableOpWithCalldata( + QuarkWallet wallet, + bytes memory scriptSource, + bytes memory scriptCalldata, + bytes[] memory ensureScripts, + ScriptType scriptType, + uint256 replays, + bytes32 nonce + ) public returns (QuarkWallet.QuarkOperation memory, bytes32[] memory submissionTokens) { + QuarkWallet.QuarkOperation memory operation = + newBasicOpWithCalldata(wallet, scriptSource, scriptCalldata, ensureScripts, scriptType, nonce); + submissionTokens = new bytes32[](replays + 1); + submissionTokens[replays] = nonce; + for (uint256 i = 0; i < replays; i++) { + nonce = keccak256(abi.encodePacked(nonce)); + submissionTokens[replays - i - 1] = nonce; + } + operation.nonce = nonce; + operation.isReplayable = true; + return (operation, submissionTokens); + } + + function cancelReplayableByNop(QuarkWallet wallet, QuarkWallet.QuarkOperation memory quarkOperation) + public + returns (QuarkWallet.QuarkOperation memory) + { + return getCancelOperation(wallet, quarkOperation.nonce, abi.encodeWithSignature("nop()")); + } + + function cancelReplayableByNewOp(QuarkWallet wallet, QuarkWallet.QuarkOperation memory quarkOperation) + public + returns (QuarkWallet.QuarkOperation memory) + { + return getCancelOperation( + wallet, semiRandomNonce(wallet), abi.encodeWithSignature("cancel(bytes32)", quarkOperation.nonce) + ); + } + + function getCancelOperation(QuarkWallet wallet, bytes32 selfNonce, bytes memory callData) + public + returns (QuarkWallet.QuarkOperation memory) + { + bytes memory cancelScript = new YulHelper().getCode("Cancel.sol/Cancel.json"); + address scriptAddress = wallet.codeJar().saveCode(cancelScript); + bytes[] memory scriptSources = new bytes[](1); + scriptSources[0] = cancelScript; + return QuarkWallet.QuarkOperation({ + scriptAddress: scriptAddress, + scriptSources: scriptSources, + scriptCalldata: callData, + nonce: selfNonce, + isReplayable: false, + expiry: block.timestamp + 1000 + }); + } + /// @dev Note: not sufficiently random for non-test case usage. function semiRandomNonce(QuarkWallet wallet) public view returns (bytes32) { if (address(wallet).code.length == 0) { @@ -71,7 +189,7 @@ contract QuarkOperationHelper is Test { function semiRandomNonce(QuarkNonceManager quarkNonceManager, QuarkWallet wallet) public view returns (bytes32) { bytes32 nonce = bytes32(uint256(keccak256(abi.encodePacked(block.timestamp))) - 1); while (true) { - if (quarkNonceManager.getNonceSubmission(address(wallet), nonce) == bytes32(uint256(0))) { + if (quarkNonceManager.submissions(address(wallet), nonce) == bytes32(uint256(0))) { return nonce; } diff --git a/test/lib/RecurringPurchase.sol b/test/lib/RecurringPurchase.sol index e9564209..591cdde5 100644 --- a/test/lib/RecurringPurchase.sol +++ b/test/lib/RecurringPurchase.sol @@ -1,121 +1,118 @@ -// TODO: Uncomment -// // SPDX-License-Identifier: BSD-3-Clause -// pragma solidity 0.8.23; - -// import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; -// import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; - -// import {ISwapRouter} from "v3-periphery/interfaces/ISwapRouter.sol"; - -// import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; -// import {QuarkScript} from "quark-core/src/QuarkScript.sol"; - -// contract RecurringPurchase is QuarkScript { -// using SafeERC20 for IERC20; - -// error PurchaseConditionNotMet(); - -// /** -// * @dev Note: This script uses the following storage layout: -// * mapping(bytes32 hashedPurchaseConfig => PurchaseState purchaseState) -// * where hashedPurchaseConfig = keccak256(PurchaseConfig) -// */ - -// // TODO: Support exact input swaps -// struct SwapParamsExactOut { -// address uniswapRouter; -// address recipient; -// address tokenFrom; -// uint256 amount; -// // Maximum amount of input token to spend (revert if input amount is greater than this) -// uint256 amountInMaximum; -// uint256 deadline; -// // Path of the swap -// bytes path; -// } - -// // TODO: Consider adding a purchaseWindow -// struct PurchaseConfig { -// uint40 interval; -// uint216 totalAmountToPurchase; -// SwapParamsExactOut swapParams; -// } - -// struct PurchaseState { -// uint216 totalPurchased; -// uint40 nextPurchaseTime; -// } - -// function purchase(PurchaseConfig calldata config) public { -// allowReplay(); - -// bytes32 hashedConfig = hashConfig(config); -// PurchaseState memory purchaseState; -// if (read(hashedConfig) == 0) { -// purchaseState = PurchaseState({totalPurchased: 0, nextPurchaseTime: uint40(block.timestamp)}); -// } else { -// bytes memory prevState = abi.encode(read(hashedConfig)); -// uint216 totalPurchased; -// uint40 nextPurchaseTime; -// // We need assembly to decode packed structs -// assembly { -// totalPurchased := mload(add(prevState, 27)) -// nextPurchaseTime := mload(add(prevState, 32)) -// } -// purchaseState = PurchaseState({totalPurchased: totalPurchased, nextPurchaseTime: nextPurchaseTime}); -// } - -// // Check conditions -// if (block.timestamp < purchaseState.nextPurchaseTime) { -// revert PurchaseConditionNotMet(); -// } -// if (purchaseState.totalPurchased + config.swapParams.amount > config.totalAmountToPurchase) { -// revert PurchaseConditionNotMet(); -// } - -// SwapParamsExactOut memory swapParams = config.swapParams; -// IERC20(swapParams.tokenFrom).forceApprove(swapParams.uniswapRouter, swapParams.amountInMaximum); -// ISwapRouter(swapParams.uniswapRouter).exactOutput( -// ISwapRouter.ExactOutputParams({ -// path: swapParams.path, -// recipient: swapParams.recipient, -// deadline: swapParams.deadline, -// amountOut: swapParams.amount, -// amountInMaximum: swapParams.amountInMaximum -// }) -// ); - -// PurchaseState memory newPurchaseState = PurchaseState({ -// totalPurchased: purchaseState.totalPurchased + uint216(config.swapParams.amount), -// // TODO: or should it be purchaseState.nextPurchaseTime + config.interval? -// nextPurchaseTime: purchaseState.nextPurchaseTime + config.interval -// }); - -// // Write new PurchaseState to storage -// write( -// hashedConfig, bytes32(abi.encodePacked(newPurchaseState.totalPurchased, newPurchaseState.nextPurchaseTime)) -// ); -// } - -// function cancel() external { -// // Not explicitly clearing the nonce just cancels the replayable txn -// } - -// function hashConfig(PurchaseConfig calldata config) internal pure returns (bytes32) { -// return keccak256( -// abi.encodePacked( -// config.interval, -// config.totalAmountToPurchase, -// abi.encodePacked( -// config.swapParams.uniswapRouter, -// config.swapParams.recipient, -// config.swapParams.tokenFrom, -// config.swapParams.amount, -// config.swapParams.amountInMaximum, -// config.swapParams.deadline, -// config.swapParams.path -// ) -// ) -// ); -// } -// } +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.27; + +import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; +import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; + +import {ISwapRouter} from "v3-periphery/interfaces/ISwapRouter.sol"; + +import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; +import {QuarkScript} from "quark-core/src/QuarkScript.sol"; + +contract RecurringPurchase is QuarkScript { + using SafeERC20 for IERC20; + + error PurchaseConditionNotMet(); + + /** + * @dev Note: This script uses the following storage layout: + * mapping(bytes32 hashedPurchaseConfig => PurchaseState purchaseState) + * where hashedPurchaseConfig = keccak256(PurchaseConfig) + */ + + // TODO: Support exact input swaps + struct SwapParamsExactOut { + address uniswapRouter; + address recipient; + address tokenFrom; + uint256 amount; + // Maximum amount of input token to spend (revert if input amount is greater than this) + uint256 amountInMaximum; + uint256 deadline; + // Path of the swap + bytes path; + } + + // TODO: Consider adding a purchaseWindow + struct PurchaseConfig { + uint40 interval; + uint216 totalAmountToPurchase; + SwapParamsExactOut swapParams; + } + + struct PurchaseState { + uint216 totalPurchased; + uint40 nextPurchaseTime; + } + + function purchase(PurchaseConfig calldata config) public { + bytes32 hashedConfig = hashConfig(config); + PurchaseState memory purchaseState; + if (read(hashedConfig) == 0) { + purchaseState = PurchaseState({totalPurchased: 0, nextPurchaseTime: uint40(block.timestamp)}); + } else { + bytes memory prevState = abi.encode(read(hashedConfig)); + uint216 totalPurchased; + uint40 nextPurchaseTime; + // We need assembly to decode packed structs + assembly { + totalPurchased := mload(add(prevState, 27)) + nextPurchaseTime := mload(add(prevState, 32)) + } + purchaseState = PurchaseState({totalPurchased: totalPurchased, nextPurchaseTime: nextPurchaseTime}); + } + + // Check conditions + if (block.timestamp < purchaseState.nextPurchaseTime) { + revert PurchaseConditionNotMet(); + } + if (purchaseState.totalPurchased + config.swapParams.amount > config.totalAmountToPurchase) { + revert PurchaseConditionNotMet(); + } + + SwapParamsExactOut memory swapParams = config.swapParams; + IERC20(swapParams.tokenFrom).forceApprove(swapParams.uniswapRouter, swapParams.amountInMaximum); + ISwapRouter(swapParams.uniswapRouter).exactOutput( + ISwapRouter.ExactOutputParams({ + path: swapParams.path, + recipient: swapParams.recipient, + deadline: swapParams.deadline, + amountOut: swapParams.amount, + amountInMaximum: swapParams.amountInMaximum + }) + ); + + PurchaseState memory newPurchaseState = PurchaseState({ + totalPurchased: purchaseState.totalPurchased + uint216(config.swapParams.amount), + // TODO: or should it be purchaseState.nextPurchaseTime + config.interval? + nextPurchaseTime: purchaseState.nextPurchaseTime + config.interval + }); + + // Write new PurchaseState to storage + write( + hashedConfig, bytes32(abi.encodePacked(newPurchaseState.totalPurchased, newPurchaseState.nextPurchaseTime)) + ); + } + + function nop() external { + // used to no-op and cancel script + } + + function hashConfig(PurchaseConfig calldata config) internal pure returns (bytes32) { + return keccak256( + abi.encodePacked( + config.interval, + config.totalAmountToPurchase, + abi.encodePacked( + config.swapParams.uniswapRouter, + config.swapParams.recipient, + config.swapParams.tokenFrom, + config.swapParams.amount, + config.swapParams.amountInMaximum, + config.swapParams.deadline, + config.swapParams.path + ) + ) + ); + } +} diff --git a/test/lib/Redeployer.sol b/test/lib/Redeployer.sol index 06ef26cc..fde64e80 100644 --- a/test/lib/Redeployer.sol +++ b/test/lib/Redeployer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; interface CodeJar { function saveCode(bytes memory code) external returns (address); diff --git a/test/lib/ReentrantTransfer.sol b/test/lib/ReentrantTransfer.sol index 011abaf2..af5908fa 100644 --- a/test/lib/ReentrantTransfer.sol +++ b/test/lib/ReentrantTransfer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; diff --git a/test/lib/Reverts.sol b/test/lib/Reverts.sol index 0cebf8c6..2b097fd1 100644 --- a/test/lib/Reverts.sol +++ b/test/lib/Reverts.sol @@ -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"; diff --git a/test/lib/SignatureHelper.sol b/test/lib/SignatureHelper.sol index 7f855f64..070e426e 100644 --- a/test/lib/SignatureHelper.sol +++ b/test/lib/SignatureHelper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "quark-core/src/QuarkWallet.sol"; @@ -55,6 +55,7 @@ contract SignatureHelper is Test { abi.encode( QuarkWalletMetadata.QUARK_OPERATION_TYPEHASH, op.nonce, + op.isReplayable, op.scriptAddress, keccak256(encodedArray), keccak256(op.scriptCalldata), diff --git a/test/lib/TickCounter.sol b/test/lib/TickCounter.sol index 2c307690..a86b0a87 100644 --- a/test/lib/TickCounter.sol +++ b/test/lib/TickCounter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; contract TickCounter { uint256 public immutable base; diff --git a/test/lib/Transfer.sol b/test/lib/Transfer.sol index 157ad062..f3146c70 100644 --- a/test/lib/Transfer.sol +++ b/test/lib/Transfer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; diff --git a/test/lib/VictimERC777.sol b/test/lib/VictimERC777.sol index c2622266..a3cb131c 100644 --- a/test/lib/VictimERC777.sol +++ b/test/lib/VictimERC777.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "openzeppelin/token/ERC777/ERC777.sol"; import "openzeppelin/token/ERC20/ERC20.sol"; diff --git a/test/lib/Wacky.sol b/test/lib/Wacky.sol index 86423272..87c89a57 100644 --- a/test/lib/Wacky.sol +++ b/test/lib/Wacky.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; contract WackyBeacon { bytes public code; diff --git a/test/lib/YulHelper.sol b/test/lib/YulHelper.sol index 6c912f53..39626a34 100644 --- a/test/lib/YulHelper.sol +++ b/test/lib/YulHelper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; diff --git a/test/quark-core-scripts/ConditionalMulticall.t.sol b/test/quark-core-scripts/ConditionalMulticall.t.sol index e9087936..59aa206c 100644 --- a/test/quark-core-scripts/ConditionalMulticall.t.sol +++ b/test/quark-core-scripts/ConditionalMulticall.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; diff --git a/test/quark-core-scripts/Ethcall.t.sol b/test/quark-core-scripts/Ethcall.t.sol index 66e15d48..53fa6d01 100644 --- a/test/quark-core-scripts/Ethcall.t.sol +++ b/test/quark-core-scripts/Ethcall.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; diff --git a/test/quark-core-scripts/Multicall.t.sol b/test/quark-core-scripts/Multicall.t.sol index ac4e0b17..d4e57de7 100644 --- a/test/quark-core-scripts/Multicall.t.sol +++ b/test/quark-core-scripts/Multicall.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; @@ -540,7 +540,7 @@ contract MulticallTest is Test { path: abi.encodePacked(USDC, uint24(500), WETH) // Path: USDC - 0.05% -> WETH }) ) - ), + ), new bytes[](0) ) ), diff --git a/test/quark-core-scripts/Paycall.t.sol b/test/quark-core-scripts/Paycall.t.sol index c15a4f0b..2276c4fc 100644 --- a/test/quark-core-scripts/Paycall.t.sol +++ b/test/quark-core-scripts/Paycall.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; diff --git a/test/quark-core-scripts/Quotecall.t.sol b/test/quark-core-scripts/Quotecall.t.sol index 4f80fb4c..4f87e0f4 100644 --- a/test/quark-core-scripts/Quotecall.t.sol +++ b/test/quark-core-scripts/Quotecall.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; diff --git a/test/quark-core-scripts/UniswapFlashLoan.t.sol b/test/quark-core-scripts/UniswapFlashLoan.t.sol index ffb55c30..11de89c4 100644 --- a/test/quark-core-scripts/UniswapFlashLoan.t.sol +++ b/test/quark-core-scripts/UniswapFlashLoan.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; @@ -236,7 +236,7 @@ contract UniswapFlashLoanTest is Test { callContract: ethcallAddress, callData: abi.encodeWithSelector( Ethcall.run.selector, USDC, abi.encodeCall(IERC20.transfer, (address(1), 1000e6)), 0 - ) + ) }); QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( @@ -271,7 +271,7 @@ contract UniswapFlashLoanTest is Test { callContract: ethcallAddress, callData: abi.encodeWithSelector( Ethcall.run.selector, USDC, abi.encodeCall(IERC20.approve, (comet, 1000e6)), 0 - ) + ) }) ), ScriptType.ScriptAddress @@ -311,24 +311,4 @@ contract UniswapFlashLoanTest is Test { // Lose 1 USDC to flash loan fee assertEq(IERC20(USDC).balanceOf(address(wallet)), 9998e6); } - - function testRevertsIfCalledDirectly() public { - // gas: do not meter set-up - vm.pauseGasMetering(); - UniswapFlashLoan.UniswapFlashLoanPayload memory payload = UniswapFlashLoan.UniswapFlashLoanPayload({ - token0: USDC, - token1: DAI, - fee: 100, - amount0: 0, - amount1: 0, - callContract: address(0), - callData: bytes("") - }); - - // gas: meter execute - vm.resumeGasMetering(); - // Reverts when calling `allowCallback()`, which tries to get the `nonceManager` from self - vm.expectRevert(); - UniswapFlashLoan(uniswapFlashLoanAddress).run(payload); - } } diff --git a/test/quark-core-scripts/UniswapFlashSwapExactOut.t.sol b/test/quark-core-scripts/UniswapFlashSwapExactOut.t.sol index 088a8cdd..f19d7a93 100644 --- a/test/quark-core-scripts/UniswapFlashSwapExactOut.t.sol +++ b/test/quark-core-scripts/UniswapFlashSwapExactOut.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; diff --git a/test/quark-core-scripts/interfaces/IComet.sol b/test/quark-core-scripts/interfaces/IComet.sol index babfdc9c..545a4b1c 100644 --- a/test/quark-core-scripts/interfaces/IComet.sol +++ b/test/quark-core-scripts/interfaces/IComet.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; interface IComet { function getAssetInfo(uint8 i) external view returns (AssetInfo memory); diff --git a/test/quark-core-scripts/interfaces/ISwapRouter.sol b/test/quark-core-scripts/interfaces/ISwapRouter.sol index 101ccd70..5b569bcb 100644 --- a/test/quark-core-scripts/interfaces/ISwapRouter.sol +++ b/test/quark-core-scripts/interfaces/ISwapRouter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; // Router interfaces thats only used in test for swapping interface ISwapRouter { diff --git a/test/quark-core/Callbacks.t.sol b/test/quark-core/Callbacks.t.sol index d71e71d3..2cbef133 100644 --- a/test/quark-core/Callbacks.t.sol +++ b/test/quark-core/Callbacks.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/console.sol"; @@ -173,34 +173,60 @@ contract CallbacksTest is Test { assertEq(counter.number(), 2); } - // TODO: Uncomment when replay tokens are supported - // function testClearCallback() public { - // // gas: do not meter set-up - // vm.pauseGasMetering(); - // bytes32 callbackKey = aliceWallet.CALLBACK_KEY(); - // bytes memory allowCallbacks = new YulHelper().getCode("AllowCallbacks.sol/AllowCallbacks.json"); - - // QuarkWallet.QuarkOperation memory op1 = new QuarkOperationHelper().newBasicOpWithCalldata( - // aliceWallet, allowCallbacks, abi.encodeWithSignature("allowCallbackAndReplay()"), ScriptType.ScriptSource - // ); - // (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); - // QuarkWallet.QuarkOperation memory op2 = new QuarkOperationHelper().newBasicOpWithCalldata( - // aliceWallet, allowCallbacks, abi.encodeWithSignature("clear()"), ScriptType.ScriptSource - // ); - // op2.nonce = op1.nonce; - // (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op2); - - // assertEq(nonceManager.walletStorage(address(aliceWallet), op1.nonce, callbackKey), bytes32(0)); - - // // gas: meter execute - // vm.resumeGasMetering(); - // aliceWallet.executeQuarkOperation(op1, v1, r1, s1); - - // assertNotEq(nonceManager.walletStorage(address(aliceWallet), op1.nonce, callbackKey), bytes32(0)); - - // aliceWallet.executeQuarkOperation(op2, v2, r2, s2); - // assertEq(nonceManager.walletStorage(address(aliceWallet), op1.nonce, callbackKey), bytes32(0)); - // } + function testSimpleCallback() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory allowCallbacks = new YulHelper().getCode("AllowCallbacks.sol/AllowCallbacks.json"); + + (QuarkWallet.QuarkOperation memory op1, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, allowCallbacks, abi.encodeWithSignature("run()"), ScriptType.ScriptSource, 1 + ); + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op1, v1, r1, s1); + uint256 res = abi.decode(result, (uint256)); + assertEq(res, 202); + + // Can run again + result = aliceWallet.executeQuarkOperationWithSubmissionToken(op1, submissionTokens[1], v1, r1, s1); + res = abi.decode(result, (uint256)); + assertEq(res, 204); + } + + function testWithoutAllowCallback() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory allowCallbacks = new YulHelper().getCode("AllowCallbacks.sol/AllowCallbacks.json"); + + (QuarkWallet.QuarkOperation memory op1,) = new QuarkOperationHelper().newReplayableOpWithCalldata( + aliceWallet, allowCallbacks, abi.encodeWithSignature("runWithoutAllow()"), ScriptType.ScriptSource, 1 + ); + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); + + // gas: meter execute + vm.resumeGasMetering(); + vm.expectRevert(abi.encodeWithSelector(QuarkWallet.NoActiveCallback.selector)); + aliceWallet.executeQuarkOperation(op1, v1, r1, s1); + } + + function testWithClearedCallback() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory allowCallbacks = new YulHelper().getCode("AllowCallbacks.sol/AllowCallbacks.json"); + + (QuarkWallet.QuarkOperation memory op1,) = new QuarkOperationHelper().newReplayableOpWithCalldata( + aliceWallet, allowCallbacks, abi.encodeWithSignature("runAllowThenClear()"), ScriptType.ScriptSource, 1 + ); + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); + + // gas: meter execute + vm.resumeGasMetering(); + vm.expectRevert(abi.encodeWithSelector(QuarkWallet.NoActiveCallback.selector)); + aliceWallet.executeQuarkOperation(op1, v1, r1, s1); + } function testRevertsOnCallbackWhenNoActiveCallback() public { // gas: do not meter set-up diff --git a/test/quark-core/EIP1271.t.sol b/test/quark-core/EIP1271.t.sol index 5d463777..2f3cbe79 100644 --- a/test/quark-core/EIP1271.t.sol +++ b/test/quark-core/EIP1271.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/StdUtils.sol"; diff --git a/test/quark-core/EIP712.t.sol b/test/quark-core/EIP712.t.sol index 160c4fb6..25ace1d3 100644 --- a/test/quark-core/EIP712.t.sol +++ b/test/quark-core/EIP712.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/StdUtils.sol"; @@ -73,7 +73,7 @@ contract EIP712Test is Test { assertEq(counter.number(), 3); // nonce is spent - assertEq(nonceManager.getNonceSubmission(address(wallet), op.nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(type(uint256).max)); } function testRevertsForBadCode() public { @@ -99,7 +99,7 @@ contract EIP712Test is Test { assertEq(counter.number(), 0); // nonce is not spent - assertEq(nonceManager.getNonceSubmission(address(wallet), op.nonce), bytes32(uint256(0))); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(uint256(0))); } function testStructHash() public { @@ -122,6 +122,7 @@ contract EIP712Test is Test { QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ nonce: nextNonce, + isReplayable: true, scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: scriptCalldata, @@ -138,6 +139,7 @@ contract EIP712Test is Test { }, { QuarkOperation: [ { name: 'nonce', type: 'bytes32' }, + { name: 'isReplayable', type: 'bool' }, { name: 'scriptAddress', type: 'address' }, { name: 'scriptSources', type: 'bytes[]' }, { name: 'scriptCalldata', type: 'bytes' }, @@ -145,6 +147,7 @@ contract EIP712Test is Test { ]}, { nonce: '0x0000000000000000000000000000000000000000000000000000000000000000', + isReplayable: true, scriptAddress: '0x5cB7957c702bB6BB8F22aCcf66657F0defd4550b', scriptSources: ['0x608060405234801561001057600080fd5b506102a7806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80636b582b7614610056578063e5910ae714610069575b73f62849f9a0b5bf2913b396098f7c7019b51a820a61005481610077565b005b610054610064366004610230565b610173565b610054610077366004610230565b806001600160a01b031663d09de08a6040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156100b257600080fd5b505af11580156100c6573d6000803e3d6000fd5b50505050806001600160a01b031663d09de08a6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561010557600080fd5b505af1158015610119573d6000803e3d6000fd5b50505050806001600160a01b031663d09de08a6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561015857600080fd5b505af115801561016c573d6000803e3d6000fd5b5050505050565b61017c81610077565b306001600160a01b0316632e716fb16040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101ba573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101de9190610254565b6001600160a01b0316631913592a6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561015857600080fd5b6001600160a01b038116811461022d57600080fd5b50565b60006020828403121561024257600080fd5b813561024d81610218565b9392505050565b60006020828403121561026657600080fd5b815161024d8161021856fea26469706673582212200d71f9cd831b3c67d6f6131f807ee7fc47d21f07fe8f7b90a01dab56abb8403464736f6c63430008170033'], scriptCalldata: '0xe5910ae7000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a', @@ -152,14 +155,16 @@ contract EIP712Test is Test { } ) - 0x1901ce5fced5138ae147492ff6ba56247e9d6f30bbbe45ae60eb0a0135d528a94be437302412583af420731c67963b8628682b151f38070c3c9142fc40054158666e + 0x1901 + ce5fced5138ae147492ff6ba56247e9d6f30bbbe45ae60eb0a0135d528a94be4 + 115a39f16a8c9e3e390e94dc858a17eba53b5358382af38b02f1ac31c2b5f9b0 */ bytes32 domainHash = new SignatureHelper().domainSeparator(wallet_); assertEq(domainHash, hex"ce5fced5138ae147492ff6ba56247e9d6f30bbbe45ae60eb0a0135d528a94be4"); bytes32 structHash = new SignatureHelper().opStructHash(op); - assertEq(structHash, hex"37302412583af420731c67963b8628682b151f38070c3c9142fc40054158666e"); + assertEq(structHash, hex"115a39f16a8c9e3e390e94dc858a17eba53b5358382af38b02f1ac31c2b5f9b0"); } function testRevertsForBadCalldata() public { @@ -182,7 +187,7 @@ contract EIP712Test is Test { assertEq(counter.number(), 0); // nonce is not spent - assertEq(nonceManager.getNonceSubmission(address(wallet), op.nonce), bytes32(uint256(0))); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(uint256(0))); } function testRevertsForBadExpiry() public { @@ -205,7 +210,7 @@ contract EIP712Test is Test { assertEq(counter.number(), 0); // alice's nonce is not set - assertEq(nonceManager.getNonceSubmission(address(wallet), op.nonce), bytes32(uint256(0))); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(uint256(0))); } function testRevertsOnReusedNonce() public { @@ -222,12 +227,12 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, s); assertEq(counter.number(), 3); - assertEq(nonceManager.getNonceSubmission(address(wallet), op.nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(type(uint256).max)); // submitter tries to reuse the same signature twice, for a non-replayable operation vm.expectRevert( abi.encodeWithSelector( - QuarkNonceManager.NonReplayableNonce.selector, address(wallet), op.nonce, bytes32(type(uint256).max) + QuarkNonceManager.NonReplayableNonce.selector, address(wallet), op.nonce, op.nonce, true ) ); wallet.executeQuarkOperation(op, v, r, s); @@ -252,7 +257,7 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, s); assertEq(counter.number(), 0); - assertEq(nonceManager.getNonceSubmission(address(wallet), op.nonce), bytes32(uint256(0))); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(uint256(0))); } function testRevertsInvalidS() public { @@ -274,7 +279,7 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, invalidS); assertEq(counter.number(), 0); - assertEq(nonceManager.getNonceSubmission(address(wallet), op.nonce), bytes32(uint256(0))); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(uint256(0))); } // TODO: Uncomment when replay tokens are supported @@ -351,7 +356,7 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, s); assertEq(counter.number(), 0); - assertEq(nonceManager.getNonceSubmission(address(wallet), op.nonce), bytes32(uint256(0))); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(uint256(0))); } function testRequirements() public { diff --git a/test/quark-core/Executor.t.sol b/test/quark-core/Executor.t.sol index 934ca616..0ecf287d 100644 --- a/test/quark-core/Executor.t.sol +++ b/test/quark-core/Executor.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/console.sol"; diff --git a/test/quark-core/Noncer.t.sol b/test/quark-core/Noncer.t.sol new file mode 100644 index 00000000..3fd66fe0 --- /dev/null +++ b/test/quark-core/Noncer.t.sol @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import {YulHelper} from "test/lib/YulHelper.sol"; +import {SignatureHelper} from "test/lib/SignatureHelper.sol"; +import {QuarkOperationHelper, ScriptType} from "test/lib/QuarkOperationHelper.sol"; + +import {CodeJar} from "codejar/src/CodeJar.sol"; + +import {QuarkScript} from "quark-core/src/QuarkScript.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; +import {QuarkWallet, QuarkWalletMetadata} from "quark-core/src/QuarkWallet.sol"; +import {QuarkWalletStandalone} from "quark-core/src/QuarkWalletStandalone.sol"; +import {IHasSignerExecutor} from "quark-core/src/interfaces/IHasSignerExecutor.sol"; + +import {QuarkMinimalProxy} from "quark-proxy/src/QuarkMinimalProxy.sol"; + +import {Stow} from "test/lib/Noncer.sol"; + +contract NoncerTest is Test { + enum ExecutionType { + Signature, + Direct + } + + CodeJar public codeJar; + QuarkNonceManager public nonceManager; + QuarkWallet public walletImplementation; + + uint256 alicePrivateKey = 0x8675309; + address aliceAccount = vm.addr(alicePrivateKey); + QuarkWallet aliceWallet; // see constructor() + + bytes32 constant EXHAUSTED_TOKEN = bytes32(type(uint256).max); + + // wallet proxy instantiation helper + function newWallet(address signer, address executor) internal returns (QuarkWallet) { + return QuarkWallet(payable(new QuarkMinimalProxy(address(walletImplementation), signer, executor))); + } + + constructor() { + codeJar = new CodeJar(); + console.log("CodeJar deployed to: %s", address(codeJar)); + + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); + + walletImplementation = new QuarkWallet(codeJar, nonceManager); + console.log("QuarkWallet implementation: %s", address(walletImplementation)); + + aliceWallet = newWallet(aliceAccount, address(0)); + console.log("Alice signer: %s", aliceAccount); + console.log("Alice wallet at: %s", address(aliceWallet)); + } + + /** + * get active nonce, submission token, replay count *************************** + * + * single + */ + function testGetActiveNonceSingle() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("checkNonce()"), ScriptType.ScriptSource + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + (bytes32 nonceResult) = abi.decode(result, (bytes32)); + assertEq(nonceResult, op.nonce); + } + + function testGetActiveSubmissionTokenSingle() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("checkSubmissionToken()"), ScriptType.ScriptSource + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + (bytes32 submissionTokenResult) = abi.decode(result, (bytes32)); + assertEq(submissionTokenResult, op.nonce); + assertEq(nonceManager.submissions(address(aliceWallet), op.nonce), bytes32(type(uint256).max)); + } + + function testGetActiveReplayCountSingle() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("checkReplayCount()"), ScriptType.ScriptSource + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + (uint256 replayCount) = abi.decode(result, (uint256)); + assertEq(replayCount, 0); + } + + /* + * nested + */ + + function testGetActiveNonceNested() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + QuarkWallet.QuarkOperation memory nestedOp = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("checkNonce()"), ScriptType.ScriptSource + ); + nestedOp.nonce = bytes32(uint256(keccak256(abi.encodePacked(block.timestamp))) - 2); // Don't overlap on nonces + (uint8 nestedV, bytes32 nestedR, bytes32 nestedS) = + new SignatureHelper().signOp(alicePrivateKey, aliceWallet, nestedOp); + + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + noncerScript, + abi.encodeWithSignature( + "nestedNonce((bytes32,bool,address,bytes[],bytes,uint256),uint8,bytes32,bytes32)", + nestedOp, + nestedV, + nestedR, + nestedS + ), + ScriptType.ScriptSource + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + (bytes32 pre, bytes32 post, bytes memory innerResult) = abi.decode(result, (bytes32, bytes32, bytes)); + assertEq(pre, op.nonce); + assertEq(post, bytes32(0)); + bytes32 innerNonce = abi.decode(innerResult, (bytes32)); + assertEq(innerNonce, nestedOp.nonce); + } + + function testGetActiveSubmissionTokenNested() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + QuarkWallet.QuarkOperation memory nestedOp = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("checkSubmissionToken()"), ScriptType.ScriptSource + ); + nestedOp.nonce = bytes32(uint256(keccak256(abi.encodePacked(block.timestamp))) - 2); // Don't overlap on nonces + (uint8 nestedV, bytes32 nestedR, bytes32 nestedS) = + new SignatureHelper().signOp(alicePrivateKey, aliceWallet, nestedOp); + + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + noncerScript, + abi.encodeWithSignature( + "nestedSubmissionToken((bytes32,bool,address,bytes[],bytes,uint256),uint8,bytes32,bytes32)", + nestedOp, + nestedV, + nestedR, + nestedS + ), + ScriptType.ScriptSource + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + (bytes32 pre, bytes32 post, bytes memory innerResult) = abi.decode(result, (bytes32, bytes32, bytes)); + assertEq(pre, op.nonce); + assertEq(post, bytes32(0)); + bytes32 innerNonce = abi.decode(innerResult, (bytes32)); + assertEq(innerNonce, nestedOp.nonce); + } + + // Complicated test for a nested script to call itself recursive, since it's fun to test wonky cases. + function testNestedPlayPullingActiveReplayCount() public { + Stow stow = new Stow(); + + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("nestedPlay(address)", stow), ScriptType.ScriptSource, 1 + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + stow.setNestedOperation(op, submissionTokens[1], v, r, s); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + (uint256 y) = abi.decode(result, (uint256)); + assertEq(y, 61); + } + + function testGetActiveReplayCountNested() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + QuarkWallet.QuarkOperation memory nestedOp = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("checkReplayCount()"), ScriptType.ScriptSource + ); + nestedOp.nonce = bytes32(uint256(keccak256(abi.encodePacked(block.timestamp))) - 2); // Don't overlap on nonces + (uint8 nestedV, bytes32 nestedR, bytes32 nestedS) = + new SignatureHelper().signOp(alicePrivateKey, aliceWallet, nestedOp); + + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + noncerScript, + abi.encodeWithSignature( + "nestedReplayCount((bytes32,bool,address,bytes[],bytes,uint256),uint8,bytes32,bytes32)", + nestedOp, + nestedV, + nestedR, + nestedS + ), + ScriptType.ScriptSource + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + (uint256 pre, uint256 post, bytes memory innerResult) = abi.decode(result, (uint256, uint256, bytes)); + assertEq(pre, 0); + assertEq(post, 0); + uint256 innerNonce = abi.decode(innerResult, (uint256)); + assertEq(innerNonce, 0); + } + + function testPostNestReadFailure() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + QuarkWallet.QuarkOperation memory nestedOp = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("checkNonce()"), ScriptType.ScriptSource + ); + nestedOp.nonce = bytes32(uint256(keccak256(abi.encodePacked(block.timestamp))) - 2); // Don't overlap on nonces + (uint8 nestedV, bytes32 nestedR, bytes32 nestedS) = + new SignatureHelper().signOp(alicePrivateKey, aliceWallet, nestedOp); + + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + noncerScript, + abi.encodeWithSignature( + "postNestRead((bytes32,bool,address,bytes[],bytes,uint256),uint8,bytes32,bytes32)", + nestedOp, + nestedV, + nestedR, + nestedS + ), + ScriptType.ScriptSource + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + vm.expectRevert(abi.encodeWithSelector(QuarkScript.NoActiveNonce.selector)); + aliceWallet.executeQuarkOperation(op, v, r, s); + } + + /* + * replayable + */ + + function testGetActiveNonceReplayable() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("checkNonce()"), ScriptType.ScriptSource, 1 + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + (bytes32 nonceResult) = abi.decode(result, (bytes32)); + assertEq(nonceResult, op.nonce); + + result = aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v, r, s); + + (nonceResult) = abi.decode(result, (bytes32)); + assertEq(nonceResult, op.nonce); + } + + function testGetActiveSubmissionTokenReplayable() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("checkSubmissionToken()"), ScriptType.ScriptSource, 1 + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + (bytes32 submissionTokenResult) = abi.decode(result, (bytes32)); + assertEq(submissionTokenResult, submissionTokens[0]); + + result = aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v, r, s); + + (submissionTokenResult) = abi.decode(result, (bytes32)); + assertEq(submissionTokenResult, submissionTokens[1]); + } + + function testGetActiveReplayCount() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("checkReplayCount()"), ScriptType.ScriptSource, 2 + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + (uint256 replayCount) = abi.decode(result, (uint256)); + assertEq(replayCount, 0); + + result = aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v, r, s); + + (replayCount) = abi.decode(result, (uint256)); + assertEq(replayCount, 1); + + result = aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[2], v, r, s); + + (replayCount) = abi.decode(result, (uint256)); + assertEq(replayCount, 2); + } + + function testGetActiveReplayCountWithNonReplaySoftCancel() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + bytes memory checkNonceScript = new YulHelper().getCode("CheckNonceScript.sol/CheckNonceScript.json"); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, noncerScript, abi.encodeWithSignature("checkReplayCount()"), ScriptType.ScriptSource, 2 + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + QuarkWallet.QuarkOperation memory checkReplayCountOp = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + checkNonceScript, + abi.encodeWithSignature("checkReplayCount()"), + ScriptType.ScriptSource, + op.nonce + ); + (uint8 checkReplayCountOpV, bytes32 checkReplayCountOpR, bytes32 checkReplayCountOpS) = + new SignatureHelper().signOp(alicePrivateKey, aliceWallet, checkReplayCountOp); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + (uint256 replayCount) = abi.decode(result, (uint256)); + assertEq(replayCount, 0); + + result = aliceWallet.executeQuarkOperationWithSubmissionToken( + checkReplayCountOp, submissionTokens[1], checkReplayCountOpV, checkReplayCountOpR, checkReplayCountOpS + ); + + (replayCount) = abi.decode(result, (uint256)); + assertEq(replayCount, 1); + + assertEq(nonceManager.submissions(address(aliceWallet), op.nonce), bytes32(type(uint256).max)); + } +} diff --git a/test/quark-core/QuarkNonceManager.t.sol b/test/quark-core/QuarkNonceManager.t.sol index 6b7ed7d0..a6af5c8a 100644 --- a/test/quark-core/QuarkNonceManager.t.sol +++ b/test/quark-core/QuarkNonceManager.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; @@ -16,7 +16,6 @@ import {QuarkOperationHelper, ScriptType} from "test/lib/QuarkOperationHelper.so import {Logger} from "test/lib/Logger.sol"; import {Counter} from "test/lib/Counter.sol"; -// import {MaxCounterScript} from "test/lib/MaxCounterScript.sol"; contract QuarkNonceManagerTest is Test { CodeJar public codeJar; @@ -39,54 +38,133 @@ contract QuarkNonceManagerTest is Test { console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); } - function testNonceZeroIsValid() public { - bytes32 nonce = bytes32(uint256(0)); - bytes32 EXHAUSTED = nonceManager.EXHAUSTED(); + function testNonceOneIsValid() public { + bytes32 nonce = bytes32(uint256(1)); - // by default, nonce 0 is not set - assertEq(nonceManager.getNonceSubmission(address(0x123), nonce), nonceManager.FREE()); + // by default, nonce 1 is not set + assertEq(nonceManager.submissions(address(0x123), nonce), nonceManager.FREE()); - // nonce 0 can be set manually + // nonce 1 can be set manually vm.prank(address(0x123)); - nonceManager.submitNonceToken(nonce, EXHAUSTED); - assertEq(nonceManager.getNonceSubmission(address(0x123), nonce), nonceManager.EXHAUSTED()); + nonceManager.submit(nonce, false, nonce); + assertEq(nonceManager.submissions(address(0x123), nonce), nonceManager.EXHAUSTED()); + } + + function testInvalidNonces() public { + vm.expectRevert(abi.encodeWithSelector(QuarkNonceManager.InvalidNonce.selector, address(this), bytes32(0))); + nonceManager.submit(bytes32(0), false, bytes32(0)); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidNonce.selector, address(this), bytes32(type(uint256).max)) + ); + nonceManager.submit(bytes32(type(uint256).max), false, bytes32(type(uint256).max)); } function testClaimsSequentialNonces() public { - for (uint256 i = 0; i <= 550; i++) { - nonceManager.submitNonceToken(bytes32(i), EXHAUSTED_TOKEN); + for (uint256 i = 1; i <= 550; i++) { + nonceManager.submit(bytes32(i), false, bytes32(i)); } - for (uint256 i = 0; i <= 20; i++) { + for (uint256 i = 1; i <= 20; i++) { vm.expectRevert( abi.encodeWithSelector( - QuarkNonceManager.NonReplayableNonce.selector, address(this), bytes32(i), bytes32(type(uint256).max) + QuarkNonceManager.NonReplayableNonce.selector, + address(this), + bytes32(i), + bytes32(type(uint256).max), + true ) ); - nonceManager.submitNonceToken(bytes32(i), EXHAUSTED_TOKEN); + nonceManager.submit(bytes32(i), false, EXHAUSTED_TOKEN); } } function testRevertsIfNonceIsAlreadySet() public { bytes32 EXHAUSTED = nonceManager.EXHAUSTED(); - bytes32 nonce = bytes32(uint256(0)); - nonceManager.submitNonceToken(nonce, EXHAUSTED); + bytes32 nonce = bytes32(uint256(1)); + nonceManager.submit(nonce, false, nonce); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, nonce, true) + ); + nonceManager.submit(nonce, false, nonce); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, EXHAUSTED, true) + ); + nonceManager.submit(nonce, false, EXHAUSTED); + } + + function testRevertsIfSubmittingNonMatchingNonceForNonReplayable() public { + bytes32 nonce = bytes32(uint256(99)); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, bytes32(0)) + ); + nonceManager.submit(nonce, false, bytes32(0)); vm.expectRevert( abi.encodeWithSelector( - QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, bytes32(type(uint256).max) + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, bytes32(uint256(1)) ) ); - nonceManager.submitNonceToken(nonce, EXHAUSTED); + nonceManager.submit(nonce, false, bytes32(uint256(1))); + + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, EXHAUSTED_TOKEN + ) + ); + nonceManager.submit(nonce, false, EXHAUSTED_TOKEN); + } + + function testChangingReplayableness() public { + bytes32 nonceSecret = bytes32(uint256(99)); + bytes32 nonce = keccak256(abi.encodePacked(nonceSecret)); + + nonceManager.submit(nonce, true, nonce); + + // Accepts as a cancel + nonceManager.submit(nonce, false, nonceSecret); + + assertEq(nonceManager.submissions(address(this), nonce), EXHAUSTED_TOKEN); + + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, nonceSecret, true + ) + ); + nonceManager.submit(nonce, true, nonceSecret); + } + + function testRevertsDefenseInDepthReplayableSubmissionTokenZero() public { + bytes32 nonce = bytes32(uint256(1)); + + // Cannot set a submission token zero + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, bytes32(0)) + ); + nonceManager.submit(nonce, true, bytes32(0)); + + // Cannot set a submission token to EXHAUSTED_TOKEN + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, EXHAUSTED_TOKEN + ) + ); + nonceManager.submit(nonce, true, EXHAUSTED_TOKEN); + + // Still valid as non-replayable nonce + nonceManager.submit(nonce, false, nonce); } function testIsSet() public { // nonce is unset by default - assertEq(nonceManager.getNonceSubmission(address(this), NONCE_ZERO), FREE_TOKEN); + assertEq(nonceManager.submissions(address(this), NONCE_ONE), FREE_TOKEN); // it can be set - nonceManager.submitNonceToken(NONCE_ZERO, EXHAUSTED_TOKEN); - assertEq(nonceManager.getNonceSubmission(address(this), NONCE_ZERO), EXHAUSTED_TOKEN); + nonceManager.submit(NONCE_ONE, false, NONCE_ONE); + assertEq(nonceManager.submissions(address(this), NONCE_ONE), EXHAUSTED_TOKEN); } function testNonLinearNonce() public { @@ -94,185 +172,270 @@ contract QuarkNonceManagerTest is Test { // long as it has not been set bytes32 nonce = bytes32(uint256(1234567890)); - assertEq(nonceManager.getNonceSubmission(address(this), NONCE_ZERO), FREE_TOKEN); + assertEq(nonceManager.submissions(address(this), NONCE_ONE), FREE_TOKEN); - nonceManager.submitNonceToken(nonce, EXHAUSTED_TOKEN); - assertEq(nonceManager.getNonceSubmission(address(this), nonce), EXHAUSTED_TOKEN); + nonceManager.submit(nonce, false, nonce); + assertEq(nonceManager.submissions(address(this), nonce), EXHAUSTED_TOKEN); vm.expectRevert( - abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, FREE_TOKEN) + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, FREE_TOKEN, true + ) ); - nonceManager.submitNonceToken(nonce, FREE_TOKEN); + nonceManager.submit(nonce, false, FREE_TOKEN); vm.expectRevert( - abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, EXHAUSTED_TOKEN) + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, EXHAUSTED_TOKEN, true + ) ); - nonceManager.submitNonceToken(nonce, EXHAUSTED_TOKEN); + nonceManager.submit(nonce, false, EXHAUSTED_TOKEN); } - function testSingleRandomValidNonce() public { + function testSingleUseRandomValidNonce() public { // nonce values are not incremental; you can use a random number as // long as it has not been set bytes32 nonce = bytes32(uint256(1234567890)); - bytes32 nonceSecret = bytes32(uint256(99)); - bytes32 nonceSecretHash = keccak256(abi.encodePacked(nonceSecret)); + bytes32 nonceHash = keccak256(abi.encodePacked(nonce)); - assertEq(nonceManager.getNonceSubmission(address(this), NONCE_ZERO), FREE_TOKEN); + assertEq(nonceManager.submissions(address(this), NONCE_ONE), FREE_TOKEN); - nonceManager.submitNonceToken(nonce, nonceSecret); - assertEq(nonceManager.getNonceSubmission(address(this), nonce), nonceSecret); + nonceManager.submit(nonce, true, nonce); + assertEq(nonceManager.submissions(address(this), nonce), nonce); vm.expectRevert( - abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonceSecret) + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonce) ); - nonceManager.submitNonceToken(nonce, nonceSecret); + nonceManager.submit(nonce, true, nonce); vm.expectRevert( - abi.encodeWithSelector( - QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonceSecretHash - ) + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonceHash) ); - nonceManager.submitNonceToken(nonce, nonceSecretHash); + nonceManager.submit(nonce, true, nonceHash); vm.expectRevert( abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, FREE_TOKEN) ); - nonceManager.submitNonceToken(nonce, FREE_TOKEN); + nonceManager.submit(nonce, true, FREE_TOKEN); vm.expectRevert( abi.encodeWithSelector( QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, EXHAUSTED_TOKEN ) ); - nonceManager.submitNonceToken(nonce, EXHAUSTED_TOKEN); + nonceManager.submit(nonce, true, EXHAUSTED_TOKEN); } function testNextNonceChain() public { // nonce values are not incremental; you can use a random number as // long as it has not been set - bytes32 nonce = bytes32(uint256(1234567890)); - - assertEq(nonceManager.getNonceSubmission(address(this), NONCE_ZERO), FREE_TOKEN); - bytes32 nonceSecret = bytes32(uint256(99)); - bytes32 replayToken2 = keccak256(abi.encodePacked(nonceSecret)); - bytes32 replayToken1 = keccak256(abi.encodePacked(replayToken2)); - bytes32 rootHash = keccak256(abi.encodePacked(replayToken1)); + bytes32 submissionToken2 = keccak256(abi.encodePacked(nonceSecret)); + bytes32 submissionToken1 = keccak256(abi.encodePacked(submissionToken2)); + bytes32 nonce = keccak256(abi.encodePacked(submissionToken1)); - nonceManager.submitNonceToken(nonce, rootHash); - assertEq(nonceManager.getNonceSubmission(address(this), nonce), rootHash); + assertEq(nonceManager.submissions(address(this), nonce), FREE_TOKEN); + + nonceManager.submit(nonce, true, nonce); + assertEq(nonceManager.submissions(address(this), nonce), nonce); vm.expectRevert( - abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, rootHash) + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonce) ); - nonceManager.submitNonceToken(nonce, rootHash); + nonceManager.submit(nonce, true, nonce); vm.expectRevert( abi.encodeWithSelector( - QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, replayToken2 + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken2 ) ); - nonceManager.submitNonceToken(nonce, replayToken2); + nonceManager.submit(nonce, true, submissionToken2); vm.expectRevert( abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonceSecret) ); - nonceManager.submitNonceToken(nonce, nonceSecret); + nonceManager.submit(nonce, true, nonceSecret); vm.expectRevert( abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, FREE_TOKEN) ); - nonceManager.submitNonceToken(nonce, FREE_TOKEN); + nonceManager.submit(nonce, true, FREE_TOKEN); vm.expectRevert( abi.encodeWithSelector( QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, EXHAUSTED_TOKEN ) ); - nonceManager.submitNonceToken(nonce, EXHAUSTED_TOKEN); + nonceManager.submit(nonce, true, EXHAUSTED_TOKEN); - nonceManager.submitNonceToken(nonce, replayToken1); - assertEq(nonceManager.getNonceSubmission(address(this), nonce), replayToken1); + nonceManager.submit(nonce, true, submissionToken1); + assertEq(nonceManager.submissions(address(this), nonce), submissionToken1); vm.expectRevert( - abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, rootHash) + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonce) ); - nonceManager.submitNonceToken(nonce, rootHash); + nonceManager.submit(nonce, true, nonce); vm.expectRevert( abi.encodeWithSelector( - QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, replayToken1 + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken1 ) ); - nonceManager.submitNonceToken(nonce, replayToken1); + nonceManager.submit(nonce, true, submissionToken1); vm.expectRevert( abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonceSecret) ); - nonceManager.submitNonceToken(nonce, nonceSecret); + nonceManager.submit(nonce, true, nonceSecret); vm.expectRevert( abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, FREE_TOKEN) ); - nonceManager.submitNonceToken(nonce, FREE_TOKEN); + nonceManager.submit(nonce, true, FREE_TOKEN); vm.expectRevert( abi.encodeWithSelector( QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, EXHAUSTED_TOKEN ) ); - nonceManager.submitNonceToken(nonce, EXHAUSTED_TOKEN); + nonceManager.submit(nonce, true, EXHAUSTED_TOKEN); - nonceManager.submitNonceToken(nonce, replayToken2); - assertEq(nonceManager.getNonceSubmission(address(this), nonce), replayToken2); + nonceManager.submit(nonce, true, submissionToken2); + assertEq(nonceManager.submissions(address(this), nonce), submissionToken2); vm.expectRevert( - abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, rootHash) + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonce) ); - nonceManager.submitNonceToken(nonce, rootHash); + nonceManager.submit(nonce, true, nonce); vm.expectRevert( abi.encodeWithSelector( - QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, replayToken1 + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken1 ) ); - nonceManager.submitNonceToken(nonce, replayToken1); + nonceManager.submit(nonce, true, submissionToken1); vm.expectRevert( abi.encodeWithSelector( - QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, replayToken2 + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken2 ) ); - nonceManager.submitNonceToken(nonce, replayToken2); + nonceManager.submit(nonce, true, submissionToken2); vm.expectRevert( abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, FREE_TOKEN) ); - nonceManager.submitNonceToken(nonce, FREE_TOKEN); + nonceManager.submit(nonce, true, FREE_TOKEN); vm.expectRevert( abi.encodeWithSelector( QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, EXHAUSTED_TOKEN ) ); - nonceManager.submitNonceToken(nonce, EXHAUSTED_TOKEN); + nonceManager.submit(nonce, true, EXHAUSTED_TOKEN); - nonceManager.submitNonceToken(nonce, nonceSecret); - assertEq(nonceManager.getNonceSubmission(address(this), nonce), nonceSecret); + nonceManager.submit(nonce, true, nonceSecret); + assertEq(nonceManager.submissions(address(this), nonce), nonceSecret); vm.expectRevert( - abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, rootHash) + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonce) ); - nonceManager.submitNonceToken(nonce, rootHash); + nonceManager.submit(nonce, true, nonce); vm.expectRevert( abi.encodeWithSelector( - QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, replayToken1 + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken1 ) ); - nonceManager.submitNonceToken(nonce, replayToken1); + nonceManager.submit(nonce, true, submissionToken1); vm.expectRevert( abi.encodeWithSelector( - QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, replayToken2 + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken2 ) ); - nonceManager.submitNonceToken(nonce, replayToken2); + nonceManager.submit(nonce, true, submissionToken2); vm.expectRevert( abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonceSecret) ); - nonceManager.submitNonceToken(nonce, nonceSecret); + nonceManager.submit(nonce, true, nonceSecret); vm.expectRevert( abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, FREE_TOKEN) ); - nonceManager.submitNonceToken(nonce, FREE_TOKEN); + nonceManager.submit(nonce, true, FREE_TOKEN); vm.expectRevert( abi.encodeWithSelector( QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, EXHAUSTED_TOKEN ) ); - nonceManager.submitNonceToken(nonce, EXHAUSTED_TOKEN); + nonceManager.submit(nonce, true, EXHAUSTED_TOKEN); + } + + function testCancelChain() public { + bytes32 nonceSecret = bytes32(uint256(99)); + bytes32 submissionToken2 = keccak256(abi.encodePacked(nonceSecret)); + bytes32 submissionToken1 = keccak256(abi.encodePacked(submissionToken2)); + bytes32 nonce = keccak256(abi.encodePacked(submissionToken1)); + + assertEq(nonceManager.submissions(address(this), nonce), FREE_TOKEN); + + nonceManager.submit(nonce, true, nonce); + assertEq(nonceManager.submissions(address(this), nonce), nonce); + + nonceManager.cancel(nonce); + assertEq(nonceManager.submissions(address(this), nonce), EXHAUSTED_TOKEN); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, nonce, true) + ); + nonceManager.submit(nonce, true, nonce); + + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, submissionToken2, true + ) + ); + nonceManager.submit(nonce, true, submissionToken2); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, submissionToken1, true + ) + ); + nonceManager.submit(nonce, true, submissionToken1); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, nonceSecret, true + ) + ); + nonceManager.submit(nonce, true, nonceSecret); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, EXHAUSTED_TOKEN, true + ) + ); + nonceManager.submit(nonce, true, EXHAUSTED_TOKEN); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, FREE_TOKEN, true + ) + ); + nonceManager.submit(nonce, true, FREE_TOKEN); + } + + function testPrecancelNonce() public { + bytes32 nonce = bytes32(uint256(1)); + + vm.prank(address(0x123)); + nonceManager.cancel(nonce); + + // by default, nonce 1 is not set + assertEq(nonceManager.submissions(address(0x123), nonce), nonceManager.EXHAUSTED()); + + // nonce 1 can be set manually + vm.prank(address(0x123)); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(0x123), nonce, nonce, true) + ); + nonceManager.submit(nonce, false, nonce); + assertEq(nonceManager.submissions(address(0x123), nonce), nonceManager.EXHAUSTED()); + } + + function testCancelExhaustedIsNoOp() public { + bytes32 nonce = bytes32(uint256(1)); + + // by default, nonce 1 is not set + assertEq(nonceManager.submissions(address(0x123), nonce), nonceManager.FREE()); + + // nonce 1 can be set manually + vm.prank(address(0x123)); + nonceManager.submit(nonce, false, nonce); + assertEq(nonceManager.submissions(address(0x123), nonce), nonceManager.EXHAUSTED()); + + vm.prank(address(0x123)); + nonceManager.cancel(nonce); + + assertEq(nonceManager.submissions(address(0x123), nonce), nonceManager.EXHAUSTED()); } } diff --git a/test/quark-core/QuarkWallet.t.sol b/test/quark-core/QuarkWallet.t.sol index 341bc541..d5a967dd 100644 --- a/test/quark-core/QuarkWallet.t.sol +++ b/test/quark-core/QuarkWallet.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; @@ -10,8 +10,10 @@ import {QuarkOperationHelper, ScriptType} from "test/lib/QuarkOperationHelper.so import {CodeJar} from "codejar/src/CodeJar.sol"; +import {QuarkScript} from "quark-core/src/QuarkScript.sol"; import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWallet, QuarkWalletMetadata} from "quark-core/src/QuarkWallet.sol"; +import {QuarkWalletStandalone} from "quark-core/src/QuarkWalletStandalone.sol"; import {IHasSignerExecutor} from "quark-core/src/interfaces/IHasSignerExecutor.sol"; import {QuarkMinimalProxy} from "quark-proxy/src/QuarkMinimalProxy.sol"; @@ -24,9 +26,9 @@ import {Reverts} from "test/lib/Reverts.sol"; import {EmptyCode} from "test/lib/EmptyCode.sol"; import {Incrementer} from "test/lib/Incrementer.sol"; import {PrecompileCaller} from "test/lib/PrecompileCaller.sol"; -// import {MaxCounterScript} from "test/lib/MaxCounterScript.sol"; +import {MaxCounterScript} from "test/lib/MaxCounterScript.sol"; import {GetMessageDetails} from "test/lib/GetMessageDetails.sol"; -import {CancelOtherScript} from "test/lib/CancelOtherScript.sol"; +import {CheckNonceScript} from "test/lib/CheckNonceScript.sol"; contract QuarkWalletTest is Test { enum ExecutionType { @@ -35,8 +37,13 @@ contract QuarkWalletTest is Test { } event Ping(uint256); - event ExecuteQuarkScript( - address indexed executor, address indexed scriptAddress, bytes32 indexed nonce, ExecutionType executionType + event QuarkExecution( + address indexed executor, + address indexed scriptAddress, + bytes32 indexed nonce, + bytes32 submissionToken, + bool isReplayable, + ExecutionType executionType ); CodeJar public codeJar; @@ -48,6 +55,8 @@ contract QuarkWalletTest is Test { address aliceAccount = vm.addr(alicePrivateKey); QuarkWallet aliceWallet; // see constructor() + bytes32 constant EXHAUSTED_TOKEN = bytes32(type(uint256).max); + // wallet proxy instantiation helper function newWallet(address signer, address executor) internal returns (QuarkWallet) { return QuarkWallet(payable(new QuarkMinimalProxy(address(walletImplementation), signer, executor))); @@ -154,14 +163,72 @@ contract QuarkWalletTest is Test { // gas: meter execute vm.resumeGasMetering(); vm.expectEmit(true, true, true, true); - emit ExecuteQuarkScript(address(this), scriptAddress, opWithScriptAddress.nonce, ExecutionType.Signature); + emit QuarkExecution( + address(this), + scriptAddress, + opWithScriptAddress.nonce, + opWithScriptAddress.nonce, + false, + ExecutionType.Signature + ); aliceWallet.executeQuarkOperation(opWithScriptAddress, v, r, s); vm.expectEmit(true, true, true, true); - emit ExecuteQuarkScript(address(this), scriptAddress, opWithScriptSource.nonce, ExecutionType.Signature); + emit QuarkExecution( + address(this), + scriptAddress, + opWithScriptSource.nonce, + opWithScriptSource.nonce, + false, + ExecutionType.Signature + ); aliceWallet.executeQuarkOperation(opWithScriptSource, v2, r2, s2); } + function testEmitsEventsInReplayableQuarkOperation() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory getMessageDetails = new YulHelper().getCode("GetMessageDetails.sol/GetMessageDetails.json"); + (QuarkWallet.QuarkOperation memory opWithScriptAddress, bytes32[] memory submissionTokens) = new QuarkOperationHelper( + ).newReplayableOpWithCalldata( + aliceWallet, + getMessageDetails, + abi.encodeWithSignature("getMsgSenderAndValue()"), + ScriptType.ScriptAddress, + 2 + ); + address scriptAddress = opWithScriptAddress.scriptAddress; + (uint8 v, bytes32 r, bytes32 s) = + new SignatureHelper().signOp(alicePrivateKey, aliceWallet, opWithScriptAddress); + + // gas: meter execute + vm.resumeGasMetering(); + vm.expectEmit(true, true, true, true); + emit QuarkExecution( + address(this), + scriptAddress, + opWithScriptAddress.nonce, + opWithScriptAddress.nonce, + true, + ExecutionType.Signature + ); + aliceWallet.executeQuarkOperation(opWithScriptAddress, v, r, s); + + // second execution + vm.expectEmit(true, true, true, true); + emit QuarkExecution( + address(this), scriptAddress, opWithScriptAddress.nonce, submissionTokens[1], true, ExecutionType.Signature + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(opWithScriptAddress, submissionTokens[1], v, r, s); + + // third execution + vm.expectEmit(true, true, true, true); + emit QuarkExecution( + address(this), scriptAddress, opWithScriptAddress.nonce, submissionTokens[2], true, ExecutionType.Signature + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(opWithScriptAddress, submissionTokens[2], v, r, s); + } + function testEmitsEventsInDirectExecute() public { // gas: do not meter set-up vm.pauseGasMetering(); @@ -176,18 +243,107 @@ contract QuarkWalletTest is Test { // gas: meter execute vm.resumeGasMetering(); vm.expectEmit(true, true, true, true); - emit ExecuteQuarkScript(address(aliceAccount), scriptAddress, nonce, ExecutionType.Direct); + emit QuarkExecution(address(aliceAccount), scriptAddress, nonce, nonce, false, ExecutionType.Direct); aliceWalletExecutable.executeScript(nonce, scriptAddress, call, new bytes[](0)); } + function testFailsWithRepeatNonceInDirectExecute() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + + assertEq(counter.number(), 0); + + bytes memory maxCounterScript = new YulHelper().getCode("MaxCounterScript.sol/MaxCounterScript.json"); + address scriptAddress = codeJar.saveCode(maxCounterScript); + bytes memory call = abi.encodeWithSignature("run(address)", address(counter)); + + QuarkWallet aliceWalletExecutable = newWallet(aliceAccount, aliceAccount); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(nonceManager, aliceWalletExecutable); + + vm.startPrank(aliceAccount); + + bytes[] memory scriptSources = new bytes[](0); + + // gas: meter execute + vm.resumeGasMetering(); + vm.expectEmit(true, true, true, true); + emit QuarkExecution(address(aliceAccount), scriptAddress, nonce, nonce, false, ExecutionType.Direct); + aliceWalletExecutable.executeScript(nonce, scriptAddress, call, scriptSources); + + assertEq(counter.number(), 1); + + // TODO: Diagnose why this revert isn't causing a general revert + // Not sure why this revert isn't showing up-- it's reverting, nonetheless. + // vm.expectRevert( + // abi.encodeWithSelector( + // QuarkNonceManager.NonReplayableNonce.selector, address(aliceWalletExecutable), nonce, nonce, true + // ) + // ); + aliceWalletExecutable.executeScript(nonce, scriptAddress, call, scriptSources); + assertEq(counter.number(), 1); + } + /* ===== general invariant tests ===== */ + function testRequiresCorrectSubmissionToken() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + + bytes memory getMessageDetails = new YulHelper().getCode("GetMessageDetails.sol/GetMessageDetails.json"); + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, getMessageDetails, abi.encodeWithSignature("getMsgSenderAndValue()"), ScriptType.ScriptSource + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + + // pass in invalid submission tokens + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op.nonce, bytes32(0)) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, bytes32(0), v, r, s); + + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op.nonce, bytes32(uint256(1)) + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, bytes32(uint256(1)), v, r, s); + + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op.nonce, EXHAUSTED_TOKEN + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, EXHAUSTED_TOKEN, v, r, s); + + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op.nonce, bytes32(uint256(op.nonce) + 1) + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, bytes32(uint256(op.nonce) + 1), v, r, s); + + // Run script + aliceWallet.executeQuarkOperationWithSubmissionToken(op, op.nonce, v, r, s); + + // Check it is no longer runnable + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(aliceWallet), op.nonce, op.nonce, true + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, op.nonce, v, r, s); + } + function testDisallowAllNullScriptAddress() public { // gas: do not meter set-up vm.pauseGasMetering(); QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ nonce: new QuarkOperationHelper().semiRandomNonce(nonceManager, aliceWallet), + isReplayable: false, scriptAddress: address(0), scriptSources: new bytes[](0), scriptCalldata: bytes(""), @@ -217,6 +373,7 @@ contract QuarkWalletTest is Test { // operation containing a valid empty script will revert QuarkWallet.QuarkOperation memory op2 = QuarkWallet.QuarkOperation({ nonce: new QuarkOperationHelper().semiRandomNonce(nonceManager, aliceWallet), + isReplayable: false, scriptAddress: emptyCodeAddress, scriptSources: new bytes[](0), scriptCalldata: bytes(""), @@ -247,6 +404,7 @@ contract QuarkWalletTest is Test { QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ nonce: new QuarkOperationHelper().semiRandomNonce(nonceManager, aliceWallet), + isReplayable: false, scriptAddress: address(0xc0c0), scriptSources: scriptSources, scriptCalldata: bytes("feefee"), @@ -263,152 +421,256 @@ contract QuarkWalletTest is Test { /* ===== storage tests ===== */ - // TODO: IMPLEMENT THIS NEW TEST (MOVED FROM STATEMANAGER) - // function testReadStorageForWallet() public { - // // gas: disable metering except while executing operations - // vm.pauseGasMetering(); + function testReadStorageForWallet() public { + // gas: disable metering except while executing operations + vm.pauseGasMetering(); - // Counter counter = new Counter(); - // assertEq(counter.number(), 0); + assertEq(counter.number(), 0); - // bytes memory maxCounterScript = new YulHelper().getCode("MaxCounterScript.sol/MaxCounterScript.json"); - // address maxCounterScriptAddress = codeJar.saveCode(maxCounterScript); - // bytes memory call = abi.encodeWithSignature("run(address)", address(counter)); + bytes memory maxCounter = new YulHelper().getCode("MaxCounterScript.sol/MaxCounterScript.json"); - // QuarkWallet wallet = new QuarkWalletStandalone(address(0), address(0), codeJar, nonceManager); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + maxCounter, + abi.encodeWithSignature("run(address)", address(counter)), + ScriptType.ScriptAddress, + 4 + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); - // vm.resumeGasMetering(); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(0)) + ); - // assertEq(nonceManager.walletStorage(address(wallet), 0, keccak256("count")), bytes32(uint256(0))); + vm.resumeGasMetering(); - // vm.prank(address(wallet)); - // nonceManager.setActiveNonceAndCallback(0, maxCounterScriptAddress, call); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[0], v, r, s); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(1)) + ); + assertEq(counter.number(), 1); - // assertEq(nonceManager.walletStorage(address(wallet), 0, keccak256("count")), bytes32(uint256(1))); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v, r, s); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(2)) + ); + assertEq(counter.number(), 2); - // vm.prank(address(wallet)); - // nonceManager.setActiveNonceAndCallback(0, maxCounterScriptAddress, call); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[2], v, r, s); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(3)) + ); + assertEq(counter.number(), 3); - // assertEq(nonceManager.walletStorage(address(wallet), 0, keccak256("count")), bytes32(uint256(2))); - // } + vm.expectRevert(abi.encodeWithSelector(MaxCounterScript.EnoughAlready.selector)); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[3], v, r, s); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(3)) + ); + assertEq(counter.number(), 3); + } /* ===== replayability tests ===== */ - // TODO: Uncomment when replay tokens are supported all of these tests - // function testCanReplaySameScriptWithDifferentCall() public { - // // gas: disable gas metering except while executing operations - // vm.pauseGasMetering(); - // bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); - - // // 1. use nonce to increment a counter - // QuarkWallet.QuarkOperation memory op1 = new QuarkOperationHelper().newBasicOpWithCalldata( - // aliceWallet, - // incrementer, - // abi.encodeWithSignature("incrementCounterReplayable(address)", address(counter)), - // ScriptType.ScriptAddress - // ); - // (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); - - // address incrementerAddress = codeJar.saveCode(incrementer); - - // QuarkWallet.QuarkOperation memory op2 = QuarkWallet.QuarkOperation({ - // nonce: op1.nonce, - // scriptAddress: incrementerAddress, - // scriptSources: new bytes[](0), - // scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", address(counter)), - // expiry: block.timestamp + 1000 - // }); - // (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op2); + function testCanReplaySameScriptWithDifferentCall() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); - // // gas: meter execute - // vm.resumeGasMetering(); - // vm.expectEmit(true, true, true, true); - // emit ClearNonce(address(aliceWallet), op1.nonce); - // aliceWallet.executeQuarkOperation(op1, v1, r1, s1); - // // incrementer increments the counter thrice - // assertEq(counter.number(), 3); - // // when reusing the nonce, you can change the call - // aliceWallet.executeQuarkOperation(op2, v2, r2, s2); - // // incrementer increments the counter thrice - // assertEq(counter.number(), 6); - // // but now that we did not use a replayable call, it is canceled - // vm.expectRevert(abi.encodeWithSelector(QuarkNonceManager.NonceAlreadySet.selector)); - // aliceWallet.executeQuarkOperation(op1, v1, r1, s1); - // } + // 1. use nonce to increment a counter + (QuarkWallet.QuarkOperation memory op1, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + incrementer, + abi.encodeWithSignature("incrementCounter(address)", address(counter)), + ScriptType.ScriptAddress, + 1 + ); + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); - // function testRevertsForReusedNonceWithChangedScript() public { - // // gas: disable gas metering except while executing operations - // vm.pauseGasMetering(); - // bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); - - // // 1. use nonce to increment a counter - // QuarkWallet.QuarkOperation memory op1 = new QuarkOperationHelper().newBasicOpWithCalldata( - // aliceWallet, - // incrementer, - // abi.encodeWithSignature("incrementCounterReplayable(address)", address(counter)), - // ScriptType.ScriptAddress - // ); - // (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); - - // QuarkWallet.QuarkOperation memory op2 = QuarkWallet.QuarkOperation({ - // nonce: op1.nonce, - // scriptAddress: address(counter), - // scriptSources: new bytes[](0), - // scriptCalldata: bytes(""), - // expiry: op1.expiry - // }); - // (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op2); + address incrementerAddress = codeJar.saveCode(incrementer); - // // gas: meter execute - // vm.resumeGasMetering(); - // vm.expectEmit(true, true, true, true); - // emit ClearNonce(address(aliceWallet), op1.nonce); - // aliceWallet.executeQuarkOperation(op1, v1, r1, s1); - // // incrementer increments the counter thrice - // assertEq(counter.number(), 3); - // // when reusing the nonce but changing the script, revert - // vm.expectRevert(abi.encodeWithSelector(QuarkNonceManager.NonceScriptMismatch.selector)); - // aliceWallet.executeQuarkOperation(op2, v2, r2, s2); - // } + QuarkWallet.QuarkOperation memory op2 = QuarkWallet.QuarkOperation({ + nonce: op1.nonce, + isReplayable: true, + scriptAddress: incrementerAddress, + scriptSources: new bytes[](0), + scriptCalldata: abi.encodeWithSignature("incrementCounter2(address)", address(counter)), + expiry: block.timestamp + 1000 + }); + (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op2); - // function testRevertsForReplayOfCanceledScript() public { - // // gas: disable gas metering except while executing operations - // vm.pauseGasMetering(); - // bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); - // bytes memory cancelOtherScript = new YulHelper().getCode("CancelOtherScript.sol/CancelOtherScript.json"); - - // QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( - // aliceWallet, - // incrementer, - // abi.encodeWithSignature("incrementCounterReplayable(address)", address(counter)), - // ScriptType.ScriptAddress - // ); - // (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + // gas: meter execute + vm.resumeGasMetering(); + aliceWallet.executeQuarkOperation(op1, v1, r1, s1); + // incrementer increments the counter thrice + assertEq(counter.number(), 3); + // when executing a replayable operation, you can change the call + aliceWallet.executeQuarkOperationWithSubmissionToken(op2, submissionTokens[1], v2, r2, s2); + // incrementer increments the counter frice + assertEq(counter.number(), 7); + // but now both operations are exhausted + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op1.nonce, op1.nonce) + ); + aliceWallet.executeQuarkOperation(op1, v1, r1, s1); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op1.nonce, submissionTokens[1] + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op1, submissionTokens[1], v1, r1, s1); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op1.nonce, op2.nonce) + ); + aliceWallet.executeQuarkOperation(op2, v2, r2, s2); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op1.nonce, submissionTokens[1] + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op2, submissionTokens[1], v2, r2, s2); + } - // // gas: meter execute - // vm.resumeGasMetering(); - // vm.expectEmit(true, true, true, true); - // emit ClearNonce(address(aliceWallet), op.nonce); - // aliceWallet.executeQuarkOperation(op, v, r, s); - // assertEq(counter.number(), 3); - // // can replay the same operation... - // aliceWallet.executeQuarkOperation(op, v, r, s); - // assertEq(counter.number(), 6); - - // // can cancel the replayable nonce... - // vm.pauseGasMetering(); - // QuarkWallet.QuarkOperation memory cancelOtherOp = new QuarkOperationHelper().newBasicOpWithCalldata( - // aliceWallet, cancelOtherScript, abi.encodeWithSignature("run(bytes32)", op.nonce), ScriptType.ScriptAddress - // ); - // (uint8 cancel_v, bytes32 cancel_r, bytes32 cancel_s) = - // new SignatureHelper().signOp(alicePrivateKey, aliceWallet, cancelOtherOp); - // vm.resumeGasMetering(); - // aliceWallet.executeQuarkOperation(cancelOtherOp, cancel_v, cancel_r, cancel_s); + function testAllowsForReusedNonceWithChangedScript() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); + bytes memory incrementerBySix = new YulHelper().getCode("Incrementer.sol/IncrementerBySix.json"); - // // and now you can no longer replay - // vm.expectRevert(abi.encodeWithSelector(QuarkNonceManager.NonceAlreadySet.selector)); - // aliceWallet.executeQuarkOperation(op, v, r, s); - // } + // 1. use nonce to increment a counter + (QuarkWallet.QuarkOperation memory op1, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + incrementer, + abi.encodeWithSignature("incrementCounter(address)", address(counter)), + ScriptType.ScriptAddress, + 1 + ); + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); + + QuarkWallet.QuarkOperation memory op2 = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + incrementerBySix, + abi.encodeWithSignature("incrementCounter(address)", address(counter)), + ScriptType.ScriptAddress + ); + op2.nonce = op1.nonce; + (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op2); + + // gas: meter execute + vm.resumeGasMetering(); + aliceWallet.executeQuarkOperation(op1, v1, r1, s1); + // incrementer increments the counter thrice + assertEq(counter.number(), 3); + // when reusing the nonce but changing the script, allow + aliceWallet.executeQuarkOperationWithSubmissionToken(op2, submissionTokens[1], v2, r2, s2); + // updated with larger incrementer script + assertEq(counter.number(), 9); + } + + function testScriptCanBeCanceledByNoOp() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); + + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + incrementer, + abi.encodeWithSignature("incrementCounter(address)", address(counter)), + ScriptType.ScriptAddress, + 1 + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + aliceWallet.executeQuarkOperation(op, v, r, s); + assertEq(counter.number(), 3); + // cannot replay the same operation directly... + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op.nonce, op.nonce) + ); + aliceWallet.executeQuarkOperation(op, v, r, s); + assertEq(counter.number(), 3); + + // can cancel the replayable nonce... + vm.pauseGasMetering(); + QuarkWallet.QuarkOperation memory cancelOtherOp = + new QuarkOperationHelper().cancelReplayableByNop(aliceWallet, op); + (uint8 cancelV, bytes32 cancelR, bytes32 cancelS) = + new SignatureHelper().signOp(alicePrivateKey, aliceWallet, cancelOtherOp); + vm.resumeGasMetering(); + aliceWallet.executeQuarkOperationWithSubmissionToken( + cancelOtherOp, submissionTokens[1], cancelV, cancelR, cancelS + ); + + // and now you can no longer replay + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(aliceWallet), op.nonce, submissionTokens[1], true + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v, r, s); + + // Ensure exhausted + assertEq(nonceManager.submissions(address(aliceWallet), op.nonce), bytes32(type(uint256).max)); + } + + function testScriptCanBeCanceledByNewOp() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); + + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + incrementer, + abi.encodeWithSignature("incrementCounter(address)", address(counter)), + ScriptType.ScriptAddress, + 1 + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + // gas: meter execute + vm.resumeGasMetering(); + aliceWallet.executeQuarkOperation(op, v, r, s); + assertEq(counter.number(), 3); + // cannot replay the same operation directly... + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op.nonce, op.nonce) + ); + aliceWallet.executeQuarkOperation(op, v, r, s); + assertEq(counter.number(), 3); + + // can cancel the replayable nonce... + vm.pauseGasMetering(); + QuarkWallet.QuarkOperation memory cancelOp = new QuarkOperationHelper().cancelReplayableByNewOp(aliceWallet, op); + (uint8 cancelV, bytes32 cancelR, bytes32 cancelS) = + new SignatureHelper().signOp(alicePrivateKey, aliceWallet, cancelOp); + vm.resumeGasMetering(); + vm.expectEmit(true, true, true, true); + emit QuarkNonceManager.NonceCanceled(address(aliceWallet), op.nonce); + aliceWallet.executeQuarkOperationWithSubmissionToken(cancelOp, submissionTokens[1], cancelV, cancelR, cancelS); + + // and now you can no longer replay + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(aliceWallet), op.nonce, submissionTokens[1], true + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v, r, s); + + // Ensure exhausted + assertEq(nonceManager.submissions(address(aliceWallet), op.nonce), bytes32(type(uint256).max)); + } /* ===== direct execution path tests ===== */ @@ -422,17 +684,19 @@ contract QuarkWalletTest is Test { bytes memory call = abi.encodeWithSignature("incrementCounter(address)", counter); assertEq(counter.number(), 0); - assertEq(nonceManager.getNonceSubmission(address(aliceWalletExecutable), nonce), bytes32(uint256(0))); + assertEq(nonceManager.submissions(address(aliceWalletExecutable), nonce), bytes32(uint256(0))); // act as the executor for the wallet vm.startPrank(aliceAccount); + bytes[] memory scriptSources = new bytes[](0); + // gas: meter execute vm.resumeGasMetering(); - aliceWalletExecutable.executeScript(nonce, incrementerAddress, call, new bytes[](0)); + aliceWalletExecutable.executeScript(nonce, incrementerAddress, call, scriptSources); assertEq(counter.number(), 3); - assertEq(nonceManager.getNonceSubmission(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); } function testDirectExecuteFromOtherQuarkWallet() public { @@ -462,14 +726,14 @@ contract QuarkWalletTest is Test { (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); assertEq(counter.number(), 0); - assertEq(nonceManager.getNonceSubmission(address(aliceWalletExecutable), nonce), bytes32(uint256(0))); + assertEq(nonceManager.submissions(address(aliceWalletExecutable), nonce), bytes32(uint256(0))); // gas: meter execute vm.resumeGasMetering(); aliceWallet.executeQuarkOperation(op, v, r, s); assertEq(counter.number(), 3); - assertEq(nonceManager.getNonceSubmission(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); } function testDirectExecuteWithScriptSources() public { @@ -484,7 +748,7 @@ contract QuarkWalletTest is Test { scriptSources[0] = incrementer; assertEq(counter.number(), 0); - assertEq(nonceManager.getNonceSubmission(address(aliceWalletExecutable), nonce), bytes32(uint256(0))); + assertEq(nonceManager.submissions(address(aliceWalletExecutable), nonce), bytes32(uint256(0))); // act as the executor for the wallet vm.startPrank(aliceAccount); @@ -494,7 +758,7 @@ contract QuarkWalletTest is Test { aliceWalletExecutable.executeScript(nonce, incrementerAddress, call, scriptSources); assertEq(counter.number(), 3); - assertEq(nonceManager.getNonceSubmission(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); } function testRevertsForDirectExecuteByNonExecutorSigner() public { @@ -549,8 +813,6 @@ contract QuarkWalletTest is Test { /* ===== MultiQuarkOperation execution path tests ===== */ - // TODO: test replayable txns - function testMultiQuarkOperationCanCallMultipleOperationsWithOneSignature() public { // gas: disable metering except while executing operations vm.pauseGasMetering(); @@ -671,84 +933,409 @@ contract QuarkWalletTest is Test { // call again using the same operation vm.expectRevert( abi.encodeWithSelector( - QuarkNonceManager.NonReplayableNonce.selector, - address(aliceWallet), - op1.nonce, - bytes32(type(uint256).max) + QuarkNonceManager.NonReplayableNonce.selector, address(aliceWallet), op1.nonce, op1.nonce, true + ) + ); + aliceWallet.executeMultiQuarkOperation(op1, opDigests, v, r, s); + + assertEq(counter.number(), 3); + } + + function testReplayableMultiQuarkOperation() public { + // gas: disable metering except while executing operations + vm.pauseGasMetering(); + + bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); + assertEq(counter.number(), 0); + + (QuarkWallet.QuarkOperation memory op1, bytes32[] memory submissionTokens1) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + incrementer, + abi.encodeWithSignature("incrementCounter(address)", counter), + ScriptType.ScriptAddress, + 2 + ); + bytes32 op1Digest = new SignatureHelper().opDigest(address(aliceWallet), op1); + + (QuarkWallet.QuarkOperation memory op2, bytes32[] memory submissionTokens2) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + incrementer, + abi.encodeWithSignature("incrementCounter2(address)", counter), + ScriptType.ScriptAddress, + 2, + new QuarkOperationHelper().incrementNonce(op1.nonce) + ); + bytes32 op2Digest = new SignatureHelper().opDigest(address(aliceWallet), op2); + + bytes32[] memory opDigests = new bytes32[](2); + opDigests[0] = op1Digest; + opDigests[1] = op2Digest; + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signMultiOp(alicePrivateKey, opDigests); + + vm.resumeGasMetering(); + + // call op1, first + assertEq(nonceManager.submissions(address(aliceWallet), op1.nonce), bytes32(0)); + aliceWallet.executeMultiQuarkOperation(op1, opDigests, v, r, s); + assertEq(counter.number(), 3); + assertEq(nonceManager.submissions(address(aliceWallet), op1.nonce), op1.nonce); + + // call op2, first + assertEq(nonceManager.submissions(address(aliceWallet), op2.nonce), bytes32(0)); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[0], opDigests, v, r, s); + assertEq(counter.number(), 7); + assertEq(nonceManager.submissions(address(aliceWallet), op2.nonce), op2.nonce); + + // call op1, second + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, submissionTokens1[1], opDigests, v, r, s); + assertEq(counter.number(), 10); + assertEq(nonceManager.submissions(address(aliceWallet), op1.nonce), submissionTokens1[1]); + + // call op1, third + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, submissionTokens1[2], opDigests, v, r, s); + assertEq(counter.number(), 13); + + // test all tokens do not replay now for op1 + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op1.nonce, EXHAUSTED_TOKEN + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, EXHAUSTED_TOKEN, opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op1.nonce, submissionTokens1[0] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, submissionTokens1[0], opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op1.nonce, submissionTokens1[1] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, submissionTokens1[1], opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op1.nonce, submissionTokens1[2] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, submissionTokens1[2], opDigests, v, r, s); + + // call op2, second + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[1], opDigests, v, r, s); + assertEq(counter.number(), 17); + + // call op2, third + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[2], opDigests, v, r, s); + assertEq(counter.number(), 21); + + // test all tokens do not replay now for op2 + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, EXHAUSTED_TOKEN + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, EXHAUSTED_TOKEN, opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens2[0] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[0], opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens2[1] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[1], opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens2[2] ) ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[2], opDigests, v, r, s); + } + + function testHalfReplayableMultiQuarkOperation() public { + // gas: disable metering except while executing operations + vm.pauseGasMetering(); + + bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); + assertEq(counter.number(), 0); + + QuarkWallet.QuarkOperation memory op1 = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + incrementer, + abi.encodeWithSignature("incrementCounter(address)", counter), + ScriptType.ScriptAddress + ); + bytes32 op1Digest = new SignatureHelper().opDigest(address(aliceWallet), op1); + + (QuarkWallet.QuarkOperation memory op2, bytes32[] memory submissionTokens2) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + incrementer, + abi.encodeWithSignature("incrementCounter2(address)", counter), + ScriptType.ScriptAddress, + 2, + new QuarkOperationHelper().incrementNonce(op1.nonce) + ); + bytes32 op2Digest = new SignatureHelper().opDigest(address(aliceWallet), op2); + + bytes32[] memory opDigests = new bytes32[](2); + opDigests[0] = op1Digest; + opDigests[1] = op2Digest; + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signMultiOp(alicePrivateKey, opDigests); + + vm.resumeGasMetering(); + + // call op1 + assertEq(nonceManager.submissions(address(aliceWallet), op1.nonce), bytes32(0)); aliceWallet.executeMultiQuarkOperation(op1, opDigests, v, r, s); + assertEq(counter.number(), 3); + assertEq(nonceManager.submissions(address(aliceWallet), op1.nonce), EXHAUSTED_TOKEN); + + // call op2, first + assertEq(nonceManager.submissions(address(aliceWallet), op2.nonce), bytes32(0)); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[0], opDigests, v, r, s); + assertEq(counter.number(), 7); + assertEq(nonceManager.submissions(address(aliceWallet), op2.nonce), op2.nonce); + + // test all tokens do not replay now for op1, which is non-replayable + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, aliceWallet, op1.nonce, EXHAUSTED_TOKEN, true + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, EXHAUSTED_TOKEN, opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, aliceWallet, op1.nonce, op1.nonce, true + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, op1.nonce, opDigests, v, r, s); + + // call op2, second + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[1], opDigests, v, r, s); + assertEq(counter.number(), 11); + + // call op2, third + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[2], opDigests, v, r, s); + assertEq(counter.number(), 15); + + // test all tokens do not replay now for op2 + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, EXHAUSTED_TOKEN + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, EXHAUSTED_TOKEN, opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens2[0] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[0], opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens2[1] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[1], opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens2[2] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[2], opDigests, v, r, s); + } + + function testReplayableMultiQuarkOperationWithSharedNonce() public { + // gas: disable metering except while executing operations + vm.pauseGasMetering(); + + bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); + assertEq(counter.number(), 0); + (QuarkWallet.QuarkOperation memory op1, bytes32[] memory submissionTokens1) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + incrementer, + abi.encodeWithSignature("incrementCounter(address)", counter), + ScriptType.ScriptAddress, + 2 + ); + bytes32 op1Digest = new SignatureHelper().opDigest(address(aliceWallet), op1); + + (QuarkWallet.QuarkOperation memory op2, bytes32[] memory submissionTokens2) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + incrementer, + abi.encodeWithSignature("incrementCounter2(address)", counter), + ScriptType.ScriptAddress, + 2, + submissionTokens1[2] // Same nonce secret + ); + bytes32 op2Digest = new SignatureHelper().opDigest(address(aliceWallet), op2); + + bytes32[] memory opDigests = new bytes32[](2); + opDigests[0] = op1Digest; + opDigests[1] = op2Digest; + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signMultiOp(alicePrivateKey, opDigests); + + vm.resumeGasMetering(); + + // call op1, first + assertEq(nonceManager.submissions(address(aliceWallet), op1.nonce), bytes32(0)); + aliceWallet.executeMultiQuarkOperation(op1, opDigests, v, r, s); assertEq(counter.number(), 3); + assertEq(nonceManager.submissions(address(aliceWallet), op1.nonce), op1.nonce); + + // ensure op1 and op2 submissions fail on submissionTokens[0] + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens2[0] + ) + ); + aliceWallet.executeMultiQuarkOperation(op2, opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens2[0] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[0], opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens1[0] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, submissionTokens1[0], opDigests, v, r, s); + + // now submit op2 with submissionTokens[1] + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[1], opDigests, v, r, s); + assertEq(counter.number(), 7); + + // ensure neither can be called with submissionTokens[1] now + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens2[1] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[1], opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens1[1] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, submissionTokens1[1], opDigests, v, r, s); + + // call op1, third + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, submissionTokens1[2], opDigests, v, r, s); + assertEq(counter.number(), 10); + + // ensure neither can be called with submissionTokens[2] now + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens2[2] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op2, submissionTokens2[2], opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, aliceWallet, op2.nonce, submissionTokens1[2] + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, submissionTokens1[2], opDigests, v, r, s); } /* ===== basic operation tests ===== */ - // TODO: Uncomment when replay tokens are supported - // function testAtomicMaxCounterScript() public { - // // gas: disable metering except while executing operations - // vm.pauseGasMetering(); + function testAtomicMaxCounterScript() public { + // gas: disable metering except while executing operations + vm.pauseGasMetering(); - // bytes memory maxCounterScript = new YulHelper().getCode("MaxCounterScript.sol/MaxCounterScript.json"); - // assertEq(counter.number(), 0); + bytes memory maxCounterScript = new YulHelper().getCode("MaxCounterScript.sol/MaxCounterScript.json"); + assertEq(counter.number(), 0); - // vm.startPrank(address(aliceAccount)); + vm.startPrank(address(aliceAccount)); - // QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( - // aliceWallet, maxCounterScript, abi.encodeCall(MaxCounterScript.run, (counter)), ScriptType.ScriptAddress - // ); - // (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, maxCounterScript, abi.encodeCall(MaxCounterScript.run, (counter)), ScriptType.ScriptAddress, 4 + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); - // // call once - // vm.resumeGasMetering(); - // aliceWallet.executeQuarkOperation(op, v, r, s); + // call once + vm.resumeGasMetering(); + vm.expectEmit(true, true, true, true); + emit MaxCounterScript.Count(1); + aliceWallet.executeQuarkOperation(op, v, r, s); - // // gas: do not meter walletStorage - // vm.pauseGasMetering(); + // gas: do not meter walletStorage + vm.pauseGasMetering(); - // assertEq(counter.number(), 1); - // assertEq(uint256(nonceManager.walletStorage(address(aliceWallet), op.nonce, keccak256("count"))), 1); + assertEq(counter.number(), 1); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(1)) + ); - // // call twice - // vm.resumeGasMetering(); - // aliceWallet.executeQuarkOperation(op, v, r, s); + // call twice + vm.resumeGasMetering(); + vm.expectEmit(true, true, true, true); + emit MaxCounterScript.Count(2); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v, r, s); - // // gas: do not meter walletStorage - // vm.pauseGasMetering(); + // gas: do not meter walletStorage + vm.pauseGasMetering(); - // assertEq(counter.number(), 2); - // assertEq(uint256(nonceManager.walletStorage(address(aliceWallet), op.nonce, keccak256("count"))), 2); + assertEq(counter.number(), 2); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(2)) + ); - // // call thrice - // vm.resumeGasMetering(); - // aliceWallet.executeQuarkOperation(op, v, r, s); + // call thrice + vm.resumeGasMetering(); + vm.expectEmit(true, true, true, true); + emit MaxCounterScript.Count(3); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[2], v, r, s); - // // gas: do not meter walletStorage - // vm.pauseGasMetering(); + // gas: do not meter walletStorage + vm.pauseGasMetering(); - // assertEq(counter.number(), 3); - // assertEq(uint256(nonceManager.walletStorage(address(aliceWallet), op.nonce, keccak256("count"))), 3); + assertEq(counter.number(), 3); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(3)) + ); - // // revert because max has been hit - // vm.expectRevert(abi.encodeWithSelector(MaxCounterScript.EnoughAlready.selector)); - // vm.resumeGasMetering(); - // aliceWallet.executeQuarkOperation(op, v, r, s); + // revert because max has been hit + vm.expectRevert(abi.encodeWithSelector(MaxCounterScript.EnoughAlready.selector)); + vm.resumeGasMetering(); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[3], v, r, s); - // // gas: do not meter walletStorage - // vm.pauseGasMetering(); + // gas: do not meter walletStorage + vm.pauseGasMetering(); - // assertEq(counter.number(), 3); - // assertEq( - // uint256(nonceManager.walletStorage(address(aliceWallet), op.nonce, keccak256("count"))), counter.number() - // ); + assertEq(counter.number(), 3); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(3)) + ); - // counter.increment(); - // assertEq(counter.number(), 4); - // assertEq(uint256(nonceManager.walletStorage(address(aliceWallet), op.nonce, keccak256("count"))), 3); + counter.increment(); + assertEq(counter.number(), 4); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(3)) + ); - // vm.resumeGasMetering(); - // vm.stopPrank(); - // } + vm.resumeGasMetering(); + vm.stopPrank(); + } function testQuarkOperationRevertsIfCallReverts() public { // gas: do not meter set-up @@ -823,6 +1410,7 @@ contract QuarkWalletTest is Test { scriptSources: scriptSources, scriptCalldata: scriptCalldata, nonce: nonce, + isReplayable: false, expiry: expiry }); @@ -836,6 +1424,7 @@ contract QuarkWalletTest is Test { }, { QuarkOperation: [ { name: 'nonce', type: 'bytes32' }, + { name: 'isReplayable', type: 'bool' }, { name: 'scriptAddress', type: 'address' }, { name: 'scriptSources', type: 'bytes[]' }, { name: 'scriptCalldata', type: 'bytes' }, @@ -843,6 +1432,7 @@ contract QuarkWalletTest is Test { ]}, { nonce: '0x0000000000000000000000000000000000000000000000000000000000000001', + isReplayable: false, scriptAddress: '0x4a925cF75dcc5708671004d9bbFAf4DCF2C762B0', scriptSources: ['0x630000003080600e6000396000f36000356000527f48257dc961b6f792c2b78a080dacfed693b660960a702de21cee364e20270e2f60206000a1600080f3'], scriptCalldata: '0x00000000000000000000000000000000000000000000000000000000000000dd', @@ -852,7 +1442,7 @@ contract QuarkWalletTest is Test { */ bytes memory sigHash = - hex"1901420cb4769bd47ac11897b8b69b8d80a84b9ec8b69437cd42529681d583a6b5211a9548fcdcb39c227cdd2ff9e13fbefc3db707f8e2216139758d6fc20328fcfd"; + hex"1901420cb4769bd47ac11897b8b69b8d80a84b9ec8b69437cd42529681d583a6b5218c7d870a6510d1840f2ec48a08d65eb874fa8af841e45e3c9b8e5c244bdc015f"; (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, keccak256(sigHash)); // gas: meter execute @@ -1238,6 +1828,7 @@ contract QuarkWalletTest is Test { scriptSources: new bytes[](0), scriptCalldata: hex"", nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); } diff --git a/test/quark-core/Reverts.t.sol b/test/quark-core/Reverts.t.sol index a5cdf183..58fb3a70 100644 --- a/test/quark-core/Reverts.t.sol +++ b/test/quark-core/Reverts.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; diff --git a/test/quark-core/isValidSignature.t.sol b/test/quark-core/isValidSignature.t.sol index 748afcd0..917fc7c4 100644 --- a/test/quark-core/isValidSignature.t.sol +++ b/test/quark-core/isValidSignature.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/StdUtils.sol"; diff --git a/test/quark-core/periphery/BatchExecutor.t.sol b/test/quark-core/periphery/BatchExecutor.t.sol index 370be70f..897548c3 100644 --- a/test/quark-core/periphery/BatchExecutor.t.sol +++ b/test/quark-core/periphery/BatchExecutor.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; @@ -266,4 +266,6 @@ contract BatchExecutorTest is Test { // // Should fail with OOG // assertEq(successes[2], false); } + + // TODO: Batch execution with submission tokens? } diff --git a/test/quark-factory/QuarkFactory.t.sol b/test/quark-factory/QuarkFactory.t.sol index 85fe5689..634b35fd 100644 --- a/test/quark-factory/QuarkFactory.t.sol +++ b/test/quark-factory/QuarkFactory.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; diff --git a/test/quark-proxy/QuarkMinimalProxy.t.sol b/test/quark-proxy/QuarkMinimalProxy.t.sol index b3e2386b..165eb0cc 100644 --- a/test/quark-proxy/QuarkMinimalProxy.t.sol +++ b/test/quark-proxy/QuarkMinimalProxy.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/console.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/test/quark-proxy/QuarkWalletProxyFactory.t.sol b/test/quark-proxy/QuarkWalletProxyFactory.t.sol index 886ce517..1af416b4 100644 --- a/test/quark-proxy/QuarkWalletProxyFactory.t.sol +++ b/test/quark-proxy/QuarkWalletProxyFactory.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import "forge-std/Test.sol"; import "forge-std/console.sol"; @@ -122,6 +122,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); @@ -145,8 +146,7 @@ contract QuarkWalletProxyFactoryTest is Test { // uses up the operation's nonce assertEq( - nonceManager.getNonceSubmission(factory.walletAddressFor(alice, address(0)), nonce), - bytes32(type(uint256).max) + nonceManager.submissions(factory.walletAddressFor(alice, address(0)), nonce), bytes32(type(uint256).max) ); } @@ -168,6 +168,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); @@ -193,7 +194,7 @@ contract QuarkWalletProxyFactoryTest is Test { // uses up the operation's nonce assertEq( - nonceManager.getNonceSubmission(factory.walletAddressForSalt(alice, address(0), salt), nonce), + nonceManager.submissions(factory.walletAddressForSalt(alice, address(0), salt), nonce), bytes32(type(uint256).max) ); } @@ -216,6 +217,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); @@ -241,8 +243,7 @@ contract QuarkWalletProxyFactoryTest is Test { // uses up the operation's nonce assertEq( - nonceManager.getNonceSubmission(factory.walletAddressFor(alice, address(0)), nonce), - bytes32(type(uint256).max) + nonceManager.submissions(factory.walletAddressFor(alice, address(0)), nonce), bytes32(type(uint256).max) ); } @@ -270,6 +271,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); bytes32 op1Digest = new SignatureHelper().opDigest(aliceWalletAddress, op1); @@ -279,6 +281,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: new QuarkOperationHelper().incrementNonce(nonce), + isReplayable: false, expiry: block.timestamp + 1000 }); op2.nonce = new QuarkOperationHelper().incrementNonce(op1.nonce); @@ -297,13 +300,13 @@ contract QuarkWalletProxyFactoryTest is Test { factory.createAndExecuteMulti(alice, address(0), op1, opDigests, v, r, s); assertEq(counter.number(), 3); - assertEq(nonceManager.getNonceSubmission(aliceWalletAddress, op1.nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(aliceWalletAddress, op1.nonce), bytes32(type(uint256).max)); // call a second time factory.createAndExecuteMulti(alice, address(0), op2, opDigests, v, r, s); assertEq(counter.number(), 6); - assertEq(nonceManager.getNonceSubmission(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); } function testCreateAndExecuteMultiWithSalt() public { @@ -325,6 +328,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); bytes32 op1Digest = new SignatureHelper().opDigest(aliceWalletAddress, op1); @@ -334,6 +338,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: new QuarkOperationHelper().incrementNonce(nonce), + isReplayable: false, expiry: block.timestamp + 1000 }); op2.nonce = new QuarkOperationHelper().incrementNonce(op1.nonce); @@ -352,13 +357,13 @@ contract QuarkWalletProxyFactoryTest is Test { factory.createAndExecuteMulti(alice, address(0), salt, op1, opDigests, v, r, s); assertEq(counter.number(), 3); - assertEq(nonceManager.getNonceSubmission(aliceWalletAddress, op1.nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(aliceWalletAddress, op1.nonce), bytes32(type(uint256).max)); // call a second time factory.createAndExecuteMulti(alice, address(0), salt, op2, opDigests, v, r, s); assertEq(counter.number(), 6); - assertEq(nonceManager.getNonceSubmission(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); } function testExecuteMultiOnExistingWallet() public { @@ -383,6 +388,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); bytes32 op1Digest = new SignatureHelper().opDigest(aliceWalletAddress, op1); @@ -392,6 +398,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: new QuarkOperationHelper().incrementNonce(nonce), + isReplayable: false, expiry: block.timestamp + 1000 }); op2.nonce = new QuarkOperationHelper().incrementNonce(op1.nonce); @@ -414,13 +421,13 @@ contract QuarkWalletProxyFactoryTest is Test { factory.createAndExecuteMulti(alice, address(0), op1, opDigests, v, r, s); assertEq(counter.number(), 3); - assertEq(nonceManager.getNonceSubmission(aliceWalletAddress, op1.nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(aliceWalletAddress, op1.nonce), bytes32(type(uint256).max)); // call a second time factory.createAndExecuteMulti(alice, address(0), op2, opDigests, v, r, s); assertEq(counter.number(), 6); - assertEq(nonceManager.getNonceSubmission(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); } /* ===== msg.value and msg.sender tests ===== */ @@ -442,6 +449,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("getMsgSenderAndValue()"), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOpForAddress(alicePrivateKey, aliceWallet, op); @@ -460,7 +468,7 @@ contract QuarkWalletProxyFactoryTest is Test { assertEq(msgValue, 0); // uses up the operation's nonce - assertEq(nonceManager.getNonceSubmission(aliceWallet, nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(aliceWallet, nonce), bytes32(type(uint256).max)); } function testCreateAndExecuteWithSaltSetsMsgSender() public { @@ -477,6 +485,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources: new bytes[](0), scriptCalldata: abi.encodeWithSignature("getMsgSenderAndValue()"), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOpForAddress(alicePrivateKey, aliceWallet, op); @@ -510,7 +519,7 @@ contract QuarkWalletProxyFactoryTest is Test { assertEq(msgValue, 0); // uses up the operation's nonce - assertEq(nonceManager.getNonceSubmission(aliceWallet, nonce), bytes32(type(uint256).max)); + assertEq(nonceManager.submissions(aliceWallet, nonce), bytes32(type(uint256).max)); } /* ===== default wallet executor role tests ===== */ @@ -552,8 +561,9 @@ contract QuarkWalletProxyFactoryTest is Test { abi.encodeWithSignature( "run(address,bytes,uint256)", address(counter), abi.encodeWithSignature("increment(uint256)", 7), 0 ) - ), + ), nonce: new QuarkOperationHelper().semiRandomNonce(nonceManager, aliceWalletPrimary), + isReplayable: false, expiry: block.timestamp + 1000 }); (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWalletPrimary, op);