diff --git a/src/quark-core/src/QuarkStateManager.sol b/src/quark-core/src/QuarkStateManager.sol index a25cbfc3..e979cbea 100644 --- a/src/quark-core/src/QuarkStateManager.sol +++ b/src/quark-core/src/QuarkStateManager.sol @@ -9,76 +9,47 @@ import {IQuarkWallet} from "quark-core/src/interfaces/IQuarkWallet.sol"; * @author Compound Labs, Inc. */ contract QuarkStateManager { - error NonceAlreadySet(); - error NoUnusedNonces(); + error NonReplayableNonce(address wallet, bytes32 nonce, bytes32 replayToken); + error InvalidReplayToken(address wallet, bytes32 nonce, bytes32 replayToken); - /// @notice Bit-packed nonce values - mapping(address wallet => mapping(uint256 bucket => uint256 bitset)) public nonces; + event NonceSubmitted(address wallet, bytes32 nonce, bytes32 replayToken); - /** - * @notice Return whether a nonce has been exhausted - * @param wallet Address of the wallet owning the nonce - * @param nonce Nonce to check - * @return Whether the nonce has been exhausted - */ - function isNonceSet(address wallet, uint96 nonce) public view returns (bool) { - (uint256 bucket, uint256 mask) = getBucket(nonce); - return isNonceSetInternal(wallet, bucket, mask); - } + /// @notice Represents the unclaimed bytes32 value. + bytes32 public constant CLAIMABLE_TOKEN = bytes32(uint256(0)); - /// @dev Returns if a given nonce is set for a wallet, using the nonce's bucket and mask - function isNonceSetInternal(address wallet, uint256 bucket, uint256 mask) internal view returns (bool) { - return (nonces[wallet][bucket] & mask) != 0; - } + /// @notice A token that implies a Quark Operation is no longer replayable. + bytes32 public constant NO_REPLAY_TOKEN = bytes32(type(uint).max); + + /// @notice Mapping from nonces to last used replay token. + mapping(address wallet => mapping(bytes32 nonce => bytes32 lastToken)) public nonceTokens; /** - * @notice Returns the next valid unset nonce for a given wallet - * @dev Any unset nonce is valid to use, but using this method - * increases the likelihood that the nonce you use will be in a bucket that - * has already been written to, which costs less gas - * @param wallet Address of the wallet to find the next nonce for - * @return The next unused nonce + * @notice Returns the nonce token (last replay 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 replayToken The last used replay token, or 0 if unused or -1 if finalized. */ - function nextNonce(address wallet) external view returns (uint96) { - // Any bucket larger than `type(uint88).max` will result in unsafe undercast when converting to nonce - for (uint256 bucket = 0; bucket <= type(uint88).max; ++bucket) { - uint96 bucketValue = uint96(bucket << 8); - uint256 bucketNonces = nonces[wallet][bucket]; - // Move on to the next bucket if all bits in this bucket are already set - if (bucketNonces == type(uint256).max) continue; - for (uint256 maskOffset = 0; maskOffset < 256; ++maskOffset) { - uint256 mask = 1 << maskOffset; - if ((bucketNonces & mask) == 0) { - uint96 nonce = uint96(bucketValue + maskOffset); - return nonce; - } - } - } - - revert NoUnusedNonces(); - } - - /// @dev Locate a nonce at a (bucket, mask) bitset position in the nonces mapping - function getBucket(uint96 nonce) internal pure returns (uint256, /* bucket */ uint256 /* mask */ ) { - uint256 bucket = nonce >> 8; - uint256 setMask = 1 << (nonce & 0xff); - return (bucket, setMask); + function getNonceToken(address wallet, bytes32 nonce) external view returns (bytes32 replayToken) { + return nonceTokens[wallet][nonce]; } /** - * @notice Claim a given nonce for the calling wallet, reverting if the nonce is already set - * @param nonce Nonce to claim for the calling wallet + * @notice Attempts a first or subsequent submission of a given nonce from a wallet. + * @param nonce The nonce of the chain to submit. + * @param replayToken The replay token of the submission. For single-use operations, set `replayToken` to `uint256(-1)`. For first-use replayable operations, set `replayToken` = `nonce`. */ - function claimNonce(uint96 nonce) external { - (uint256 bucket, uint256 setMask) = getBucket(nonce); - if (isNonceSetInternal(msg.sender, bucket, setMask)) { - revert NonceAlreadySet(); + function submitNonceToken(bytes32 nonce, bytes32 replayToken) external { + bytes32 lastToken = nonceTokens[msg.sender][nonce]; + if (lastToken == NO_REPLAY_TOKEN) { + revert NonReplayableNonce(msg.sender, nonce, replayToken); + } + + bool validReplay = lastToken == CLAIMABLE_TOKEN || keccak256(abi.encodePacked(replayToken)) == lastToken; + if (!validReplay) { + revert InvalidReplayToken(msg.sender, nonce, replayToken); } - setNonceInternal(bucket, setMask); - } - /// @dev Set a nonce for the msg.sender, using the nonce's bucket and mask - function setNonceInternal(uint256 bucket, uint256 setMask) internal { - nonces[msg.sender][bucket] |= setMask; + nonceTokens[msg.sender][nonce] = replayToken; + emit NonceSubmitted(msg.sender, nonce, replayToken); } } diff --git a/src/quark-core/src/QuarkWallet.sol b/src/quark-core/src/QuarkWallet.sol index 80e4230f..918deba3 100644 --- a/src/quark-core/src/QuarkWallet.sol +++ b/src/quark-core/src/QuarkWallet.sol @@ -23,7 +23,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(uint96 nonce,address scriptAddress,bytes[] scriptSources,bytes scriptCalldata,uint256 expiry)" + "QuarkOperation(bytes32 nonce,address scriptAddress,bytes[] scriptSources,bytes scriptCalldata,uint256 expiry)" ); /// @notice The EIP-712 typehash for authorizing a MultiQuarkOperation for this version of QuarkWallet @@ -65,7 +65,7 @@ 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, uint96 indexed nonce, ExecutionType executionType + address indexed executor, address indexed scriptAddress, bytes32 indexed nonce, ExecutionType executionType ); /// @notice Address of CodeJar contract used to deploy transaction script source code @@ -113,13 +113,16 @@ contract QuarkWallet is IERC1271 { /// @notice Well-known storage slot for the currently executing script's address (if any) bytes32 public constant ACTIVE_SCRIPT_SLOT = bytes32(uint256(keccak256("quark.v1.active.script")) - 1); + /// @notice A token that implies a Quark Operation is no longer replayable. + bytes32 public constant NO_REPLAY_TOKEN = bytes32(type(uint).max); + /// @notice The magic value to return for valid ERC1271 signature bytes4 internal constant EIP_1271_MAGIC_VALUE = 0x1626ba7e; /// @notice The structure of a signed operation to execute in the context of this wallet struct QuarkOperation { /// @notice Nonce identifier for the operation - uint96 nonce; + bytes32 nonce; /// @notice The address of the transaction script to run address scriptAddress; /// @notice Creation codes Quark must ensure are deployed before executing this operation @@ -193,7 +196,7 @@ contract QuarkWallet is IERC1271 { } /** - * @notice Verify a signature and execute a QuarkOperation + * @notice Verify a signature and execute a single-use QuarkOperation * @param op A QuarkOperation struct * @param digest A EIP-712 digest for either a QuarkOperation or MultiQuarkOperation to verify the signature against * @param v EIP-712 signature v value @@ -220,7 +223,44 @@ contract QuarkWallet is IERC1271 { codeJar.saveCode(op.scriptSources[i]); } - stateManager.claimNonce(op.nonce); + stateManager.submitNonceToken(op.nonce, NO_REPLAY_TOKEN); + + emit ExecuteQuarkScript(msg.sender, op.scriptAddress, op.nonce, ExecutionType.Signature); + + return executeScriptInternal(op.scriptAddress, op.scriptCalldata); + } + + /** + * @notice Verify a signature and execute a replayable QuarkOperation + * @param op A QuarkOperation struct + * @param replayToken The replay token for the replayable quark operation. For the first submission, this is generally the `rootHash`. + * @param 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( + QuarkOperation calldata op, + bytes32 replayToken, + bytes32 digest, + 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); + + // guarantee every script in scriptSources is deployed + for (uint256 i = 0; i < op.scriptSources.length; ++i) { + codeJar.saveCode(op.scriptSources[i]); + } + + stateManager.submitNonceToken(op.nonce, replayToken); emit ExecuteQuarkScript(msg.sender, op.scriptAddress, op.nonce, ExecutionType.Signature); @@ -237,7 +277,7 @@ contract QuarkWallet is IERC1271 { * @return Return value from the executed operation */ function executeScript( - uint96 nonce, + bytes32 nonce, address scriptAddress, bytes calldata scriptCalldata, bytes[] calldata scriptSources @@ -252,7 +292,7 @@ contract QuarkWallet is IERC1271 { codeJar.saveCode(scriptSources[i]); } - stateManager.claimNonce(nonce); + stateManager.submitNonceToken(nonce, NO_REPLAY_TOKEN); emit ExecuteQuarkScript(msg.sender, scriptAddress, nonce, ExecutionType.Direct); diff --git a/src/quark-core/src/interfaces/IQuarkWallet.sol b/src/quark-core/src/interfaces/IQuarkWallet.sol index 871c0526..83bfa536 100644 --- a/src/quark-core/src/interfaces/IQuarkWallet.sol +++ b/src/quark-core/src/interfaces/IQuarkWallet.sol @@ -10,7 +10,7 @@ interface IQuarkWallet { /// @notice The structure of a signed operation to execute in the context of this wallet struct QuarkOperation { /// @notice Nonce identifier for the operation - uint96 nonce; + bytes32 nonce; /// @notice The address of the transaction script to run address scriptAddress; /// @notice Creation codes Quark must ensure are deployed before executing this operation @@ -32,7 +32,7 @@ interface IQuarkWallet { bytes32 s ) external returns (bytes memory); function executeScript( - uint96 nonce, + bytes32 nonce, address scriptAddress, bytes calldata scriptCalldata, bytes[] calldata scriptSources diff --git a/test/lib/CancelOtherScript.sol b/test/lib/CancelOtherScript.sol index c96e10e2..7d0dac82 100644 --- a/test/lib/CancelOtherScript.sol +++ b/test/lib/CancelOtherScript.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.23; import "quark-core/src/QuarkWallet.sol"; contract CancelOtherScript { - function run(uint96 nonce) public { - return QuarkWallet(payable(address(this))).stateManager().claimNonce(nonce); + function run(bytes32 nonce) public { + return QuarkWallet(payable(address(this))).stateManager().submitNonceToken(nonce, bytes32(type(uint).max)); } } diff --git a/test/lib/ExecuteOnBehalf.sol b/test/lib/ExecuteOnBehalf.sol index 0d70f12c..bfec2cc7 100644 --- a/test/lib/ExecuteOnBehalf.sol +++ b/test/lib/ExecuteOnBehalf.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.23; import "quark-core/src/QuarkWallet.sol"; contract ExecuteOnBehalf { - function run(QuarkWallet targetWallet, uint96 nonce, address scriptAddress, bytes calldata scriptCalldata) + function run(QuarkWallet targetWallet, bytes32 nonce, address scriptAddress, bytes calldata scriptCalldata) public returns (bytes memory) { diff --git a/test/lib/ExecuteWithRequirements.sol b/test/lib/ExecuteWithRequirements.sol index 95c3f765..baf9d59b 100644 --- a/test/lib/ExecuteWithRequirements.sol +++ b/test/lib/ExecuteWithRequirements.sol @@ -5,16 +5,16 @@ import "quark-core/src/QuarkWallet.sol"; import "quark-core/src/QuarkStateManager.sol"; contract ExecuteWithRequirements { - error RequirementNotMet(uint96 nonce); + error RequirementNotMet(bytes32 nonce); - function runWithRequirements(uint96[] memory requirements, address scriptAddress, bytes calldata scriptCalldata) + function runWithRequirements(bytes32[] memory requirements, address scriptAddress, bytes calldata scriptCalldata) public returns (bytes memory) { QuarkWallet wallet = QuarkWallet(payable(address(this))); QuarkStateManager stateManager = wallet.stateManager(); - for (uint96 i = 0; i < requirements.length; i++) { - if (!stateManager.isNonceSet(address(wallet), requirements[i])) { + for (uint256 i = 0; i < requirements.length; i++) { + if (stateManager.getNonceToken(address(wallet), requirements[i]) == bytes32(uint256(0))) { revert RequirementNotMet(requirements[i]); } } diff --git a/test/lib/QuarkOperationHelper.sol b/test/lib/QuarkOperationHelper.sol index f06649b2..0068b4a5 100644 --- a/test/lib/QuarkOperationHelper.sol +++ b/test/lib/QuarkOperationHelper.sol @@ -12,6 +12,9 @@ enum ScriptType { // TODO: QuarkOperationHelper ScriptType doesn't really make sense anymore, since scriptSource // has been replaced with scriptSources and scriptAddress is now always required. contract QuarkOperationHelper is Test { + error SemiRandomNonceRequiresQuarkStateManagerOrInitializedQuarkWallet(address quarkWallet); + error Impossible(); + function newBasicOp(QuarkWallet wallet, bytes memory scriptSource, ScriptType scriptType) external returns (QuarkWallet.QuarkOperation memory) @@ -41,7 +44,7 @@ contract QuarkOperationHelper is Test { scriptAddress: scriptAddress, scriptSources: ensureScripts, scriptCalldata: scriptCalldata, - nonce: wallet.stateManager().nextNonce(address(wallet)), + nonce: semiRandomNonce(wallet), expiry: block.timestamp + 1000 }); } else { @@ -49,9 +52,35 @@ contract QuarkOperationHelper is Test { scriptAddress: scriptAddress, scriptSources: ensureScripts, scriptCalldata: scriptCalldata, - nonce: wallet.stateManager().nextNonce(address(wallet)), + nonce: semiRandomNonce(wallet), 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) { + revert SemiRandomNonceRequiresQuarkStateManagerOrInitializedQuarkWallet(address(wallet)); + } + + return semiRandomNonce(wallet.stateManager(), wallet); + } + + /// @dev Note: not sufficiently random for non-test case usage. + function semiRandomNonce(QuarkStateManager quarkStateManager, QuarkWallet wallet) public view returns (bytes32) { + bytes32 nonce = bytes32(uint256(keccak256(abi.encodePacked(block.timestamp))) - 1); + while (true) { + if (quarkStateManager.getNonceToken(address(wallet), nonce) == bytes32(uint256(0))) { + return nonce; + } + + nonce = bytes32(uint256(keccak256(abi.encodePacked(nonce))) - 1); + } + revert Impossible(); + } + + function incrementNonce(bytes32 nonce) public pure returns (bytes32) { + return bytes32(uint256(nonce) + 1); + } } diff --git a/test/quark-core-scripts/Multicall.t.sol b/test/quark-core-scripts/Multicall.t.sol index 0ddbee3f..532ac624 100644 --- a/test/quark-core-scripts/Multicall.t.sol +++ b/test/quark-core-scripts/Multicall.t.sol @@ -391,8 +391,10 @@ contract MulticallTest is Test { // 1. transfer 0.5 WETH from wallet A to wallet B wallets[0] = address(walletA); walletCalls[0] = abi.encodeWithSignature( - "executeScript(uint96,address,bytes,bytes[])", - QuarkWallet(payable(factory.walletImplementation())).stateManager().nextNonce(address(walletA)), + "executeScript(bytes32,address,bytes,bytes[])", + new QuarkOperationHelper().semiRandomNonce( + QuarkWallet(payable(factory.walletImplementation())).stateManager(), walletA + ), ethcallAddress, abi.encodeWithSelector( Ethcall.run.selector, @@ -404,11 +406,12 @@ contract MulticallTest is Test { ); // 2. approve Comet cUSDCv3 to receive 0.5 WETH from wallet B - uint96 walletBNextNonce = - QuarkWallet(payable(factory.walletImplementation())).stateManager().nextNonce(address(walletB)); + bytes32 walletBNextNonce = new QuarkOperationHelper().semiRandomNonce( + QuarkWallet(payable(factory.walletImplementation())).stateManager(), walletB + ); wallets[1] = address(walletB); walletCalls[1] = abi.encodeWithSignature( - "executeScript(uint96,address,bytes,bytes[])", + "executeScript(bytes32,address,bytes,bytes[])", walletBNextNonce, ethcallAddress, abi.encodeWithSelector( @@ -423,8 +426,8 @@ contract MulticallTest is Test { // 3. supply 0.5 WETH from wallet B to Comet cUSDCv3 wallets[2] = address(walletB); walletCalls[2] = abi.encodeWithSignature( - "executeScript(uint96,address,bytes,bytes[])", - walletBNextNonce + 1, + "executeScript(bytes32,address,bytes,bytes[])", + bytes32(uint256(walletBNextNonce) + 1), ethcallAddress, abi.encodeWithSelector( Ethcall.run.selector, @@ -493,7 +496,10 @@ contract MulticallTest is Test { deal(WETH, address(wallet), 100 ether); address subWallet1 = factory.walletAddressForSalt(alice, address(wallet), bytes32("1")); - uint96 nonce = QuarkWallet(payable(factory.walletImplementation())).stateManager().nextNonce(subWallet1); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce( + QuarkWallet(payable(factory.walletImplementation())).stateManager(), QuarkWallet(payable(subWallet1)) + ); + // Steps: Wallet#1: Supply WETH to Comet -> Borrow USDC from Comet(USDC) to subwallet -> Create subwallet // -> Swap USDC to WETH on Uniswap -> Supply WETH to Comet(WETH) address[] memory callContracts = new address[](5); @@ -534,7 +540,7 @@ contract MulticallTest is Test { path: abi.encodePacked(USDC, uint24(500), WETH) // Path: USDC - 0.05% -> WETH }) ) - ), + ), new bytes[](0) ) ), @@ -548,7 +554,7 @@ contract MulticallTest is Test { abi.encodeCall( QuarkWallet.executeScript, ( - nonce + 1, + new QuarkOperationHelper().incrementNonce(nonce), legendCometSupplyScriptAddress, abi.encodeCall(CometSupplyActions.supply, (cWETHv3, WETH, 2 ether)), new bytes[](0) diff --git a/test/quark-core/Callbacks.t.sol b/test/quark-core/Callbacks.t.sol index 86b23dc4..49eeefb6 100644 --- a/test/quark-core/Callbacks.t.sol +++ b/test/quark-core/Callbacks.t.sol @@ -131,7 +131,7 @@ contract CallbacksTest is Test { ScriptType.ScriptAddress ); - parentOp.nonce = nestedOp.nonce + 1; + parentOp.nonce = new QuarkOperationHelper().incrementNonce(nestedOp.nonce); (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, parentOp); @@ -163,7 +163,7 @@ contract CallbacksTest is Test { ScriptType.ScriptAddress ); - parentOp.nonce = nestedOp.nonce + 1; + parentOp.nonce = new QuarkOperationHelper().incrementNonce(nestedOp.nonce); (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, parentOp); diff --git a/test/quark-core/EIP712.t.sol b/test/quark-core/EIP712.t.sol index ce09bd9a..d366926d 100644 --- a/test/quark-core/EIP712.t.sol +++ b/test/quark-core/EIP712.t.sol @@ -73,7 +73,7 @@ contract EIP712Test is Test { assertEq(counter.number(), 3); // nonce is spent - assertEq(stateManager.isNonceSet(address(wallet), op.nonce), true); + assertEq(stateManager.getNonceToken(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(stateManager.isNonceSet(address(wallet), op.nonce), false); + assertEq(stateManager.getNonceToken(address(wallet), op.nonce), bytes32(uint256(0))); } function testStructHash() public { @@ -114,7 +114,7 @@ contract EIP712Test is Test { bytes[] memory scriptSources = new bytes[](1); scriptSources[0] = incrementer; - uint96 nextNonce = 0; + bytes32 nextNonce = bytes32(uint256(0)); bytes memory scriptCalldata = abi.encodeWithSignature("incrementCounter(address)", counter); assertEq(scriptCalldata, hex"e5910ae7000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a"); @@ -137,14 +137,14 @@ contract EIP712Test is Test { verifyingContract: '0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9' }, { QuarkOperation: [ - { name: 'nonce', type: 'uint96' }, + { name: 'nonce', type: 'bytes32' }, { name: 'scriptAddress', type: 'address' }, { name: 'scriptSources', type: 'bytes[]' }, { name: 'scriptCalldata', type: 'bytes' }, { name: 'expiry', type: 'uint256' } ]}, { - nonce: 0, + nonce: '0x0000000000000000000000000000000000000000000000000000000000000000', scriptAddress: '0x5cB7957c702bB6BB8F22aCcf66657F0defd4550b', scriptSources: ['0x608060405234801561001057600080fd5b506102a7806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80636b582b7614610056578063e5910ae714610069575b73f62849f9a0b5bf2913b396098f7c7019b51a820a61005481610077565b005b610054610064366004610230565b610173565b610054610077366004610230565b806001600160a01b031663d09de08a6040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156100b257600080fd5b505af11580156100c6573d6000803e3d6000fd5b50505050806001600160a01b031663d09de08a6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561010557600080fd5b505af1158015610119573d6000803e3d6000fd5b50505050806001600160a01b031663d09de08a6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561015857600080fd5b505af115801561016c573d6000803e3d6000fd5b5050505050565b61017c81610077565b306001600160a01b0316632e716fb16040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101ba573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101de9190610254565b6001600160a01b0316631913592a6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561015857600080fd5b6001600160a01b038116811461022d57600080fd5b50565b60006020828403121561024257600080fd5b813561024d81610218565b9392505050565b60006020828403121561026657600080fd5b815161024d8161021856fea26469706673582212200d71f9cd831b3c67d6f6131f807ee7fc47d21f07fe8f7b90a01dab56abb8403464736f6c63430008170033'], scriptCalldata: '0xe5910ae7000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a', @@ -152,14 +152,14 @@ contract EIP712Test is Test { } ) - 0x1901ce5fced5138ae147492ff6ba56247e9d6f30bbbe45ae60eb0a0135d528a94be4aa19c4de25dfba6a38836420cc4ecf14048cee3f258a3329bfeb40856daf159b + 0x1901ce5fced5138ae147492ff6ba56247e9d6f30bbbe45ae60eb0a0135d528a94be437302412583af420731c67963b8628682b151f38070c3c9142fc40054158666e */ bytes32 domainHash = new SignatureHelper().domainSeparator(wallet_); assertEq(domainHash, hex"ce5fced5138ae147492ff6ba56247e9d6f30bbbe45ae60eb0a0135d528a94be4"); bytes32 structHash = new SignatureHelper().opStructHash(op); - assertEq(structHash, hex"aa19c4de25dfba6a38836420cc4ecf14048cee3f258a3329bfeb40856daf159b"); + assertEq(structHash, hex"37302412583af420731c67963b8628682b151f38070c3c9142fc40054158666e"); } function testRevertsForBadCalldata() public { @@ -182,7 +182,7 @@ contract EIP712Test is Test { assertEq(counter.number(), 0); // nonce is not spent - assertEq(stateManager.isNonceSet(address(wallet), op.nonce), false); + assertEq(stateManager.getNonceToken(address(wallet), op.nonce), bytes32(uint256(0))); } function testRevertsForBadExpiry() public { @@ -204,8 +204,8 @@ contract EIP712Test is Test { // counter is unchanged assertEq(counter.number(), 0); - // alice's nonce is not incremented - assertEq(stateManager.nextNonce(address(wallet)), op.nonce); + // alice's nonce is not set + assertEq(stateManager.getNonceToken(address(wallet), op.nonce), bytes32(uint256(0))); } function testRevertsOnReusedNonce() public { @@ -222,10 +222,10 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, s); assertEq(counter.number(), 3); - assertEq(stateManager.nextNonce(address(wallet)), op.nonce + 1); + assertEq(stateManager.getNonceToken(address(wallet), op.nonce), bytes32(type(uint256).max)); // submitter tries to reuse the same signature twice, for a non-replayable operation - vm.expectRevert(QuarkStateManager.NonceAlreadySet.selector); + vm.expectRevert(abi.encodeWithSelector(QuarkStateManager.NonReplayableNonce.selector, address(wallet), op.nonce, bytes32(type(uint256).max))); wallet.executeQuarkOperation(op, v, r, s); } @@ -248,7 +248,7 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, s); assertEq(counter.number(), 0); - assertEq(stateManager.nextNonce(address(wallet)), op.nonce); + assertEq(stateManager.getNonceToken(address(wallet), op.nonce), bytes32(uint256(0))); } function testRevertsInvalidS() public { @@ -270,7 +270,7 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, invalidS); assertEq(counter.number(), 0); - assertEq(stateManager.nextNonce(address(wallet)), op.nonce); + assertEq(stateManager.getNonceToken(address(wallet), op.nonce), bytes32(uint256(0))); } // TODO: Uncomment when replay tokens are supported @@ -325,15 +325,15 @@ contract EIP712Test is Test { executeWithRequirements, abi.encodeCall( ExecuteWithRequirements.runWithRequirements, - (new uint96[](0), incrementerAddress, abi.encodeWithSignature("incrementCounter(address)", counter)) + (new bytes32[](0), incrementerAddress, abi.encodeWithSignature("incrementCounter(address)", counter)) ), ScriptType.ScriptSource ); (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, wallet, op); // submitter alters the requirements - uint96[] memory badRequirements = new uint96[](1); - badRequirements[0] = 123; + bytes32[] memory badRequirements = new bytes32[](1); + badRequirements[0] = bytes32(uint256(123)); op.scriptCalldata = abi.encodeCall( ExecuteWithRequirements.runWithRequirements, (badRequirements, incrementerAddress, abi.encodeWithSignature("incrementCounter(address)", counter)) @@ -347,7 +347,7 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, s); assertEq(counter.number(), 0); - assertEq(stateManager.nextNonce(address(wallet)), op.nonce); + assertEq(stateManager.getNonceToken(address(wallet), op.nonce), bytes32(uint256(0))); } function testRequirements() public { @@ -362,7 +362,7 @@ contract EIP712Test is Test { QuarkWallet.QuarkOperation memory firstOp = incrementCounterOperation(wallet); (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, wallet, firstOp); - uint96[] memory requirements = new uint96[](1); + bytes32[] memory requirements = new bytes32[](1); requirements[0] = firstOp.nonce; QuarkWallet.QuarkOperation memory dependentOp = new QuarkOperationHelper().newBasicOpWithCalldata( wallet, @@ -374,7 +374,7 @@ contract EIP712Test is Test { ScriptType.ScriptSource ); - dependentOp.nonce = firstOp.nonce + 1; + dependentOp.nonce = new QuarkOperationHelper().incrementNonce(firstOp.nonce); (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, wallet, dependentOp); diff --git a/test/quark-core/Executor.t.sol b/test/quark-core/Executor.t.sol index 57cabf3e..3bb31f1f 100644 --- a/test/quark-core/Executor.t.sol +++ b/test/quark-core/Executor.t.sol @@ -65,12 +65,12 @@ contract ExecutorTest is Test { // execute counter.increment(5) as bob from alice's wallet (that is, from bob's wallet's executor) aliceWallet.executeScript( - stateManager.nextNonce(address(aliceWallet)), + new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWallet), executeOnBehalfAddress, abi.encodeWithSignature( - "run(address,uint96,address,bytes)", + "run(address,bytes32,address,bytes)", address(bobWallet), - stateManager.nextNonce(address(bobWallet)), + new QuarkOperationHelper().semiRandomNonce(stateManager, bobWallet), address(ethcallAddress), abi.encodeWithSignature( "run(address,bytes,uint256)", address(counter), abi.encodeWithSignature("increment(uint256)", 5), 0 @@ -95,9 +95,9 @@ contract ExecutorTest is Test { aliceWallet, executeOnBehalf, abi.encodeWithSignature( - "run(address,uint96,address,bytes)", + "run(address,bytes32,address,bytes)", address(bobWallet), - stateManager.nextNonce(address(bobWallet)), + new QuarkOperationHelper().semiRandomNonce(stateManager, bobWallet), address(ethcallAddress), abi.encodeWithSignature( "run(address,bytes,uint256)", address(counter), abi.encodeWithSignature("increment(uint256)", 3), 0 diff --git a/test/quark-core/Nonce.t.sol b/test/quark-core/Nonce.t.sol index 7e923f09..0538e865 100644 --- a/test/quark-core/Nonce.t.sol +++ b/test/quark-core/Nonce.t.sol @@ -12,6 +12,15 @@ import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; contract NonceTest is Test { QuarkStateManager public stateManager; + /// @notice Represents the unclaimed bytes32 value. + bytes32 public constant CLAIMABLE_TOKEN = bytes32(uint256(0)); + + /// @notice A token that implies a Quark Operation is no longer replayable. + bytes32 public constant NO_REPLAY_TOKEN = bytes32(type(uint256).max); + + bytes32 public constant NONCE_ZERO = bytes32(uint256(0)); + bytes32 public constant NONCE_ONE = bytes32(uint256(1)); + function setUp() public { stateManager = new QuarkStateManager(); console.log("QuarkStateManager deployed to: %s", address(stateManager)); @@ -19,30 +28,23 @@ contract NonceTest is Test { function testIsSet() public { // nonce is unset by default - assertEq(stateManager.isNonceSet(address(this), 0), false); + assertEq(stateManager.getNonceToken(address(this), NONCE_ZERO), CLAIMABLE_TOKEN); + // it can be set - stateManager.claimNonce(0); - assertEq(stateManager.isNonceSet(address(this), 0), true); + stateManager.submitNonceToken(NONCE_ZERO, NO_REPLAY_TOKEN); + assertEq(stateManager.getNonceToken(address(this), NONCE_ZERO), NO_REPLAY_TOKEN); } function testNonLinearNonce() public { // nonce values are not incremental; you can use a random number as // long as it has not been set - uint96 nonce = 1234567890; + bytes32 nonce = bytes32(uint256(1234567890)); - assertEq(stateManager.isNonceSet(address(this), nonce), false); + assertEq(stateManager.getNonceToken(address(this), NONCE_ZERO), CLAIMABLE_TOKEN); - stateManager.claimNonce(nonce); - assertEq(stateManager.isNonceSet(address(this), nonce), true); + stateManager.submitNonceToken(nonce, NO_REPLAY_TOKEN); + assertEq(stateManager.getNonceToken(address(this), nonce), NO_REPLAY_TOKEN); } - function testNextUnusedNonce() public { - uint96 nonce1 = stateManager.nextNonce(address(this)); - - stateManager.claimNonce(nonce1); - assertEq(stateManager.nextNonce(address(this)), nonce1 + 1); - - stateManager.claimNonce(nonce1 + 1); - assertEq(stateManager.nextNonce(address(this)), nonce1 + 2); - } + // TODO: ADD TESTS FOR NONCE CHAIN } diff --git a/test/quark-core/QuarkStateManager.t.sol b/test/quark-core/QuarkStateManager.t.sol index 7d1f5d07..5aeaddc4 100644 --- a/test/quark-core/QuarkStateManager.t.sol +++ b/test/quark-core/QuarkStateManager.t.sol @@ -31,39 +31,47 @@ contract QuarkStateManagerTest is Test { } function testNonceZeroIsValid() public { + bytes32 nonce = bytes32(uint256(0)); + bytes32 NO_REPLAY_TOKEN = stateManager.NO_REPLAY_TOKEN(); + // by default, nonce 0 is not set - assertEq(stateManager.isNonceSet(address(0x123), 0), false); + assertEq(stateManager.getNonceToken(address(0x123), nonce), stateManager.CLAIMABLE_TOKEN()); // nonce 0 can be set manually vm.prank(address(0x123)); - stateManager.claimNonce(0); - assertEq(stateManager.isNonceSet(address(0x123), 0), true); + stateManager.submitNonceToken(nonce, NO_REPLAY_TOKEN); + assertEq(stateManager.getNonceToken(address(0x123), nonce), stateManager.NO_REPLAY_TOKEN()); } - function testSetsAndGetsNextNonces() public { - assertEq(stateManager.nextNonce(address(this)), 0); + // TODO: We should really replace this test with one that + // checks for a replay chain. We can check multiple nonces, but + // it's not strictly as interesting now. + // function testSetsAndGetsNextNonces() public { + // assertEq(stateManager.nextNonce(address(this)), 0); - for (uint96 i = 0; i <= 550; i++) { - stateManager.claimNonce(i); - } + // for (uint96 i = 0; i <= 550; i++) { + // stateManager.claimNonce(i); + // } - assertEq(stateManager.nextNonce(address(this)), 551); + // assertEq(stateManager.nextNonce(address(this)), 551); - for (uint96 i = 552; i <= 570; i++) { - stateManager.claimNonce(i); - } + // for (uint96 i = 552; i <= 570; i++) { + // stateManager.claimNonce(i); + // } - assertEq(stateManager.nextNonce(address(this)), 551); + // assertEq(stateManager.nextNonce(address(this)), 551); - stateManager.claimNonce(551); + // stateManager.claimNonce(551); - assertEq(stateManager.nextNonce(address(this)), 571); - } + // assertEq(stateManager.nextNonce(address(this)), 571); + // } function testRevertsIfNonceIsAlreadySet() public { - stateManager.claimNonce(0); + bytes32 NO_REPLAY_TOKEN = stateManager.NO_REPLAY_TOKEN(); + bytes32 nonce = bytes32(uint256(0)); + stateManager.submitNonceToken(nonce, NO_REPLAY_TOKEN); - vm.expectRevert(abi.encodeWithSelector(QuarkStateManager.NonceAlreadySet.selector)); - stateManager.claimNonce(0); + vm.expectRevert(abi.encodeWithSelector(QuarkStateManager.NonReplayableNonce.selector, address(this), nonce, bytes32(type(uint256).max))); + stateManager.submitNonceToken(nonce, NO_REPLAY_TOKEN); } } diff --git a/test/quark-core/QuarkWallet.t.sol b/test/quark-core/QuarkWallet.t.sol index 2598dfe9..97e4b5a4 100644 --- a/test/quark-core/QuarkWallet.t.sol +++ b/test/quark-core/QuarkWallet.t.sol @@ -35,9 +35,8 @@ contract QuarkWalletTest is Test { } event Ping(uint256); - event ClearNonce(address indexed wallet, uint96 nonce); event ExecuteQuarkScript( - address indexed executor, address indexed scriptAddress, uint96 indexed nonce, ExecutionType executionType + address indexed executor, address indexed scriptAddress, bytes32 indexed nonce, ExecutionType executionType ); CodeJar public codeJar; @@ -116,7 +115,7 @@ contract QuarkWalletTest is Test { vm.pauseGasMetering(); QuarkWallet aliceWalletExecutable = newWallet(aliceAccount, aliceAccount); bytes memory getMessageDetails = new YulHelper().getCode("GetMessageDetails.sol/GetMessageDetails.json"); - uint96 nonce = stateManager.nextNonce(address(aliceWalletExecutable)); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWalletExecutable); address scriptAddress = codeJar.saveCode(getMessageDetails); bytes memory call = abi.encodeWithSignature("getMsgSenderAndValue()"); @@ -147,7 +146,7 @@ contract QuarkWalletTest is Test { QuarkWallet.QuarkOperation memory opWithScriptSource = new QuarkOperationHelper().newBasicOpWithCalldata( aliceWallet, getMessageDetails, abi.encodeWithSignature("getMsgSenderAndValue()"), ScriptType.ScriptSource ); - opWithScriptSource.nonce += 1; + opWithScriptSource.nonce = new QuarkOperationHelper().incrementNonce(opWithScriptSource.nonce); (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, opWithScriptSource); address scriptAddress = opWithScriptAddress.scriptAddress; @@ -168,7 +167,7 @@ contract QuarkWalletTest is Test { vm.pauseGasMetering(); QuarkWallet aliceWalletExecutable = newWallet(aliceAccount, aliceAccount); bytes memory getMessageDetails = new YulHelper().getCode("GetMessageDetails.sol/GetMessageDetails.json"); - uint96 nonce = stateManager.nextNonce(address(aliceWalletExecutable)); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWalletExecutable); address scriptAddress = codeJar.saveCode(getMessageDetails); bytes memory call = abi.encodeWithSignature("getMsgSenderAndValue()"); @@ -188,7 +187,7 @@ contract QuarkWalletTest is Test { vm.pauseGasMetering(); QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ - nonce: stateManager.nextNonce(address(aliceWallet)), + nonce: new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWallet), scriptAddress: address(0), scriptSources: new bytes[](0), scriptCalldata: bytes(""), @@ -204,7 +203,7 @@ contract QuarkWalletTest is Test { aliceWallet.executeQuarkOperation(op, v, r, s); // direct execution of the null script will revert - uint96 nonce = stateManager.nextNonce(address(aliceWallet)); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWallet); vm.prank(IHasSignerExecutor(address(aliceWallet)).executor()); vm.expectRevert(abi.encodeWithSelector(QuarkWallet.EmptyCode.selector)); aliceWallet.executeScript(nonce, address(0), bytes(""), new bytes[](0)); @@ -217,7 +216,7 @@ contract QuarkWalletTest is Test { // operation containing a valid empty script will revert QuarkWallet.QuarkOperation memory op2 = QuarkWallet.QuarkOperation({ - nonce: stateManager.nextNonce(address(aliceWallet)), + nonce: new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWallet), scriptAddress: emptyCodeAddress, scriptSources: new bytes[](0), scriptCalldata: bytes(""), @@ -233,7 +232,7 @@ contract QuarkWalletTest is Test { aliceWallet.executeQuarkOperation(op2, v2, r2, s2); // direct execution of empty script will revert - uint96 nonce2 = stateManager.nextNonce(address(aliceWallet)); + bytes32 nonce2 = new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWallet); vm.prank(IHasSignerExecutor(address(aliceWallet)).executor()); vm.expectRevert(abi.encodeWithSelector(QuarkWallet.EmptyCode.selector)); aliceWallet.executeScript(nonce2, emptyCodeAddress, bytes(""), new bytes[](0)); @@ -247,7 +246,7 @@ contract QuarkWalletTest is Test { scriptSources[0] = new YulHelper().stub(hex"f00f00"); QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ - nonce: stateManager.nextNonce(address(aliceWallet)), + nonce: new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWallet), scriptAddress: address(0xc0c0), scriptSources: scriptSources, scriptCalldata: bytes("feefee"), @@ -399,7 +398,7 @@ contract QuarkWalletTest is Test { // // can cancel the replayable nonce... // vm.pauseGasMetering(); // QuarkWallet.QuarkOperation memory cancelOtherOp = new QuarkOperationHelper().newBasicOpWithCalldata( - // aliceWallet, cancelOtherScript, abi.encodeWithSignature("run(uint96)", op.nonce), ScriptType.ScriptAddress + // 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); @@ -419,11 +418,11 @@ contract QuarkWalletTest is Test { QuarkWallet aliceWalletExecutable = newWallet(aliceAccount, aliceAccount); bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); address incrementerAddress = codeJar.saveCode(incrementer); - uint96 nonce = stateManager.nextNonce(address(aliceWalletExecutable)); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWalletExecutable); bytes memory call = abi.encodeWithSignature("incrementCounter(address)", counter); assertEq(counter.number(), 0); - assertEq(stateManager.nextNonce(address(aliceWalletExecutable)), 0); + assertEq(stateManager.getNonceToken(address(aliceWalletExecutable), nonce), bytes32(uint256(0))); // act as the executor for the wallet vm.startPrank(aliceAccount); @@ -433,7 +432,7 @@ contract QuarkWalletTest is Test { aliceWalletExecutable.executeScript(nonce, incrementerAddress, call, new bytes[](0)); assertEq(counter.number(), 3); - assertEq(stateManager.nextNonce(address(aliceWalletExecutable)), 1); + assertEq(stateManager.getNonceToken(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); } function testDirectExecuteFromOtherQuarkWallet() public { @@ -443,12 +442,13 @@ contract QuarkWalletTest is Test { bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); bytes memory ethcall = new YulHelper().getCode("Ethcall.sol/Ethcall.json"); address incrementerAddress = codeJar.saveCode(incrementer); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWalletExecutable); bytes memory ethcallCalldata = abi.encodeWithSelector( Ethcall.run.selector, address(aliceWalletExecutable), abi.encodeWithSignature( - "executeScript(uint96,address,bytes,bytes[])", - stateManager.nextNonce(address(aliceWalletExecutable)), + "executeScript(bytes32,address,bytes,bytes[])", + nonce, incrementerAddress, abi.encodeWithSignature("incrementCounter(address)", counter), new bytes[](0) @@ -462,14 +462,14 @@ contract QuarkWalletTest is Test { (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); assertEq(counter.number(), 0); - assertEq(stateManager.nextNonce(address(aliceWalletExecutable)), 0); + assertEq(stateManager.getNonceToken(address(aliceWalletExecutable), nonce), bytes32(uint256(0))); // gas: meter execute vm.resumeGasMetering(); aliceWallet.executeQuarkOperation(op, v, r, s); assertEq(counter.number(), 3); - assertEq(stateManager.nextNonce(address(aliceWalletExecutable)), 1); + assertEq(stateManager.getNonceToken(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); } function testDirectExecuteWithScriptSources() public { @@ -478,13 +478,13 @@ contract QuarkWalletTest is Test { QuarkWallet aliceWalletExecutable = newWallet(aliceAccount, aliceAccount); bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); address incrementerAddress = codeJar.getCodeAddress(incrementer); - uint96 nonce = stateManager.nextNonce(address(aliceWalletExecutable)); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWalletExecutable); bytes memory call = abi.encodeWithSignature("incrementCounter(address)", counter); bytes[] memory scriptSources = new bytes[](1); scriptSources[0] = incrementer; assertEq(counter.number(), 0); - assertEq(stateManager.nextNonce(address(aliceWalletExecutable)), 0); + assertEq(stateManager.getNonceToken(address(aliceWalletExecutable), nonce), bytes32(uint256(0))); // act as the executor for the wallet vm.startPrank(aliceAccount); @@ -494,7 +494,7 @@ contract QuarkWalletTest is Test { aliceWalletExecutable.executeScript(nonce, incrementerAddress, call, scriptSources); assertEq(counter.number(), 3); - assertEq(stateManager.nextNonce(address(aliceWalletExecutable)), 1); + assertEq(stateManager.getNonceToken(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); } function testRevertsForDirectExecuteByNonExecutorSigner() public { @@ -507,7 +507,7 @@ contract QuarkWalletTest is Test { vm.startPrank(IHasSignerExecutor(address(aliceWallet)).signer()); // pre-compute execution parameters so that the revert is expected from the right call - uint96 nonce = stateManager.nextNonce(address(aliceWallet)); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWallet); address target = codeJar.saveCode(incrementer); bytes memory call = abi.encodeWithSignature("incrementCounter(address)", counter); @@ -529,7 +529,7 @@ contract QuarkWalletTest is Test { assertEq(counter.number(), 0); // pre-compute execution parameters so that the revert is expected from the right call - uint96 nonce = stateManager.nextNonce(address(aliceWallet)); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWallet); address target = codeJar.saveCode(incrementer); bytes memory call = abi.encodeWithSignature("incrementCounter(address)", counter); @@ -574,7 +574,7 @@ contract QuarkWalletTest is Test { abi.encodeWithSignature("incrementCounter(address)", counter), ScriptType.ScriptAddress ); - op2.nonce = op1.nonce + 1; + op2.nonce = new QuarkOperationHelper().incrementNonce(op1.nonce); bytes32 op2Digest = new SignatureHelper().opDigest(address(aliceWallet), op2); bytes32[] memory opDigests = new bytes32[](2); @@ -617,7 +617,7 @@ contract QuarkWalletTest is Test { abi.encodeWithSignature("incrementCounter(address)", counter), ScriptType.ScriptAddress ); - op2.nonce = op1.nonce + 1; + op2.nonce = new QuarkOperationHelper().incrementNonce(op1.nonce); bytes32[] memory opDigests = new bytes32[](1); opDigests[0] = op1Digest; @@ -654,7 +654,7 @@ contract QuarkWalletTest is Test { abi.encodeWithSignature("incrementCounter(address)", counter), ScriptType.ScriptAddress ); - op2.nonce = op1.nonce + 1; + op2.nonce = new QuarkOperationHelper().incrementNonce(op1.nonce); bytes32 op2Digest = new SignatureHelper().opDigest(address(aliceWallet), op2); bytes32[] memory opDigests = new bytes32[](2); @@ -669,7 +669,7 @@ contract QuarkWalletTest is Test { assertEq(counter.number(), 3); // call again using the same operation - vm.expectRevert(abi.encodeWithSelector(QuarkStateManager.NonceAlreadySet.selector)); + vm.expectRevert(abi.encodeWithSelector(QuarkStateManager.NonReplayableNonce.selector, address(aliceWallet), op1.nonce, bytes32(type(uint256).max))); aliceWallet.executeMultiQuarkOperation(op1, opDigests, v, r, s); assertEq(counter.number(), 3); @@ -782,7 +782,7 @@ contract QuarkWalletTest is Test { Run `cat test/lib/Ping.yul0 | solc --bin --yul --evm-version paris -` */ - uint96 nonce = aliceWallet.stateManager().nextNonce(address(aliceWallet)); + bytes32 nonce = bytes32(uint256(1)); bytes memory pingCode = hex"6000356000527f48257dc961b6f792c2b78a080dacfed693b660960a702de21cee364e20270e2f60206000a1600080f3"; bytes memory pingInitCode = new YulHelper().stub(pingCode); @@ -801,7 +801,7 @@ contract QuarkWalletTest is Test { assertEq(block.chainid, 31337); assertEq(address(aliceWallet), address(0xc7183455a4C133Ae270771860664b6B7ec320bB1)); - assertEq(nonce, 0); // nonce + assertEq(nonce, bytes32(uint256(1))); // nonce assertEq(scriptAddress, address(0x4a925cF75dcc5708671004d9bbFAf4DCF2C762B0)); // scriptAddress assertEq(scriptSources.length, 1); // scriptSources assertEq( @@ -828,14 +828,14 @@ contract QuarkWalletTest is Test { verifyingContract: '0xc7183455a4C133Ae270771860664b6B7ec320bB1' }, { QuarkOperation: [ - { name: 'nonce', type: 'uint96' }, + { name: 'nonce', type: 'bytes32' }, { name: 'scriptAddress', type: 'address' }, { name: 'scriptSources', type: 'bytes[]' }, { name: 'scriptCalldata', type: 'bytes' }, { name: 'expiry', type: 'uint256' } ]}, { - nonce: 0, + nonce: '0x0000000000000000000000000000000000000000000000000000000000000001', scriptAddress: '0x4a925cF75dcc5708671004d9bbFAf4DCF2C762B0', scriptSources: ['0x630000003080600e6000396000f36000356000527f48257dc961b6f792c2b78a080dacfed693b660960a702de21cee364e20270e2f60206000a1600080f3'], scriptCalldata: '0x00000000000000000000000000000000000000000000000000000000000000dd', @@ -845,7 +845,7 @@ contract QuarkWalletTest is Test { */ bytes memory sigHash = - hex"1901420cb4769bd47ac11897b8b69b8d80a84b9ec8b69437cd42529681d583a6b5216eda58953a1afd7dbc4ddbbef80dbca893fb0a87251b79a6b856708f619d9fcc"; + hex"1901420cb4769bd47ac11897b8b69b8d80a84b9ec8b69437cd42529681d583a6b5211a9548fcdcb39c227cdd2ff9e13fbefc3db707f8e2216139758d6fc20328fcfd"; (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, keccak256(sigHash)); // gas: meter execute @@ -1209,10 +1209,11 @@ contract QuarkWalletTest is Test { function testRevertOnAllPrecompilesDirectCall() public { vm.pauseGasMetering(); - uint96 nonce = stateManager.nextNonce(address(aliceWallet)); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWallet); for (uint256 i = 1; i <= 9; i++) { vm.pauseGasMetering(); - QuarkWallet.QuarkOperation memory op = DummyQuarkOperation(address(uint160(i)), nonce++); + nonce = new QuarkOperationHelper().incrementNonce(nonce); + QuarkWallet.QuarkOperation memory op = DummyQuarkOperation(address(uint160(i)), nonce); (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); vm.resumeGasMetering(); vm.expectRevert(abi.encodeWithSelector(QuarkWallet.EmptyCode.selector)); @@ -1220,7 +1221,7 @@ contract QuarkWalletTest is Test { } } - function DummyQuarkOperation(address preCompileAddress, uint96 nonce) + function DummyQuarkOperation(address preCompileAddress, bytes32 nonce) internal view returns (QuarkWallet.QuarkOperation memory) diff --git a/test/quark-proxy/QuarkWalletProxyFactory.t.sol b/test/quark-proxy/QuarkWalletProxyFactory.t.sol index a6dc3454..669bfa4b 100644 --- a/test/quark-proxy/QuarkWalletProxyFactory.t.sol +++ b/test/quark-proxy/QuarkWalletProxyFactory.t.sol @@ -11,6 +11,8 @@ import {QuarkWallet, IHasSignerExecutor} from "quark-core/src/QuarkWallet.sol"; import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol"; +import {QuarkOperationHelper} from "test/lib/QuarkOperationHelper.sol"; + import {Counter} from "test/lib/Counter.sol"; import {Ethcall} from "quark-core-scripts/src/Ethcall.sol"; @@ -112,7 +114,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources[0] = incrementer; address incrementerAddress = codeJar.getCodeAddress(incrementer); - uint96 nonce = stateManager.nextNonce(factory.walletAddressFor(alice, address(0))); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, QuarkWallet(payable(factory.walletAddressFor(alice, address(0))))); QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, scriptSources: scriptSources, @@ -140,7 +142,7 @@ contract QuarkWalletProxyFactoryTest is Test { assertEq(counter.number(), 3); // uses up the operation's nonce - assertEq(stateManager.isNonceSet(factory.walletAddressFor(alice, address(0)), nonce), true); + assertEq(stateManager.getNonceToken(factory.walletAddressFor(alice, address(0)), nonce), bytes32(type(uint256).max)); } function testCreateAndExecuteWithSalt() public { @@ -153,7 +155,7 @@ contract QuarkWalletProxyFactoryTest is Test { scriptSources[0] = incrementer; address incrementerAddress = codeJar.getCodeAddress(incrementer); - uint96 nonce = stateManager.nextNonce(factory.walletAddressFor(alice, address(0))); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, QuarkWallet(payable(factory.walletAddressFor(alice, address(0))))); QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, scriptSources: scriptSources, @@ -183,7 +185,7 @@ contract QuarkWalletProxyFactoryTest is Test { assertEq(counter.number(), 3); // uses up the operation's nonce - assertEq(stateManager.isNonceSet(factory.walletAddressForSalt(alice, address(0), salt), nonce), true); + assertEq(stateManager.getNonceToken(factory.walletAddressForSalt(alice, address(0), salt), nonce), bytes32(type(uint256).max)); } function testExecuteOnExistingWallet() public { @@ -195,7 +197,7 @@ contract QuarkWalletProxyFactoryTest is Test { bytes[] memory scriptSources = new bytes[](1); scriptSources[0] = incrementer; - uint96 nonce = stateManager.nextNonce(factory.walletAddressFor(alice, address(0))); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, QuarkWallet(payable(factory.walletAddressFor(alice, address(0))))); address incrementerAddress = codeJar.getCodeAddress(incrementer); QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, @@ -226,7 +228,7 @@ contract QuarkWalletProxyFactoryTest is Test { assertEq(counter.number(), 3); // uses up the operation's nonce - assertEq(stateManager.isNonceSet(factory.walletAddressFor(alice, address(0)), nonce), true); + assertEq(stateManager.getNonceToken(factory.walletAddressFor(alice, address(0)), nonce), bytes32(type(uint256).max)); } /* ===== create and execute MultiQuarkOperation tests ===== */ @@ -246,7 +248,7 @@ contract QuarkWalletProxyFactoryTest is Test { address incrementerAddress = codeJar.getCodeAddress(incrementer); address aliceWalletAddress = factory.walletAddressFor(alice, address(0)); - uint96 nonce = stateManager.nextNonce(aliceWalletAddress); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, QuarkWallet(payable(aliceWalletAddress))); QuarkWallet.QuarkOperation memory op1 = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, scriptSources: scriptSources, @@ -260,10 +262,10 @@ contract QuarkWalletProxyFactoryTest is Test { scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), - nonce: nonce + 1, + nonce: new QuarkOperationHelper().incrementNonce(nonce), expiry: block.timestamp + 1000 }); - op2.nonce = op1.nonce + 1; + op2.nonce = new QuarkOperationHelper().incrementNonce(op1.nonce); bytes32 op2Digest = new SignatureHelper().opDigest(aliceWalletAddress, op2); bytes32[] memory opDigests = new bytes32[](2); @@ -279,13 +281,13 @@ contract QuarkWalletProxyFactoryTest is Test { factory.createAndExecuteMulti(alice, address(0), op1, opDigests, v, r, s); assertEq(counter.number(), 3); - assertEq(stateManager.isNonceSet(aliceWalletAddress, op1.nonce), true); + assertEq(stateManager.getNonceToken(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(stateManager.isNonceSet(aliceWalletAddress, op2.nonce), true); + assertEq(stateManager.getNonceToken(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); } function testCreateAndExecuteMultiWithSalt() public { @@ -300,7 +302,7 @@ contract QuarkWalletProxyFactoryTest is Test { address incrementerAddress = codeJar.getCodeAddress(incrementer); bytes32 salt = bytes32("salty salt salt"); address aliceWalletAddress = factory.walletAddressForSalt(alice, address(0), salt); - uint96 nonce = stateManager.nextNonce(aliceWalletAddress); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, QuarkWallet(payable(aliceWalletAddress))); QuarkWallet.QuarkOperation memory op1 = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, scriptSources: scriptSources, @@ -314,10 +316,10 @@ contract QuarkWalletProxyFactoryTest is Test { scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), - nonce: nonce + 1, + nonce: new QuarkOperationHelper().incrementNonce(nonce), expiry: block.timestamp + 1000 }); - op2.nonce = op1.nonce + 1; + op2.nonce = new QuarkOperationHelper().incrementNonce(op1.nonce); bytes32 op2Digest = new SignatureHelper().opDigest(aliceWalletAddress, op2); bytes32[] memory opDigests = new bytes32[](2); @@ -333,13 +335,13 @@ contract QuarkWalletProxyFactoryTest is Test { factory.createAndExecuteMulti(alice, address(0), salt, op1, opDigests, v, r, s); assertEq(counter.number(), 3); - assertEq(stateManager.isNonceSet(aliceWalletAddress, op1.nonce), true); + assertEq(stateManager.getNonceToken(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(stateManager.isNonceSet(aliceWalletAddress, op2.nonce), true); + assertEq(stateManager.getNonceToken(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); } function testExecuteMultiOnExistingWallet() public { @@ -357,7 +359,7 @@ contract QuarkWalletProxyFactoryTest is Test { address incrementerAddress = codeJar.getCodeAddress(incrementer); address aliceWalletAddress = factory.walletAddressFor(alice, address(0)); - uint96 nonce = stateManager.nextNonce(aliceWalletAddress); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, QuarkWallet(payable(aliceWalletAddress))); QuarkWallet.QuarkOperation memory op1 = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, scriptSources: scriptSources, @@ -371,10 +373,10 @@ contract QuarkWalletProxyFactoryTest is Test { scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), - nonce: nonce + 1, + nonce: new QuarkOperationHelper().incrementNonce(nonce), expiry: block.timestamp + 1000 }); - op2.nonce = op1.nonce + 1; + op2.nonce = new QuarkOperationHelper().incrementNonce(op1.nonce); bytes32 op2Digest = new SignatureHelper().opDigest(aliceWalletAddress, op2); bytes32[] memory opDigests = new bytes32[](2); @@ -394,13 +396,13 @@ contract QuarkWalletProxyFactoryTest is Test { factory.createAndExecuteMulti(alice, address(0), op1, opDigests, v, r, s); assertEq(counter.number(), 3); - assertEq(stateManager.isNonceSet(aliceWalletAddress, op1.nonce), true); + assertEq(stateManager.getNonceToken(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(stateManager.isNonceSet(aliceWalletAddress, op2.nonce), true); + assertEq(stateManager.getNonceToken(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); } /* ===== msg.value and msg.sender tests ===== */ @@ -410,7 +412,7 @@ contract QuarkWalletProxyFactoryTest is Test { vm.pauseGasMetering(); bytes memory getMessageDetails = new YulHelper().getCode("GetMessageDetails.sol/GetMessageDetails.json"); address aliceWallet = factory.walletAddressFor(alice, address(0)); - uint96 nonce = stateManager.nextNonce(aliceWallet); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, QuarkWallet(payable(aliceWallet))); address getMessageDetailsAddress = codeJar.getCodeAddress(getMessageDetails); @@ -440,7 +442,7 @@ contract QuarkWalletProxyFactoryTest is Test { assertEq(msgValue, 0); // uses up the operation's nonce - assertEq(stateManager.isNonceSet(aliceWallet, nonce), true); + assertEq(stateManager.getNonceToken(aliceWallet, nonce), bytes32(type(uint256).max)); } function testCreateAndExecuteWithSaltSetsMsgSender() public { @@ -449,7 +451,7 @@ contract QuarkWalletProxyFactoryTest is Test { bytes memory getMessageDetails = new YulHelper().getCode("GetMessageDetails.sol/GetMessageDetails.json"); bytes32 salt = bytes32("salty salt salt"); address aliceWallet = factory.walletAddressForSalt(alice, address(0), salt); - uint96 nonce = stateManager.nextNonce(aliceWallet); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(stateManager, QuarkWallet(payable(aliceWallet))); address getMessageDetailsAddress = codeJar.getCodeAddress(getMessageDetails); QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ @@ -490,7 +492,7 @@ contract QuarkWalletProxyFactoryTest is Test { assertEq(msgValue, 0); // uses up the operation's nonce - assertEq(stateManager.isNonceSet(aliceWallet, nonce), true); + assertEq(stateManager.getNonceToken(aliceWallet, nonce), bytes32(type(uint256).max)); } /* ===== default wallet executor role tests ===== */ @@ -525,15 +527,15 @@ contract QuarkWalletProxyFactoryTest is Test { scriptAddress: executeOnBehalfAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature( - "run(address,uint96,address,bytes)", + "run(address,bytes32,address,bytes)", address(aliceWalletSecondary), - stateManager.nextNonce(address(aliceWalletSecondary)), + new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWalletSecondary), ethcallAddress, abi.encodeWithSignature( "run(address,bytes,uint256)", address(counter), abi.encodeWithSignature("increment(uint256)", 7), 0 ) ), - nonce: stateManager.nextNonce(address(aliceWalletPrimary)), + nonce: new QuarkOperationHelper().semiRandomNonce(stateManager, aliceWalletPrimary), expiry: block.timestamp + 1000 }); (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWalletPrimary, op);