diff --git a/.gas-snapshot b/.gas-snapshot index 0ba08261..c0d4c1b3 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,182 +1,206 @@ -BatchExecutorTest:testBatchExecuteWithPartialFailures() (gas: 129689) -BatchExecutorTest:testBatchExecuteWithPartialFailuresDoesNotRevertIfAnyCallsRevert() (gas: 123262) -BatchExecutorTest:testBatchExecuteWithoutPartialFailures() (gas: 129953) -BatchExecutorTest:testBatchExecuteWithoutPartialFailuresRevertsIfAnyCallsRevert() (gas: 120583) -CallbacksTest:testAllowNestedCallbacks() (gas: 157701) -CallbacksTest:testCallbackFromCounter() (gas: 100757) -CallbacksTest:testCallcodeReentrancyExploitWithProtectedScript() (gas: 104642) -CallbacksTest:testCallcodeReentrancyExploitWithUnprotectedScript() (gas: 110546) -CallbacksTest:testCallcodeReentrancyProtectionWithProtectedScript() (gas: 198448) -CallbacksTest:testClearCallback() (gas: 141519) -CallbacksTest:testNestedCallWithNoCallbackSucceeds() (gas: 124467) -CallbacksTest:testPayableCallback() (gas: 108389) -CallbacksTest:testRevertsOnCallbackWhenNoActiveCallback() (gas: 98345) -CodeJarTest:testCodeJarCanBeWacky() (gas: 139555) -CodeJarTest:testCodeJarCanDeployCodeThatHadEthSent() (gas: 4945665) -CodeJarTest:testCodeJarCounter() (gas: 358702) -CodeJarTest:testCodeJarDeployNotAffectedByChangedCodeHash() (gas: 1052869) -CodeJarTest:testCodeJarDeploysAnother() (gas: 236368) -CodeJarTest:testCodeJarDifferentZeros() (gas: 2033220) -CodeJarTest:testCodeJarFirstDeploy() (gas: 1016149) -CodeJarTest:testCodeJarInputVariety() (gas: 20799111) -CodeJarTest:testCodeJarLarge() (gas: 76749588) -CodeJarTest:testCodeJarRefusesToDeployEmptyCode() (gas: 2963612) -CodeJarTest:testCodeJarSecondDeploy() (gas: 1018505) -CodeJarTest:testCodeJarSelfDestruct() (gas: 1022534) -CodeJarTest:testCodeJarStoresSelfReference() (gas: 107362) -CodeJarTest:testCodeJarTickCounter() (gas: 176903) -CodeJarTest:testRevertsOnConstructorRevert() (gas: 85881) -CodeJarTest:testRevertsOnSelfDestructingConstructor() (gas: 54396) -ConditionalMulticallTest:testConditionalRunEmptyInputIsValid() (gas: 52025) -ConditionalMulticallTest:testConditionalRunInvalidInput() (gas: 69668) -ConditionalMulticallTest:testConditionalRunMulticallError() (gas: 317487) -ConditionalMulticallTest:testConditionalRunOnPeriodicRepay() (gas: 344428) -ConditionalMulticallTest:testConditionalRunPassed() (gas: 285870) -ConditionalMulticallTest:testConditionalRunUnmet() (gas: 104614) -EIP1271Test:testReturnsMagicValueForValidSignature() (gas: 74964) -EIP1271Test:testRevertsIfSignerContractDoesNotReturnMagic() (gas: 18700) -EIP712Test:testExecuteQuarkOperation() (gas: 74375) -EIP712Test:testNonceIsNotSetForReplayableOperation() (gas: 143904) -EIP712Test:testRequirements() (gas: 175383) -EIP712Test:testRevertBadRequirements() (gas: 27347) -EIP712Test:testRevertsForBadCalldata() (gas: 21387) -EIP712Test:testRevertsForBadCode() (gas: 23453) -EIP712Test:testRevertsForBadExpiry() (gas: 21940) -EIP712Test:testRevertsForExpiredSignature() (gas: 14842) -EIP712Test:testRevertsInvalidS() (gas: 18518) -EIP712Test:testRevertsOnReusedNonce() (gas: 92932) +BatchCallbackTest:testBatchCallWithCallback() (gas: 113046) +BatchExecutorTest:testBatchExecuteWithPartialFailures() (gas: 120248) +BatchExecutorTest:testBatchExecuteWithPartialFailuresDoesNotRevertIfAnyCallsRevert() (gas: 97967) +BatchExecutorTest:testBatchExecuteWithoutPartialFailures() (gas: 120555) +BatchExecutorTest:testBatchExecuteWithoutPartialFailuresRevertsIfAnyCallsRevert() (gas: 95516) +CallbacksTest:testAllowNestedCallbacks() (gas: 115811) +CallbacksTest:testCallbackFromCounter() (gas: 69183) +CallbacksTest:testCallcodeReentrancyExploitWithProtectedScript() (gas: 64790) +CallbacksTest:testCallcodeReentrancyExploitWithUnprotectedScript() (gas: 70779) +CallbacksTest:testCallcodeReentrancyProtectionWithProtectedScript() (gas: 113241) +CallbacksTest:testNestedCallWithNoCallbackSucceeds() (gas: 109049) +CallbacksTest:testPayableCallback() (gas: 76729) +CallbacksTest:testRevertsOnCallbackWhenNoActiveCallback() (gas: 72391) +CallbacksTest:testSimpleCallback() (gas: 247105) +CallbacksTest:testWithClearedCallback() (gas: 137539) +CallbacksTest:testWithoutAllowCallback() (gas: 137308) +CodeJarTest:testCodeJarCanBeWacky() (gas: 133917) +CodeJarTest:testCodeJarCanDeployCodeThatHadEthSent() (gas: 4705748) +CodeJarTest:testCodeJarCounter() (gas: 349138) +CodeJarTest:testCodeJarDeployNotAffectedByChangedCodeHash() (gas: 1005025) +CodeJarTest:testCodeJarDeploysAnother() (gas: 212161) +CodeJarTest:testCodeJarDifferentZeros() (gas: 1935201) +CodeJarTest:testCodeJarFirstDeploy() (gas: 968124) +CodeJarTest:testCodeJarInputVariety() (gas: 19788591) +CodeJarTest:testCodeJarLarge() (gas: 73621689) +CodeJarTest:testCodeJarRefusesToDeployEmptyCode() (gas: 2820030) +CodeJarTest:testCodeJarSecondDeploy() (gas: 970419) +CodeJarTest:testCodeJarSelfDestruct() (gas: 974534) +CodeJarTest:testCodeJarStoresSelfReference() (gas: 105606) +CodeJarTest:testCodeJarTickCounter() (gas: 162059) +CodeJarTest:testRevertsOnConstructorRevert() (gas: 85595) +CodeJarTest:testRevertsOnSelfDestructingConstructor() (gas: 54228) +ConditionalMulticallTest:testConditionalRunEmptyInputIsValid() (gas: 45625) +ConditionalMulticallTest:testConditionalRunInvalidInput() (gas: 45725) +ConditionalMulticallTest:testConditionalRunMulticallError() (gas: 289702) +ConditionalMulticallTest:testConditionalRunOnPeriodicRepay() (gas: 292132) +ConditionalMulticallTest:testConditionalRunPassed() (gas: 268006) +ConditionalMulticallTest:testConditionalRunUnmet() (gas: 79693) +EIP1271Test:testReturnsMagicValueForValidSignature() (gas: 69743) +EIP1271Test:testRevertsIfSignerContractDoesNotReturnMagic() (gas: 17088) +EIP712Test:testExecuteQuarkOperation() (gas: 70970) +EIP712Test:testRequirements() (gas: 165373) +EIP712Test:testRevertBadRequirements() (gas: 24146) +EIP712Test:testRevertsForBadCalldata() (gas: 19472) +EIP712Test:testRevertsForBadCode() (gas: 21433) +EIP712Test:testRevertsForBadExpiry() (gas: 19780) +EIP712Test:testRevertsForExpiredSignature() (gas: 12708) +EIP712Test:testRevertsInvalidS() (gas: 16378) +EIP712Test:testRevertsOnReusedNonce() (gas: 82781) EIP712Test:testStructHash() (gas: 9223372036854754743) -EthcallTest:testEthcallCallReraiseError() (gas: 77132) -EthcallTest:testEthcallCounter() (gas: 70500) -EthcallTest:testEthcallShouldReturnCallResult() (gas: 53256) -EthcallTest:testEthcallSupplyUSDCToComet() (gas: 173400) -EthcallTest:testEthcallWithdrawUSDCFromComet() (gas: 298210) -ExecutorTest:testExecutorCanDirectCall() (gas: 132195) -ExecutorTest:testExecutorCanDirectCallBySig() (gas: 118698) -MulticallTest:testCallcodeToMulticallSucceedsWhenUninitialized() (gas: 84259) -MulticallTest:testCreateSubWalletAndExecute() (gas: 602979) -MulticallTest:testEmptyInputIsValid() (gas: 51541) -MulticallTest:testExecutorCanMulticallAcrossSubwallets() (gas: 305285) -MulticallTest:testInvalidInput() (gas: 69156) -MulticallTest:testInvokeCounterTwice() (gas: 84413) -MulticallTest:testMulticallError() (gas: 310696) -MulticallTest:testMulticallShouldReturnCallResults() (gas: 87559) +EthcallTest:testEthcallCallReraiseError() (gas: 54102) +EthcallTest:testEthcallCounter() (gas: 65854) +EthcallTest:testEthcallShouldReturnCallResult() (gas: 46717) +EthcallTest:testEthcallSupplyUSDCToComet() (gas: 154299) +EthcallTest:testEthcallWithdrawUSDCFromComet() (gas: 307596) +ExecutorTest:testExecutorCanDirectCall() (gas: 101209) +ExecutorTest:testExecutorCanDirectCallBySig() (gas: 104404) +MulticallTest:testCallcodeToMulticallSucceedsWhenUninitialized() (gas: 73864) +MulticallTest:testCreateSubWalletAndExecute() (gas: 596442) +MulticallTest:testEmptyInputIsValid() (gas: 45286) +MulticallTest:testExecutorCanMulticallAcrossSubwallets() (gas: 253358) +MulticallTest:testInvalidInput() (gas: 45497) +MulticallTest:testInvokeCounterTwice() (gas: 74018) +MulticallTest:testMulticallError() (gas: 284883) +MulticallTest:testMulticallShouldReturnCallResults() (gas: 76709) MulticallTest:testRevertsForInvalidCallContext() (gas: 11778) -MulticallTest:testSupplyWETHWithdrawUSDCOnComet() (gas: 260802) -NonceTest:testIsSet() (gas: 30425) -NonceTest:testNextUnusedNonce() (gas: 40315) -NonceTest:testNonLinearNonce() (gas: 30757) -PaycallTest:testInitializeProperlyFromConstructor() (gas: 6567) -PaycallTest:testPaycallForPayWithUSDT() (gas: 126172) -PaycallTest:testPaycallForPayWithWBTC() (gas: 120663) -PaycallTest:testPaycallRevertsWhenCallReverts() (gas: 73223) -PaycallTest:testReturnCallResult() (gas: 93230) -PaycallTest:testRevertsForInvalidCallContext() (gas: 16811) -PaycallTest:testRevertsWhenCostIsMoreThanMaxPaymentCost() (gas: 120410) -PaycallTest:testSimpleCounterAndPayWithUSDC() (gas: 150109) -PaycallTest:testSimpleTransferTokenAndPayWithUSDC() (gas: 145456) -PaycallTest:testSupplyWETHWithdrawUSDCOnCometAndPayWithUSDC() (gas: 306454) -QuarkFactoryTest:testInvariantAddressesBetweenNonces() (gas: 3361264) -QuarkFactoryTest:testQuarkFactoryDeployToDeterministicAddresses() (gas: 3360997) -QuarkFactoryTest:testQuarkFactoryDeployTwice() (gas: 3454221) -QuarkMinimalProxyTest:testSignerExecutor() (gas: 75518) -QuarkStateManagerTest:testNonceZeroIsValid() (gas: 91493) -QuarkStateManagerTest:testReadStorageForWallet() (gas: 148724) -QuarkStateManagerTest:testRevertsForNoActiveNonce() (gas: 22564) -QuarkStateManagerTest:testRevertsIfScriptAddressIsEOA() (gas: 61808) -QuarkStateManagerTest:testRevertsIfScriptAddressIsNull() (gas: 42235) -QuarkStateManagerTest:testSetActiveNonceAndCallbackNotImplemented() (gas: 92067) -QuarkStateManagerTest:testSetsAndGetsNextNonces() (gas: 810598) -QuarkWalletProxyFactoryTest:testCreateAdditionalWalletWithSalt() (gas: 212726) -QuarkWalletProxyFactoryTest:testCreateAndExecuteCreatesWallet() (gas: 412006) -QuarkWalletProxyFactoryTest:testCreateAndExecuteMultiCreatesWallet() (gas: 445108) -QuarkWalletProxyFactoryTest:testCreateAndExecuteMultiWithSalt() (gas: 449281) -QuarkWalletProxyFactoryTest:testCreateAndExecuteSetsMsgSender() (gas: 223967) -QuarkWalletProxyFactoryTest:testCreateAndExecuteWithSalt() (gas: 416581) -QuarkWalletProxyFactoryTest:testCreateAndExecuteWithSaltSetsMsgSender() (gas: 396196) -QuarkWalletProxyFactoryTest:testCreateRevertsOnRepeat() (gas: 8937393460516733241) -QuarkWalletProxyFactoryTest:testCreatesWalletAtDeterministicAddress() (gas: 222644) -QuarkWalletProxyFactoryTest:testExecuteMultiOnExistingWallet() (gas: 443430) -QuarkWalletProxyFactoryTest:testExecuteOnExistingWallet() (gas: 410716) -QuarkWalletProxyFactoryTest:testExecutorIsOtherWallet() (gas: 313753) -QuarkWalletProxyFactoryTest:testExecutorSetInCreate() (gas: 103646) -QuarkWalletProxyFactoryTest:testVersion() (gas: 6130) -QuarkWalletTest:testAtomicIncrementer() (gas: 71987) -QuarkWalletTest:testAtomicMaxCounterScript() (gas: 271134) -QuarkWalletTest:testAtomicPing() (gas: 51746) -QuarkWalletTest:testAtomicPingWithExternalSignature() (gas: 99075) -QuarkWalletTest:testCanReplaySameScriptWithDifferentCall() (gas: 157277) -QuarkWalletTest:testDirectExecuteFromEOA() (gas: 68704) -QuarkWalletTest:testDirectExecuteFromOtherQuarkWallet() (gas: 117804) -QuarkWalletTest:testDirectExecuteWithScriptSources() (gas: 286178) -QuarkWalletTest:testDisallowAllNullScriptAddress() (gas: 223275) -QuarkWalletTest:testEmitsEventsInDirectExecute() (gas: 47349) -QuarkWalletTest:testEmitsEventsInExecuteQuarkOperation() (gas: 88701) -QuarkWalletTest:testGetCodeJar() (gas: 11204) -QuarkWalletTest:testGetExecutor() (gas: 5469) -QuarkWalletTest:testGetSigner() (gas: 8500) -QuarkWalletTest:testGetStateManager() (gas: 10612) -QuarkWalletTest:testMultiQuarkOperationCanCallMultipleOperationsWithOneSignature() (gas: 107739) -QuarkWalletTest:testPrecompileBigModExp() (gas: 51036) -QuarkWalletTest:testPrecompileBlake2F() (gas: 53508) -QuarkWalletTest:testPrecompileBn256Add() (gas: 52522) -QuarkWalletTest:testPrecompileBn256ScalarMul() (gas: 56487) -QuarkWalletTest:testPrecompileDataCopy() (gas: 59460) -QuarkWalletTest:testPrecompileEcRecover() (gas: 56560) -QuarkWalletTest:testPrecompileRipemd160() (gas: 52524) -QuarkWalletTest:testPrecompileSha256() (gas: 52967) -QuarkWalletTest:testQuarkOperationRevertsIfCallReverts() (gas: 67575) -QuarkWalletTest:testRevertOnAllPrecompilesDirectCall() (gas: 633031) -QuarkWalletTest:testRevertsForBadInputsInMultiQuarkOperation() (gas: 13452) -QuarkWalletTest:testRevertsForDirectExecuteByNonExecutorSigner() (gas: 13835) -QuarkWalletTest:testRevertsForNonceReuse() (gas: 91723) -QuarkWalletTest:testRevertsForRandomEmptyScriptAddress() (gas: 117042) -QuarkWalletTest:testRevertsForReplayOfCanceledScript() (gas: 209767) -QuarkWalletTest:testRevertsForReusedNonceWithChangedScript() (gas: 106673) -QuarkWalletTest:testRevertsForUnauthorizedDirectExecuteByRandomAddress() (gas: 16023) -QuarkWalletTest:testSetsMsgSender() (gas: 51350) -QuarkWalletTest:testSetsMsgSenderDuringDirectExecute() (gas: 45371) -QuarkWalletTest:testSingleNonceCanCallDifferentScriptsAndCacheNonTargetScript() (gas: 135418) -QuotecallTest:testInitializeProperlyFromConstructor() (gas: 7213) -QuotecallTest:testQuotecallForPayWithUSDT() (gas: 126304) -QuotecallTest:testQuotecallForPayWithWBTC() (gas: 120860) -QuotecallTest:testQuotecallRevertsWhenCallReverts() (gas: 109497) -QuotecallTest:testReturnCallResult() (gas: 113327) -QuotecallTest:testRevertsForInvalidCallContext() (gas: 16792) -QuotecallTest:testRevertsWhenQuoteTooHigh() (gas: 156348) -QuotecallTest:testRevertsWhenQuoteTooLow() (gas: 156190) -QuotecallTest:testSimpleCounterAndPayWithUSDC() (gas: 150274) -QuotecallTest:testSimpleTransferTokenAndPayWithUSDC() (gas: 145818) -ReplayableTransactionsTest:testCancelRecurringPurchase() (gas: 271116) -ReplayableTransactionsTest:testRecurringPurchaseHappyPath() (gas: 211835) -ReplayableTransactionsTest:testRecurringPurchaseMultiplePurchases() (gas: 370049) -ReplayableTransactionsTest:testRecurringPurchaseWithDifferentCalldata() (gas: 596914) -ReplayableTransactionsTest:testRevertsForExpiredQuarkOperation() (gas: 12385) -ReplayableTransactionsTest:testRevertsForExpiredUniswapParams() (gas: 119578) -ReplayableTransactionsTest:testRevertsForPurchaseBeforeNextPurchasePeriod() (gas: 286655) -ReplayableTransactionsTest:testRevertsForPurchasingOverTheLimit() (gas: 287208) -RevertsTest:testRevertsInteger() (gas: 66969) -RevertsTest:testRevertsInvalidOpcode() (gas: 8391762413094949948) -RevertsTest:testRevertsOutOfGas() (gas: 295979) -RevertsTest:testRevertsWhenDividingByZero() (gas: 66815) -UniswapFlashLoanTest:testFlashLoanForCollateralSwapOnCompound() (gas: 428563) -UniswapFlashLoanTest:testRevertsForInsufficientFundsToRepayFlashLoan() (gas: 194222) -UniswapFlashLoanTest:testRevertsForInvalidCaller() (gas: 71191) -UniswapFlashLoanTest:testRevertsIfCalledDirectly() (gas: 10769) -UniswapFlashLoanTest:testTokensOrderInvariant() (gas: 96235) -UniswapFlashSwapExactOutTest:testInvalidCallerFlashSwap() (gas: 71169) -UniswapFlashSwapExactOutTest:testNotEnoughToPayFlashSwap() (gas: 294961) -UniswapFlashSwapExactOutTest:testRevertsIfCalledDirectly() (gas: 10871) -UniswapFlashSwapExactOutTest:testUniswapFlashSwapExactOutLeverageComet() (gas: 355100) -isValidSignatureTest:testIsValidSignatureForEOAOwner() (gas: 13445) -isValidSignatureTest:testReturnsMagicValueForValidSignature() (gas: 9376) -isValidSignatureTest:testRevertsForEmptyContract() (gas: 11305) -isValidSignatureTest:testRevertsForInvalidSignature() (gas: 16630) -isValidSignatureTest:testRevertsForMessageWithoutDomainTypehash() (gas: 13968) -isValidSignatureTest:testRevertsForPermit2SignatureReuse() (gas: 65628) -isValidSignatureTest:testRevertsForPermit2SignatureWithoutDomainTypehash() (gas: 31054) -isValidSignatureTest:testRevertsForWrongSigner() (gas: 14011) -isValidSignatureTest:testRevertsIfSignatureExceeds65Bytes() (gas: 5756) -isValidSignatureTest:testRevertsIfSignerContractDoesNotReturnMagic() (gas: 9548) -isValidSignatureTest:testRevertsIfSignerContractReverts() (gas: 9341) -isValidSignatureTest:testRevertsInvalidS() (gas: 13178) \ No newline at end of file +MulticallTest:testSupplyWETHWithdrawUSDCOnComet() (gas: 247623) +NoncerTest:testGetActiveNonceNested() (gas: 87683) +NoncerTest:testGetActiveNonceReplayable() (gas: 65580) +NoncerTest:testGetActiveNonceSingle() (gas: 44706) +NoncerTest:testGetActiveReplayCount() (gas: 85199) +NoncerTest:testGetActiveReplayCountNested() (gas: 88009) +NoncerTest:testGetActiveReplayCountSingle() (gas: 44517) +NoncerTest:testGetActiveReplayCountWithNonReplaySoftCancel() (gas: 69133) +NoncerTest:testGetActiveSubmissionTokenNested() (gas: 87131) +NoncerTest:testGetActiveSubmissionTokenReplayable() (gas: 65474) +NoncerTest:testGetActiveSubmissionTokenSingle() (gas: 47510) +NoncerTest:testNestedPlayPullingActiveReplayCount() (gas: 504200) +NoncerTest:testPostNestReadsCorrectValue() (gas: 140533) +PaycallTest:testInitializeProperlyFromConstructor() (gas: 6509) +PaycallTest:testPaycallForPayWithUSDT() (gas: 116523) +PaycallTest:testPaycallForPayWithWBTC() (gas: 109666) +PaycallTest:testPaycallRevertsWhenCallReverts() (gas: 50407) +PaycallTest:testReturnCallResult() (gas: 86383) +PaycallTest:testRevertsForInvalidCallContext() (gas: 16102) +PaycallTest:testRevertsWhenCostIsMoreThanMaxPaymentCost() (gas: 96263) +PaycallTest:testSimpleCounterAndPayWithUSDC() (gas: 137730) +PaycallTest:testSimpleTransferTokenAndPayWithUSDC() (gas: 138990) +PaycallTest:testSupplyWETHWithdrawUSDCOnCometAndPayWithUSDC() (gas: 290956) +QuarkFactoryTest:testInvariantAddressesBetweenNonces() (gas: 2773010) +QuarkFactoryTest:testQuarkFactoryDeployToDeterministicAddresses() (gas: 2772738) +QuarkFactoryTest:testQuarkFactoryDeployTwice() (gas: 2801449) +QuarkMinimalProxyTest:testSignerExecutor() (gas: 61848) +QuarkNonceManagerTest:testCancelChain() (gas: 50401) +QuarkNonceManagerTest:testCancelExhaustedIsNoOp() (gas: 41156) +QuarkNonceManagerTest:testChangingReplayableness() (gas: 38646) +QuarkNonceManagerTest:testClaimsSequentialNonces() (gas: 13877370) +QuarkNonceManagerTest:testInvalidNonces() (gas: 15224) +QuarkNonceManagerTest:testIsSet() (gas: 31777) +QuarkNonceManagerTest:testNextNonceChain() (gas: 95315) +QuarkNonceManagerTest:testNonLinearNonce() (gas: 40662) +QuarkNonceManagerTest:testNonceOneIsValid() (gas: 35909) +QuarkNonceManagerTest:testPrecancelNonce() (gas: 38351) +QuarkNonceManagerTest:testRevertsDefenseInDepthReplayableSubmissionTokenZero() (gas: 40305) +QuarkNonceManagerTest:testRevertsIfNonceIsAlreadySet() (gas: 37110) +QuarkNonceManagerTest:testRevertsIfSubmittingNonMatchingNonceForNonReplayable() (gas: 19848) +QuarkNonceManagerTest:testSingleUseRandomValidNonce() (gas: 45655) +QuarkWalletProxyFactoryTest:testCreateAdditionalWalletWithSalt() (gas: 207145) +QuarkWalletProxyFactoryTest:testCreateAndExecuteCreatesWallet() (gas: 424213) +QuarkWalletProxyFactoryTest:testCreateAndExecuteMultiCreatesWallet() (gas: 476642) +QuarkWalletProxyFactoryTest:testCreateAndExecuteMultiWithSalt() (gas: 480674) +QuarkWalletProxyFactoryTest:testCreateAndExecuteSetsMsgSender() (gas: 214683) +QuarkWalletProxyFactoryTest:testCreateAndExecuteWithSalt() (gas: 426450) +QuarkWalletProxyFactoryTest:testCreateAndExecuteWithSaltSetsMsgSender() (gas: 360114) +QuarkWalletProxyFactoryTest:testCreateRevertsOnRepeat() (gas: 8937393460516733150) +QuarkWalletProxyFactoryTest:testCreatesWalletAtDeterministicAddress() (gas: 214396) +QuarkWalletProxyFactoryTest:testExecuteMultiOnExistingWallet() (gas: 474889) +QuarkWalletProxyFactoryTest:testExecuteOnExistingWallet() (gas: 422847) +QuarkWalletProxyFactoryTest:testExecutorIsOtherWallet() (gas: 281126) +QuarkWalletProxyFactoryTest:testExecutorSetInCreate() (gas: 102057) +QuarkWalletProxyFactoryTest:testVersion() (gas: 6074) +QuarkWalletTest:testAllowsForReusedNonceWithChangedScript() (gas: 99999) +QuarkWalletTest:testAtomicIncrementer() (gas: 68779) +QuarkWalletTest:testAtomicMaxCounterScript() (gas: 162544) +QuarkWalletTest:testAtomicPing() (gas: 46925) +QuarkWalletTest:testAtomicPingWithExternalSignature() (gas: 100238) +QuarkWalletTest:testCanReplaySameScriptWithDifferentCall() (gas: 155178) +QuarkWalletTest:testDirectExecuteFromEOA() (gas: 62635) +QuarkWalletTest:testDirectExecuteFromOtherQuarkWallet() (gas: 100822) +QuarkWalletTest:testDirectExecuteWithScriptSources() (gas: 303606) +QuarkWalletTest:testDisallowAllNullScriptAddress() (gas: 164253) +QuarkWalletTest:testEmitsEventsInDirectExecute() (gas: 41222) +QuarkWalletTest:testEmitsEventsInExecuteQuarkOperation() (gas: 91026) +QuarkWalletTest:testEmitsEventsInReplayableQuarkOperation() (gas: 94306) +QuarkWalletTest:testFailsWithRepeatNonceInDirectExecute() (gas: 90892) +QuarkWalletTest:testGetCodeJar() (gas: 11216) +QuarkWalletTest:testGetExecutor() (gas: 5485) +QuarkWalletTest:testGetNonceManager() (gas: 11150) +QuarkWalletTest:testGetSigner() (gas: 8579) +QuarkWalletTest:testHalfReplayableMultiQuarkOperation() (gas: 279150) +QuarkWalletTest:testMultiQuarkOperationCanCallMultipleOperationsWithOneSignature() (gas: 115330) +QuarkWalletTest:testPrecompileBigModExp() (gas: 45241) +QuarkWalletTest:testPrecompileBlake2F() (gas: 47653) +QuarkWalletTest:testPrecompileBn256Add() (gas: 46741) +QuarkWalletTest:testPrecompileBn256ScalarMul() (gas: 51725) +QuarkWalletTest:testPrecompileDataCopy() (gas: 54344) +QuarkWalletTest:testPrecompileEcRecover() (gas: 51109) +QuarkWalletTest:testPrecompileRipemd160() (gas: 46938) +QuarkWalletTest:testPrecompileSha256() (gas: 46748) +QuarkWalletTest:testQuarkOperationRevertsIfCallReverts() (gas: 45356) +QuarkWalletTest:testReadStorageForWallet() (gas: 162610) +QuarkWalletTest:testReplayableMultiQuarkOperation() (gas: 369919) +QuarkWalletTest:testReplayableMultiQuarkOperationWithSharedNonce() (gas: 250415) +QuarkWalletTest:testRequiresCorrectSubmissionToken() (gas: 124618) +QuarkWalletTest:testRevertOnAllPrecompilesDirectCall() (gas: 415118) +QuarkWalletTest:testRevertsForBadInputsInMultiQuarkOperation() (gas: 11805) +QuarkWalletTest:testRevertsForDirectExecuteByNonExecutorSigner() (gas: 12546) +QuarkWalletTest:testRevertsForNonceReuse() (gas: 83768) +QuarkWalletTest:testRevertsForRandomEmptyScriptAddress() (gas: 93737) +QuarkWalletTest:testRevertsForUnauthorizedDirectExecuteByRandomAddress() (gas: 14760) +QuarkWalletTest:testScriptCanBeCanceledByNewOp() (gas: 153480) +QuarkWalletTest:testScriptCanBeCanceledByNoOp() (gas: 128155) +QuarkWalletTest:testSetsMsgSender() (gas: 45563) +QuarkWalletTest:testSetsMsgSenderDuringDirectExecute() (gas: 38180) +QuotecallTest:testInitializeProperlyFromConstructor() (gas: 7166) +QuotecallTest:testQuotecallForPayWithUSDT() (gas: 116647) +QuotecallTest:testQuotecallForPayWithWBTC() (gas: 109874) +QuotecallTest:testQuotecallRevertsWhenCallReverts() (gas: 86744) +QuotecallTest:testReturnCallResult() (gas: 106475) +QuotecallTest:testRevertsForInvalidCallContext() (gas: 16077) +QuotecallTest:testRevertsWhenQuoteTooHigh() (gas: 132252) +QuotecallTest:testRevertsWhenQuoteTooLow() (gas: 132094) +QuotecallTest:testSimpleCounterAndPayWithUSDC() (gas: 137871) +QuotecallTest:testSimpleTransferTokenAndPayWithUSDC() (gas: 139143) +ReplayableTransactionsTest:testCancelRecurringPurchase() (gas: 249993) +ReplayableTransactionsTest:testRecurringPurchaseHappyPath() (gas: 198750) +ReplayableTransactionsTest:testRecurringPurchaseMultiplePurchases() (gas: 297155) +ReplayableTransactionsTest:testRecurringPurchaseWithDifferentCalldata() (gas: 486918) +ReplayableTransactionsTest:testRevertsForExpiredQuarkOperation() (gas: 9786) +ReplayableTransactionsTest:testRevertsForExpiredUniswapParams() (gas: 89451) +ReplayableTransactionsTest:testRevertsForPurchaseBeforeNextPurchasePeriod() (gas: 226133) +ReplayableTransactionsTest:testRevertsForPurchasingOverTheLimit() (gas: 226536) +RevertsTest:testRevertsInteger() (gas: 45121) +RevertsTest:testRevertsInvalidOpcode() (gas: 8660281895700907463) +RevertsTest:testRevertsOutOfGas() (gas: 302901) +RevertsTest:testRevertsWhenDividingByZero() (gas: 44965) +UniswapFlashLoanTest:testFlashLoanForCollateralSwapOnCompound() (gas: 370366) +UniswapFlashLoanTest:testRevertsForInsufficientFundsToRepayFlashLoan() (gas: 140032) +UniswapFlashLoanTest:testRevertsForInvalidCaller() (gas: 47143) +UniswapFlashLoanTest:testTokensOrderInvariant() (gas: 83062) +UniswapFlashSwapExactOutTest:testInvalidCallerFlashSwap() (gas: 47333) +UniswapFlashSwapExactOutTest:testNotEnoughToPayFlashSwap() (gas: 237417) +UniswapFlashSwapExactOutTest:testRevertsIfCalledDirectly() (gas: 105147) +UniswapFlashSwapExactOutTest:testUniswapFlashSwapExactOutLeverageComet() (gas: 312622) +isValidSignatureTest:testIsValidSignatureForEOAOwner() (gas: 13409) +isValidSignatureTest:testReturnsMagicValueForValidSignature() (gas: 9134) +isValidSignatureTest:testRevertsForEmptyContract() (gas: 11311) +isValidSignatureTest:testRevertsForInvalidSignature() (gas: 16668) +isValidSignatureTest:testRevertsForMessageWithoutDomainTypehash() (gas: 13998) +isValidSignatureTest:testRevertsForPermit2SignatureReuse() (gas: 65676) +isValidSignatureTest:testRevertsForPermit2SignatureWithoutDomainTypehash() (gas: 31170) +isValidSignatureTest:testRevertsForWrongSigner() (gas: 14044) +isValidSignatureTest:testRevertsIfSignatureExceeds65Bytes() (gas: 5680) +isValidSignatureTest:testRevertsIfSignerContractDoesNotReturnMagic() (gas: 9389) +isValidSignatureTest:testRevertsIfSignerContractReverts() (gas: 9129) +isValidSignatureTest:testRevertsInvalidS() (gas: 13252) \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6070a68b..3afc3cf6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,7 +30,7 @@ jobs: - name: Run Forge build run: | forge --version - forge build --sizes + forge build --via-ir --sizes id: build - name: Run Forge Format diff --git a/README.md b/README.md index 754bcec7..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 @@ -18,9 +18,9 @@ _Quark Wallet_ executes _Quark Operations_ containing a transaction script (or a _Quark Operations_ are either directly executed or authorized by signature, and can include replayable transactions and support callbacks for complex operations like flash-loans. See the [Quark Wallet Features](#quark-wallet-features) section for more details. -### Quark State Manager +### Quark Nonce Manager -_Quark State Manager_ is a contract that manages nonces and ensures isolated storage for each Quark wallet and operation, preventing storage clashes between different wallets and operations. +_Quark Nonce Manager_ is a contract that manages nonces for each Quark wallet and operation, preventing accidental replays of operations. Quark operations can be replayable by generating a secret key and building a hash-chain to allow N replays of a given script. ### Wallet Factory @@ -44,14 +44,12 @@ flowchart TB wallet[Quark Wallet] jar[Code Jar] script[Quark Script] - state[Quark State Manager] + state[Quark Nonce Manager] factory -- 1. createAndExecute --> wallet wallet -- 2. saveCode --> jar jar -- 3. CREATE2 --> script - wallet -- 4. setActiveNonceAndCallback --> state - state -- 5. executeScriptWithNonceLock --> wallet - wallet -- 6. Executes script\nusing callcode --> script + wallet -- 4. Executes script\nusing callcode --> script ``` ## Quark Wallet Features @@ -76,23 +74,25 @@ For example, let _Wallet A_ be the `executor` of _Wallet B_. Alice is the `signe ### Replayable Scripts -Replayable scripts are Quark scripts that can re-executed multiple times using the same signature of a _Quark operation_. More specifically, replayable scripts explicitly clear the nonce used by the transaction (can be done via the `allowReplay` helper function in [`QuarkScript.sol`](./quark-core/src/QuarkScript.sol)) to allow for the same nonce to be re-used with the same script address. +Replayable scripts are Quark scripts that can be re-executed N times using the same signature of a _Quark operation_. More specifically, replayable scripts generate a nonce chain where the original signer knows a secret and hashes that secret N times. The signer can reveal a single "submission token" to replay the script which is easily verified on-chain. When the signer reveals the last submission token (the original secret) and submits it on-chain, no more replays are allowed (assuming the secret was chosen as a strong random). The signer can always cancel replays by submitting a nop non-replayable script on-chain or simply forgetting the secret. Note: the chain can be arbitrarily long and does not expend any additional gas on-chain for being longer (except if a script wants to know its position in the chain). -An example use-case for replayable scripts is recurring purchases. If a user wanted to buy X WETH using 1,000 USDC every Wednesday until 10,000 USDC is spent, they can achieve this by signing a single _Quark operation_ of a replayable script ([example](./test/lib/RecurringPurchase.sol)). A submitter can then submit this same signed _Quark operation_ every Wednesday to execute the recurring purchase. The replayable script should have checks to ensure conditions are met before purchasing the WETH. - -#### Same script address, but different calldata - -For replayable transactions where the nonce is cleared, _Quark State Manager_ requires future transactions using that nonce to use the same script. This is to ensure that the same nonce is not accidentally used by two different scripts. However, it does not require the `calldata` passed to that script to be the same. This means that a cleared nonce can be executed with the same script but different calldata. +``` +Nonce hash chain: -Allowing the calldata to change greatly increases the flexibility of replayable scripts. One can think of a replayable script like a sub-module of a wallet that supports different functionality. In the [example script](./test/lib/RecurringPurchase.sol) for recurring purchases, there is a separate `cancel` function that the user can sign to cancel the nonce, and therefore, cancel all the recurring purchases that use this nonce. The user can also also sign multiple `purchase` calls, each with different purchase configurations. This means that multiple variations of recurring purchases can exist on the same nonce and can all be cancelled together. +Final replay = "nonceSecret" + N-1 replay = hash ("nonceSecret") + N-2 replay = hash^2("nonceSecret") + ... + First play = hash^n("nonceSecret") = operation.nonce +``` -One danger of flexible `calldata` in replayable scripts is that previously signed `calldata` can always be re-executed. The Quark system does not disallow previously used calldata when a new calldata is executed. This means that scripts may need to implement their own method of invalidating previously-used `calldata`. +An example use-case for replayable scripts is recurring purchases. If a user wanted to buy X WETH using 1,000 USDC every Wednesday until 10,000 USDC is spent, they can achieve this by signing a single _Quark operation_ of a replayable script ([example](./test/lib/RecurringPurchase.sol)). A submitter can then submit this same signed _Quark operation_ every Wednesday to execute the recurring purchase. The replayable script should have checks to ensure conditions are met before purchasing the WETH. ### Callbacks Callbacks are an opt-in feature of Quark scripts that allow for an external contract to call into the Quark script (in the context of the _Quark wallet_) during the same transaction. An example use-case of callbacks is Uniswap flashloans ([example script](./quark-core-scripts/src/UniswapFlashLoan.sol)), where the Uniswap pool will call back into the _Quark wallet_ to make sure that the loan is paid off before ending the transaction. -Callbacks need to be explicitly turned on by Quark scripts. Specifically, this is done by writing the callback target address to the callback storage slot in _Quark State Manager_ (can be done via the `allowCallback` helper function in [`QuarkScript.sol`](./quark-core/src/QuarkScript.sol)). +Callbacks need to be explicitly turned on by Quark scripts. Specifically, this is done by writing the callback target address to the callback storage slot in _Quark Nonce Manager_ (can be done via the `allowCallback` helper function in [`QuarkScript.sol`](./quark-core/src/QuarkScript.sol)). ### EIP-1271 Signatures 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 33fd83b0..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"; @@ -8,7 +8,7 @@ import {CodeJar} from "codejar/src/CodeJar.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; import {BatchExecutor} from "quark-core/src/periphery/BatchExecutor.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol"; import {QuarkFactory} from "quark-factory/src/QuarkFactory.sol"; @@ -49,7 +49,7 @@ contract DeployQuarkWalletFactory is Script { quarkFactory.deployQuarkContracts(); - console.log("Quark State Manager Deployed:", address(quarkFactory.quarkStateManager())); + console.log("Quark Nonce Manager Deployed:", address(quarkFactory.quarkNonceManager())); console.log("Quark Wallet Implementation Deployed:", address(quarkFactory.quarkWalletImpl())); console.log("Quark Wallet Proxy Factory Deployed:", address(quarkFactory.quarkWalletProxyFactory())); console.log("Batch Executor Deployed:", address(quarkFactory.batchExecutor())); 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 new file mode 100644 index 00000000..748d527e --- /dev/null +++ b/src/quark-core/src/QuarkNonceManager.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.27; + +import {IQuarkWallet} from "quark-core/src/interfaces/IQuarkWallet.sol"; + +library QuarkNonceManagerMetadata { + /// @notice Represents the unclaimed bytes32 value. + bytes32 internal constant FREE = bytes32(uint256(0)); + + /// @notice A token that implies a Quark Operation is no longer replayable. + bytes32 internal constant EXHAUSTED = bytes32(type(uint256).max); +} + +/** + * @title Quark Nonce Manager + * @notice Contract for managing nonces for Quark wallets + * @author Compound Labs, Inc. + */ +contract QuarkNonceManager { + error NonReplayableNonce(address wallet, bytes32 nonce, bytes32 submissionToken); + error InvalidNonce(address wallet, bytes32 nonce); + error InvalidSubmissionToken(address wallet, bytes32 nonce, bytes32 submissionToken); + + event NonceSubmitted(address wallet, bytes32 nonce, bytes32 submissionToken); + + /// @notice Represents the unclaimed bytes32 value. + bytes32 public constant FREE = QuarkNonceManagerMetadata.FREE; + + /// @notice A token that implies a Quark Operation is no longer replayable. + bytes32 public constant EXHAUSTED = QuarkNonceManagerMetadata.EXHAUSTED; + + /// @notice Mapping from nonces to last used submission token. + mapping(address wallet => mapping(bytes32 nonce => bytes32 lastToken)) public submissions; + + /** + * @notice Ensures a given nonce is canceled for sender. An un-used nonce will not be usable in the future, and a replayable nonce will no longer be replayable. This is a no-op for already canceled operations. + * @param nonce The nonce of the chain to cancel. + */ + function cancel(bytes32 nonce) external { + submissions[msg.sender][nonce] = EXHAUSTED; + emit NonceSubmitted(msg.sender, nonce, EXHAUSTED); + } + + /** + * @notice Attempts a first or subsequent submission of a given nonce from a wallet. + * @param nonce The nonce of the chain to submit. + * @param isReplayable True only if the operation has been marked as replayable. Otherwise, submission token must be the EXHAUSTED value. + * @param submissionToken The token for this submission. For single-use operations, set `submissionToken` to `uint256(-1)`. For first-use replayable operations, set `submissionToken` = `nonce`. Otherwise, the next submission token from the nonce-chain. + */ + function submit(bytes32 nonce, bool isReplayable, bytes32 submissionToken) external { + bytes32 lastTokenSubmission = submissions[msg.sender][nonce]; + if (lastTokenSubmission == EXHAUSTED) { + revert NonReplayableNonce(msg.sender, nonce, submissionToken); + } + // Defense-in-depth check for `nonce != FREE` and `nonce != EXHAUSTED` + if (nonce == FREE || nonce == EXHAUSTED) { + revert InvalidNonce(msg.sender, nonce); + } + // Defense-in-depth check for `submissionToken != FREE` and `submissionToken != EXHAUSTED` + if (submissionToken == FREE || submissionToken == EXHAUSTED) { + revert InvalidSubmissionToken(msg.sender, nonce, submissionToken); + } + + bool validFirstPlay = lastTokenSubmission == FREE && submissionToken == nonce; + + /* let validFirstPlayOrReplay = validFirstPlay or validReplay [with short-circuiting] */ + bool validFirstPlayOrReplay = + validFirstPlay || keccak256(abi.encodePacked(submissionToken)) == lastTokenSubmission; + + if (!validFirstPlayOrReplay) { + revert InvalidSubmissionToken(msg.sender, nonce, submissionToken); + } + + // Note: even with a valid submission token, we always set non-replayables to exhausted (e.g. for cancellations) + submissions[msg.sender][nonce] = isReplayable ? submissionToken : EXHAUSTED; + emit NonceSubmitted(msg.sender, nonce, submissionToken); + } +} diff --git a/src/quark-core/src/QuarkScript.sol b/src/quark-core/src/QuarkScript.sol index 43ebaa87..4c4a062b 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,21 +11,32 @@ import {QuarkWallet, IHasSignerExecutor} from "quark-core/src/QuarkWallet.sol"; */ abstract contract QuarkScript { error ReentrantCall(); + error InvalidActiveNonce(); + error InvalidActiveSubmissionToken(); /// @notice Storage location for the re-entrancy guard bytes32 internal constant REENTRANCY_FLAG_SLOT = bytes32(uint256(keccak256("quark.scripts.reentrancy.guard.v1")) - 1); - /// @notice A safer, but gassier reentrancy guard that writes the flag to the QuarkStateManager + /// @notice A safer, but gassier reentrancy guard that writes the flag to the QuarkNonceManager modifier nonReentrant() { - if (read(REENTRANCY_FLAG_SLOT) == bytes32(uint256(1))) { + bytes32 slot = REENTRANCY_FLAG_SLOT; + bytes32 flag; + assembly { + flag := tload(slot) + } + if (flag == bytes32(uint256(1))) { revert ReentrantCall(); } - write(REENTRANCY_FLAG_SLOT, bytes32(uint256(1))); + assembly { + tstore(slot, 1) + } _; - write(REENTRANCY_FLAG_SLOT, bytes32(uint256(0))); + assembly { + tstore(slot, 0) + } } /** @@ -56,18 +68,24 @@ abstract contract QuarkScript { return IHasSignerExecutor(address(this)).executor(); } - function allowCallback() internal { - QuarkWallet self = QuarkWallet(payable(address(this))); - self.stateManager().write(self.CALLBACK_KEY(), bytes32(uint256(uint160(self.stateManager().getActiveScript())))); + function nonceManager() internal view returns (QuarkNonceManager) { + return QuarkNonceManager(IQuarkWallet(address(this)).nonceManager()); } - function clearCallback() internal { - QuarkWallet self = QuarkWallet(payable(address(this))); - self.stateManager().write(self.CALLBACK_KEY(), bytes32(0)); + function allowCallback() internal { + bytes32 callbackSlot = QuarkWalletMetadata.CALLBACK_SLOT; + bytes32 activeScriptSlot = QuarkWalletMetadata.ACTIVE_SCRIPT_SLOT; + assembly { + let activeScript := tload(activeScriptSlot) + tstore(callbackSlot, activeScript) + } } - function allowReplay() internal { - return QuarkWallet(payable(address(this))).stateManager().clearNonce(); + function clearCallback() internal { + bytes32 callbackSlot = QuarkWalletMetadata.CALLBACK_SLOT; + assembly { + tstore(callbackSlot, 0) + } } function readU256(string memory key) internal view returns (uint256) { @@ -79,7 +97,12 @@ abstract contract QuarkScript { } function read(bytes32 key) internal view returns (bytes32) { - return QuarkWallet(payable(address(this))).stateManager().read(key); + bytes32 value; + bytes32 isolatedKey = getNonceIsolatedKey(key); + assembly { + value := sload(isolatedKey) + } + return value; } function writeU256(string memory key, uint256 value) internal { @@ -91,6 +114,56 @@ abstract contract QuarkScript { } function write(bytes32 key, bytes32 value) internal { - return QuarkWallet(payable(address(this))).stateManager().write(key, value); + bytes32 isolatedKey = getNonceIsolatedKey(key); + assembly { + 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(); + 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/QuarkStateManager.sol b/src/quark-core/src/QuarkStateManager.sol deleted file mode 100644 index 2fe1ca91..00000000 --- a/src/quark-core/src/QuarkStateManager.sol +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; - -import {IQuarkWallet} from "quark-core/src/interfaces/IQuarkWallet.sol"; - -/** - * @title Quark State Manager - * @notice Contract for managing nonces and storage for Quark wallets, guaranteeing storage isolation across wallets - * and Quark operations - * @author Compound Labs, Inc. - */ -contract QuarkStateManager { - event ClearNonce(address indexed wallet, uint96 nonce); - - error NoActiveNonce(); - error NoUnusedNonces(); - error NonceAlreadySet(); - error NonceScriptMismatch(); - - /// @notice Bit-packed structure of a nonce-script pair - struct NonceScript { - uint96 nonce; - address scriptAddress; - } - - /// @notice Bit-packed nonce values - mapping(address wallet => mapping(uint256 bucket => uint256 bitset)) public nonces; - - /// @notice Per-wallet-nonce address for preventing replays with changed script address - mapping(address wallet => mapping(uint96 nonce => address scriptAddress)) public nonceScriptAddress; - - /// @notice Per-wallet-nonce storage space that can be utilized while a nonce is active - mapping(address wallet => mapping(uint96 nonce => mapping(bytes32 key => bytes32 value))) public walletStorage; - - /// @notice Currently active nonce-script pair for a wallet, if any, for which storage is accessible - mapping(address wallet => NonceScript) internal activeNonceScript; - - /** - * @notice Return whether a nonce has been exhausted; note that if a nonce is not set, that does not mean it has not been used before - * @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); - } - - /// @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 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 - */ - 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); - // The next available nonce should not be reserved for a replayable transaction - if (nonceScriptAddress[wallet][nonce] == address(0)) { - return nonce; - } - } - } - } - - revert NoUnusedNonces(); - } - - /** - * @notice Return the script address associated with the currently active nonce; revert if none - * @return Currently active script address - */ - function getActiveScript() external view returns (address) { - address scriptAddress = activeNonceScript[msg.sender].scriptAddress; - if (scriptAddress == address(0)) { - revert NoActiveNonce(); - } - return scriptAddress; - } - - /// @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); - } - - /// @notice Clears (un-sets) the active nonce to allow its reuse; allows a script to be replayed - function clearNonce() external { - if (activeNonceScript[msg.sender].scriptAddress == address(0)) { - revert NoActiveNonce(); - } - - uint96 nonce = activeNonceScript[msg.sender].nonce; - (uint256 bucket, uint256 setMask) = getBucket(nonce); - nonces[msg.sender][bucket] &= ~setMask; - emit ClearNonce(msg.sender, nonce); - } - - /** - * @notice Set a given nonce for the calling wallet; effectively cancels any replayable script using that nonce - * @param nonce Nonce to set for the calling wallet - */ - function setNonce(uint96 nonce) external { - // TODO: should we check whether there exists a nonceScriptAddress? - (uint256 bucket, uint256 setMask) = getBucket(nonce); - 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; - } - - /** - * @notice Set a wallet nonce as the active nonce and yield control back to the wallet by calling into callback - * @param nonce Nonce to activate for the transaction - * @param scriptAddress Address of script to invoke with nonce lock - * @param scriptCalldata Calldata for script call to invoke with nonce lock - * @return Return value from the executed operation - * @dev The script is expected to clearNonce() if it wishes to be replayable - */ - function setActiveNonceAndCallback(uint96 nonce, address scriptAddress, bytes calldata scriptCalldata) - external - returns (bytes memory) - { - // retrieve the (bucket, mask) pair that addresses the nonce in memory - (uint256 bucket, uint256 setMask) = getBucket(nonce); - - // ensure nonce is not already set - if (isNonceSetInternal(msg.sender, bucket, setMask)) { - revert NonceAlreadySet(); - } - - address cachedScriptAddress = nonceScriptAddress[msg.sender][nonce]; - // if the nonce has been used before, check if the script address matches, and revert if not - if ((cachedScriptAddress != address(0)) && (cachedScriptAddress != scriptAddress)) { - revert NonceScriptMismatch(); - } - - // spend the nonce; only if the callee chooses to clear it will it get un-set and become replayable - setNonceInternal(bucket, setMask); - - // set the nonce-script pair active and yield to the wallet callback - NonceScript memory previousNonceScript = activeNonceScript[msg.sender]; - activeNonceScript[msg.sender] = NonceScript({nonce: nonce, scriptAddress: scriptAddress}); - - bytes memory result = IQuarkWallet(msg.sender).executeScriptWithNonceLock(scriptAddress, scriptCalldata); - - // if a nonce was cleared, set the nonceScriptAddress to lock nonce re-use to the same script address - if (cachedScriptAddress == address(0) && !isNonceSetInternal(msg.sender, bucket, setMask)) { - nonceScriptAddress[msg.sender][nonce] = scriptAddress; - } - - // release the nonce when the wallet finishes executing callback - activeNonceScript[msg.sender] = previousNonceScript; - - return result; - } - - /// @notice Write arbitrary bytes to storage namespaced by the currently active nonce; reverts if no nonce is currently active - function write(bytes32 key, bytes32 value) external { - if (activeNonceScript[msg.sender].scriptAddress == address(0)) { - revert NoActiveNonce(); - } - walletStorage[msg.sender][activeNonceScript[msg.sender].nonce][key] = value; - } - - /** - * @notice Read from storage namespaced by the currently active nonce; reverts if no nonce is currently active - * @return Value at the nonce storage location, as bytes - */ - function read(bytes32 key) external view returns (bytes32) { - if (activeNonceScript[msg.sender].scriptAddress == address(0)) { - revert NoActiveNonce(); - } - return walletStorage[msg.sender][activeNonceScript[msg.sender].nonce][key]; - } -} diff --git a/src/quark-core/src/QuarkWallet.sol b/src/quark-core/src/QuarkWallet.sol index dc95681e..1aebe699 100644 --- a/src/quark-core/src/QuarkWallet.sol +++ b/src/quark-core/src/QuarkWallet.sol @@ -1,13 +1,14 @@ // 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"; import {CodeJar} from "codejar/src/CodeJar.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.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(uint96 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,15 +78,20 @@ 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 + 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 QuarkStateManager contract that manages nonces and nonce-namespaced transaction script storage - QuarkStateManager public immutable stateManager; + /// @notice Address of QuarkNonceManager contract that manages nonces for this quark wallet + QuarkNonceManager public immutable nonceManager; /// @notice Name of contract string public constant NAME = QuarkWalletMetadata.NAME; @@ -107,8 +126,17 @@ contract QuarkWallet is IERC1271 { ) ); - /// @notice Well-known stateManager key for the currently executing script's callback address (if any) - bytes32 public constant CALLBACK_KEY = keccak256("callback.v1.quark"); + /// @notice Well-known storage slot for the currently executing script's callback address (if any) + 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 = 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 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; @@ -116,7 +144,9 @@ contract QuarkWallet is IERC1271 { /// @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 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 @@ -130,11 +160,11 @@ contract QuarkWallet is IERC1271 { /** * @notice Construct a new QuarkWalletImplementation * @param codeJar_ The CodeJar contract used to deploy scripts - * @param stateManager_ The QuarkStateManager 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_, QuarkStateManager stateManager_) { + constructor(CodeJar codeJar_, QuarkNonceManager nonceManager_) { codeJar = codeJar_; - stateManager = stateManager_; + nonceManager = nonceManager_; } /** @@ -150,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 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 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); } /** @@ -171,6 +221,28 @@ contract QuarkWallet is IERC1271 { uint8 v, bytes32 r, bytes32 s + ) public returns (bytes memory) { + return executeMultiQuarkOperationWithSubmissionToken(op, op.nonce, opDigests, v, r, s); + } + + /** + * @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 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 executeMultiQuarkOperationWithSubmissionToken( + QuarkOperation calldata op, + bytes32 submissionToken, + bytes32[] memory opDigests, + uint8 v, + bytes32 r, + bytes32 s ) public returns (bytes memory) { bytes32 opDigest = getDigestForQuarkOperation(op); @@ -186,12 +258,13 @@ contract QuarkWallet is IERC1271 { } bytes32 multiOpDigest = getDigestForMultiQuarkOperation(opDigests); - return verifySigAndExecuteQuarkOperation(op, multiOpDigest, v, r, s); + return verifySigAndExecuteQuarkOperation(op, submissionToken, multiOpDigest, v, r, s); } /** - * @notice Verify a signature and execute a QuarkOperation + * @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. 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 @@ -200,6 +273,7 @@ contract QuarkWallet is IERC1271 { */ function verifySigAndExecuteQuarkOperation( QuarkOperation calldata op, + bytes32 submissionToken, bytes32 digest, uint8 v, bytes32 r, @@ -217,9 +291,13 @@ contract QuarkWallet is IERC1271 { codeJar.saveCode(op.scriptSources[i]); } - emit ExecuteQuarkScript(msg.sender, op.scriptAddress, op.nonce, ExecutionType.Signature); + nonceManager.submit(op.nonce, op.isReplayable, submissionToken); + + emit QuarkExecution( + msg.sender, op.scriptAddress, op.nonce, submissionToken, op.isReplayable, ExecutionType.Signature + ); - return stateManager.setActiveNonceAndCallback(op.nonce, op.scriptAddress, op.scriptCalldata); + return executeScriptInternal(op.scriptAddress, op.scriptCalldata, op.nonce, submissionToken); } /** @@ -232,7 +310,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 @@ -247,9 +325,11 @@ contract QuarkWallet is IERC1271 { codeJar.saveCode(scriptSources[i]); } - emit ExecuteQuarkScript(msg.sender, scriptAddress, nonce, ExecutionType.Direct); + nonceManager.submit(nonce, false, nonce); - return stateManager.setActiveNonceAndCallback(nonce, scriptAddress, scriptCalldata); + emit QuarkExecution(msg.sender, scriptAddress, nonce, nonce, false, ExecutionType.Direct); + + return executeScriptInternal(scriptAddress, scriptCalldata, nonce, nonce); } /** @@ -277,6 +357,7 @@ contract QuarkWallet is IERC1271 { abi.encode( QUARK_OPERATION_TYPEHASH, op.nonce, + op.isReplayable, op.scriptAddress, keccak256(encodedScriptSources), keccak256(op.scriptCalldata), @@ -378,17 +459,19 @@ contract QuarkWallet is IERC1271 { } /** - * @notice Execute a QuarkOperation with a lock acquired on nonce-namespaced storage - * @dev Can only be called by stateManager during setActiveNonceAndCallback() + * @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 executeScriptWithNonceLock(address scriptAddress, bytes memory scriptCalldata) - external - returns (bytes memory) - { - require(msg.sender == address(stateManager)); + function executeScriptInternal( + address scriptAddress, + bytes memory scriptCalldata, + bytes32 nonce, + bytes32 submissionToken + ) internal returns (bytes memory) { if (scriptAddress.code.length == 0) { revert EmptyCode(); } @@ -396,11 +479,49 @@ contract QuarkWallet is IERC1271 { bool success; uint256 returnSize; uint256 scriptCalldataLen = scriptCalldata.length; + bytes32 activeScriptSlot = ACTIVE_SCRIPT_SLOT; + bytes32 activeNonceSlot = ACTIVE_NONCE_SLOT; + bytes32 activeSubmissionTokenSlot = ACTIVE_SUBMISSION_TOKEN_SLOT; + bytes32 callbackSlot = CALLBACK_SLOT; + address oldActiveScript; + bytes32 oldActiveNonce; + bytes32 oldActiveSubmissionToken; + address oldCallback; assembly { + // Cache the previous values in each of the transient slots so they can be restored after the callcode + oldActiveScript := tload(activeScriptSlot) + oldActiveNonce := tload(activeNonceSlot) + oldActiveSubmissionToken := tload(activeSubmissionTokenSlot) + oldCallback := tload(callbackSlot) + + // 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) + + // Transiently set the callback slot to 0 + tstore(callbackSlot, 0) + // 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() + + // Transiently restore the active script + tstore(activeScriptSlot, oldActiveScript) + + // Transiently restore the active nonce + tstore(activeNonceSlot, oldActiveNonce) + + // Transiently restore the active submission token + tstore(activeSubmissionTokenSlot, oldActiveSubmissionToken) + + // Transiently restore the callback slot + tstore(callbackSlot, oldCallback) } bytes memory returnData = new bytes(returnSize); @@ -422,7 +543,11 @@ contract QuarkWallet is IERC1271 { * @dev Reverts if callback is not enabled by the script */ fallback(bytes calldata data) external payable returns (bytes memory) { - address callback = address(uint160(uint256(stateManager.read(CALLBACK_KEY)))); + bytes32 callbackSlot = CALLBACK_SLOT; + address callback; + assembly { + callback := tload(callbackSlot) + } if (callback != address(0)) { (bool success, bytes memory result) = callback.delegatecall(data); if (!success) { diff --git a/src/quark-core/src/QuarkWalletStandalone.sol b/src/quark-core/src/QuarkWalletStandalone.sol index f9949dc2..e48bdfd1 100644 --- a/src/quark-core/src/QuarkWalletStandalone.sol +++ b/src/quark-core/src/QuarkWalletStandalone.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; import {CodeJar} from "codejar/src/CodeJar.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWallet, IHasSignerExecutor} from "quark-core/src/QuarkWallet.sol"; /** @@ -23,10 +23,10 @@ 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 stateManager_ The QuarkStateManager 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_, QuarkStateManager stateManager_) - QuarkWallet(codeJar_, stateManager_) + constructor(address signer_, address executor_, CodeJar codeJar_, QuarkNonceManager nonceManager_) + QuarkWallet(codeJar_, nonceManager_) { signer = signer_; executor = executor_; 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 871c0526..24d5c50b 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 @@ -10,7 +10,9 @@ 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 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 @@ -21,6 +23,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); @@ -32,7 +35,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/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 93e956f4..9eefcbb3 100644 --- a/src/quark-factory/src/QuarkFactory.sol +++ b/src/quark-factory/src/QuarkFactory.sol @@ -1,9 +1,9 @@ // 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"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol"; import {BatchExecutor} from "quark-core/src/periphery/BatchExecutor.sol"; @@ -16,7 +16,7 @@ contract QuarkFactory { CodeJar public immutable codeJar; QuarkWallet public quarkWalletImpl; QuarkWalletProxyFactory public quarkWalletProxyFactory; - QuarkStateManager public quarkStateManager; + QuarkNonceManager public quarkNonceManager; BatchExecutor public batchExecutor; constructor(CodeJar codeJar_) { @@ -24,12 +24,12 @@ contract QuarkFactory { } function deployQuarkContracts() external { - quarkStateManager = - QuarkStateManager(payable(codeJar.saveCode(abi.encodePacked(type(QuarkStateManager).creationCode)))); + quarkNonceManager = + QuarkNonceManager(payable(codeJar.saveCode(abi.encodePacked(type(QuarkNonceManager).creationCode)))); quarkWalletImpl = QuarkWallet( payable( codeJar.saveCode( - abi.encodePacked(type(QuarkWallet).creationCode, abi.encode(codeJar, quarkStateManager)) + abi.encodePacked(type(QuarkWallet).creationCode, abi.encode(codeJar, quarkNonceManager)) ) ) ); 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 eee1bd15..0baca2fe 100644 --- a/test/ReplayableTransactions.t.sol +++ b/test/ReplayableTransactions.t.sol @@ -1,12 +1,12 @@ // 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"; import {CodeJar} from "codejar/src/CodeJar.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.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"; @@ -26,7 +26,7 @@ contract ReplayableTransactionsTest is Test { event ClearNonce(address indexed wallet, uint96 nonce); CodeJar public codeJar; - QuarkStateManager public stateManager; + QuarkNonceManager public nonceManager; QuarkWallet public walletImplementation; uint256 alicePrivateKey = 0x8675309; @@ -54,10 +54,10 @@ contract ReplayableTransactionsTest is Test { codeJar = new CodeJar(); console.log("CodeJar deployed to: %s", address(codeJar)); - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); - walletImplementation = new QuarkWallet(codeJar, stateManager); + walletImplementation = new QuarkWallet(codeJar, nonceManager); console.log("QuarkWallet implementation: %s", address(walletImplementation)); aliceWallet = @@ -130,11 +130,13 @@ contract ReplayableTransactionsTest is Test { uint216 totalAmountToPurchase = 20 ether; RecurringPurchase.PurchaseConfig memory purchaseConfig = createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); - QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( aliceWallet, recurringPurchase, abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), - ScriptType.ScriptAddress + ScriptType.ScriptAddress, + 1 ); op.expiry = purchaseConfig.swapParams.deadline; (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); @@ -150,11 +152,11 @@ contract ReplayableTransactionsTest is Test { // 2a. Cannot buy again unless time interval has passed vm.expectRevert(RecurringPurchase.PurchaseConditionNotMet.selector); - aliceWallet.executeQuarkOperation(op, v1, r1, s1); + 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.executeQuarkOperation(op, v1, r1, s1); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 20 ether); } @@ -169,11 +171,13 @@ contract ReplayableTransactionsTest is Test { uint216 totalAmountToPurchase = 20 ether; RecurringPurchase.PurchaseConfig memory purchaseConfig = createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); - QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( aliceWallet, recurringPurchase, abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), - ScriptType.ScriptAddress + ScriptType.ScriptAddress, + 2 ); op.expiry = purchaseConfig.swapParams.deadline; (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); @@ -181,9 +185,10 @@ contract ReplayableTransactionsTest is Test { QuarkWallet.QuarkOperation memory cancelOp = new QuarkOperationHelper().newBasicOpWithCalldata( aliceWallet, recurringPurchase, - abi.encodeWithSelector(RecurringPurchase.cancel.selector), + 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); @@ -196,12 +201,23 @@ contract ReplayableTransactionsTest is Test { assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); // 2. Cancel replayable transaction - aliceWallet.executeQuarkOperation(cancelOp, v2, r2, s2); + aliceWallet.executeQuarkOperationWithSubmissionToken(cancelOp, submissionTokens[1], v2, r2, s2); // 3. Replayable transaction can no longer be executed vm.warp(block.timestamp + purchaseInterval); - vm.expectRevert(QuarkStateManager.NonceAlreadySet.selector); - aliceWallet.executeQuarkOperation(op, v1, r1, s1); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(aliceWallet), op.nonce, submissionTokens[1] + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); + + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(aliceWallet), op.nonce, submissionTokens[2] + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[2], v1, r1, s1); assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); } @@ -213,6 +229,7 @@ contract ReplayableTransactionsTest is Test { 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 @@ -223,11 +240,12 @@ contract ReplayableTransactionsTest is Test { // 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( + (op1, submissionTokens) = new QuarkOperationHelper().newReplayableOpWithCalldata( aliceWallet, recurringPurchase, abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig1), - ScriptType.ScriptAddress + ScriptType.ScriptAddress, + 5 ); op1.expiry = purchaseConfig1.swapParams.deadline; RecurringPurchase.PurchaseConfig memory purchaseConfig2 = @@ -239,13 +257,16 @@ contract ReplayableTransactionsTest is Test { ScriptType.ScriptAddress ); op2.expiry = purchaseConfig2.swapParams.deadline; + op2.nonce = op1.nonce; + op2.isReplayable = true; cancelOp = new QuarkOperationHelper().newBasicOpWithCalldata( aliceWallet, recurringPurchase, - abi.encodeWithSelector(RecurringPurchase.cancel.selector), + 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); @@ -261,7 +282,7 @@ contract ReplayableTransactionsTest is Test { assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); // 1b. Execute recurring purchase order #2 - aliceWallet.executeQuarkOperation(op2, v2, r2, s2); + aliceWallet.executeQuarkOperationWithSubmissionToken(op2, submissionTokens[1], v2, r2, s2); assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 15 ether); @@ -269,26 +290,26 @@ contract ReplayableTransactionsTest is Test { vm.warp(block.timestamp + purchaseInterval); // 3a. Execute recurring purchase order #1 - aliceWallet.executeQuarkOperation(op1, v1, r1, s1); + aliceWallet.executeQuarkOperationWithSubmissionToken(op1, submissionTokens[2], v1, r1, s1); assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 25 ether); // 3b. Execute recurring purchase order #2 - aliceWallet.executeQuarkOperation(op2, v2, r2, s2); + aliceWallet.executeQuarkOperationWithSubmissionToken(op2, submissionTokens[3], v2, r2, s2); assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 30 ether); // 4. Cancel replayable transaction - aliceWallet.executeQuarkOperation(cancelOp, v3, r3, s3); + 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(QuarkStateManager.NonceAlreadySet.selector); - aliceWallet.executeQuarkOperation(op1, v1, r1, s1); - vm.expectRevert(QuarkStateManager.NonceAlreadySet.selector); - aliceWallet.executeQuarkOperation(op2, v2, r2, s2); + 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); } @@ -303,11 +324,13 @@ contract ReplayableTransactionsTest is Test { uint216 totalAmountToPurchase = 20 ether; RecurringPurchase.PurchaseConfig memory purchaseConfig = createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); - QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( aliceWallet, recurringPurchase, abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), - ScriptType.ScriptAddress + ScriptType.ScriptAddress, + 1 ); op.expiry = purchaseConfig.swapParams.deadline; (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); @@ -317,13 +340,13 @@ contract ReplayableTransactionsTest is Test { // gas: meter execute vm.resumeGasMetering(); // 1. Execute recurring purchase for the first time - aliceWallet.executeQuarkOperation(op, v1, r1, s1); + 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.executeQuarkOperation(op, v1, r1, s1); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 10 ether); } @@ -338,11 +361,12 @@ contract ReplayableTransactionsTest is Test { uint216 totalAmountToPurchase = 10 ether; RecurringPurchase.PurchaseConfig memory purchaseConfig = createPurchaseConfig(purchaseInterval, timesToPurchase, totalAmountToPurchase); - QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + (QuarkWallet.QuarkOperation memory op,) = new QuarkOperationHelper().newReplayableOpWithCalldata( aliceWallet, recurringPurchase, abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), - ScriptType.ScriptAddress + 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); @@ -364,11 +388,12 @@ contract ReplayableTransactionsTest is Test { 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( + (QuarkWallet.QuarkOperation memory op,) = new QuarkOperationHelper().newReplayableOpWithCalldata( aliceWallet, recurringPurchase, abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), - ScriptType.ScriptAddress + ScriptType.ScriptAddress, + 0 ); (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); @@ -389,11 +414,13 @@ contract ReplayableTransactionsTest is Test { 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( + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( aliceWallet, recurringPurchase, abi.encodeWithSelector(RecurringPurchase.purchase.selector, purchaseConfig), - ScriptType.ScriptAddress + ScriptType.ScriptAddress, + 1 ); op.expiry = purchaseConfig.swapParams.deadline; (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); @@ -412,7 +439,7 @@ contract ReplayableTransactionsTest is Test { // 3. Purchasing again will go over the `totalAmountToPurchase` cap vm.expectRevert(RecurringPurchase.PurchaseConditionNotMet.selector); - aliceWallet.executeQuarkOperation(op, v1, r1, s1); + 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 8af9c5d1..50a707b4 100644 --- a/test/lib/AllowCallbacks.sol +++ b/test/lib/AllowCallbacks.sol @@ -1,21 +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 { - function run(address callbackAddress) public { - QuarkWallet self = QuarkWallet(payable(address(this))); - self.stateManager().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(); - 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/BatchCallback.sol b/test/lib/BatchCallback.sol new file mode 100644 index 00000000..b1491e6a --- /dev/null +++ b/test/lib/BatchCallback.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.27; + +import "quark-core/src/QuarkScript.sol"; +import "quark-core/src/QuarkWallet.sol"; + +contract BatchSend { + function submitTwo( + QuarkWallet wallet1, + QuarkWallet.QuarkOperation memory op1, + uint8 v1, + bytes32 r1, + bytes32 s1, + QuarkWallet wallet2, + QuarkWallet.QuarkOperation memory op2, + uint8 v2, + bytes32 r2, + bytes32 s2 + ) public returns (uint256) { + wallet1.executeQuarkOperation(op1, v1, r1, s1); + wallet2.executeQuarkOperation(op2, v2, r2, s2); + return IncrementByCallback(address(wallet1)).number(); + } +} + +contract IncrementByCallback is QuarkScript { + uint256 public number; + + function run() public { + allowCallback(); + IncrementByCallback(address(this)).increment(); + IncrementByCallback(address(this)).increment(); + } + + function increment() external { + number++; + } +} + +contract CallIncrement is QuarkScript { + function run(address wallet) public { + IncrementByCallback(wallet).increment(); + } +} diff --git a/test/lib/CallOtherScript.sol b/test/lib/CallOtherScript.sol deleted file mode 100644 index fb669ca0..00000000 --- a/test/lib/CallOtherScript.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; - -import "quark-core/src/QuarkScript.sol"; -import "quark-core/src/QuarkWallet.sol"; - -contract CallOtherScript is QuarkScript { - function call(uint96 nonce, address scriptAddress, bytes calldata scriptCalldata) public { - // Anti-pattern to clear replay first, but this is necessary to hit our edge case - allowReplay(); - QuarkWallet(payable(address(this))).stateManager().setActiveNonceAndCallback( - nonce, scriptAddress, scriptCalldata - ); - QuarkWallet(payable(address(this))).stateManager().setNonce(nonce); - } -} 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 7e3e2059..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(uint96 nonce) public { - return QuarkWallet(payable(address(this))).stateManager().setNonce(nonce); - } -} 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 0d70f12c..1b1baa61 100644 --- a/test/lib/ExecuteOnBehalf.sol +++ b/test/lib/ExecuteOnBehalf.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.23; +pragma solidity 0.8.27; 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/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 95c3f765..ed5465f9 100644 --- a/test/lib/ExecuteWithRequirements.sol +++ b/test/lib/ExecuteWithRequirements.sol @@ -1,20 +1,20 @@ // 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/QuarkStateManager.sol"; +import "quark-core/src/QuarkNonceManager.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])) { + QuarkNonceManager nonceManager = wallet.nonceManager(); + for (uint256 i = 0; i < requirements.length; i++) { + if (nonceManager.submissions(address(wallet), requirements[i]) == bytes32(uint256(0))) { revert RequirementNotMet(requirements[i]); } } diff --git a/test/lib/GetCallbackDetails.sol b/test/lib/GetCallbackDetails.sol new file mode 100644 index 00000000..e2c2379c --- /dev/null +++ b/test/lib/GetCallbackDetails.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.27; + +import {QuarkWalletMetadata} from "quark-core/src/QuarkWallet.sol"; +import {QuarkScript} from "quark-core/src/QuarkScript.sol"; + +contract GetCallbackDetails is QuarkScript { + function getCallbackAddress() public view returns (address) { + bytes32 callbackSlot = QuarkWalletMetadata.CALLBACK_SLOT; + address callbackAddress; + assembly { + callbackAddress := tload(callbackSlot) + } + return callbackAddress; + } +} 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 453453b0..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,9 +12,11 @@ contract Incrementer { Counter(counter).increment(); } - function incrementCounterReplayable(Counter counter) public { - incrementCounter(counter); - QuarkWallet(payable(address(this))).stateManager().clearNonce(); + function incrementCounter2(Counter counter) public { + Counter(counter).increment(); + Counter(counter).increment(); + Counter(counter).increment(); + Counter(counter).increment(); } fallback() external { @@ -23,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 d3a3c568..ff31ae3d 100644 --- a/test/lib/MaxCounterScript.sol +++ b/test/lib/MaxCounterScript.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/QuarkScript.sol"; import "test/lib/Counter.sol"; @@ -7,6 +7,8 @@ import "test/lib/Counter.sol"; contract MaxCounterScript is QuarkScript { error EnoughAlready(); + event Count(uint256 c); + function run(Counter c) external returns (bytes memory) { c.increment(); uint256 count = readU256("count"); @@ -16,7 +18,7 @@ contract MaxCounterScript is QuarkScript { } writeU256("count", count + 1); - allowReplay(); + 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 f06649b2..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, @@ -12,6 +13,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 SemiRandomNonceRequiresQuarkNonceManagerOrInitializedQuarkWallet(address quarkWallet); + error Impossible(); + function newBasicOp(QuarkWallet wallet, bytes memory scriptSource, ScriptType scriptType) external returns (QuarkWallet.QuarkOperation memory) @@ -34,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) { @@ -41,7 +68,8 @@ contract QuarkOperationHelper is Test { scriptAddress: scriptAddress, scriptSources: ensureScripts, scriptCalldata: scriptCalldata, - nonce: wallet.stateManager().nextNonce(address(wallet)), + nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); } else { @@ -49,9 +77,128 @@ contract QuarkOperationHelper is Test { scriptAddress: scriptAddress, scriptSources: ensureScripts, scriptCalldata: scriptCalldata, - nonce: wallet.stateManager().nextNonce(address(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) { + revert SemiRandomNonceRequiresQuarkNonceManagerOrInitializedQuarkWallet(address(wallet)); + } + + return semiRandomNonce(wallet.nonceManager(), wallet); + } + + /// @dev Note: not sufficiently random for non-test case usage. + function semiRandomNonce(QuarkNonceManager quarkNonceManager, QuarkWallet wallet) public view returns (bytes32) { + bytes32 nonce = bytes32(uint256(keccak256(abi.encodePacked(block.timestamp))) - 1); + while (true) { + if (quarkNonceManager.submissions(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/lib/RecurringPurchase.sol b/test/lib/RecurringPurchase.sol index 9b7fc77f..591cdde5 100644 --- a/test/lib/RecurringPurchase.sol +++ b/test/lib/RecurringPurchase.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"; @@ -46,8 +46,6 @@ contract RecurringPurchase is QuarkScript { } function purchase(PurchaseConfig calldata config) public { - allowReplay(); - bytes32 hashedConfig = hashConfig(config); PurchaseState memory purchaseState; if (read(hashedConfig) == 0) { @@ -96,8 +94,8 @@ contract RecurringPurchase is QuarkScript { ); } - function cancel() external { - // Not explicitly clearing the nonce just cancels the replayable txn + function nop() external { + // used to no-op and cancel script } function hashConfig(PurchaseConfig calldata config) internal pure returns (bytes32) { 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 a993bbc3..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"; @@ -10,7 +10,7 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol"; @@ -48,7 +48,7 @@ contract ConditionalMulticallTest is Test { ), 18429607 // 2023-10-25 13:24:00 PST ); - factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkStateManager()))); + factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkNonceManager()))); counter = new Counter(); counter.setNumber(0); ethcallAddress = QuarkWallet(payable(factory.walletImplementation())).codeJar().saveCode(ethcall); diff --git a/test/quark-core-scripts/Ethcall.t.sol b/test/quark-core-scripts/Ethcall.t.sol index 3345a28c..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"; @@ -11,7 +11,7 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol"; @@ -48,7 +48,7 @@ contract EthcallTest is Test { ); counter = new Counter(); - factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkStateManager()))); + factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkNonceManager()))); counter.setNumber(0); QuarkWallet(payable(factory.walletImplementation())).codeJar().saveCode(ethcall); } diff --git a/test/quark-core-scripts/Multicall.t.sol b/test/quark-core-scripts/Multicall.t.sol index 0ddbee3f..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"; @@ -8,7 +8,7 @@ import "forge-std/StdUtils.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol"; @@ -58,7 +58,7 @@ contract MulticallTest is Test { ), 18429607 // 2023-10-25 13:24:00 PST ); - factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkStateManager()))); + factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkNonceManager()))); counter = new Counter(); counter.setNumber(0); @@ -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())).nonceManager(), 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())).nonceManager(), 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())).nonceManager(), 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); @@ -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-scripts/Paycall.t.sol b/test/quark-core-scripts/Paycall.t.sol index 4cdf1831..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"; @@ -8,7 +8,7 @@ import "forge-std/StdUtils.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol"; @@ -81,7 +81,7 @@ contract PaycallTest is Test { ), 18429607 // 2023-10-25 13:24:00 PST ); - factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkStateManager()))); + factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkNonceManager()))); counter = new Counter(); counter.setNumber(0); diff --git a/test/quark-core-scripts/Quotecall.t.sol b/test/quark-core-scripts/Quotecall.t.sol index f0fd943b..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"; @@ -8,7 +8,7 @@ import "forge-std/StdUtils.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol"; @@ -82,7 +82,7 @@ contract QuotecallTest is Test { ), 18429607 // 2023-10-25 13:24:00 PST ); - factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkStateManager()))); + factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkNonceManager()))); counter = new Counter(); counter.setNumber(0); diff --git a/test/quark-core-scripts/UniswapFlashLoan.t.sol b/test/quark-core-scripts/UniswapFlashLoan.t.sol index 1d49e43e..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"; @@ -10,7 +10,7 @@ import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol"; @@ -56,7 +56,7 @@ contract UniswapFlashLoanTest is Test { ), 18429607 // 2023-10-25 13:24:00 PST ); - factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkStateManager()))); + factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkNonceManager()))); CodeJar codeJar = QuarkWallet(payable(factory.walletImplementation())).codeJar(); ethcallAddress = codeJar.saveCode(ethcall); multicallAddress = codeJar.saveCode(multicall); @@ -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 `stateManager` 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 1ec5786c..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"; @@ -10,7 +10,7 @@ import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol"; @@ -52,7 +52,7 @@ contract UniswapFlashSwapExactOutTest is Test { ), 18429607 // 2023-10-25 13:24:00 PST ); - factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkStateManager()))); + factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkNonceManager()))); ethcallAddress = QuarkWallet(payable(factory.walletImplementation())).codeJar().saveCode(ethcall); multicallAddress = QuarkWallet(payable(factory.walletImplementation())).codeJar().saveCode(multicall); uniswapFlashSwapExactOutAddress = @@ -211,7 +211,7 @@ contract UniswapFlashSwapExactOutTest is Test { // gas: meter execute vm.resumeGasMetering(); - // Reverts when calling `allowCallback()`, which tries to get the `stateManager` from self + // Reverts when calling `allowCallback()`, which tries to get the `nonceManager` from self vm.expectRevert(); UniswapFlashSwapExactOut(uniswapFlashSwapExactOutAddress).run(payload); } 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/BatchCallback.t.sol b/test/quark-core/BatchCallback.t.sol new file mode 100644 index 00000000..ab49410a --- /dev/null +++ b/test/quark-core/BatchCallback.t.sol @@ -0,0 +1,96 @@ +// 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 {BatchSend} from "test/lib/BatchCallback.sol"; + +contract BatchCallbackTest 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() + + uint256 bobPrivateKey = 0x8675309; + address bobAccount = vm.addr(bobPrivateKey); + QuarkWallet bobWallet; // 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)); + + bobWallet = newWallet(bobAccount, address(0)); + console.log("Bob signer: %s", bobAccount); + console.log("Bob wallet at: %s", address(bobWallet)); + } + + /** + * get active nonce, submission token, replay count *************************** + * + * single + */ + function testBatchCallWithCallback() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + BatchSend batchSend = new BatchSend(); + bytes memory incrementByCallbackScript = new YulHelper().getCode("BatchCallback.sol/IncrementByCallback.json"); + QuarkWallet.QuarkOperation memory op1 = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, incrementByCallbackScript, abi.encodeWithSignature("run()"), ScriptType.ScriptSource + ); + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); + + bytes memory callIncrementScript = new YulHelper().getCode("BatchCallback.sol/CallIncrement.json"); + QuarkWallet.QuarkOperation memory op2 = new QuarkOperationHelper().newBasicOpWithCalldata( + bobWallet, + callIncrementScript, + abi.encodeWithSignature("run(address)", address(aliceWallet)), + ScriptType.ScriptSource + ); + (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(bobPrivateKey, bobWallet, op2); + + // gas: meter execute + vm.resumeGasMetering(); + vm.expectRevert(abi.encodeWithSelector(QuarkWallet.NoActiveCallback.selector)); + batchSend.submitTwo(aliceWallet, op1, v1, r1, s1, bobWallet, op2, v2, r2, s2); + } +} diff --git a/test/quark-core/Callbacks.t.sol b/test/quark-core/Callbacks.t.sol index 3c6ae50e..cf546b3f 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"; @@ -7,7 +7,7 @@ import {Test} from "forge-std/Test.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.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"; @@ -31,7 +31,7 @@ import {Ethcall} from "quark-core-scripts/src/Ethcall.sol"; contract CallbacksTest is Test { CodeJar public codeJar; Counter public counter; - QuarkStateManager public stateManager; + QuarkNonceManager public nonceManager; QuarkWallet public walletImplementation; uint256 alicePrivateKey = 0x9810473; @@ -42,14 +42,14 @@ contract CallbacksTest is Test { codeJar = new CodeJar(); console.log("CodeJar deployed to: %s", address(codeJar)); - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); counter = new Counter(); counter.setNumber(0); console.log("Counter deployed to: %s", address(counter)); - walletImplementation = new QuarkWallet(codeJar, stateManager); + walletImplementation = new QuarkWallet(codeJar, nonceManager); console.log("QuarkWallet implementation: %s", address(walletImplementation)); aliceAccount = vm.addr(alicePrivateKey); @@ -131,16 +131,51 @@ 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); // gas: meter execute vm.resumeGasMetering(); aliceWallet.executeQuarkOperation(parentOp, v, r, s); + assertEq(counter.number(), 11); } + function testNestedCallbackResetsCallbackSlot() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory getCallbackDetails = new YulHelper().getCode("GetCallbackDetails.sol/GetCallbackDetails.json"); + bytes memory executeOtherScript = + new YulHelper().getCode("ExecuteOtherOperation.sol/ExecuteOtherOperation.json"); + + QuarkWallet.QuarkOperation memory nestedOp = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, getCallbackDetails, abi.encodeWithSignature("getCallbackAddress()"), ScriptType.ScriptAddress + ); + + (uint8 v_, bytes32 r_, bytes32 s_) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, nestedOp); + + QuarkWallet.QuarkOperation memory parentOp = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, + executeOtherScript, + abi.encodeWithSelector(ExecuteOtherOperation.run.selector, nestedOp, v_, r_, s_), + ScriptType.ScriptAddress + ); + + parentOp.nonce = new QuarkOperationHelper().incrementNonce(nestedOp.nonce); + + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, parentOp); + + // gas: meter execute + vm.resumeGasMetering(); + bytes memory result = aliceWallet.executeQuarkOperation(parentOp, v, r, s); + // We decode twice because the result is encoded twice due to the nested operation + address innerCallbackAddress = abi.decode(abi.decode(result, (bytes)), (address)); + + // The inner callback address should be 0 + assertEq(innerCallbackAddress, address(0)); + } + function testNestedCallWithNoCallbackSucceeds() public { // gas: do not meter set-up vm.pauseGasMetering(); @@ -163,7 +198,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); @@ -173,32 +208,59 @@ contract CallbacksTest is Test { assertEq(counter.number(), 2); } - function testClearCallback() public { + function testSimpleCallback() 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 + (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); - 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(stateManager.walletStorage(address(aliceWallet), op1.nonce, callbackKey), bytes32(0)); + // 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"); - assertNotEq(stateManager.walletStorage(address(aliceWallet), op1.nonce, callbackKey), bytes32(0)); + (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); - aliceWallet.executeQuarkOperation(op2, v2, r2, s2); - assertEq(stateManager.walletStorage(address(aliceWallet), op1.nonce, callbackKey), bytes32(0)); + // gas: meter execute + vm.resumeGasMetering(); + vm.expectRevert(abi.encodeWithSelector(QuarkWallet.NoActiveCallback.selector)); + aliceWallet.executeQuarkOperation(op1, v1, r1, s1); } function testRevertsOnCallbackWhenNoActiveCallback() public { diff --git a/test/quark-core/EIP1271.t.sol b/test/quark-core/EIP1271.t.sol index 972ea6c9..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"; @@ -7,7 +7,7 @@ import "forge-std/console.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; import {QuarkWalletStandalone} from "quark-core/src/QuarkWalletStandalone.sol"; @@ -21,7 +21,7 @@ import {QuarkOperationHelper, ScriptType} from "test/lib/QuarkOperationHelper.so contract EIP1271Test is Test { CodeJar public codeJar; Counter public counter; - QuarkStateManager public stateManager; + QuarkNonceManager public nonceManager; QuarkWallet public aliceWallet; uint256 alicePrivateKey = 0xa11ce; @@ -35,11 +35,11 @@ contract EIP1271Test is Test { counter.setNumber(0); console.log("Counter deployed to: %s", address(counter)); - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); alice = vm.addr(alicePrivateKey); - aliceWallet = new QuarkWalletStandalone(alice, address(0), codeJar, stateManager); + aliceWallet = new QuarkWalletStandalone(alice, address(0), codeJar, nonceManager); } function incrementCounterOperation(QuarkWallet targetWallet) public returns (QuarkWallet.QuarkOperation memory) { @@ -59,7 +59,7 @@ contract EIP1271Test is Test { // QuarkWallet is owned by a smart contract that always approves signatures EIP1271Signer signatureApprover = new EIP1271Signer(true); QuarkWallet contractWallet = - new QuarkWalletStandalone(address(signatureApprover), address(0), codeJar, stateManager); + new QuarkWalletStandalone(address(signatureApprover), address(0), codeJar, nonceManager); // signature from alice; doesn't matter because the EIP1271Signer will approve anything QuarkWallet.QuarkOperation memory op = incrementCounterOperation(aliceWallet); @@ -79,7 +79,7 @@ contract EIP1271Test is Test { // QuarkWallet is owned by a smart contract that always rejects signatures EIP1271Signer signatureApprover = new EIP1271Signer(false); QuarkWallet contractWallet = - new QuarkWalletStandalone(address(signatureApprover), address(0), codeJar, stateManager); + new QuarkWalletStandalone(address(signatureApprover), address(0), codeJar, nonceManager); // signature from alice; doesn't matter because the EIP1271Signer will reject anything QuarkWallet.QuarkOperation memory op = incrementCounterOperation(aliceWallet); diff --git a/test/quark-core/EIP712.t.sol b/test/quark-core/EIP712.t.sol index 74f9b5c1..002c49c3 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"; @@ -9,7 +9,7 @@ import {Test} from "forge-std/Test.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; import {QuarkWalletStandalone} from "quark-core/src/QuarkWalletStandalone.sol"; @@ -25,7 +25,7 @@ contract EIP712Test is Test { CodeJar public codeJar; Counter public counter; QuarkWallet public wallet; - QuarkStateManager public stateManager; + QuarkNonceManager public nonceManager; uint256 alicePrivateKey = 0xa11ce; address alice; // see setup() @@ -35,15 +35,15 @@ contract EIP712Test is Test { codeJar = new CodeJar(); console.log("CodeJar deployed to: %s", address(codeJar)); - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); counter = new Counter(); counter.setNumber(0); console.log("Counter deployed to: %s", address(counter)); alice = vm.addr(alicePrivateKey); - wallet = new QuarkWalletStandalone(alice, address(0), codeJar, stateManager); + wallet = new QuarkWalletStandalone(alice, address(0), codeJar, nonceManager); } function incrementCounterOperation(QuarkWallet targetWallet) public returns (QuarkWallet.QuarkOperation memory) { @@ -73,7 +73,7 @@ contract EIP712Test is Test { assertEq(counter.number(), 3); // nonce is spent - assertEq(stateManager.isNonceSet(address(wallet), op.nonce), true); + 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(stateManager.isNonceSet(address(wallet), op.nonce), false); + assertEq(nonceManager.submissions(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"); @@ -122,6 +122,7 @@ contract EIP712Test is Test { QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ nonce: nextNonce, + isReplayable: true, scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: scriptCalldata, @@ -137,14 +138,16 @@ contract EIP712Test is Test { verifyingContract: '0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9' }, { QuarkOperation: [ - { name: 'nonce', type: 'uint96' }, + { name: 'nonce', type: 'bytes32' }, + { name: 'isReplayable', type: 'bool' }, { name: 'scriptAddress', type: 'address' }, { name: 'scriptSources', type: 'bytes[]' }, { name: 'scriptCalldata', type: 'bytes' }, { name: 'expiry', type: 'uint256' } ]}, { - nonce: 0, + nonce: '0x0000000000000000000000000000000000000000000000000000000000000000', + isReplayable: true, scriptAddress: '0x5cB7957c702bB6BB8F22aCcf66657F0defd4550b', scriptSources: ['0x608060405234801561001057600080fd5b506102a7806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80636b582b7614610056578063e5910ae714610069575b73f62849f9a0b5bf2913b396098f7c7019b51a820a61005481610077565b005b610054610064366004610230565b610173565b610054610077366004610230565b806001600160a01b031663d09de08a6040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156100b257600080fd5b505af11580156100c6573d6000803e3d6000fd5b50505050806001600160a01b031663d09de08a6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561010557600080fd5b505af1158015610119573d6000803e3d6000fd5b50505050806001600160a01b031663d09de08a6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561015857600080fd5b505af115801561016c573d6000803e3d6000fd5b5050505050565b61017c81610077565b306001600160a01b0316632e716fb16040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101ba573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101de9190610254565b6001600160a01b0316631913592a6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561015857600080fd5b6001600160a01b038116811461022d57600080fd5b50565b60006020828403121561024257600080fd5b813561024d81610218565b9392505050565b60006020828403121561026657600080fd5b815161024d8161021856fea26469706673582212200d71f9cd831b3c67d6f6131f807ee7fc47d21f07fe8f7b90a01dab56abb8403464736f6c63430008170033'], scriptCalldata: '0xe5910ae7000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a', @@ -152,14 +155,16 @@ contract EIP712Test is Test { } ) - 0x1901ce5fced5138ae147492ff6ba56247e9d6f30bbbe45ae60eb0a0135d528a94be4aa19c4de25dfba6a38836420cc4ecf14048cee3f258a3329bfeb40856daf159b + 0x1901 + ce5fced5138ae147492ff6ba56247e9d6f30bbbe45ae60eb0a0135d528a94be4 + 115a39f16a8c9e3e390e94dc858a17eba53b5358382af38b02f1ac31c2b5f9b0 */ bytes32 domainHash = new SignatureHelper().domainSeparator(wallet_); assertEq(domainHash, hex"ce5fced5138ae147492ff6ba56247e9d6f30bbbe45ae60eb0a0135d528a94be4"); bytes32 structHash = new SignatureHelper().opStructHash(op); - assertEq(structHash, hex"aa19c4de25dfba6a38836420cc4ecf14048cee3f258a3329bfeb40856daf159b"); + assertEq(structHash, hex"115a39f16a8c9e3e390e94dc858a17eba53b5358382af38b02f1ac31c2b5f9b0"); } function testRevertsForBadCalldata() public { @@ -182,7 +187,7 @@ contract EIP712Test is Test { assertEq(counter.number(), 0); // nonce is not spent - assertEq(stateManager.isNonceSet(address(wallet), op.nonce), false); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(uint256(0))); } function testRevertsForBadExpiry() public { @@ -204,8 +209,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(nonceManager.submissions(address(wallet), op.nonce), bytes32(uint256(0))); } function testRevertsOnReusedNonce() public { @@ -222,10 +227,12 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, s); assertEq(counter.number(), 3); - assertEq(stateManager.nextNonce(address(wallet)), op.nonce + 1); + 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(QuarkStateManager.NonceAlreadySet.selector); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(wallet), op.nonce, op.nonce) + ); wallet.executeQuarkOperation(op, v, r, s); } @@ -248,7 +255,7 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, s); assertEq(counter.number(), 0); - assertEq(stateManager.nextNonce(address(wallet)), op.nonce); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(uint256(0))); } function testRevertsInvalidS() public { @@ -270,44 +277,7 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, invalidS); assertEq(counter.number(), 0); - assertEq(stateManager.nextNonce(address(wallet)), op.nonce); - } - - function testNonceIsNotSetForReplayableOperation() public { - // gas: do not meter set-up - vm.pauseGasMetering(); - bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); - - assertEq(counter.number(), 0); - - QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( - wallet, - incrementer, - abi.encodeWithSignature("incrementCounterReplayable(address)", counter), - ScriptType.ScriptSource - ); - - (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, wallet, op); - - // submitter calls executeQuarkOperation with the signed operation - // gas: meter execute - vm.resumeGasMetering(); - wallet.executeQuarkOperation(op, v, r, s); - - // counter is incremented - assertEq(counter.number(), 3); - - // nonce is NOT spent; the operation is replayable - assertEq(stateManager.isNonceSet(address(wallet), op.nonce), false); - - // submitter executes the operation a second time - wallet.executeQuarkOperation(op, v, r, s); - - // counter is incremented - assertEq(counter.number(), 6); - - // nonce is still not spent - assertEq(stateManager.isNonceSet(address(wallet), op.nonce), false); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(uint256(0))); } function testRevertBadRequirements() public { @@ -324,15 +294,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)) @@ -346,7 +316,7 @@ contract EIP712Test is Test { wallet.executeQuarkOperation(op, v, r, s); assertEq(counter.number(), 0); - assertEq(stateManager.nextNonce(address(wallet)), op.nonce); + assertEq(nonceManager.submissions(address(wallet), op.nonce), bytes32(uint256(0))); } function testRequirements() public { @@ -361,7 +331,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, @@ -373,7 +343,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..04af7a32 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"; @@ -7,7 +7,7 @@ import {Test} from "forge-std/Test.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; import {QuarkWalletStandalone} from "quark-core/src/QuarkWalletStandalone.sol"; @@ -19,7 +19,7 @@ import {QuarkOperationHelper, ScriptType} from "test/lib/QuarkOperationHelper.so contract ExecutorTest is Test { CodeJar public codeJar; Counter public counter; - QuarkStateManager public stateManager; + QuarkNonceManager public nonceManager; uint256 alicePrivateKey = 0xa11ce; address aliceAccount = vm.addr(alicePrivateKey); @@ -33,18 +33,18 @@ contract ExecutorTest is Test { codeJar = new CodeJar(); console.log("CodeJar deployed to: %s", address(codeJar)); - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); counter = new Counter(); console.log("Counter deployed to: %s", address(counter)); // alice sets her EOA to be her wallet's executor - aliceWallet = new QuarkWalletStandalone(aliceAccount, aliceAccount, codeJar, stateManager); + aliceWallet = new QuarkWalletStandalone(aliceAccount, aliceAccount, codeJar, nonceManager); console.log("aliceWallet at: %s", address(aliceWallet)); // bob sets alice's wallet as his wallet's executor - bobWallet = new QuarkWalletStandalone(bobAccount, address(aliceWallet), codeJar, stateManager); + bobWallet = new QuarkWalletStandalone(bobAccount, address(aliceWallet), codeJar, nonceManager); console.log("bobWallet at: %s", address(bobWallet)); } @@ -60,17 +60,20 @@ contract ExecutorTest is Test { vm.startPrank(aliceAccount); + bytes32 nonce0 = new QuarkOperationHelper().semiRandomNonce(nonceManager, aliceWallet); + bytes32 nonce1 = new QuarkOperationHelper().semiRandomNonce(nonceManager, bobWallet); + // gas: meter execute vm.resumeGasMetering(); // execute counter.increment(5) as bob from alice's wallet (that is, from bob's wallet's executor) aliceWallet.executeScript( - stateManager.nextNonce(address(aliceWallet)), + nonce0, executeOnBehalfAddress, abi.encodeWithSignature( - "run(address,uint96,address,bytes)", + "run(address,bytes32,address,bytes)", address(bobWallet), - stateManager.nextNonce(address(bobWallet)), + nonce1, address(ethcallAddress), abi.encodeWithSignature( "run(address,bytes,uint256)", address(counter), abi.encodeWithSignature("increment(uint256)", 5), 0 @@ -95,9 +98,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(nonceManager, 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 deleted file mode 100644 index dd41f156..00000000 --- a/test/quark-core/Nonce.t.sol +++ /dev/null @@ -1,48 +0,0 @@ -// 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 {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; - -contract NonceTest is Test { - QuarkStateManager public stateManager; - - function setUp() public { - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); - } - - function testIsSet() public { - // nonce is unset by default - assertEq(stateManager.isNonceSet(address(this), 0), false); - // it can be set - stateManager.setNonce(0); - assertEq(stateManager.isNonceSet(address(this), 0), true); - } - - 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; - - assertEq(stateManager.isNonceSet(address(this), nonce), false); - - stateManager.setNonce(nonce); - assertEq(stateManager.isNonceSet(address(this), nonce), true); - } - - function testNextUnusedNonce() public { - uint96 nonce1 = stateManager.nextNonce(address(this)); - - stateManager.setNonce(nonce1); - assertEq(stateManager.nextNonce(address(this)), nonce1 + 1); - - stateManager.setNonce(nonce1 + 1); - assertEq(stateManager.nextNonce(address(this)), nonce1 + 2); - } -} diff --git a/test/quark-core/Noncer.t.sol b/test/quark-core/Noncer.t.sol new file mode 100644 index 00000000..a75236a8 --- /dev/null +++ b/test/quark-core/Noncer.t.sol @@ -0,0 +1,416 @@ +// 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 {Counter} from "test/lib/Counter.sol"; +import {MaxCounterScript} from "test/lib/MaxCounterScript.sol"; +import {Stow} from "test/lib/Noncer.sol"; + +contract NoncerTest is Test { + enum ExecutionType { + Signature, + Direct + } + + CodeJar public codeJar; + Counter public counter; + 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)); + + counter = new Counter(); + counter.setNumber(0); + console.log("Counter deployed to: %s", address(counter)); + + 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, op.nonce); + 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, op.nonce); + 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 testPostNestReadsCorrectValue() public { + // gas: do not meter set-up + vm.pauseGasMetering(); + bytes memory noncerScript = new YulHelper().getCode("Noncer.sol/Noncer.json"); + bytes memory maxCounter = new YulHelper().getCode("MaxCounterScript.sol/MaxCounterScript.json"); + QuarkWallet.QuarkOperation memory nestedOp = new QuarkOperationHelper().newBasicOpWithCalldata( + aliceWallet, maxCounter, abi.encodeWithSignature("run(address)", address(counter)), 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(); + bytes memory result = aliceWallet.executeQuarkOperation(op, v, r, s); + + uint256 value = abi.decode(result, (uint256)); + assertEq(value, 0); + // Counter should be incremented in storage for the inner op, not the outer op + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(0)) + ); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(nestedOp.nonce, keccak256("count")))), + bytes32(uint256(1)) + ); + } + + /* + * 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 new file mode 100644 index 00000000..826a38b1 --- /dev/null +++ b/test/quark-core/QuarkNonceManager.t.sol @@ -0,0 +1,425 @@ +// 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 {QuarkWalletStandalone} from "quark-core/src/QuarkWalletStandalone.sol"; + +import {YulHelper} from "test/lib/YulHelper.sol"; +import {SignatureHelper} from "test/lib/SignatureHelper.sol"; +import {QuarkOperationHelper, ScriptType} from "test/lib/QuarkOperationHelper.sol"; + +import {Logger} from "test/lib/Logger.sol"; +import {Counter} from "test/lib/Counter.sol"; + +contract QuarkNonceManagerTest is Test { + CodeJar public codeJar; + QuarkNonceManager public nonceManager; + + /// @notice Represents the unclaimed bytes32 value. + bytes32 public constant FREE_TOKEN = bytes32(uint256(0)); + + /// @notice A token that implies a Quark Operation is no longer replayable. + bytes32 public constant EXHAUSTED_TOKEN = bytes32(type(uint256).max); + + bytes32 public constant NONCE_ZERO = bytes32(uint256(0)); + bytes32 public constant NONCE_ONE = bytes32(uint256(1)); + + constructor() { + codeJar = new CodeJar(); + console.log("CodeJar deployed to: %s", address(codeJar)); + + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); + } + + function testNonceOneIsValid() 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()); + } + + 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 = 1; i <= 550; i++) { + nonceManager.submit(bytes32(i), false, bytes32(i)); + } + + for (uint256 i = 1; i <= 20; i++) { + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(this), bytes32(i), bytes32(type(uint256).max) + ) + ); + nonceManager.submit(bytes32(i), false, EXHAUSTED_TOKEN); + } + } + + function testRevertsIfNonceIsAlreadySet() public { + bytes32 EXHAUSTED = nonceManager.EXHAUSTED(); + bytes32 nonce = bytes32(uint256(1)); + nonceManager.submit(nonce, false, nonce); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, nonce) + ); + nonceManager.submit(nonce, false, nonce); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, EXHAUSTED) + ); + 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.InvalidSubmissionToken.selector, address(this), nonce, bytes32(uint256(1)) + ) + ); + 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) + ); + 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.submissions(address(this), NONCE_ONE), FREE_TOKEN); + + // it can be set + nonceManager.submit(NONCE_ONE, false, NONCE_ONE); + assertEq(nonceManager.submissions(address(this), NONCE_ONE), EXHAUSTED_TOKEN); + } + + function testNonLinearNonce() 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.submissions(address(this), NONCE_ONE), FREE_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) + ); + nonceManager.submit(nonce, false, FREE_TOKEN); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, EXHAUSTED_TOKEN) + ); + nonceManager.submit(nonce, false, EXHAUSTED_TOKEN); + } + + 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 nonceHash = keccak256(abi.encodePacked(nonce)); + + assertEq(nonceManager.submissions(address(this), NONCE_ONE), FREE_TOKEN); + + nonceManager.submit(nonce, true, nonce); + assertEq(nonceManager.submissions(address(this), nonce), nonce); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonce) + ); + nonceManager.submit(nonce, true, nonce); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonceHash) + ); + nonceManager.submit(nonce, true, nonceHash); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, FREE_TOKEN) + ); + nonceManager.submit(nonce, true, FREE_TOKEN); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), 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 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); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonce) + ); + nonceManager.submit(nonce, true, nonce); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken2 + ) + ); + nonceManager.submit(nonce, true, submissionToken2); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonceSecret) + ); + nonceManager.submit(nonce, true, nonceSecret); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, FREE_TOKEN) + ); + nonceManager.submit(nonce, true, FREE_TOKEN); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, EXHAUSTED_TOKEN + ) + ); + nonceManager.submit(nonce, true, EXHAUSTED_TOKEN); + + nonceManager.submit(nonce, true, submissionToken1); + assertEq(nonceManager.submissions(address(this), nonce), submissionToken1); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonce) + ); + nonceManager.submit(nonce, true, nonce); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken1 + ) + ); + nonceManager.submit(nonce, true, submissionToken1); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonceSecret) + ); + nonceManager.submit(nonce, true, nonceSecret); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, FREE_TOKEN) + ); + nonceManager.submit(nonce, true, FREE_TOKEN); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, EXHAUSTED_TOKEN + ) + ); + nonceManager.submit(nonce, true, EXHAUSTED_TOKEN); + + nonceManager.submit(nonce, true, submissionToken2); + assertEq(nonceManager.submissions(address(this), nonce), submissionToken2); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonce) + ); + nonceManager.submit(nonce, true, nonce); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken1 + ) + ); + nonceManager.submit(nonce, true, submissionToken1); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken2 + ) + ); + nonceManager.submit(nonce, true, submissionToken2); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, FREE_TOKEN) + ); + nonceManager.submit(nonce, true, FREE_TOKEN); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, EXHAUSTED_TOKEN + ) + ); + nonceManager.submit(nonce, true, EXHAUSTED_TOKEN); + + nonceManager.submit(nonce, true, nonceSecret); + assertEq(nonceManager.submissions(address(this), nonce), nonceSecret); + + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonce) + ); + nonceManager.submit(nonce, true, nonce); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken1 + ) + ); + nonceManager.submit(nonce, true, submissionToken1); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, submissionToken2 + ) + ); + nonceManager.submit(nonce, true, submissionToken2); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, nonceSecret) + ); + nonceManager.submit(nonce, true, nonceSecret); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.InvalidSubmissionToken.selector, address(this), nonce, FREE_TOKEN) + ); + nonceManager.submit(nonce, true, FREE_TOKEN); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.InvalidSubmissionToken.selector, address(this), 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) + ); + nonceManager.submit(nonce, true, nonce); + + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, submissionToken2 + ) + ); + nonceManager.submit(nonce, true, submissionToken2); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, submissionToken1 + ) + ); + nonceManager.submit(nonce, true, submissionToken1); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, nonceSecret) + ); + nonceManager.submit(nonce, true, nonceSecret); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, EXHAUSTED_TOKEN) + ); + nonceManager.submit(nonce, true, EXHAUSTED_TOKEN); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, address(this), nonce, FREE_TOKEN) + ); + 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) + ); + 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/QuarkStateManager.t.sol b/test/quark-core/QuarkStateManager.t.sol deleted file mode 100644 index 559d54b3..00000000 --- a/test/quark-core/QuarkStateManager.t.sol +++ /dev/null @@ -1,152 +0,0 @@ -// 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 {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; -import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; -import {QuarkWalletStandalone} from "quark-core/src/QuarkWalletStandalone.sol"; - -import {YulHelper} from "test/lib/YulHelper.sol"; -import {SignatureHelper} from "test/lib/SignatureHelper.sol"; -import {QuarkOperationHelper, ScriptType} from "test/lib/QuarkOperationHelper.sol"; - -import {Logger} from "test/lib/Logger.sol"; -import {Counter} from "test/lib/Counter.sol"; -import {MaxCounterScript} from "test/lib/MaxCounterScript.sol"; - -contract QuarkStateManagerTest is Test { - CodeJar public codeJar; - QuarkStateManager public stateManager; - - constructor() { - codeJar = new CodeJar(); - console.log("CodeJar deployed to: %s", address(codeJar)); - - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); - } - - function testRevertsForNoActiveNonce() public { - // this contract does not have an active nonce - vm.expectRevert(abi.encodeWithSelector(QuarkStateManager.NoActiveNonce.selector)); - stateManager.clearNonce(); - - vm.expectRevert(abi.encodeWithSelector(QuarkStateManager.NoActiveNonce.selector)); - stateManager.read(bytes32("hello")); - - vm.expectRevert(abi.encodeWithSelector(QuarkStateManager.NoActiveNonce.selector)); - stateManager.write(bytes32("hello"), bytes32("goodbye")); - - vm.expectRevert(abi.encodeWithSelector(QuarkStateManager.NoActiveNonce.selector)); - stateManager.getActiveScript(); - } - - function testNonceZeroIsValid() public { - // by default, nonce 0 is not set - assertEq(stateManager.isNonceSet(address(0x123), 0), false); - - // nonce 0 can be set manually - vm.prank(address(0x123)); - stateManager.setNonce(0); - assertEq(stateManager.isNonceSet(address(0x123), 0), true); - - // a QuarkWallet can use nonce 0 as the active nonce - vm.pauseGasMetering(); // do not meter deployment gas - QuarkWallet wallet = new QuarkWalletStandalone(address(0), address(0), codeJar, stateManager); - bytes memory getMessageDetails = new YulHelper().getCode("GetMessageDetails.sol/GetMessageDetails.json"); - address scriptAddress = codeJar.saveCode(getMessageDetails); - bytes memory call = abi.encodeWithSignature("getMsgSenderAndValue()"); - vm.resumeGasMetering(); - - vm.prank(address(wallet)); - bytes memory result = stateManager.setActiveNonceAndCallback(0, scriptAddress, call); - assertEq(result, abi.encode(address(wallet), 0)); - assertEq(stateManager.isNonceSet(address(wallet), 0), true); - // TODO: enable this test-case once nonceScriptAddress is public - // assertEq(stateManager.nonceScriptAddress(address(wallet), 0), scriptAddress); - } - - function testSetActiveNonceAndCallbackNotImplemented() public { - // address(this) is a contract that does not implement a fallback; should revert - vm.expectRevert(); - stateManager.setActiveNonceAndCallback(0, address(0), bytes("")); - - // for an EOA, setActiveNonceAndCallback will also revert... - vm.expectRevert(); - vm.prank(address(0x123)); - stateManager.setActiveNonceAndCallback(0, address(0x123), bytes("")); - } - - function testRevertsIfScriptAddressIsNull() public { - // the null script is not allowed, it will revert with EmptyCode - vm.pauseGasMetering(); - QuarkWallet wallet = new QuarkWalletStandalone(address(0), address(0), codeJar, stateManager); - vm.resumeGasMetering(); - vm.prank(address(wallet)); - vm.expectRevert(abi.encodeWithSelector(QuarkWallet.EmptyCode.selector)); - bytes memory result = stateManager.setActiveNonceAndCallback(0, address(0), bytes("")); - assertEq(result, bytes("")); - } - - function testRevertsIfScriptAddressIsEOA() public { - // an EOA can be passed as scriptAddress and it will just return empty bytes - vm.pauseGasMetering(); - QuarkWallet wallet = new QuarkWalletStandalone(address(0), address(0), codeJar, stateManager); - vm.resumeGasMetering(); - vm.prank(address(wallet)); - vm.expectRevert(abi.encodeWithSelector(QuarkWallet.EmptyCode.selector)); - stateManager.setActiveNonceAndCallback(0, address(0x123), bytes("")); - } - - function testReadStorageForWallet() public { - // gas: disable metering except while executing operations - vm.pauseGasMetering(); - - Counter counter = new Counter(); - 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)); - - QuarkWallet wallet = new QuarkWalletStandalone(address(0), address(0), codeJar, stateManager); - - vm.resumeGasMetering(); - - assertEq(stateManager.walletStorage(address(wallet), 0, keccak256("count")), bytes32(uint256(0))); - - vm.prank(address(wallet)); - stateManager.setActiveNonceAndCallback(0, maxCounterScriptAddress, call); - - assertEq(stateManager.walletStorage(address(wallet), 0, keccak256("count")), bytes32(uint256(1))); - - vm.prank(address(wallet)); - stateManager.setActiveNonceAndCallback(0, maxCounterScriptAddress, call); - - assertEq(stateManager.walletStorage(address(wallet), 0, keccak256("count")), bytes32(uint256(2))); - } - - function testSetsAndGetsNextNonces() public { - assertEq(stateManager.nextNonce(address(this)), 0); - - for (uint96 i = 0; i <= 550; i++) { - stateManager.setNonce(i); - } - - assertEq(stateManager.nextNonce(address(this)), 551); - - for (uint96 i = 552; i <= 570; i++) { - stateManager.setNonce(i); - } - - assertEq(stateManager.nextNonce(address(this)), 551); - - stateManager.setNonce(551); - - assertEq(stateManager.nextNonce(address(this)), 571); - } -} diff --git a/test/quark-core/QuarkWallet.t.sol b/test/quark-core/QuarkWallet.t.sol index 22f3fee5..b560cec2 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 {QuarkStateManager} from "quark-core/src/QuarkStateManager.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"; @@ -26,7 +28,7 @@ import {Incrementer} from "test/lib/Incrementer.sol"; import {PrecompileCaller} from "test/lib/PrecompileCaller.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,20 +37,26 @@ 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 + event QuarkExecution( + address indexed executor, + address indexed scriptAddress, + bytes32 indexed nonce, + bytes32 submissionToken, + bool isReplayable, + ExecutionType executionType ); CodeJar public codeJar; Counter public counter; - QuarkStateManager public stateManager; + 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))); @@ -62,10 +70,10 @@ contract QuarkWalletTest is Test { counter.setNumber(0); console.log("Counter deployed to: %s", address(counter)); - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); - walletImplementation = new QuarkWallet(codeJar, stateManager); + walletImplementation = new QuarkWallet(codeJar, nonceManager); console.log("QuarkWallet implementation: %s", address(walletImplementation)); aliceWallet = newWallet(aliceAccount, address(0)); @@ -87,8 +95,8 @@ contract QuarkWalletTest is Test { assertEq(address(aliceWallet.codeJar()), address(codeJar)); } - function testGetStateManager() public { - assertEq(address(aliceWallet.stateManager()), address(stateManager)); + function testGetNonceManager() public { + assertEq(address(aliceWallet.nonceManager()), address(nonceManager)); } /* ===== msg.value and msg.sender tests ===== */ @@ -116,7 +124,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(nonceManager, aliceWalletExecutable); address scriptAddress = codeJar.saveCode(getMessageDetails); bytes memory call = abi.encodeWithSignature("getMsgSenderAndValue()"); @@ -147,7 +155,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; @@ -155,20 +163,78 @@ 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(); 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(nonceManager, aliceWalletExecutable); address scriptAddress = codeJar.saveCode(getMessageDetails); bytes memory call = abi.encodeWithSignature("getMsgSenderAndValue()"); @@ -177,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 + // ) + // ); + 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 + ) + ); + 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: stateManager.nextNonce(address(aliceWallet)), + nonce: new QuarkOperationHelper().semiRandomNonce(nonceManager, aliceWallet), + isReplayable: false, scriptAddress: address(0), scriptSources: new bytes[](0), scriptCalldata: bytes(""), @@ -204,7 +359,9 @@ contract QuarkWalletTest is Test { aliceWallet.executeQuarkOperation(op, v, r, s); // direct execution of the null script will revert - uint96 nonce = stateManager.nextNonce(address(aliceWallet)); + vm.pauseGasMetering(); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(nonceManager, aliceWallet); + vm.resumeGasMetering(); vm.prank(IHasSignerExecutor(address(aliceWallet)).executor()); vm.expectRevert(abi.encodeWithSelector(QuarkWallet.EmptyCode.selector)); aliceWallet.executeScript(nonce, address(0), bytes(""), new bytes[](0)); @@ -217,7 +374,8 @@ 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(nonceManager, aliceWallet), + isReplayable: false, scriptAddress: emptyCodeAddress, scriptSources: new bytes[](0), scriptCalldata: bytes(""), @@ -233,7 +391,9 @@ contract QuarkWalletTest is Test { aliceWallet.executeQuarkOperation(op2, v2, r2, s2); // direct execution of empty script will revert - uint96 nonce2 = stateManager.nextNonce(address(aliceWallet)); + vm.pauseGasMetering(); + bytes32 nonce2 = new QuarkOperationHelper().semiRandomNonce(nonceManager, aliceWallet); + vm.resumeGasMetering(); vm.prank(IHasSignerExecutor(address(aliceWallet)).executor()); vm.expectRevert(abi.encodeWithSelector(QuarkWallet.EmptyCode.selector)); aliceWallet.executeScript(nonce2, emptyCodeAddress, bytes(""), new bytes[](0)); @@ -247,7 +407,8 @@ 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(nonceManager, aliceWallet), + isReplayable: false, scriptAddress: address(0xc0c0), scriptSources: scriptSources, scriptCalldata: bytes("feefee"), @@ -262,6 +423,63 @@ contract QuarkWalletTest is Test { aliceWallet.executeQuarkOperation(op, v, r, s); } + /* ===== storage tests ===== */ + + function testReadStorageForWallet() public { + // gas: disable metering except while executing operations + vm.pauseGasMetering(); + + assertEq(counter.number(), 0); + + bytes memory maxCounter = new YulHelper().getCode("MaxCounterScript.sol/MaxCounterScript.json"); + + (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); + + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(0)) + ); + + vm.resumeGasMetering(); + + 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); + + 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); + + 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); + + 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 ===== */ function testCanReplaySameScriptWithDifferentCall() public { @@ -270,11 +488,13 @@ contract QuarkWalletTest is Test { bytes memory incrementer = new YulHelper().getCode("Incrementer.sol/Incrementer.json"); // 1. use nonce to increment a counter - QuarkWallet.QuarkOperation memory op1 = new QuarkOperationHelper().newBasicOpWithCalldata( + (QuarkWallet.QuarkOperation memory op1, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( aliceWallet, incrementer, - abi.encodeWithSignature("incrementCounterReplayable(address)", address(counter)), - ScriptType.ScriptAddress + abi.encodeWithSignature("incrementCounter(address)", address(counter)), + ScriptType.ScriptAddress, + 1 ); (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op1); @@ -282,101 +502,178 @@ contract QuarkWalletTest is Test { QuarkWallet.QuarkOperation memory op2 = QuarkWallet.QuarkOperation({ nonce: op1.nonce, + isReplayable: true, scriptAddress: incrementerAddress, scriptSources: new bytes[](0), - scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", address(counter)), + scriptCalldata: abi.encodeWithSignature("incrementCounter2(address)", address(counter)), expiry: block.timestamp + 1000 }); (uint8 v2, bytes32 r2, bytes32 s2) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op2); // 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(QuarkStateManager.NonceAlreadySet.selector)); + // 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); } - function testRevertsForReusedNonceWithChangedScript() public { + 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"); // 1. use nonce to increment a counter - QuarkWallet.QuarkOperation memory op1 = new QuarkOperationHelper().newBasicOpWithCalldata( + (QuarkWallet.QuarkOperation memory op1, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( aliceWallet, incrementer, - abi.encodeWithSignature("incrementCounterReplayable(address)", address(counter)), - ScriptType.ScriptAddress + 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 = QuarkWallet.QuarkOperation({ - nonce: op1.nonce, - scriptAddress: address(counter), - scriptSources: new bytes[](0), - scriptCalldata: bytes(""), - expiry: op1.expiry - }); + 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(); - 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(QuarkStateManager.NonceScriptMismatch.selector)); - aliceWallet.executeQuarkOperation(op2, v2, r2, s2); + // 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 testRevertsForReplayOfCanceledScript() public { + function testScriptCanBeCanceledByNoOp() 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( + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( aliceWallet, incrementer, - abi.encodeWithSignature("incrementCounterReplayable(address)", address(counter)), - ScriptType.ScriptAddress + 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(); - 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... + // 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(), 6); + assertEq(counter.number(), 3); // can cancel the replayable nonce... vm.pauseGasMetering(); - QuarkWallet.QuarkOperation memory cancelOtherOp = new QuarkOperationHelper().newBasicOpWithCalldata( - aliceWallet, cancelOtherScript, abi.encodeWithSignature("run(uint96)", op.nonce), ScriptType.ScriptAddress - ); - (uint8 cancel_v, bytes32 cancel_r, bytes32 cancel_s) = + QuarkWallet.QuarkOperation memory cancelOtherOp = + new QuarkOperationHelper().cancelReplayableByNop(aliceWallet, op); + (uint8 cancelV, bytes32 cancelR, bytes32 cancelS) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, cancelOtherOp); vm.resumeGasMetering(); - aliceWallet.executeQuarkOperation(cancelOtherOp, cancel_v, cancel_r, cancel_s); + aliceWallet.executeQuarkOperationWithSubmissionToken( + cancelOtherOp, submissionTokens[1], cancelV, cancelR, cancelS + ); // and now you can no longer replay - vm.expectRevert(abi.encodeWithSelector(QuarkStateManager.NonceAlreadySet.selector)); + vm.expectRevert( + abi.encodeWithSelector( + QuarkNonceManager.NonReplayableNonce.selector, address(aliceWallet), op.nonce, submissionTokens[1] + ) + ); + 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.NonceSubmitted(address(aliceWallet), op.nonce, bytes32(type(uint256).max)); + 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] + ) + ); + 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 ===== */ @@ -387,21 +684,23 @@ 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(nonceManager, aliceWalletExecutable); bytes memory call = abi.encodeWithSignature("incrementCounter(address)", counter); assertEq(counter.number(), 0); - assertEq(stateManager.nextNonce(address(aliceWalletExecutable)), 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(stateManager.nextNonce(address(aliceWalletExecutable)), 1); + assertEq(nonceManager.submissions(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); } function testDirectExecuteFromOtherQuarkWallet() public { @@ -411,12 +710,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(nonceManager, 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) @@ -430,14 +730,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(nonceManager.submissions(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(nonceManager.submissions(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); } function testDirectExecuteWithScriptSources() public { @@ -446,13 +746,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(nonceManager, 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(nonceManager.submissions(address(aliceWalletExecutable), nonce), bytes32(uint256(0))); // act as the executor for the wallet vm.startPrank(aliceAccount); @@ -462,7 +762,7 @@ contract QuarkWalletTest is Test { aliceWalletExecutable.executeScript(nonce, incrementerAddress, call, scriptSources); assertEq(counter.number(), 3); - assertEq(stateManager.nextNonce(address(aliceWalletExecutable)), 1); + assertEq(nonceManager.submissions(address(aliceWalletExecutable), nonce), bytes32(type(uint256).max)); } function testRevertsForDirectExecuteByNonExecutorSigner() public { @@ -475,7 +775,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(nonceManager, aliceWallet); address target = codeJar.saveCode(incrementer); bytes memory call = abi.encodeWithSignature("incrementCounter(address)", counter); @@ -497,7 +797,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(nonceManager, aliceWallet); address target = codeJar.saveCode(incrementer); bytes memory call = abi.encodeWithSignature("incrementCounter(address)", counter); @@ -517,8 +817,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(); @@ -542,7 +840,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); @@ -585,7 +883,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; @@ -622,7 +920,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); @@ -637,12 +935,323 @@ 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( + QuarkNonceManager.NonReplayableNonce.selector, address(aliceWallet), op1.nonce, op1.nonce + ) + ); 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 + ) + ); + aliceWallet.executeMultiQuarkOperationWithSubmissionToken(op1, EXHAUSTED_TOKEN, opDigests, v, r, s); + vm.expectRevert( + abi.encodeWithSelector(QuarkNonceManager.NonReplayableNonce.selector, aliceWallet, op1.nonce, op1.nonce) + ); + 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 ===== */ function testAtomicMaxCounterScript() public { @@ -654,57 +1263,77 @@ contract QuarkWalletTest is Test { vm.startPrank(address(aliceAccount)); - QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( - aliceWallet, maxCounterScript, abi.encodeCall(MaxCounterScript.run, (counter)), ScriptType.ScriptAddress + (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(); + vm.expectEmit(true, true, true, true); + emit MaxCounterScript.Count(1); aliceWallet.executeQuarkOperation(op, v, r, s); // gas: do not meter walletStorage vm.pauseGasMetering(); assertEq(counter.number(), 1); - assertEq(uint256(stateManager.walletStorage(address(aliceWallet), op.nonce, keccak256("count"))), 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); + 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(); assertEq(counter.number(), 2); - assertEq(uint256(stateManager.walletStorage(address(aliceWallet), op.nonce, keccak256("count"))), 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); + 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(); assertEq(counter.number(), 3); - assertEq(uint256(stateManager.walletStorage(address(aliceWallet), op.nonce, keccak256("count"))), 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); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[3], v, r, s); // gas: do not meter walletStorage vm.pauseGasMetering(); assertEq(counter.number(), 3); assertEq( - uint256(stateManager.walletStorage(address(aliceWallet), op.nonce, keccak256("count"))), counter.number() + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(3)) ); counter.increment(); assertEq(counter.number(), 4); - assertEq(uint256(stateManager.walletStorage(address(aliceWallet), op.nonce, keccak256("count"))), 3); + assertEq( + vm.load(address(aliceWallet), keccak256(abi.encodePacked(op.nonce, keccak256("count")))), + bytes32(uint256(3)) + ); vm.resumeGasMetering(); vm.stopPrank(); @@ -749,7 +1378,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); @@ -768,7 +1397,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( @@ -783,6 +1412,7 @@ contract QuarkWalletTest is Test { scriptSources: scriptSources, scriptCalldata: scriptCalldata, nonce: nonce, + isReplayable: false, expiry: expiry }); @@ -795,14 +1425,16 @@ contract QuarkWalletTest is Test { verifyingContract: '0xc7183455a4C133Ae270771860664b6B7ec320bB1' }, { QuarkOperation: [ - { name: 'nonce', type: 'uint96' }, + { name: 'nonce', type: 'bytes32' }, + { name: 'isReplayable', type: 'bool' }, { name: 'scriptAddress', type: 'address' }, { name: 'scriptSources', type: 'bytes[]' }, { name: 'scriptCalldata', type: 'bytes' }, { name: 'expiry', type: 'uint256' } ]}, { - nonce: 0, + nonce: '0x0000000000000000000000000000000000000000000000000000000000000001', + isReplayable: false, scriptAddress: '0x4a925cF75dcc5708671004d9bbFAf4DCF2C762B0', scriptSources: ['0x630000003080600e6000396000f36000356000527f48257dc961b6f792c2b78a080dacfed693b660960a702de21cee364e20270e2f60206000a1600080f3'], scriptCalldata: '0x00000000000000000000000000000000000000000000000000000000000000dd', @@ -812,7 +1444,7 @@ contract QuarkWalletTest is Test { */ bytes memory sigHash = - hex"1901420cb4769bd47ac11897b8b69b8d80a84b9ec8b69437cd42529681d583a6b5216eda58953a1afd7dbc4ddbbef80dbca893fb0a87251b79a6b856708f619d9fcc"; + hex"1901420cb4769bd47ac11897b8b69b8d80a84b9ec8b69437cd42529681d583a6b5218c7d870a6510d1840f2ec48a08d65eb874fa8af841e45e3c9b8e5c244bdc015f"; (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, keccak256(sigHash)); // gas: meter execute @@ -842,39 +1474,6 @@ contract QuarkWalletTest is Test { assertEq(counter.number(), 3); } - /* ===== caching unintended script address edge case ===== */ - - // Script A clears nonce -> Call script B -> Script B clears nonce -> Returns to script A -> Script A sets nonce -> Script B is cached - // Note: This is not desired behavior. It is a benign edge-case that would be gassy (~15k gas overhead) to prevent. - function testSingleNonceCanCallDifferentScriptsAndCacheNonTargetScript() public { - // gas: do not meter set-up - vm.pauseGasMetering(); - bytes memory callOtherScript = new YulHelper().getCode("CallOtherScript.sol/CallOtherScript.json"); - // We just need to call any script that allows replays (clears the nonce) - bytes memory allowReplay = new YulHelper().getCode("AllowCallbacks.sol/AllowCallbacks.json"); - address allowReplayAddress = codeJar.saveCode(allowReplay); - uint96 nonce = stateManager.nextNonce(address(aliceWallet)); - QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( - aliceWallet, - callOtherScript, - abi.encodeWithSignature( - "call(uint96,address,bytes)", - nonce, - allowReplayAddress, - abi.encodeWithSignature("allowCallbackAndReplay()") - ), - ScriptType.ScriptAddress - ); - (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); - - // gas: meter execute - vm.resumeGasMetering(); - aliceWallet.executeQuarkOperation(op, v, r, s); - - // The cached script address is different from the script address in the Quark Operation - assertEq(stateManager.nonceScriptAddress(address(aliceWallet), nonce), allowReplayAddress); - } - /* ===== execution on Precompiles ===== */ // Quark is no longer able call precompiles directly due to empty code check, so these tests are commented out @@ -904,7 +1503,7 @@ contract QuarkWalletTest is Test { // scriptAddress: address(0x1), // scriptSources: new bytes[](0), // scriptCalldata: abi.encode(testHash, vt, rt, st), - // nonce: aliceWallet.stateManager().nextNonce(address(aliceWallet)), + // nonce: aliceWallet.nonceManager().nextNonce(address(aliceWallet)), // expiry: block.timestamp + 1000 // }); @@ -938,7 +1537,7 @@ contract QuarkWalletTest is Test { // scriptAddress: address(0x2), // scriptSources: new bytes[](0), // scriptCalldata: abi.encode(numberToHash), - // nonce: aliceWallet.stateManager().nextNonce(address(aliceWallet)), + // nonce: aliceWallet.nonceManager().nextNonce(address(aliceWallet)), // expiry: block.timestamp + 1000 // }); @@ -971,7 +1570,7 @@ contract QuarkWalletTest is Test { // scriptAddress: address(0x3), // scriptSources: new bytes[](0), // scriptCalldata: testBytes, - // nonce: aliceWallet.stateManager().nextNonce(address(aliceWallet)), + // nonce: aliceWallet.nonceManager().nextNonce(address(aliceWallet)), // expiry: block.timestamp + 1000 // }); // (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); @@ -1003,7 +1602,7 @@ contract QuarkWalletTest is Test { // scriptAddress: address(0x4), // scriptSources: new bytes[](0), // scriptCalldata: testBytes, - // nonce: aliceWallet.stateManager().nextNonce(address(aliceWallet)), + // nonce: aliceWallet.nonceManager().nextNonce(address(aliceWallet)), // expiry: block.timestamp + 1000 // }); // (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); @@ -1043,7 +1642,7 @@ contract QuarkWalletTest is Test { // scriptAddress: address(0x5), // scriptSources: new bytes[](0), // scriptCalldata: abi.encode(uint256(0x20), uint256(0x20), uint256(0x20), base, exponent, modulus), - // nonce: aliceWallet.stateManager().nextNonce(address(aliceWallet)), + // nonce: aliceWallet.nonceManager().nextNonce(address(aliceWallet)), // expiry: block.timestamp + 1000 // }); // (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); @@ -1080,7 +1679,7 @@ contract QuarkWalletTest is Test { // scriptAddress: address(0x6), // scriptSources: new bytes[](0), // scriptCalldata: abi.encode(input), - // nonce: aliceWallet.stateManager().nextNonce(address(aliceWallet)), + // nonce: aliceWallet.nonceManager().nextNonce(address(aliceWallet)), // expiry: block.timestamp + 1000 // }); // (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); @@ -1118,7 +1717,7 @@ contract QuarkWalletTest is Test { // scriptAddress: address(0x7), // scriptSources: new bytes[](0), // scriptCalldata: abi.encode(input), - // nonce: aliceWallet.stateManager().nextNonce(address(aliceWallet)), + // nonce: aliceWallet.nonceManager().nextNonce(address(aliceWallet)), // expiry: block.timestamp + 1000 // }); // (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); @@ -1196,7 +1795,7 @@ contract QuarkWalletTest is Test { // scriptAddress: address(0x9), // scriptSources: new bytes[](0), // scriptCalldata: abi.encodePacked(rounds, h[0], h[1], m[0], m[1], m[2], m[3], t[0], t[1], f), - // nonce: aliceWallet.stateManager().nextNonce(address(aliceWallet)), + // nonce: aliceWallet.nonceManager().nextNonce(address(aliceWallet)), // expiry: block.timestamp + 1000 // }); // (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); @@ -1209,10 +1808,11 @@ contract QuarkWalletTest is Test { function testRevertOnAllPrecompilesDirectCall() public { vm.pauseGasMetering(); - uint96 nonce = stateManager.nextNonce(address(aliceWallet)); + bytes32 nonce = new QuarkOperationHelper().semiRandomNonce(nonceManager, 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 +1820,7 @@ contract QuarkWalletTest is Test { } } - function DummyQuarkOperation(address preCompileAddress, uint96 nonce) + function DummyQuarkOperation(address preCompileAddress, bytes32 nonce) internal view returns (QuarkWallet.QuarkOperation memory) @@ -1230,6 +1830,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 67a679f4..58fb3a70 100644 --- a/test/quark-core/Reverts.t.sol +++ b/test/quark-core/Reverts.t.sol @@ -1,18 +1,17 @@ // 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"; import {CodeJar} from "codejar/src/CodeJar.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; import {QuarkWalletStandalone} from "quark-core/src/QuarkWalletStandalone.sol"; import {Counter} from "test/lib/Counter.sol"; import {Reverts} from "test/lib/Reverts.sol"; -import {MaxCounterScript} from "test/lib/MaxCounterScript.sol"; import {YulHelper} from "test/lib/YulHelper.sol"; import {SignatureHelper} from "test/lib/SignatureHelper.sol"; @@ -21,7 +20,7 @@ import {QuarkOperationHelper, ScriptType} from "test/lib/QuarkOperationHelper.so contract RevertsTest is Test { CodeJar public codeJar; Counter public counter; - QuarkStateManager public stateManager; + QuarkNonceManager public nonceManager; uint256 alicePrivateKey = 0x8675309; address aliceAccount = vm.addr(alicePrivateKey); @@ -35,10 +34,10 @@ contract RevertsTest is Test { counter.setNumber(0); console.log("Counter deployed to: %s", address(counter)); - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); - aliceWallet = new QuarkWalletStandalone(aliceAccount, address(0), codeJar, stateManager); + aliceWallet = new QuarkWalletStandalone(aliceAccount, address(0), codeJar, nonceManager); console.log("Alice signer: %s", aliceAccount); console.log("Alice wallet at: %s", address(aliceWallet)); } diff --git a/test/quark-core/isValidSignature.t.sol b/test/quark-core/isValidSignature.t.sol index ff7c050c..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"; @@ -7,7 +7,7 @@ import "forge-std/console.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.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"; @@ -23,7 +23,7 @@ contract isValidSignatureTest is Test { CodeJar public codeJar; QuarkWallet aliceWallet; QuarkWallet bobWallet; - QuarkStateManager public stateManager; + QuarkNonceManager public nonceManager; Permit2 permit2; bytes4 internal constant EIP_1271_MAGIC_VALUE = 0x1626ba7e; @@ -51,14 +51,14 @@ contract isValidSignatureTest is Test { codeJar = new CodeJar(); console.log("CodeJar deployed to: %s", address(codeJar)); - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); alice = vm.addr(alicePrivateKey); - aliceWallet = new QuarkWalletStandalone(alice, address(0), codeJar, stateManager); + aliceWallet = new QuarkWalletStandalone(alice, address(0), codeJar, nonceManager); bob = vm.addr(bobPrivateKey); - bobWallet = new QuarkWalletStandalone(bob, address(0), codeJar, stateManager); + bobWallet = new QuarkWalletStandalone(bob, address(0), codeJar, nonceManager); permit2 = Permit2(PERMIT2_ADDRESS); } @@ -189,7 +189,7 @@ contract isValidSignatureTest is Test { // QuarkWallet is owned by a smart contract that always approves signatures EIP1271Signer signatureApprover = new EIP1271Signer(true); QuarkWallet contractWallet = - new QuarkWalletStandalone(address(signatureApprover), address(0), codeJar, stateManager); + new QuarkWalletStandalone(address(signatureApprover), address(0), codeJar, nonceManager); // signature from bob; doesn't matter because the EIP1271Signer will approve anything ( /* bytes32 digest */ , bytes memory signature) = createTestSignature(bobPrivateKey, bobWallet); // gas: meter execute @@ -205,7 +205,7 @@ contract isValidSignatureTest is Test { // QuarkWallet is owned by a smart contract that always rejects signatures EIP1271Signer signatureApprover = new EIP1271Signer(false); QuarkWallet contractWallet = - new QuarkWalletStandalone(address(signatureApprover), address(0), codeJar, stateManager); + new QuarkWalletStandalone(address(signatureApprover), address(0), codeJar, nonceManager); // signature from bob; doesn't matter because the EIP1271Signer will reject everything ( /* bytes32 digest */ , bytes memory signature) = createTestSignature(bobPrivateKey, bobWallet); // gas: meter execute @@ -222,7 +222,7 @@ contract isValidSignatureTest is Test { // QuarkWallet is owned by a smart contract that always reverts EIP1271Reverter signatureApprover = new EIP1271Reverter(); QuarkWallet contractWallet = - new QuarkWalletStandalone(address(signatureApprover), address(0), codeJar, stateManager); + new QuarkWalletStandalone(address(signatureApprover), address(0), codeJar, nonceManager); // signature from bob; doesn't matter because the EIP1271Signer will revert ( /* bytes32 digest */ , bytes memory signature) = createTestSignature(bobPrivateKey, bobWallet); // gas: meter execute @@ -237,7 +237,7 @@ contract isValidSignatureTest is Test { vm.pauseGasMetering(); address emptyCodeContract = address(new EmptyCode()); - QuarkWallet contractWallet = new QuarkWalletStandalone(emptyCodeContract, address(0), codeJar, stateManager); + QuarkWallet contractWallet = new QuarkWalletStandalone(emptyCodeContract, address(0), codeJar, nonceManager); // signature from bob; doesn't matter because the empty contract will be treated as an EOA and revert ( /* bytes32 digest */ , bytes memory signature) = createTestSignature(bobPrivateKey, bobWallet); // gas: meter execute @@ -255,7 +255,7 @@ contract isValidSignatureTest is Test { function testRevertsForPermit2SignatureReuse() public { // gas: do not meter set-up vm.pauseGasMetering(); - QuarkWallet aliceWallet2 = new QuarkWalletStandalone(alice, address(0), codeJar, stateManager); + QuarkWallet aliceWallet2 = new QuarkWalletStandalone(alice, address(0), codeJar, nonceManager); Permit2Helper.PermitDetails memory permitDetails = Permit2Helper.PermitDetails({ token: USDC, diff --git a/test/quark-core/periphery/BatchExecutor.t.sol b/test/quark-core/periphery/BatchExecutor.t.sol index 8b171074..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"; @@ -7,13 +7,12 @@ import "forge-std/console.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; import {BatchExecutor} from "quark-core/src/periphery/BatchExecutor.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; import {QuarkMinimalProxy} from "quark-proxy/src/QuarkMinimalProxy.sol"; import {Counter} from "test/lib/Counter.sol"; -import {MaxCounterScript} from "test/lib/MaxCounterScript.sol"; import {Reverts} from "test/lib/Reverts.sol"; import {YulHelper} from "test/lib/YulHelper.sol"; @@ -26,7 +25,7 @@ contract BatchExecutorTest is Test { BatchExecutor public batchExecutor; CodeJar public codeJar; Counter public counter; - QuarkStateManager public stateManager; + QuarkNonceManager public nonceManager; QuarkWallet public walletImplementation; uint256 alicePrivateKey = 0x8675309; @@ -47,10 +46,10 @@ contract BatchExecutorTest is Test { counter.setNumber(0); console.log("Counter deployed to: %s", address(counter)); - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); - walletImplementation = new QuarkWallet(codeJar, stateManager); + walletImplementation = new QuarkWallet(codeJar, nonceManager); console.log("QuarkWallet implementation: %s", address(walletImplementation)); aliceWallet = @@ -267,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 5e67363e..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"; @@ -11,7 +11,7 @@ import {CodeJar} from "codejar/src/CodeJar.sol"; import {CodeJarFactory} from "codejar/src/CodeJarFactory.sol"; import {QuarkWallet} from "quark-core/src/QuarkWallet.sol"; import {QuarkWalletProxyFactory} from "quark-proxy/src/QuarkWalletProxyFactory.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {BatchExecutor} from "quark-core/src/periphery/BatchExecutor.sol"; contract QuarkFactoryTest is Test { @@ -29,15 +29,15 @@ contract QuarkFactoryTest is Test { vm.pauseGasMetering(); address expectedCodeJarAddress = getCreate2AddressHelper(address(codeJarFactory), bytes32(0), type(CodeJar).creationCode); - address expectedQuarkStateManagerAddress = - getCreate2AddressHelper(address(codeJar), bytes32(0), type(QuarkStateManager).creationCode); + address expectedQuarkNonceManagerAddress = + getCreate2AddressHelper(address(codeJar), bytes32(0), type(QuarkNonceManager).creationCode); address expectedQuarkWalletImplAddress = getCreate2AddressHelper( address(codeJar), bytes32(0), abi.encodePacked( type(QuarkWallet).creationCode, abi.encode(expectedCodeJarAddress), - abi.encode(expectedQuarkStateManagerAddress) + abi.encode(expectedQuarkNonceManagerAddress) ) ); address expectedQuarkWalletProxyFactoryAddress = getCreate2AddressHelper( @@ -53,7 +53,7 @@ contract QuarkFactoryTest is Test { assertEq(address(factory.codeJar()), expectedCodeJarAddress); assertEq(address(factory.quarkWalletImpl()), expectedQuarkWalletImplAddress); assertEq(address(factory.quarkWalletProxyFactory()), expectedQuarkWalletProxyFactoryAddress); - assertEq(address(factory.quarkStateManager()), expectedQuarkStateManagerAddress); + assertEq(address(factory.quarkNonceManager()), expectedQuarkNonceManagerAddress); assertEq(address(factory.batchExecutor()), expectedBatchExecutorAddress); } @@ -61,8 +61,8 @@ contract QuarkFactoryTest is Test { vm.pauseGasMetering(); address expectedCodeJarAddress = getCreate2AddressHelper(address(codeJarFactory), bytes32(0), abi.encodePacked(type(CodeJar).creationCode)); - address expectedQuarkStateManagerAddress = getCreate2AddressHelper( - address(codeJar), bytes32(0), abi.encodePacked(type(QuarkStateManager).creationCode) + address expectedQuarkNonceManagerAddress = getCreate2AddressHelper( + address(codeJar), bytes32(0), abi.encodePacked(type(QuarkNonceManager).creationCode) ); address expectedQuarkWalletImplAddress = getCreate2AddressHelper( address(codeJar), @@ -70,7 +70,7 @@ contract QuarkFactoryTest is Test { abi.encodePacked( type(QuarkWallet).creationCode, abi.encode(expectedCodeJarAddress), - abi.encode(expectedQuarkStateManagerAddress) + abi.encode(expectedQuarkNonceManagerAddress) ) ); address expectedQuarkWalletProxyFactoryAddress = getCreate2AddressHelper( @@ -87,7 +87,7 @@ contract QuarkFactoryTest is Test { assertEq(address(factory.codeJar()), expectedCodeJarAddress); assertEq(address(factory.quarkWalletImpl()), expectedQuarkWalletImplAddress); assertEq(address(factory.quarkWalletProxyFactory()), expectedQuarkWalletProxyFactoryAddress); - assertEq(address(factory.quarkStateManager()), expectedQuarkStateManagerAddress); + assertEq(address(factory.quarkNonceManager()), expectedQuarkNonceManagerAddress); assertEq(address(factory.batchExecutor()), expectedBatchExecutorAddress); // This doesn't need to revert. It's mostly a no-op @@ -95,7 +95,7 @@ contract QuarkFactoryTest is Test { assertEq(address(factory.codeJar()), expectedCodeJarAddress); assertEq(address(factory.quarkWalletImpl()), expectedQuarkWalletImplAddress); assertEq(address(factory.quarkWalletProxyFactory()), expectedQuarkWalletProxyFactoryAddress); - assertEq(address(factory.quarkStateManager()), expectedQuarkStateManagerAddress); + assertEq(address(factory.quarkNonceManager()), expectedQuarkNonceManagerAddress); assertEq(address(factory.batchExecutor()), expectedBatchExecutorAddress); } @@ -103,15 +103,15 @@ contract QuarkFactoryTest is Test { vm.pauseGasMetering(); address expectedCodeJarAddress = getCreate2AddressHelper(address(codeJarFactory), bytes32(0), type(CodeJar).creationCode); - address expectedQuarkStateManagerAddress = - getCreate2AddressHelper(address(codeJar), bytes32(0), type(QuarkStateManager).creationCode); + address expectedQuarkNonceManagerAddress = + getCreate2AddressHelper(address(codeJar), bytes32(0), type(QuarkNonceManager).creationCode); address expectedQuarkWalletImplAddress = getCreate2AddressHelper( address(codeJar), bytes32(0), abi.encodePacked( type(QuarkWallet).creationCode, abi.encode(expectedCodeJarAddress), - abi.encode(expectedQuarkStateManagerAddress) + abi.encode(expectedQuarkNonceManagerAddress) ) ); address expectedQuarkWalletProxyFactoryAddress = getCreate2AddressHelper( @@ -131,7 +131,7 @@ contract QuarkFactoryTest is Test { assertEq(address(factory.codeJar()), expectedCodeJarAddress); assertEq(address(factory.quarkWalletImpl()), expectedQuarkWalletImplAddress); assertEq(address(factory.quarkWalletProxyFactory()), expectedQuarkWalletProxyFactoryAddress); - assertEq(address(factory.quarkStateManager()), expectedQuarkStateManagerAddress); + assertEq(address(factory.quarkNonceManager()), expectedQuarkNonceManagerAddress); assertEq(address(factory.batchExecutor()), expectedBatchExecutorAddress); } diff --git a/test/quark-proxy/QuarkMinimalProxy.t.sol b/test/quark-proxy/QuarkMinimalProxy.t.sol index 99f3bf06..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"; @@ -7,7 +7,7 @@ import {Test} from "forge-std/Test.sol"; import {CodeJar} from "codejar/src/CodeJar.sol"; import {QuarkMinimalProxy} from "quark-proxy/src/QuarkMinimalProxy.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; import {QuarkWallet, IHasSignerExecutor} from "quark-core/src/QuarkWallet.sol"; import {YulHelper} from "test/lib/YulHelper.sol"; @@ -17,7 +17,7 @@ import {QuarkOperationHelper, ScriptType} from "test/lib/QuarkOperationHelper.so contract QuarkMinimalProxyTest is Test { CodeJar public codeJar; QuarkWallet public walletImplementation; - QuarkStateManager public stateManager; + QuarkNonceManager public nonceManager; uint256 alicePrivateKey = 0x8675309; address aliceAccount; @@ -27,10 +27,10 @@ contract QuarkMinimalProxyTest is Test { codeJar = new CodeJar(); console.log("CodeJar deployed to %s", address(codeJar)); - stateManager = new QuarkStateManager(); - console.log("QuarkStateManager deployed to %s", address(stateManager)); + nonceManager = new QuarkNonceManager(); + console.log("QuarkNonceManager deployed to %s", address(nonceManager)); - walletImplementation = new QuarkWallet(codeJar, stateManager); + walletImplementation = new QuarkWallet(codeJar, nonceManager); console.log("QuarkWallet implementation deployed to %s", address(walletImplementation)); aliceAccount = vm.addr(alicePrivateKey); diff --git a/test/quark-proxy/QuarkWalletProxyFactory.t.sol b/test/quark-proxy/QuarkWalletProxyFactory.t.sol index a6dc3454..1af416b4 100644 --- a/test/quark-proxy/QuarkWalletProxyFactory.t.sol +++ b/test/quark-proxy/QuarkWalletProxyFactory.t.sol @@ -1,16 +1,18 @@ // 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"; import {CodeJar} from "codejar/src/CodeJar.sol"; -import {QuarkStateManager} from "quark-core/src/QuarkStateManager.sol"; +import {QuarkNonceManager} from "quark-core/src/QuarkNonceManager.sol"; 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"; @@ -24,7 +26,7 @@ contract QuarkWalletProxyFactoryTest is Test { event WalletDeploy(address indexed account, address indexed executor, address walletAddress, bytes32 salt); CodeJar public codeJar; - QuarkStateManager public stateManager; + QuarkNonceManager public nonceManager; QuarkWalletProxyFactory public factory; uint256 alicePrivateKey = 0xa11ce; @@ -32,7 +34,7 @@ contract QuarkWalletProxyFactoryTest is Test { address bob = address(11); constructor() { - factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkStateManager()))); + factory = new QuarkWalletProxyFactory(address(new QuarkWallet(new CodeJar(), new QuarkNonceManager()))); console.log("QuarkWalletFactory deployed to: %s", address(factory)); console.log("wallet implementation address is: %s", factory.walletImplementation()); @@ -40,8 +42,8 @@ contract QuarkWalletProxyFactoryTest is Test { codeJar = QuarkWallet(payable(factory.walletImplementation())).codeJar(); console.log("CodeJar deployed to: %s", address(codeJar)); - stateManager = QuarkWallet(payable(factory.walletImplementation())).stateManager(); - console.log("QuarkStateManager deployed to: %s", address(stateManager)); + nonceManager = QuarkWallet(payable(factory.walletImplementation())).nonceManager(); + console.log("QuarkNonceManager deployed to: %s", address(nonceManager)); alice = vm.addr(alicePrivateKey); console.log("alice address: %s", alice); @@ -112,12 +114,15 @@ 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( + nonceManager, QuarkWallet(payable(factory.walletAddressFor(alice, address(0)))) + ); QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); @@ -140,7 +145,9 @@ 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( + nonceManager.submissions(factory.walletAddressFor(alice, address(0)), nonce), bytes32(type(uint256).max) + ); } function testCreateAndExecuteWithSalt() public { @@ -153,12 +160,15 @@ 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( + nonceManager, QuarkWallet(payable(factory.walletAddressFor(alice, address(0)))) + ); QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); @@ -183,7 +193,10 @@ 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( + nonceManager.submissions(factory.walletAddressForSalt(alice, address(0), salt), nonce), + bytes32(type(uint256).max) + ); } function testExecuteOnExistingWallet() public { @@ -195,13 +208,16 @@ 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( + nonceManager, QuarkWallet(payable(factory.walletAddressFor(alice, address(0)))) + ); address incrementerAddress = codeJar.getCodeAddress(incrementer); QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); @@ -226,7 +242,9 @@ 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( + nonceManager.submissions(factory.walletAddressFor(alice, address(0)), nonce), bytes32(type(uint256).max) + ); } /* ===== create and execute MultiQuarkOperation tests ===== */ @@ -246,12 +264,14 @@ 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(nonceManager, QuarkWallet(payable(aliceWalletAddress))); QuarkWallet.QuarkOperation memory op1 = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); bytes32 op1Digest = new SignatureHelper().opDigest(aliceWalletAddress, op1); @@ -260,10 +280,11 @@ contract QuarkWalletProxyFactoryTest is Test { scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), - nonce: nonce + 1, + nonce: new QuarkOperationHelper().incrementNonce(nonce), + isReplayable: false, 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 +300,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(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(stateManager.isNonceSet(aliceWalletAddress, op2.nonce), true); + assertEq(nonceManager.submissions(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); } function testCreateAndExecuteMultiWithSalt() public { @@ -300,12 +321,14 @@ 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(nonceManager, QuarkWallet(payable(aliceWalletAddress))); QuarkWallet.QuarkOperation memory op1 = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); bytes32 op1Digest = new SignatureHelper().opDigest(aliceWalletAddress, op1); @@ -314,10 +337,11 @@ contract QuarkWalletProxyFactoryTest is Test { scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), - nonce: nonce + 1, + nonce: new QuarkOperationHelper().incrementNonce(nonce), + isReplayable: false, 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 +357,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(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(stateManager.isNonceSet(aliceWalletAddress, op2.nonce), true); + assertEq(nonceManager.submissions(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); } function testExecuteMultiOnExistingWallet() public { @@ -357,12 +381,14 @@ 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(nonceManager, QuarkWallet(payable(aliceWalletAddress))); QuarkWallet.QuarkOperation memory op1 = QuarkWallet.QuarkOperation({ scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), nonce: nonce, + isReplayable: false, expiry: block.timestamp + 1000 }); bytes32 op1Digest = new SignatureHelper().opDigest(aliceWalletAddress, op1); @@ -371,10 +397,11 @@ contract QuarkWalletProxyFactoryTest is Test { scriptAddress: incrementerAddress, scriptSources: scriptSources, scriptCalldata: abi.encodeWithSignature("incrementCounter(address)", counter), - nonce: nonce + 1, + nonce: new QuarkOperationHelper().incrementNonce(nonce), + isReplayable: false, 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 +421,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(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(stateManager.isNonceSet(aliceWalletAddress, op2.nonce), true); + assertEq(nonceManager.submissions(aliceWalletAddress, op2.nonce), bytes32(type(uint256).max)); } /* ===== msg.value and msg.sender tests ===== */ @@ -410,7 +437,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(nonceManager, QuarkWallet(payable(aliceWallet))); address getMessageDetailsAddress = codeJar.getCodeAddress(getMessageDetails); @@ -422,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); @@ -440,7 +468,7 @@ contract QuarkWalletProxyFactoryTest is Test { assertEq(msgValue, 0); // uses up the operation's nonce - assertEq(stateManager.isNonceSet(aliceWallet, nonce), true); + assertEq(nonceManager.submissions(aliceWallet, nonce), bytes32(type(uint256).max)); } function testCreateAndExecuteWithSaltSetsMsgSender() public { @@ -449,7 +477,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(nonceManager, QuarkWallet(payable(aliceWallet))); address getMessageDetailsAddress = codeJar.getCodeAddress(getMessageDetails); QuarkWallet.QuarkOperation memory op = QuarkWallet.QuarkOperation({ @@ -457,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); @@ -490,7 +519,7 @@ contract QuarkWalletProxyFactoryTest is Test { assertEq(msgValue, 0); // uses up the operation's nonce - assertEq(stateManager.isNonceSet(aliceWallet, nonce), true); + assertEq(nonceManager.submissions(aliceWallet, nonce), bytes32(type(uint256).max)); } /* ===== default wallet executor role tests ===== */ @@ -525,15 +554,16 @@ 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(nonceManager, 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(nonceManager, aliceWalletPrimary), + isReplayable: false, expiry: block.timestamp + 1000 }); (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, aliceWalletPrimary, op);