diff --git a/.env.example b/.env.example index 7498114..5e6ebc3 100644 --- a/.env.example +++ b/.env.example @@ -15,5 +15,8 @@ SETTLEMENT=0x9008D19f58AAbD9eD0D60971565AA8510560ab41 # SAFE= # TWAP= +# Owner of the rotatable handlers, read by deploy_GnosisStack. +# ADMIN= + # Read by dev/verify-contracts.sh, and by forge script --verify. # ETHERSCAN_API_KEY= diff --git a/.gas-snapshot b/.gas-snapshot index 737a38e..3e3632a 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -8,36 +8,36 @@ ComposableCowDescriptorDocTest:test_descriptor_SelectorMatchesDeclaredName() (ga ComposableCowDescriptorDocTest:test_descriptor_SelectorsAreDistinct() (gas: 163733) ComposableCowDiscoveryTest:test_SetUpState_ComposableCowDomainSeparator_is_set() (gas: 11305) ComposableCowDiscoveryTest:test_SetUpState_ComposableCowDomainVerifier_is_set() (gas: 18325) -ComposableCowDiscoveryTest:test_descriptor_CommittedAdvertisesAndRoundTrips() (gas: 2033244) -ComposableCowDiscoveryTest:test_descriptor_ConstructorEmitsUpdate() (gas: 2027846) -ComposableCowDiscoveryTest:test_descriptor_UncommittedDoesNotAdvertise() (gas: 1931847) -ComposableCowDiscoveryTest:test_module_CommittedAdvertisesAndRoundTrips() (gas: 1498629) -ComposableCowDiscoveryTest:test_module_ContentAddressedNeedsNoURI() (gas: 1398108) -ComposableCowDiscoveryTest:test_module_NeedsInputSignal() (gas: 1405789) -ComposableCowDiscoveryTest:test_module_RevertsContentAddressedWithURI() (gas: 39723) -ComposableCowDiscoveryTest:test_module_RevertsSha256WithoutURI() (gas: 38899) -ComposableCowDiscoveryTest:test_module_RevertsUncommittedURI() (gas: 40273) -ComposableCowDiscoveryTest:test_module_UncommittedDoesNotAdvertise() (gas: 1395475) +ComposableCowDiscoveryTest:test_descriptor_CommittedAdvertisesAndRoundTrips() (gas: 2061293) +ComposableCowDiscoveryTest:test_descriptor_ConstructorEmitsUpdate() (gas: 2055583) +ComposableCowDiscoveryTest:test_descriptor_UncommittedDoesNotAdvertise() (gas: 1919984) +ComposableCowDiscoveryTest:test_module_CommittedAdvertisesAndRoundTrips() (gas: 1526676) +ComposableCowDiscoveryTest:test_module_ContentAddressedNeedsNoURI() (gas: 1406046) +ComposableCowDiscoveryTest:test_module_NeedsInputSignal() (gas: 1413627) +ComposableCowDiscoveryTest:test_module_RevertsContentAddressedWithURI() (gas: 40179) +ComposableCowDiscoveryTest:test_module_RevertsSha256WithoutURI() (gas: 39351) +ComposableCowDiscoveryTest:test_module_RevertsUncommittedURI() (gas: 40730) +ComposableCowDiscoveryTest:test_module_UncommittedDoesNotAdvertise() (gas: 1383610) ComposableCowForwarderTest:test_ERC1271Forwarder_isValidSignature_RevertsOnBadHash() (gas: 630770) ComposableCowForwarderTest:test_SetUpState_ComposableCowDomainSeparator_is_set() (gas: 11443) ComposableCowForwarderTest:test_SetUpState_ComposableCowDomainVerifier_is_set() (gas: 18481) ComposableCowGatTest:test_SetUpState_ComposableCowDomainSeparator_is_set() (gas: 11487) ComposableCowGatTest:test_SetUpState_ComposableCowDomainVerifier_is_set() (gas: 18657) -ComposableCowGatTest:test_generateOrder_FuzzContext(address,address,address,uint256,uint256,uint256,uint256,bool) (runs: 256, μ: 121422, ~: 121420) +ComposableCowGatTest:test_generateOrder_FuzzContext(address,address,address,uint256,uint256,uint256,uint256,bool) (runs: 256, μ: 121422, ~: 121422) ComposableCowGatTest:test_generateOrder_FuzzRevertBeforeStartTime(uint256,uint256) (runs: 256, μ: 21804, ~: 21804) -ComposableCowGatTest:test_generateOrder_FuzzRevertBelowMinBalance(uint256,uint256) (runs: 256, μ: 114446, ~: 114454) -ComposableCowGatTest:test_generateOrder_FuzzRevertTooLowOutput(uint256,uint256,uint256) (runs: 256, μ: 124256, ~: 124320) +ComposableCowGatTest:test_generateOrder_FuzzRevertBelowMinBalance(uint256,uint256) (runs: 256, μ: 114446, ~: 114453) +ComposableCowGatTest:test_generateOrder_FuzzRevertTooLowOutput(uint256,uint256,uint256) (runs: 256, μ: 124242, ~: 124309) ComposableCowGatTest:test_generateOrder_RevertZeroAmount() (gas: 113369) -ComposableCowGatTest:test_generateOrder_e2e_Fuzz(uint256,uint256,uint256,uint256,uint256,uint256) (runs: 256, μ: 299418, ~: 299344) -ComposableCowGatTest:test_generateOrder_e2e_FuzzWithPriceChecker(uint256,uint256,uint256,uint256,uint256) (runs: 256, μ: 308759, ~: 308596) +ComposableCowGatTest:test_generateOrder_e2e_Fuzz(uint256,uint256,uint256,uint256,uint256,uint256) (runs: 256, μ: 299402, ~: 299342) +ComposableCowGatTest:test_generateOrder_e2e_FuzzWithPriceChecker(uint256,uint256,uint256,uint256,uint256) (runs: 256, μ: 308748, ~: 308594) ComposableCowGatTest:test_pollHints_SingleShot() (gas: 16098) ComposableCowGatTest:test_settle_e2e() (gas: 491417) -ComposableCowGatTest:test_verify_e2e_fuzz(uint256,uint256,uint256,uint256,uint256) (runs: 256, μ: 137200, ~: 137037) +ComposableCowGatTest:test_verify_e2e_fuzz(uint256,uint256,uint256,uint256,uint256) (runs: 256, μ: 137188, ~: 137035) ComposableCowGuardsTest:test_BaseSwapGuard_supportsInterface() (gas: 138710) ComposableCowGuardsTest:test_ReceiverLock_verify_FuzzRevertsWhenReceiverNotSelf(address) (runs: 256, μ: 299499, ~: 299499) ComposableCowGuardsTest:test_SetUpState_ComposableCowDomainSeparator_is_set() (gas: 11487) ComposableCowGuardsTest:test_SetUpState_ComposableCowDomainVerifier_is_set() (gas: 18546) -ComposableCowGuardsTest:test_setSwapGuard_FuzzSetAndEmit(address,address) (runs: 256, μ: 31254, ~: 31236) +ComposableCowGuardsTest:test_setSwapGuard_FuzzSetAndEmit(address,address) (runs: 256, μ: 31255, ~: 31236) ComposableCowGuardsTest:test_setSwapGuard_e2e() (gas: 1030509) ComposableCowManifestTest:test_PSS_ManifestPage_NotFundedCarriesStatus() (gas: 37596) ComposableCowManifestTest:test_PSS_ManifestPage_WithBalance() (gas: 125366) @@ -60,6 +60,16 @@ ComposableCowManifestTest:test_manifestPage_EmptyPageCarriesWaitReason() (gas: 1 ComposableCowManifestTest:test_manifestPage_OutOfRangeTerminates() (gas: 1545516) ComposableCowManifestTest:test_manifest_DoesNotPerturbGeneratorInterfaceId() (gas: 855) ComposableCowManifestTest:test_manifest_SupportsInterface() (gas: 1538778) +ComposableCowOwnedTest:test_SetUpState_ComposableCowDomainSeparator_is_set() (gas: 11305) +ComposableCowOwnedTest:test_SetUpState_ComposableCowDomainVerifier_is_set() (gas: 18259) +ComposableCowOwnedTest:test_owned_EnforcesURIInvariants() (gas: 25494) +ComposableCowOwnedTest:test_owned_OrderGenerationIsUnchanged() (gas: 1937389) +ComposableCowOwnedTest:test_owned_RevertsForStranger() (gas: 18066) +ComposableCowOwnedTest:test_owned_RotationReplacesCommitment() (gas: 116398) +ComposableCowOwnedTest:test_owned_SetDescriptorAdvertisesAndRoundTrips() (gas: 49354) +ComposableCowOwnedTest:test_owned_SetModuleIsIndependentOfDescriptor() (gas: 156075) +ComposableCowOwnedTest:test_owned_TransferIsTwoStep() (gas: 40006) +ComposableCowOwnedTest:test_owned_UncommittedOnDeploy() (gas: 21451) ComposableCowPollTest:test_SetUpState_ComposableCowDomainSeparator_is_set() (gas: 11531) ComposableCowPollTest:test_SetUpState_ComposableCowDomainVerifier_is_set() (gas: 19119) ComposableCowPollTest:test_checkOrder_ComposesFillOverlay() (gas: 1833891) @@ -80,9 +90,9 @@ ComposableCowPollTest:test_poll_DecodesPollNeedsOffchainInput() (gas: 1402236) ComposableCowPollTest:test_poll_DecodesPollTryAtBlock() (gas: 1330995) ComposableCowPollTest:test_poll_DecodesPollTryAtTimestamp() (gas: 1330907) ComposableCowPollTest:test_poll_DecodesPollTryNextBlock() (gas: 1290285) -ComposableCowPollTest:test_poll_FuzzOrderNotValid(bytes4) (runs: 256, μ: 1283632, ~: 1291095) -ComposableCowPollTest:test_poll_FuzzPollTryAtBlock(uint256,bytes4) (runs: 256, μ: 1323738, ~: 1330890) -ComposableCowPollTest:test_poll_FuzzPollTryAtTimestamp(uint256,bytes4) (runs: 256, μ: 1324266, ~: 1331418) +ComposableCowPollTest:test_poll_FuzzOrderNotValid(bytes4) (runs: 256, μ: 1283943, ~: 1291095) +ComposableCowPollTest:test_poll_FuzzPollTryAtBlock(uint256,bytes4) (runs: 256, μ: 1323272, ~: 1330890) +ComposableCowPollTest:test_poll_FuzzPollTryAtTimestamp(uint256,bytes4) (runs: 256, μ: 1323800, ~: 1331418) ComposableCowPollTest:test_poll_NeedsInputHandlerPostsWithInput() (gas: 1404879) ComposableCowPollTest:test_poll_PanicMapsToTryNextBlock() (gas: 1294536) ComposableCowPollTest:test_poll_ReturnsPostOnValidOrder() (gas: 1763768) @@ -107,7 +117,7 @@ ComposableCowProofTest:test_setRoot_RevertsZeroRootWithBlobs() (gas: 13434) ComposableCowProofTest:test_setRoot_RevertsZeroRootWithUris() (gas: 12784) ComposableCowProofTest:test_setRoot_ZeroRootClears() (gas: 32934) ComposableCowStopLossTest:test_OracleNormalisesPrice_concrete() (gas: 27435) -ComposableCowStopLossTest:test_OracleNormalisesPrice_fuzz(uint8,uint8,uint8,uint8) (runs: 256, μ: 29593, ~: 29613) +ComposableCowStopLossTest:test_OracleNormalisesPrice_fuzz(uint8,uint8,uint8,uint8) (runs: 256, μ: 29592, ~: 29613) ComposableCowStopLossTest:test_OracleRevertOnExpiredOrder_fuzz(uint32,uint32) (runs: 256, μ: 23185, ~: 23185) ComposableCowStopLossTest:test_OracleRevertOnInvalidPrice_fuzz(int256,int256) (runs: 256, μ: 36635, ~: 36635) ComposableCowStopLossTest:test_OracleRevertOnStalePrice_fuzz(uint256,uint256,uint256) (runs: 256, μ: 25348, ~: 25348) @@ -118,15 +128,15 @@ ComposableCowStopLossTest:test_generateOrder_RevertZeroAmount() (gas: 22286) ComposableCowStopLossTest:test_pollHints_SingleShot() (gas: 15780) ComposableCowStopLossTest:test_strikePriceMet_fuzz(int256,int256,int256,uint32) (runs: 256, μ: 29077, ~: 29077) ComposableCowStopLossTest:test_strikePriceNotMet_concrete() (gas: 26555) -ComposableCowTatTest:test_BalanceMet_fuzz(address,uint256,bytes32,uint256) (runs: 256, μ: 113402, ~: 113386) +ComposableCowTatTest:test_BalanceMet_fuzz(address,uint256,bytes32,uint256) (runs: 256, μ: 113402, ~: 113384) ComposableCowTatTest:test_SetUpState_ComposableCowDomainSeparator_is_set() (gas: 11283) ComposableCowTatTest:test_SetUpState_ComposableCowDomainVerifier_is_set() (gas: 18177) -ComposableCowTatTest:test_generateOrder_FuzzRevertBelowThreshold(uint256,uint256) (runs: 256, μ: 111617, ~: 111620) +ComposableCowTatTest:test_generateOrder_FuzzRevertBelowThreshold(uint256,uint256) (runs: 256, μ: 111619, ~: 111626) ComposableCowTest:test_SetUpState_ComposableCowDomainSeparator_is_set() (gas: 11575) ComposableCowTest:test_SetUpState_ComposableCowDomainVerifier_is_set() (gas: 18877) -ComposableCowTest:test_createAndRemove_FuzzSetAndEmit(address,address,bytes32,bytes) (runs: 256, μ: 39997, ~: 39883) +ComposableCowTest:test_createAndRemove_FuzzSetAndEmit(address,address,bytes32,bytes) (runs: 256, μ: 39999, ~: 39883) ComposableCowTest:test_createAndRemove_e2e() (gas: 468135) -ComposableCowTest:test_createWithContextAndRemove_FuzzSetAndEmit(address,address,bytes32,bytes,bytes32) (runs: 256, μ: 63661, ~: 63592) +ComposableCowTest:test_createWithContextAndRemove_FuzzSetAndEmit(address,address,bytes32,bytes,bytes32) (runs: 256, μ: 63662, ~: 63596) ComposableCowTest:test_create_RevertOnInvalidHandler() (gas: 10433) ComposableCowTest:test_getTradeableOrderWithSignature_FuzzRevertInvalidProof(address,bytes32[],bytes32,address,bytes32,bytes) (runs: 256, μ: 97007, ~: 96804) ComposableCowTest:test_getTradeableOrderWithSignature_FuzzRevertInvalidSingleOrder(address,address,bytes32,bytes) (runs: 256, μ: 18659, ~: 18642) @@ -135,25 +145,25 @@ ComposableCowTest:test_getTradeableOrderWithSignature_ReturnsValidPayloadForSafe ComposableCowTest:test_getTradeableOrderWithSignature_RevertInterfaceNotSupported() (gas: 52457) ComposableCowTest:test_isValidSafeSignature_BaseConditionalOrder_RevertOnInvalidHash() (gas: 60746) ComposableCowTest:test_isValidSafeSignature_FuzzPassesContextToHandler(address,bytes32) (runs: 256, μ: 57671, ~: 57671) -ComposableCowTest:test_isValidSafeSignature_FuzzRevertInvalidProof(address,bytes32[],bytes32,address,bytes32,bytes) (runs: 256, μ: 98629, ~: 98333) -ComposableCowTest:test_isValidSafeSignature_FuzzRevertInvalidSingleOrder(address,address,bytes32,bytes) (runs: 256, μ: 18637, ~: 18615) +ComposableCowTest:test_isValidSafeSignature_FuzzRevertInvalidProof(address,bytes32[],bytes32,address,bytes32,bytes) (runs: 256, μ: 98630, ~: 98333) +ComposableCowTest:test_isValidSafeSignature_FuzzRevertInvalidSingleOrder(address,address,bytes32,bytes) (runs: 256, μ: 18638, ~: 18615) ComposableCowTest:test_remove_EmitsConditionalOrderRemoved() (gas: 36281) ComposableCowTest:test_remove_FuzzEmitsEvent(address,bytes32) (runs: 256, μ: 31569, ~: 31544) ComposableCowTest:test_safeSignaturePayload_SelectorMatchesMuxerMagicValue() (gas: 1145) ComposableCowTest:test_setRootWithContext_FuzzSetAndEmit(address,bytes32,bytes32) (runs: 256, μ: 73696, ~: 73774) ComposableCowTest:test_setRootWithContext_e2e() (gas: 13599638) -ComposableCowTest:test_setRoot_FuzzSetAndEmit(address,bytes32) (runs: 256, μ: 41952, ~: 42105) +ComposableCowTest:test_setRoot_FuzzSetAndEmit(address,bytes32) (runs: 256, μ: 42028, ~: 42105) ComposableCowTest:test_setRoot_e2e() (gas: 13568277) ComposableCowTwapTest:test_SetUpState_ComposableCowDomainSeparator_is_set() (gas: 11488) ComposableCowTwapTest:test_SetUpState_ComposableCowDomainVerifier_is_set() (gas: 18987) -ComposableCowTwapTest:test_TWAPOrderMathLib_calculateValidTo(uint256,uint256,uint256,uint256,uint256) (runs: 256, μ: 12642, ~: 12389) +ComposableCowTwapTest:test_TWAPOrderMathLib_calculateValidTo(uint256,uint256,uint256,uint256,uint256) (runs: 256, μ: 12636, ~: 12389) ComposableCowTwapTest:test_describeOrder_RevertOnZeroFrequency() (gas: 21049) ComposableCowTwapTest:test_describeOrder_TwapParts() (gas: 29107) ComposableCowTwapTest:test_generateOrder_FuzzRevertIfBeforeStart(uint256,uint256) (runs: 256, μ: 29035, ~: 29035) ComposableCowTwapTest:test_generateOrder_FuzzRevertIfExpired(uint256,uint256) (runs: 256, μ: 30257, ~: 30108) ComposableCowTwapTest:test_generateOrder_FuzzRevertIfOrderAfterBlocktimestampValidity(uint256,uint256) (runs: 256, μ: 186801, ~: 187176) ComposableCowTwapTest:test_generateOrder_FuzzRevertIfOrderBeforeBlockTimestamp(uint256,uint256) (runs: 256, μ: 187290, ~: 187290) -ComposableCowTwapTest:test_generateOrder_FuzzRevertIfOutsideSpan(uint256,uint256) (runs: 256, μ: 32236, ~: 32137) +ComposableCowTwapTest:test_generateOrder_FuzzRevertIfOutsideSpan(uint256,uint256) (runs: 256, μ: 32285, ~: 32137) ComposableCowTwapTest:test_generateOrder_FuzzRevertOnInvalidFrequency(uint256) (runs: 256, μ: 19744, ~: 19747) ComposableCowTwapTest:test_generateOrder_FuzzRevertOnInvalidNumParts(uint256) (runs: 256, μ: 19601, ~: 19605) ComposableCowTwapTest:test_generateOrder_FuzzRevertOnInvalidSpan(uint256,uint256) (runs: 256, μ: 20164, ~: 20164) @@ -169,5 +179,19 @@ ComposableCowTwapTest:test_getNextPollTimestamp_PointsAtNextPart() (gas: 20153) ComposableCowTwapTest:test_getNextPollTimestamp_RevertOnZeroFrequency() (gas: 19285) ComposableCowTwapTest:test_getNextPollTimestamp_RevertOnZeroNumParts() (gas: 19451) ComposableCowTwapTest:test_settle_e2e() (gas: 13514423) -ComposableCowTwapTest:test_simulate_fuzz(uint32,uint32,uint32) (runs: 256, μ: 20619419, ~: 21359116) -ComposableCowTwapTest:test_verify_e2e_fuzz(uint256,uint256) (runs: 256, μ: 36221, ~: 35995) \ No newline at end of file +ComposableCowTwapTest:test_simulate_fuzz(uint32,uint32,uint32) (runs: 256, μ: 20318564, ~: 20794728) +ComposableCowTwapTest:test_verify_e2e_fuzz(uint256,uint256) (runs: 256, μ: 36221, ~: 35995) +DeployAddressTest:test_predict_AdminMovesHandlersOnly() (gas: 125019) +DeployAddressTest:test_predict_IsTheSameOnEveryChain() (gas: 185119) +DeployAddressTest:test_predict_MatchesCreate2() (gas: 73174) +DeployAddressTest:test_predict_TwapEncodesTheRegistryNotTheSettlement() (gas: 94443) +DeployAddressTest:test_predict_TwapFollowsTheRegistry() (gas: 124229) +DeployStackTest:test_validate_AcceptsAValidConfig() (gas: 12459) +DeployStackTest:test_validate_RejectsCodelessSettlement() (gas: 13780) +DeployStackTest:test_validate_RejectsWrongChain() (gas: 13497) +DeployStackTest:test_validate_RejectsZeroOwner() (gas: 13402) +DescriptorPublishTest:test_descriptor_DigestIsTheDocumentsSha256() (gas: 22715) +DescriptorPublishTest:test_descriptor_OptionalWhenUriUnset() (gas: 7536) +DescriptorPublishTest:test_descriptor_RejectsANonHttpsUri() (gas: 13730) +DescriptorPublishTest:test_descriptor_RequiresTheOwnerKey() (gas: 12366) +DescriptorPublishTest:test_isHttps() (gas: 13639) \ No newline at end of file diff --git a/README.md b/README.md index 4037cae..3dcd185 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ The following audits cover the **upstream** contracts before the changes in this ### Environment setup -Copy `.env.example` to `.env`. Every script reads `PRIVATE_KEY`; `SETTLEMENT` is needed to deploy `ComposableCow`, `COMPOSABLE_COW` to deploy order types against it, and `SAFE` plus `TWAP` to submit a single order. Contract verification reads `ETHERSCAN_API_KEY`. The RPC endpoint is passed per command with `--rpc-url`. +Copy `.env.example` to `.env`. Every script reads `PRIVATE_KEY`; `SETTLEMENT` is needed to deploy `ComposableCow`, `COMPOSABLE_COW` to deploy order types against it, and `SAFE` plus `TWAP` to submit a single order. The Gnosis integration deployment also reads `ADMIN`. Contract verification reads `ETHERSCAN_API_KEY`. The RPC endpoint is passed per command with `--rpc-url`. ### Testing diff --git a/script/deploy_AnvilStack.s.sol b/script/deploy_AnvilStack.s.sol index 9dac18e..8a55f9c 100644 --- a/script/deploy_AnvilStack.s.sol +++ b/script/deploy_AnvilStack.s.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import {Commitment} from "../src/libraries/Commitment.sol"; import {Script, console} from "forge-std/Script.sol"; // CoW Protocol @@ -62,9 +63,9 @@ contract DeployAnvilStack is Script { // deploy the Composable CoW ComposableCow composableCow = new ComposableCow(address(settlement)); - new TWAP(composableCow, new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); - new GoodAfterTime(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); - new PerpetualStableSwap(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); + new TWAP(composableCow, Commitment.none()); + new GoodAfterTime(Commitment.none()); + new PerpetualStableSwap(Commitment.none()); new TradeAboveThreshold(); vm.stopBroadcast(); diff --git a/script/deploy_OrderTypes.s.sol b/script/deploy_OrderTypes.s.sol index 9fbab50..6c52820 100644 --- a/script/deploy_OrderTypes.s.sol +++ b/script/deploy_OrderTypes.s.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import {Commitment} from "../src/libraries/Commitment.sol"; import {Script} from "forge-std/Script.sol"; import {ComposableCow} from "../src/ComposableCow.sol"; @@ -17,9 +18,9 @@ contract DeployOrderTypes is Script { address composableCow = vm.envAddress("COMPOSABLE_COW"); vm.startBroadcast(deployerPrivateKey); - new TWAP(ComposableCow(composableCow), new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); - new GoodAfterTime(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); - new PerpetualStableSwap(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); + new TWAP(ComposableCow(composableCow), Commitment.none()); + new GoodAfterTime(Commitment.none()); + new PerpetualStableSwap(Commitment.none()); new TradeAboveThreshold(); vm.stopBroadcast(); diff --git a/script/deploy_OwnedStack.s.sol b/script/deploy_OwnedStack.s.sol new file mode 100644 index 0000000..6b12c8a --- /dev/null +++ b/script/deploy_OwnedStack.s.sol @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Script.sol"; + +import {ComposableCow} from "../src/ComposableCow.sol"; +import {Commitment} from "../src/libraries/Commitment.sol"; +import {PackageKind} from "../src/interfaces/PackageKind.sol"; +import {OwnedGoodAfterTime, OwnedStopLoss, OwnedTWAP} from "../src/types/Owned.sol"; + +/** + * @title Rotatable-handler deployments + * @dev Deploys the registry and rotatable handlers at the same addresses on + * every chain, via CREATE2 through the canonical factory. + * + * That works here because nothing in the initcode is chain-specific. + * `GPv2Settlement` is at one address on every chain CoW deploys to, so the + * registry's constructor argument does not vary; the registry's address + * therefore does not vary either, which in turn fixes each handler's + * constructor argument. The chain-specific part of the registry, the + * settlement domain separator, is read in the constructor and held as an + * immutable, so it lands in the deployed code without touching the address. + * + * Two preconditions come with that, and neither is enforceable from here: + * `SETTLEMENT` must be the same address used on every other chain, and + * `ADMIN` must be too, or the handlers diverge while the registry does not. + * Run `predict` first and compare against a chain already deployed to. + * + * CREATE2 is available to these handlers precisely because they deploy + * uncommitted. On the immutable handlers the descriptor digest is a + * constructor argument, so the address would depend on a descriptor that + * cannot be built until the address is known. The `Owned*` variants set + * their descriptor afterwards with `setDescriptor`, which breaks the cycle. + * + * `ComposableCow` takes no commitment mixin: it has no descriptor. + * + * Requires `SETTLEMENT` and `ADMIN`, plus a signer: either + * `PRIVATE_KEY` in the environment or `--account`/`--keystore`. + */ +abstract contract DeployOwnedStack is Script { + bytes32 internal constant SALT = bytes32(0); + + /// @dev A stale `ETH_RPC_URL` pointing at the wrong network. + error WrongChain(uint256 expected, uint256 actual); + + /// @dev `SETTLEMENT` has no code here, so it is the wrong address or the + /// wrong chain. Every order would be unsettleable. + error SettlementHasNoCode(address settlement); + + /// @dev A zero owner would strand both commitments permanently: the handler + /// would deploy uncommitted with no key able to call `setDescriptor`. + error OwnerIsZero(); + + /// @dev A deployment did not land where it was predicted to. + error AddressMismatch(address predicted, address actual); + + /// @dev `setDescriptor` is `onlyOwner`, so the deploying key must be the + /// owner to publish in the same run. Otherwise deploy uncommitted and + /// call `setDescriptor` separately from the owner. + error DescriptorNeedsOwnerKey(address deployer, address admin); + + /// @dev The descriptor is committed as `SHA256` over the published bytes, + /// so the URI has to be the thing serving those bytes over https. + error DescriptorUriNotHttps(string uri); + + /// @dev Split from `_preflight` so the checks are reachable without the + /// environment: `vm.setEnv` writes the host process environment, which + /// foundry's per-test isolation does not roll back, so a test that went + /// through the env would race every other test in the run. + function _validate(uint256 expectedChain, address settlement, address admin) internal view { + require(block.chainid == expectedChain, WrongChain(expectedChain, block.chainid)); + require(settlement.code.length != 0, SettlementHasNoCode(settlement)); + require(admin != address(0), OwnerIsZero()); + } + + function _preflight(uint256 expectedChain) internal view returns (address settlement, address admin) { + settlement = vm.envAddress("SETTLEMENT"); + admin = vm.envAddress("ADMIN"); + _validate(expectedChain, settlement, admin); + } + + /// @dev The TWAP descriptor document in this repository. The digest is + /// computed from it rather than supplied, so what is committed on + /// chain cannot drift from the document that was generated. + string internal constant TWAP_DESCRIPTOR = "descriptors/TWAP.json"; + + /// @dev `TWAP_DESCRIPTOR_URI` is optional. Unset, the handler deploys + /// uncommitted exactly as before and the descriptor is published + /// later with `setDescriptor`. + function _twapDescriptor(address deployer, address admin) internal view returns (bool, Commitment.Data memory) { + return _descriptorFor(vm.envOr("TWAP_DESCRIPTOR_URI", string("")), deployer, admin); + } + + /// @dev Split from the environment read for the same reason `_validate` is: + /// `vm.setEnv` writes the host process environment, which foundry does + /// not roll back between tests. + function _descriptorFor(string memory uri, address deployer, address admin) + internal + view + returns (bool set, Commitment.Data memory descriptor) + { + if (bytes(uri).length == 0) return (false, descriptor); + + require(deployer == admin, DescriptorNeedsOwnerKey(deployer, admin)); + require(_isHttps(uri), DescriptorUriNotHttps(uri)); + + string[] memory uris = new string[](1); + uris[0] = uri; + + set = true; + descriptor = Commitment.Data({ + uris: uris, digest: sha256(bytes(vm.readFile(TWAP_DESCRIPTOR))), kind: PackageKind.SHA256 + }); + } + + function _isHttps(string memory uri) internal pure returns (bool) { + bytes memory b = bytes(uri); + bytes memory prefix = "https://"; + if (b.length <= prefix.length) return false; + for (uint256 i; i < prefix.length; ++i) { + if (b[i] != prefix[i]) return false; + } + return true; + } + + /// @dev Skips a redundant transaction when re-running against a chain that + /// already carries this exact document. + function _publish(OwnedTWAP twap, Commitment.Data memory descriptor) internal { + (bytes32 current,) = twap.descriptorCommitment(); + if (current != descriptor.digest) twap.setDescriptor(descriptor); + } + + /// @dev `PRIVATE_KEY` if set, otherwise whatever `--account`, `--keystore` + /// or `--ledger` supplies on the command line. A raw key in the + /// environment is the worse of the two, so it is not the only option. + function _broadcaster() internal view returns (address deployer, uint256 pk) { + pk = vm.envOr("PRIVATE_KEY", uint256(0)); + deployer = pk == 0 ? msg.sender : vm.addr(pk); + } + + function _begin(uint256 pk) internal { + if (pk == 0) { + vm.startBroadcast(); + } else { + vm.startBroadcast(pk); + } + } + + function _at(bytes memory initCode) internal pure returns (address) { + return vm.computeCreate2Address(SALT, keccak256(initCode)); + } + + /// @dev Every address this script can produce, without deploying anything. + /// The TWAP address depends on the registry's, which is why it is + /// derived here rather than computed independently. + function predict(address settlement, address admin) + public + pure + returns (address composableCow, address twap, address stopLoss, address goodAfterTime) + { + composableCow = _at(abi.encodePacked(type(ComposableCow).creationCode, abi.encode(settlement))); + twap = _at(abi.encodePacked(type(OwnedTWAP).creationCode, abi.encode(composableCow, admin))); + stopLoss = _at(abi.encodePacked(type(OwnedStopLoss).creationCode, abi.encode(admin))); + goodAfterTime = _at(abi.encodePacked(type(OwnedGoodAfterTime).creationCode, abi.encode(admin))); + } + + /// @dev The registry and the one handler every deployment wants. Returns + /// what is already there rather than reverting, so re-running across a + /// set of chains converges instead of failing on the second attempt. + function _core(address settlement, address admin) internal returns (ComposableCow composableCow, OwnedTWAP twap) { + (address at, address twapAt,,) = predict(settlement, admin); + + composableCow = at.code.length != 0 ? ComposableCow(at) : new ComposableCow{salt: SALT}(settlement); + require(address(composableCow) == at, AddressMismatch(at, address(composableCow))); + + twap = twapAt.code.length != 0 ? OwnedTWAP(twapAt) : new OwnedTWAP{salt: SALT}(composableCow, admin); + require(address(twap) == twapAt, AddressMismatch(twapAt, address(twap))); + } + + function _report(address admin, ComposableCow composableCow, OwnedTWAP twap) internal view { + console.log("chainId ", block.chainid); + console.log("owner ", admin); + console.log("ComposableCow ", address(composableCow)); + console.log("OwnedTWAP ", address(twap)); + } + + function _next() internal pure { + console.log(""); + console.log("Record these in deployments/networks.json, then publish each"); + console.log("descriptor and call setDescriptor from the owner."); + } +} + +/** + * @title Ethereum mainnet deployment + * @dev The registry and TWAP only. `StopLoss` and `GoodAfterTime` are not + * deployed here: `GoodAfterTime` still has the open `TRY_NEXT_BLOCK` + * defect in #58, and neither is wanted on mainnet yet. Deploying them + * later against this same registry moves no address, since neither is an + * input to anything else. + */ +contract DeployMainnetStack is DeployOwnedStack { + uint256 private constant MAINNET = 1; + + function run() external { + (address settlement, address admin) = _preflight(MAINNET); + + (address deployer, uint256 pk) = _broadcaster(); + (bool set, Commitment.Data memory descriptor) = _twapDescriptor(deployer, admin); + + _begin(pk); + (ComposableCow composableCow, OwnedTWAP twap) = _core(settlement, admin); + if (set) _publish(twap, descriptor); + vm.stopBroadcast(); + + _report(admin, composableCow, twap); + if (set) { + console.log("TWAP descriptor ", descriptor.uris[0]); + console.logBytes32(descriptor.digest); + } + _next(); + } +} + +/** + * @title Gnosis Chain integration deployment + * @dev The registry and all three rotatable handlers. + */ +contract DeployGnosisStack is DeployOwnedStack { + uint256 private constant GNOSIS = 100; + + function run() external { + (address settlement, address admin) = _preflight(GNOSIS); + (,, address stopLossAt, address gatAt) = predict(settlement, admin); + + (address deployer, uint256 pk) = _broadcaster(); + (bool set, Commitment.Data memory descriptor) = _twapDescriptor(deployer, admin); + + _begin(pk); + + (ComposableCow composableCow, OwnedTWAP twap) = _core(settlement, admin); + if (set) _publish(twap, descriptor); + + OwnedStopLoss stopLoss = + stopLossAt.code.length != 0 ? OwnedStopLoss(stopLossAt) : new OwnedStopLoss{salt: SALT}(admin); + OwnedGoodAfterTime goodAfterTime = + gatAt.code.length != 0 ? OwnedGoodAfterTime(gatAt) : new OwnedGoodAfterTime{salt: SALT}(admin); + + vm.stopBroadcast(); + + require(address(stopLoss) == stopLossAt, AddressMismatch(stopLossAt, address(stopLoss))); + require(address(goodAfterTime) == gatAt, AddressMismatch(gatAt, address(goodAfterTime))); + + _report(admin, composableCow, twap); + console.log("OwnedStopLoss ", address(stopLoss)); + console.log("OwnedGoodAfterTime", address(goodAfterTime)); + _next(); + } +} diff --git a/script/deploy_ProdStack.s.sol b/script/deploy_ProdStack.s.sol index 3f1eae7..a7b9093 100644 --- a/script/deploy_ProdStack.s.sol +++ b/script/deploy_ProdStack.s.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import {Commitment} from "../src/libraries/Commitment.sol"; import {Script} from "forge-std/Script.sol"; // ExtensibleFallbackHandler @@ -34,11 +35,11 @@ contract DeployProdStack is Script { ComposableCow composableCow = new ComposableCow{salt: bytes32(0)}(settlement); // Deploy order types - new TWAP{salt: bytes32(0)}(composableCow, new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); - new GoodAfterTime{salt: bytes32(0)}(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); - new PerpetualStableSwap{salt: bytes32(0)}(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); + new TWAP{salt: bytes32(0)}(composableCow, Commitment.none()); + new GoodAfterTime{salt: bytes32(0)}(Commitment.none()); + new PerpetualStableSwap{salt: bytes32(0)}(Commitment.none()); new TradeAboveThreshold{salt: bytes32(0)}(); - new StopLoss{salt: bytes32(0)}(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); + new StopLoss{salt: bytes32(0)}(Commitment.none()); // Deploy value factories new CurrentBlockTimestampFactory{salt: bytes32(0)}(); diff --git a/src/OrderDescriptor.sol b/src/OrderDescriptor.sol index aca1fab..1acf639 100644 --- a/src/OrderDescriptor.sol +++ b/src/OrderDescriptor.sol @@ -1,86 +1,70 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import {Ownable} from "solady/auth/Ownable.sol"; + import {IOrderDescriptor} from "./interfaces/IOrderDescriptor.sol"; import {PackageKind} from "./interfaces/PackageKind.sol"; -import {BaseConditionalOrder} from "./BaseConditionalOrder.sol"; +import {Commitment} from "./libraries/Commitment.sol"; /** * @title Order Descriptor mixin - opt-in descriptor commitment for handlers * @author mfw78 - * @dev Immutable by omission: there is no setter, and `DescriptorUpdate` is - * emitted exactly once, from the constructor. Deployments that support - * rotation add their own access-controlled setter and re-emit. + * @dev Inherits nothing beyond its own interface, so a handler may compose it + * with `BaseConditionalOrder` and `OrderModule` without the three meeting + * at a common base. * - * A handler constructed with no URIs does NOT advertise - * `IOrderDescriptor` - feature detection stays honest for deployments - * that predate their descriptor document; committing requires a - * redeployment. The digest is a constructor argument and therefore - * part of the initcode that fixes a CREATE2 address, so the document it - * commits to carries no handler identity of its own. + * Immutable by omission: no setter, and `DescriptorUpdate` is emitted + * exactly once, from the constructor. The commitment is what advertises + * `IOrderDescriptor`, not the URI list, since a `BZZ_MANIFEST` commitment + * locates its own document and publishes no URI. Handlers consult + * `_descriptorAdvertised` from `supportsInterface`. */ -abstract contract OrderDescriptor is IOrderDescriptor, BaseConditionalOrder { - /** - * @dev URIs cannot be published without a commitment to verify them against - */ - error UncommittedDescriptorURI(); - - /** - * @dev `SHA256` does not locate the document, so it requires a URI - */ - error DescriptorURIRequired(); +abstract contract OrderDescriptor is IOrderDescriptor { + using Commitment for Commitment.Data; - /** - * @dev A `BZZ_MANIFEST` commitment locates its own document; a URI could not - * be verified against a structure root anyway - */ - error DescriptorURINotUsed(); + /// @dev `internal` so `OwnedOrderDescriptor` writes what these accessors read. + Commitment.Data internal _descriptor; - string[] private _descriptorUris; - bytes32 private immutable _DESCRIPTOR_DIGEST; - PackageKind private immutable _DESCRIPTOR_KIND; - - constructor(string[] memory uris, bytes32 digest, PackageKind kind) { - if (digest != bytes32(0)) { - if (kind == PackageKind.SHA256) { - require(uris.length > 0, DescriptorURIRequired()); - } else { - require(uris.length == 0, DescriptorURINotUsed()); - } - emit DescriptorUpdate(uris, digest, kind); - } else { - require(uris.length == 0, UncommittedDescriptorURI()); + constructor(Commitment.Data memory descriptor) { + _descriptor.set(descriptor); + if (descriptor.digest != bytes32(0)) { + emit DescriptorUpdate(descriptor.uris, descriptor.digest, descriptor.kind); } - _descriptorUris = uris; - _DESCRIPTOR_DIGEST = digest; - _DESCRIPTOR_KIND = kind; } /** * @inheritdoc IOrderDescriptor */ function descriptorURI() external view returns (string[] memory uris) { - return _descriptorUris; + return _descriptor.uris; } /** * @inheritdoc IOrderDescriptor */ function descriptorCommitment() external view returns (bytes32 digest, PackageKind kind) { - return (_DESCRIPTOR_DIGEST, _DESCRIPTOR_KIND); + return (_descriptor.digest, _descriptor.kind); } - /** - * @dev Advertise `IOrderDescriptor` only when a descriptor is committed: - * claiming the interface while returning empty values is - * non-conformant per the discovery specification. The commitment is - * the gate, not the URI list, since a `BZZ_MANIFEST` commitment - * locates its own document and publishes no URI. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - if (interfaceId == type(IOrderDescriptor).interfaceId) { - return _DESCRIPTOR_DIGEST != bytes32(0); - } - return super.supportsInterface(interfaceId); + function _descriptorAdvertised() internal view returns (bool) { + return _descriptor.advertised(); + } +} + +/** + * @dev Adds rotation. The setter writes the storage the read-only accessors + * already read, so nothing has to be overridden, and `DescriptorUpdate` is + * re-emitted so indexers observe the change without polling. + * + * Ownership is `Ownable2Step`: the commitment is the only mutable state + * here, so a mistyped transfer would strand it. The recipient must accept. + */ +abstract contract OwnedOrderDescriptor is OrderDescriptor, Ownable { + using Commitment for Commitment.Data; + + function setDescriptor(Commitment.Data memory descriptor) external onlyOwner { + _descriptor.set(descriptor); + emit DescriptorUpdate(descriptor.uris, descriptor.digest, descriptor.kind); } } diff --git a/src/OrderModule.sol b/src/OrderModule.sol index d2c8949..5345fe7 100644 --- a/src/OrderModule.sol +++ b/src/OrderModule.sol @@ -1,76 +1,68 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import {Ownable} from "solady/auth/Ownable.sol"; + import {IOrderModule} from "./interfaces/IOrderModule.sol"; import {PackageKind} from "./interfaces/PackageKind.sol"; -import {BaseConditionalOrder} from "./BaseConditionalOrder.sol"; +import {Commitment} from "./libraries/Commitment.sol"; /** * @title Order Module mixin - opt-in module commitment for handlers * @author mfw78 - * @dev Immutable by omission, as for `OrderDescriptor`. The commitment is what - * advertises `IOrderModule`, not the URI list: a `BZZ_MANIFEST` - * commitment locates its own package and so publishes no URI. Constructed - * with a zero digest, the handler does not advertise the interface. + * @dev Inherits nothing beyond its own interface, so a handler may compose it + * with `BaseConditionalOrder` and `OrderModule` without the three meeting + * at a common base. + * + * Immutable by omission: no setter, and `ModuleUpdate` is emitted + * exactly once, from the constructor. The commitment is what advertises + * `IOrderModule`, not the URI list, since a `BZZ_MANIFEST` commitment + * locates its own package and publishes no URI. Handlers consult + * `_moduleAdvertised` from `supportsInterface`. */ -abstract contract OrderModule is IOrderModule, BaseConditionalOrder { - /** - * @dev URIs cannot be published without a commitment to verify them against - */ - error UncommittedModuleURI(); - - /** - * @dev `SHA256` does not locate the package, so it requires a URI - */ - error ModuleURIRequired(); - - /** - * @dev A `BZZ_MANIFEST` commitment locates its own package; a URI could not - * be verified against a structure root anyway - */ - error ModuleURINotUsed(); +abstract contract OrderModule is IOrderModule { + using Commitment for Commitment.Data; - string[] private _moduleUris; - bytes32 private immutable _MODULE_DIGEST; - PackageKind private immutable _MODULE_KIND; + /// @dev `internal` so `OwnedOrderModule` writes what these accessors read. + Commitment.Data internal _module; - constructor(string[] memory uris, bytes32 digest, PackageKind kind) { - if (digest != bytes32(0)) { - if (kind == PackageKind.SHA256) { - require(uris.length > 0, ModuleURIRequired()); - } else { - require(uris.length == 0, ModuleURINotUsed()); - } - emit ModuleUpdate(uris, digest, kind); - } else { - require(uris.length == 0, UncommittedModuleURI()); - } - _moduleUris = uris; - _MODULE_DIGEST = digest; - _MODULE_KIND = kind; + constructor(Commitment.Data memory module) { + _module.set(module); + if (module.digest != bytes32(0)) emit ModuleUpdate(module.uris, module.digest, module.kind); } /** * @inheritdoc IOrderModule */ function moduleURI() external view returns (string[] memory uris) { - return _moduleUris; + return _module.uris; } /** * @inheritdoc IOrderModule */ function moduleCommitment() external view returns (bytes32 digest, PackageKind kind) { - return (_MODULE_DIGEST, _MODULE_KIND); + return (_module.digest, _module.kind); } - /** - * @dev Advertise `IOrderModule` only when a module is committed - */ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - if (interfaceId == type(IOrderModule).interfaceId) { - return _MODULE_DIGEST != bytes32(0); - } - return super.supportsInterface(interfaceId); + function _moduleAdvertised() internal view returns (bool) { + return _module.advertised(); + } +} + +/** + * @dev Adds rotation. The setter writes the storage the read-only accessors + * already read, so nothing has to be overridden, and `ModuleUpdate` is + * re-emitted so indexers observe the change without polling. + * + * Ownership is `Ownable2Step`: the commitment is the only mutable state + * here, so a mistyped transfer would strand it. The recipient must accept. + */ +abstract contract OwnedOrderModule is OrderModule, Ownable { + using Commitment for Commitment.Data; + + function setModule(Commitment.Data memory module) external onlyOwner { + _module.set(module); + emit ModuleUpdate(module.uris, module.digest, module.kind); } } diff --git a/src/libraries/Commitment.sol b/src/libraries/Commitment.sol new file mode 100644 index 0000000..17c4e61 --- /dev/null +++ b/src/libraries/Commitment.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {PackageKind} from "../interfaces/PackageKind.sol"; + +/** + * @title A content commitment and where to fetch it from + * @author mfw78 + * @dev Shared by the descriptor and module surfaces, which differ in what they + * commit to and in nothing else. Each holds its own `Data`, so the two + * occupy distinct slots and are set independently. + */ +library Commitment { + /** + * @dev URIs cannot be published without a commitment to verify them against + */ + error UncommittedURI(); + + /** + * @dev `SHA256` does not locate the content, so it requires a URI + */ + error URIRequired(); + + /** + * @dev A `BZZ_MANIFEST` commitment locates its own content; a URI could not + * be verified against a structure root anyway + */ + error URINotUsed(); + + struct Data { + string[] uris; + bytes32 digest; + PackageKind kind; + } + + /** + * @dev An uncommitted surface. The kind is immaterial while the digest is + * zero, so this spells that out rather than leaving an arbitrary + * `PackageKind` at each construction site. + */ + function none() internal pure returns (Data memory) { + return Data({uris: new string[](0), digest: bytes32(0), kind: PackageKind.BZZ_MANIFEST}); + } + + /// @dev `bytes32(0)` means uncommitted; such a surface is not advertised. + function advertised(Data storage self) internal view returns (bool) { + return self.digest != bytes32(0); + } + + function set(Data storage self, Data memory value) internal { + validate(value.uris.length, value.digest, value.kind); + self.uris = value.uris; + self.digest = value.digest; + self.kind = value.kind; + } + + function validate(uint256 uriCount, bytes32 digest, PackageKind kind) internal pure { + if (digest == bytes32(0)) { + require(uriCount == 0, UncommittedURI()); + } else if (kind == PackageKind.SHA256) { + require(uriCount > 0, URIRequired()); + } else { + require(uriCount == 0, URINotUsed()); + } + } +} diff --git a/src/types/GoodAfterTime.sol b/src/types/GoodAfterTime.sol index 1b13aa2..9c44ed4 100644 --- a/src/types/GoodAfterTime.sol +++ b/src/types/GoodAfterTime.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import {Commitment} from "../libraries/Commitment.sol"; +import {BaseConditionalOrder} from "../BaseConditionalOrder.sol"; +import {IOrderDescriptor} from "../interfaces/IOrderDescriptor.sol"; import {SafeCastLib} from "solady/utils/SafeCastLib.sol"; import {IExpectedOutCalculator} from "../vendored/Milkman.sol"; @@ -45,10 +48,8 @@ error PriceCheckerFailed(); * ensure that the order is not filled multiple times, a `minSellBalance` is * checked before the order is placed. */ -contract GoodAfterTime is OrderDescriptor { - constructor(string[] memory descriptorUris, bytes32 descriptorDigest_, PackageKind descriptorKind) - OrderDescriptor(descriptorUris, descriptorDigest_, descriptorKind) - {} +contract GoodAfterTime is BaseConditionalOrder, OrderDescriptor { + constructor(Commitment.Data memory descriptor) OrderDescriptor(descriptor) {} using SafeCastLib for uint256; @@ -157,4 +158,13 @@ contract GoodAfterTime is OrderDescriptor { { return "good-after-time order ready"; } + + /** + * @inheritdoc BaseConditionalOrder + * @dev Adds the descriptor sidecar, which is advertised only once committed. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + if (interfaceId == type(IOrderDescriptor).interfaceId) return _descriptorAdvertised(); + return super.supportsInterface(interfaceId); + } } diff --git a/src/types/Owned.sol b/src/types/Owned.sol new file mode 100644 index 0000000..ae9ae30 --- /dev/null +++ b/src/types/Owned.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ComposableCow} from "../ComposableCow.sol"; +import {OwnedOrderDescriptor} from "../OrderDescriptor.sol"; +import {OrderModule, OwnedOrderModule} from "../OrderModule.sol"; +import {IOrderDescriptor} from "../interfaces/IOrderDescriptor.sol"; +import {IOrderModule} from "../interfaces/IOrderModule.sol"; +import {Commitment} from "../libraries/Commitment.sol"; +import {GoodAfterTime} from "./GoodAfterTime.sol"; +import {StopLoss} from "./StopLoss.sol"; +import {TWAP} from "./twap/TWAP.sol"; + +/** + * @dev The handlers with rotatable commitments. Each extends the handler it is + * named for, so order generation is the same code and only the discovery + * surface differs. One owner governs both commitments. + * + * Constructed uncommitted, then committed by the owner. That ordering is + * forced rather than convenient: on the immutable handlers the digest is a + * constructor argument, so under CREATE2 the address would depend on a + * descriptor that cannot be built until the address is known. + */ +contract OwnedTWAP is TWAP, OwnedOrderDescriptor, OwnedOrderModule { + constructor(ComposableCow composableCow_, address owner_) + TWAP(composableCow_, Commitment.none()) + OrderModule(Commitment.none()) + { + _initializeOwner(owner_); + } + + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + if (interfaceId == type(IOrderModule).interfaceId) return _moduleAdvertised(); + return super.supportsInterface(interfaceId); + } +} + +contract OwnedStopLoss is StopLoss, OwnedOrderDescriptor, OwnedOrderModule { + constructor(address owner_) StopLoss(Commitment.none()) OrderModule(Commitment.none()) { + _initializeOwner(owner_); + } + + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + if (interfaceId == type(IOrderModule).interfaceId) return _moduleAdvertised(); + return super.supportsInterface(interfaceId); + } +} + +contract OwnedGoodAfterTime is GoodAfterTime, OwnedOrderDescriptor, OwnedOrderModule { + constructor(address owner_) GoodAfterTime(Commitment.none()) OrderModule(Commitment.none()) { + _initializeOwner(owner_); + } + + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + if (interfaceId == type(IOrderModule).interfaceId) return _moduleAdvertised(); + return super.supportsInterface(interfaceId); + } +} diff --git a/src/types/PerpetualStableSwap.sol b/src/types/PerpetualStableSwap.sol index e3e1e33..1ff773d 100644 --- a/src/types/PerpetualStableSwap.sol +++ b/src/types/PerpetualStableSwap.sol @@ -8,6 +8,9 @@ import { IConditionalOrderGenerator, BaseConditionalOrder } from "../BaseConditionalOrder.sol"; +import {Commitment} from "../libraries/Commitment.sol"; +import {BaseConditionalOrder} from "../BaseConditionalOrder.sol"; +import {IOrderDescriptor} from "../interfaces/IOrderDescriptor.sol"; import {IOrderManifest} from "../interfaces/IOrderManifest.sol"; import {ConditionalOrdersUtilsLib as Utils} from "./ConditionalOrdersUtilsLib.sol"; import {OrderDescriptor} from "../OrderDescriptor.sol"; @@ -23,10 +26,8 @@ error NotFunded(); * @title A smart contract that is always willing to trade between tokenA and tokenB 1:1, * taking decimals into account (and adding specifiable spread) */ -contract PerpetualStableSwap is OrderDescriptor { - constructor(string[] memory descriptorUris, bytes32 descriptorDigest_, PackageKind descriptorKind) - OrderDescriptor(descriptorUris, descriptorDigest_, descriptorKind) - {} +contract PerpetualStableSwap is BaseConditionalOrder, OrderDescriptor { + constructor(Commitment.Data memory descriptor) OrderDescriptor(descriptor) {} /** * Creates a new perpetual swap order. All resulting swaps will be made from the target contract. @@ -183,4 +184,13 @@ contract PerpetualStableSwap is OrderDescriptor { return (new ManifestEntry[](0), false, _manifestStatus(errorData)); } } + + /** + * @inheritdoc BaseConditionalOrder + * @dev Adds the descriptor sidecar, which is advertised only once committed. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + if (interfaceId == type(IOrderDescriptor).interfaceId) return _descriptorAdvertised(); + return super.supportsInterface(interfaceId); + } } diff --git a/src/types/StopLoss.sol b/src/types/StopLoss.sol index cf6234c..0cf3362 100644 --- a/src/types/StopLoss.sol +++ b/src/types/StopLoss.sol @@ -8,6 +8,9 @@ import { IConditionalOrderGenerator, BaseConditionalOrder } from "../BaseConditionalOrder.sol"; +import {Commitment} from "../libraries/Commitment.sol"; +import {BaseConditionalOrder} from "../BaseConditionalOrder.sol"; +import {IOrderDescriptor} from "../interfaces/IOrderDescriptor.sol"; import {IAggregatorV3Interface} from "../interfaces/IAggregatorV3Interface.sol"; import {ConditionalOrdersUtilsLib as Utils} from "./ConditionalOrdersUtilsLib.sol"; import {OrderDescriptor} from "../OrderDescriptor.sol"; @@ -42,10 +45,8 @@ error OrderExpired(); * @notice Both oracles need to be denominated in the same quote currency (e.g. GNO/ETH and USD/ETH for GNO/USD stop loss orders) * @dev This order type has replay protection due to the `validTo` parameter, ensuring it will just execute one time */ -contract StopLoss is OrderDescriptor { - constructor(string[] memory descriptorUris, bytes32 descriptorDigest_, PackageKind descriptorKind) - OrderDescriptor(descriptorUris, descriptorDigest_, descriptorKind) - {} +contract StopLoss is BaseConditionalOrder, OrderDescriptor { + constructor(Commitment.Data memory descriptor) OrderDescriptor(descriptor) {} /** * @dev Scaling factor for the strike price @@ -169,4 +170,13 @@ contract StopLoss is OrderDescriptor { { return "stop-loss triggered"; } + + /** + * @inheritdoc BaseConditionalOrder + * @dev Adds the descriptor sidecar, which is advertised only once committed. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + if (interfaceId == type(IOrderDescriptor).interfaceId) return _descriptorAdvertised(); + return super.supportsInterface(interfaceId); + } } diff --git a/src/types/twap/TWAP.sol b/src/types/twap/TWAP.sol index c88da65..1170bc7 100644 --- a/src/types/twap/TWAP.sol +++ b/src/types/twap/TWAP.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import {Commitment} from "../../libraries/Commitment.sol"; +import {BaseConditionalOrder} from "../../BaseConditionalOrder.sol"; +import {IOrderDescriptor} from "../../interfaces/IOrderDescriptor.sol"; import {SafeCastLib} from "solady/utils/SafeCastLib.sol"; import {ComposableCow} from "../../ComposableCow.sol"; @@ -36,17 +39,12 @@ error OrderNotInitialized(); * specific price, even if the price of the token changes during the trade. * @dev Designed to be used with the CoW Protocol Conditional Order Framework. */ -contract TWAP is OrderDescriptor { +contract TWAP is BaseConditionalOrder, OrderDescriptor { using SafeCastLib for uint256; ComposableCow public immutable composableCow; - constructor( - ComposableCow _composableCow, - string[] memory descriptorUris, - bytes32 descriptorDigest_, - PackageKind descriptorKind - ) OrderDescriptor(descriptorUris, descriptorDigest_, descriptorKind) { + constructor(ComposableCow _composableCow, Commitment.Data memory descriptor) OrderDescriptor(descriptor) { composableCow = _composableCow; } @@ -301,4 +299,13 @@ contract TWAP is OrderDescriptor { isActive: block.timestamp >= validFrom && block.timestamp <= validTo }); } + + /** + * @inheritdoc BaseConditionalOrder + * @dev Adds the descriptor sidecar, which is advertised only once committed. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + if (interfaceId == type(IOrderDescriptor).interfaceId) return _descriptorAdvertised(); + return super.supportsInterface(interfaceId); + } } diff --git a/test/ComposableCow.base.t.sol b/test/ComposableCow.base.t.sol index b3b86cc..b7e91b9 100644 --- a/test/ComposableCow.base.t.sol +++ b/test/ComposableCow.base.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import {Commitment} from "../src/libraries/Commitment.sol"; + import {Safe} from "safe/Safe.sol"; import {Enum} from "safe/libraries/Enum.sol"; import {IERC165} from "safe/interfaces/IERC165.sol"; @@ -76,7 +78,10 @@ contract BaseComposableCowTest is Base { passThrough = new TestConditionalOrderGenerator(); mirror = new MirrorConditionalOrder(); - twap = new TWAP(composableCow, testDescriptorUris(), TEST_DESCRIPTOR_DIGEST, PackageKind.SHA256); + twap = new TWAP( + composableCow, + Commitment.Data({uris: testDescriptorUris(), digest: TEST_DESCRIPTOR_DIGEST, kind: PackageKind.SHA256}) + ); } /** diff --git a/test/ComposableCow.discovery.t.sol b/test/ComposableCow.discovery.t.sol index e6a148f..c4074f8 100644 --- a/test/ComposableCow.discovery.t.sol +++ b/test/ComposableCow.discovery.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import {Commitment} from "../src/libraries/Commitment.sol"; import {GPv2Order} from "cowprotocol/contracts/libraries/GPv2Order.sol"; import {IERC165} from "safe/interfaces/IERC165.sol"; @@ -10,6 +11,7 @@ import {IOrderDescriptor} from "../src/interfaces/IOrderDescriptor.sol"; import {IOrderModule} from "../src/interfaces/IOrderModule.sol"; import {OrderDescriptor} from "../src/OrderDescriptor.sol"; import {OrderModule} from "../src/OrderModule.sol"; +import {BaseConditionalOrder} from "../src/BaseConditionalOrder.sol"; import {StopLoss} from "../src/types/StopLoss.sol"; import {PackageKind} from "../src/interfaces/PackageKind.sol"; @@ -18,8 +20,13 @@ error TestNoOrder(); /** * @dev Minimal handler committing a module: the OrderModule mixin under test */ -contract ModuleHandler is OrderModule { - constructor(string[] memory uris, bytes32 digest, PackageKind kind) OrderModule(uris, digest, kind) {} +contract ModuleHandler is BaseConditionalOrder, OrderModule { + constructor(Commitment.Data memory commitment) OrderModule(commitment) {} + + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + if (interfaceId == type(IOrderModule).interfaceId) return _moduleAdvertised(); + return super.supportsInterface(interfaceId); + } function generateOrder(address, bytes32, bytes calldata, bytes calldata) public @@ -49,7 +56,9 @@ contract ComposableCowDiscoveryTest is BaseComposableCowTest { * super chain */ function test_descriptor_CommittedAdvertisesAndRoundTrips() public { - StopLoss handler = new StopLoss(testDescriptorUris(), TEST_DESCRIPTOR_DIGEST, PackageKind.SHA256); + StopLoss handler = new StopLoss( + Commitment.Data({uris: testDescriptorUris(), digest: TEST_DESCRIPTOR_DIGEST, kind: PackageKind.SHA256}) + ); assertTrue(handler.supportsInterface(type(IOrderDescriptor).interfaceId)); assertTrue(handler.supportsInterface(type(IConditionalOrderGenerator).interfaceId)); @@ -68,7 +77,7 @@ contract ComposableCowDiscoveryTest is BaseComposableCowTest { * sidecar - feature detection never lies about empty metadata */ function test_descriptor_UncommittedDoesNotAdvertise() public { - StopLoss handler = new StopLoss(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); + StopLoss handler = new StopLoss(Commitment.none()); assertFalse(handler.supportsInterface(type(IOrderDescriptor).interfaceId)); // the handler remains a fully functional generator @@ -82,7 +91,9 @@ contract ComposableCowDiscoveryTest is BaseComposableCowTest { function test_descriptor_ConstructorEmitsUpdate() public { vm.expectEmit(true, true, true, true); emit IOrderDescriptor.DescriptorUpdate(testDescriptorUris(), TEST_DESCRIPTOR_DIGEST, PackageKind.SHA256); - new StopLoss(testDescriptorUris(), TEST_DESCRIPTOR_DIGEST, PackageKind.SHA256); + new StopLoss( + Commitment.Data({uris: testDescriptorUris(), digest: TEST_DESCRIPTOR_DIGEST, kind: PackageKind.SHA256}) + ); } // --- module --- @@ -95,7 +106,8 @@ contract ComposableCowDiscoveryTest is BaseComposableCowTest { vm.expectEmit(true, true, true, true); emit IOrderModule.ModuleUpdate(moduleUris(), digest, PackageKind.SHA256); - ModuleHandler handler = new ModuleHandler(moduleUris(), digest, PackageKind.SHA256); + ModuleHandler handler = + new ModuleHandler(Commitment.Data({uris: moduleUris(), digest: digest, kind: PackageKind.SHA256})); assertTrue(handler.supportsInterface(type(IOrderModule).interfaceId)); assertFalse(handler.supportsInterface(type(IOrderDescriptor).interfaceId)); @@ -110,8 +122,8 @@ contract ComposableCowDiscoveryTest is BaseComposableCowTest { * non-conformant */ function test_module_RevertsUncommittedURI() public { - vm.expectRevert(OrderModule.UncommittedModuleURI.selector); - new ModuleHandler(moduleUris(), bytes32(0), PackageKind.BZZ_MANIFEST); + vm.expectRevert(Commitment.UncommittedURI.selector); + new ModuleHandler(Commitment.Data({uris: moduleUris(), digest: bytes32(0), kind: PackageKind.BZZ_MANIFEST})); } /** @@ -120,16 +132,18 @@ contract ComposableCowDiscoveryTest is BaseComposableCowTest { * root, and publishing one would write a gateway into the contract */ function test_module_RevertsContentAddressedWithURI() public { - vm.expectRevert(OrderModule.ModuleURINotUsed.selector); - new ModuleHandler(moduleUris(), keccak256("pkg"), PackageKind.BZZ_MANIFEST); + vm.expectRevert(Commitment.URINotUsed.selector); + new ModuleHandler( + Commitment.Data({uris: moduleUris(), digest: keccak256("pkg"), kind: PackageKind.BZZ_MANIFEST}) + ); } /** * @dev `SHA256` does not locate the package, so it requires a URI */ function test_module_RevertsSha256WithoutURI() public { - vm.expectRevert(OrderModule.ModuleURIRequired.selector); - new ModuleHandler(new string[](0), keccak256("pkg"), PackageKind.SHA256); + vm.expectRevert(Commitment.URIRequired.selector); + new ModuleHandler(Commitment.Data({uris: new string[](0), digest: keccak256("pkg"), kind: PackageKind.SHA256})); } /** @@ -137,7 +151,9 @@ contract ComposableCowDiscoveryTest is BaseComposableCowTest { * advertises with no URI published at all */ function test_module_ContentAddressedNeedsNoURI() public { - ModuleHandler handler = new ModuleHandler(new string[](0), keccak256("pkg"), PackageKind.BZZ_MANIFEST); + ModuleHandler handler = new ModuleHandler( + Commitment.Data({uris: new string[](0), digest: keccak256("pkg"), kind: PackageKind.BZZ_MANIFEST}) + ); assertTrue(handler.supportsInterface(type(IOrderModule).interfaceId)); assertEq(handler.moduleURI().length, 0); } @@ -146,7 +162,7 @@ contract ComposableCowDiscoveryTest is BaseComposableCowTest { * @dev No module committed: no advertising, handler still functions */ function test_module_UncommittedDoesNotAdvertise() public { - ModuleHandler handler = new ModuleHandler(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST); + ModuleHandler handler = new ModuleHandler(Commitment.none()); assertFalse(handler.supportsInterface(type(IOrderModule).interfaceId)); assertTrue(handler.supportsInterface(type(IConditionalOrderGenerator).interfaceId)); } @@ -156,7 +172,9 @@ contract ComposableCowDiscoveryTest is BaseComposableCowTest { * empty - the discovery trigger end to end */ function test_module_NeedsInputSignal() public { - ModuleHandler handler = new ModuleHandler(new string[](0), keccak256("module"), PackageKind.BZZ_MANIFEST); + ModuleHandler handler = new ModuleHandler( + Commitment.Data({uris: new string[](0), digest: keccak256("module"), kind: PackageKind.BZZ_MANIFEST}) + ); IConditionalOrderGenerator.GeneratorResult memory result = handler.poll(address(safe1), bytes32(0), bytes(""), bytes("")); diff --git a/test/ComposableCow.gat.t.sol b/test/ComposableCow.gat.t.sol index 1190500..6c5b8ef 100644 --- a/test/ComposableCow.gat.t.sol +++ b/test/ComposableCow.gat.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import {Commitment} from "../src/libraries/Commitment.sol"; import {ERC1271} from "safe/handler/extensible/SignatureVerifierMuxer.sol"; import { @@ -37,7 +38,9 @@ contract ComposableCowGatTest is BaseComposableCowTest { super.setUp(); // deploy the GAT handler - gat = new GoodAfterTime(testDescriptorUris(), TEST_DESCRIPTOR_DIGEST, PackageKind.SHA256); + gat = new GoodAfterTime( + Commitment.Data({uris: testDescriptorUris(), digest: TEST_DESCRIPTOR_DIGEST, kind: PackageKind.SHA256}) + ); // deploy the test expected out calculator testOutCalculator = new TestExpectedOutCalculator(); diff --git a/test/ComposableCow.manifest.t.sol b/test/ComposableCow.manifest.t.sol index 59486b3..a6b3236 100644 --- a/test/ComposableCow.manifest.t.sol +++ b/test/ComposableCow.manifest.t.sol @@ -11,6 +11,7 @@ import { PollTryAtTimestampHandler, SuccessHandler } from "./ComposableCow.base.t.sol"; +import {Commitment} from "../src/libraries/Commitment.sol"; import {IOrderManifest} from "../src/interfaces/IOrderManifest.sol"; import {IERC165} from "safe/interfaces/IERC165.sol"; import {TWAPOrder} from "../src/types/twap/libraries/TWAPOrder.sol"; @@ -42,7 +43,9 @@ contract ComposableCowManifestTest is BaseComposableCowTest { function setUp() public virtual override(BaseComposableCowTest) { super.setUp(); - perpetualSwap = new PerpetualStableSwap(testDescriptorUris(), TEST_DESCRIPTOR_DIGEST, PackageKind.SHA256); + perpetualSwap = new PerpetualStableSwap( + Commitment.Data({uris: testDescriptorUris(), digest: TEST_DESCRIPTOR_DIGEST, kind: PackageKind.SHA256}) + ); currentBlockTimestampFactory = new CurrentBlockTimestampFactory(); } diff --git a/test/ComposableCow.owned.t.sol b/test/ComposableCow.owned.t.sol new file mode 100644 index 0000000..ca0f744 --- /dev/null +++ b/test/ComposableCow.owned.t.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {Ownable} from "solady/auth/Ownable.sol"; + +import {Commitment} from "../src/libraries/Commitment.sol"; +import "./ComposableCow.base.t.sol"; + +import {OrderDescriptor} from "../src/OrderDescriptor.sol"; +import {IOrderDescriptor} from "../src/interfaces/IOrderDescriptor.sol"; +import {IOrderModule} from "../src/interfaces/IOrderModule.sol"; +import {PackageKind} from "../src/interfaces/PackageKind.sol"; +import {OwnedGoodAfterTime, OwnedStopLoss, OwnedTWAP} from "../src/types/Owned.sol"; +import {StopLoss} from "../src/types/StopLoss.sol"; + +contract ComposableCowOwnedTest is BaseComposableCowTest { + OwnedTWAP private ownedTwap; + OwnedStopLoss private ownedStopLoss; + OwnedGoodAfterTime private ownedGat; + + address private constant OWNER = address(0xA11CE); + address private constant STRANGER = address(0xBAD); + + bytes32 private constant DIGEST = keccak256("descriptor"); + + function setUp() public virtual override(BaseComposableCowTest) { + super.setUp(); + ownedTwap = new OwnedTWAP(composableCow, OWNER); + ownedStopLoss = new OwnedStopLoss(OWNER); + ownedGat = new OwnedGoodAfterTime(OWNER); + } + + function _uris(string memory u) private pure returns (string[] memory out) { + out = new string[](1); + out[0] = u; + } + + /// @dev Uncommitted at construction: nothing to advertise until the owner sets it. + function test_owned_UncommittedOnDeploy() public { + (bytes32 digest,) = ownedTwap.descriptorCommitment(); + assertEq(digest, bytes32(0)); + assertEq(ownedTwap.descriptorURI().length, 0); + assertFalse(ownedTwap.supportsInterface(type(IOrderDescriptor).interfaceId)); + assertFalse(ownedTwap.supportsInterface(type(IOrderModule).interfaceId)); + assertEq(ownedTwap.owner(), OWNER, "deployer must not retain ownership"); + } + + function test_owned_SetDescriptorAdvertisesAndRoundTrips() public { + vm.expectEmit(true, true, true, true); + emit IOrderDescriptor.DescriptorUpdate(new string[](0), DIGEST, PackageKind.BZZ_MANIFEST); + + vm.prank(OWNER); + ownedTwap.setDescriptor( + Commitment.Data({uris: new string[](0), digest: DIGEST, kind: PackageKind.BZZ_MANIFEST}) + ); + + (bytes32 digest, PackageKind kind) = ownedTwap.descriptorCommitment(); + assertEq(digest, DIGEST); + assertEq(uint256(kind), uint256(PackageKind.BZZ_MANIFEST)); + assertTrue(ownedTwap.supportsInterface(type(IOrderDescriptor).interfaceId)); + assertFalse(ownedTwap.supportsInterface(type(IOrderModule).interfaceId)); + } + + /// @dev The two commitments are independent surfaces under one owner. + function test_owned_SetModuleIsIndependentOfDescriptor() public { + vm.prank(OWNER); + ownedStopLoss.setModule( + Commitment.Data({ + uris: _uris("https://example.invalid/module.tar.zst"), digest: DIGEST, kind: PackageKind.SHA256 + }) + ); + + (bytes32 digest, PackageKind kind) = ownedStopLoss.moduleCommitment(); + assertEq(digest, DIGEST); + assertEq(uint256(kind), uint256(PackageKind.SHA256)); + assertTrue(ownedStopLoss.supportsInterface(type(IOrderModule).interfaceId)); + assertFalse(ownedStopLoss.supportsInterface(type(IOrderDescriptor).interfaceId)); + } + + /// @dev Rotation is the whole point: a second set must replace the first. + function test_owned_RotationReplacesCommitment() public { + vm.startPrank(OWNER); + ownedGat.setDescriptor(Commitment.Data({uris: new string[](0), digest: DIGEST, kind: PackageKind.BZZ_MANIFEST})); + bytes32 next = keccak256("rotated"); + ownedGat.setDescriptor( + Commitment.Data({uris: _uris("https://example.invalid/d.json"), digest: next, kind: PackageKind.SHA256}) + ); + vm.stopPrank(); + + (bytes32 digest, PackageKind kind) = ownedGat.descriptorCommitment(); + assertEq(digest, next); + assertEq(uint256(kind), uint256(PackageKind.SHA256)); + assertEq(ownedGat.descriptorURI().length, 1); + } + + function test_owned_RevertsForStranger() public { + vm.startPrank(STRANGER); + + vm.expectRevert(Ownable.Unauthorized.selector); + ownedTwap.setDescriptor( + Commitment.Data({uris: new string[](0), digest: DIGEST, kind: PackageKind.BZZ_MANIFEST}) + ); + + vm.expectRevert(Ownable.Unauthorized.selector); + ownedTwap.setModule(Commitment.Data({uris: new string[](0), digest: DIGEST, kind: PackageKind.BZZ_MANIFEST})); + + vm.stopPrank(); + } + + /// @dev The constructor invariants of the immutable mixin, enforced on write. + function test_owned_EnforcesURIInvariants() public { + vm.startPrank(OWNER); + + vm.expectRevert(Commitment.URIRequired.selector); + ownedTwap.setDescriptor(Commitment.Data({uris: new string[](0), digest: DIGEST, kind: PackageKind.SHA256})); + + vm.expectRevert(Commitment.URINotUsed.selector); + ownedTwap.setDescriptor( + Commitment.Data({uris: _uris("bzz://x"), digest: DIGEST, kind: PackageKind.BZZ_MANIFEST}) + ); + + vm.expectRevert(Commitment.UncommittedURI.selector); + ownedTwap.setDescriptor( + Commitment.Data({uris: _uris("bzz://x"), digest: bytes32(0), kind: PackageKind.BZZ_MANIFEST}) + ); + + vm.stopPrank(); + } + + /** + * @dev Two-step handover, in the stronger direction. Solady moves control + * only to a recipient that has itself asked for it, so a mistyped + * address cannot strand the commitments: it will never have asked. + */ + function test_owned_TransferIsTwoStep() public { + // ownership cannot be pushed at an address that has not asked for it + vm.prank(OWNER); + vm.expectRevert(Ownable.NoHandoverRequest.selector); + ownedTwap.completeOwnershipHandover(STRANGER); + assertEq(ownedTwap.owner(), OWNER, "ownership moved without a request"); + + vm.prank(STRANGER); + ownedTwap.requestOwnershipHandover(); + assertEq(ownedTwap.owner(), OWNER, "ownership moved on the request alone"); + assertGt(ownedTwap.ownershipHandoverExpiresAt(STRANGER), block.timestamp); + + vm.prank(OWNER); + ownedTwap.completeOwnershipHandover(STRANGER); + assertEq(ownedTwap.owner(), STRANGER); + + vm.prank(OWNER); + vm.expectRevert(Ownable.Unauthorized.selector); + ownedTwap.setDescriptor( + Commitment.Data({uris: new string[](0), digest: DIGEST, kind: PackageKind.BZZ_MANIFEST}) + ); + } + + /** + * @dev The variants exist to change the discovery surface and nothing else. + * One owner governs both commitments. + */ + function test_owned_OrderGenerationIsUnchanged() public { + StopLoss.Data memory o; // zero amounts, rejected before any oracle is consulted + + StopLoss immutableHandler = new StopLoss(Commitment.none()); + + bytes memory expected = + abi.encodeWithSelector(IConditionalOrder.OrderNotValid.selector, bytes4(keccak256("ZeroAmount()"))); + + vm.expectRevert(expected); + immutableHandler.generateOrder(address(safe1), bytes32(0), abi.encode(o), bytes("")); + + vm.expectRevert(expected); + ownedStopLoss.generateOrder(address(safe1), bytes32(0), abi.encode(o), bytes("")); + } +} diff --git a/test/ComposableCow.stoploss.t.sol b/test/ComposableCow.stoploss.t.sol index b02e076..a37a547 100644 --- a/test/ComposableCow.stoploss.t.sol +++ b/test/ComposableCow.stoploss.t.sol @@ -8,6 +8,7 @@ import { BaseComposableCowTest, IConditionalOrderGenerator } from "./ComposableCow.base.t.sol"; +import {Commitment} from "../src/libraries/Commitment.sol"; import {IAggregatorV3Interface} from "../src/interfaces/IAggregatorV3Interface.sol"; import { StopLoss, @@ -34,7 +35,9 @@ contract ComposableCowStopLossTest is BaseComposableCowTest { function setUp() public virtual override(BaseComposableCowTest) { super.setUp(); - stopLoss = new StopLoss(testDescriptorUris(), TEST_DESCRIPTOR_DIGEST, PackageKind.SHA256); + stopLoss = new StopLoss( + Commitment.Data({uris: testDescriptorUris(), digest: TEST_DESCRIPTOR_DIGEST, kind: PackageKind.SHA256}) + ); } function priceToAddress(int256 price) internal returns (address) { diff --git a/test/ComposableCow.twap.t.sol b/test/ComposableCow.twap.t.sol index 8413356..5f8a5da 100644 --- a/test/ComposableCow.twap.t.sol +++ b/test/ComposableCow.twap.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import {Commitment} from "../src/libraries/Commitment.sol"; import {SafeCastLib} from "solady/utils/SafeCastLib.sol"; import {ERC1271} from "safe/handler/extensible/SignatureVerifierMuxer.sol"; @@ -56,7 +57,10 @@ contract ComposableCowTwapTest is BaseComposableCowTest { super.setUp(); // deploy the TWAP handler - twap = new TWAP(composableCow, testDescriptorUris(), TEST_DESCRIPTOR_DIGEST, PackageKind.SHA256); + twap = new TWAP( + composableCow, + Commitment.Data({uris: testDescriptorUris(), digest: TEST_DESCRIPTOR_DIGEST, kind: PackageKind.SHA256}) + ); // deploy the current block timestamp factory currentBlockTimestampFactory = new CurrentBlockTimestampFactory(); diff --git a/test/DeployStack.t.sol b/test/DeployStack.t.sol new file mode 100644 index 0000000..c030ef0 --- /dev/null +++ b/test/DeployStack.t.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {Test} from "forge-std/Test.sol"; + +import {ComposableCow} from "../src/ComposableCow.sol"; +import {Commitment} from "../src/libraries/Commitment.sol"; +import {PackageKind} from "../src/interfaces/PackageKind.sol"; +import {OwnedTWAP} from "../src/types/Owned.sol"; +import {DeployOwnedStack} from "../script/deploy_OwnedStack.s.sol"; + +contract ValidateHarness is DeployOwnedStack { + function validate(uint256 expectedChain, address settlement, address admin) external view { + _validate(expectedChain, settlement, admin); + } + + function descriptorFor(string calldata uri, address deployer, address admin) + external + view + returns (bool, Commitment.Data memory) + { + return _descriptorFor(uri, deployer, admin); + } + + function isHttps(string calldata uri) external pure returns (bool) { + return _isHttps(uri); + } +} + +/** + * @dev The descriptor is committed as a sha256 over published bytes, so the + * digest going on chain has to be the digest of the document that is + * actually served. The script computes it from the repository rather than + * taking it on trust, and these check that it does. + */ +contract DescriptorPublishTest is Test { + address internal constant ADMIN = address(0xA11CE); + string internal constant URI = "https://example.invalid/twap.json"; + + ValidateHarness internal harness; + + function setUp() public { + harness = new ValidateHarness(); + } + + /** + * @dev The digest is sha256 over the document as it sits in the repo. + * + * Pinned against the value `sha256sum descriptors/TWAP.json` prints, + * rather than against a second `sha256(vm.readFile(...))` in the test, + * which would only prove the test and the script agree with each other. + * What needs proving is that both agree with the tool anyone verifying + * the published document will reach for, and that `vm.readFile` hands + * back the file byte for byte: the document ends in a newline, and + * dropping it would silently change every digest ever published. + * + * Update this constant deliberately if the document changes. + */ + function test_descriptor_DigestIsTheDocumentsSha256() public view { + (bool set, Commitment.Data memory d) = harness.descriptorFor(URI, ADMIN, ADMIN); + + assertTrue(set); + assertEq( + d.digest, + 0xb499b04e292a7fe808b982113e7e931446261bbc5d702517ba89a8403536ce1a, + "digest does not match sha256sum of the document" + ); + assertEq(bytes(vm.readFile("descriptors/TWAP.json")).length, 1825, "readFile did not return the file exactly"); + assertEq(d.digest, sha256(bytes(vm.readFile("descriptors/TWAP.json"))), "digest is not the document's"); + assertEq(uint256(d.kind), uint256(PackageKind.SHA256)); + assertEq(d.uris.length, 1); + assertEq(d.uris[0], URI); + } + + /// @dev `setDescriptor` is `onlyOwner`, so publishing in the same run needs + /// the owner's key. Failing loudly beats a reverting broadcast. + function test_descriptor_RequiresTheOwnerKey() public { + vm.expectRevert( + abi.encodeWithSelector(DeployOwnedStack.DescriptorNeedsOwnerKey.selector, address(0xB0B), ADMIN) + ); + harness.descriptorFor(URI, address(0xB0B), ADMIN); + } + + function test_descriptor_RejectsANonHttpsUri() public { + vm.expectRevert( + abi.encodeWithSelector(DeployOwnedStack.DescriptorUriNotHttps.selector, "http://example.invalid/twap.json") + ); + harness.descriptorFor("http://example.invalid/twap.json", ADMIN, ADMIN); + } + + /// @dev Unset, the handler deploys uncommitted exactly as before. + function test_descriptor_OptionalWhenUriUnset() public view { + (bool set,) = harness.descriptorFor("", ADMIN, ADMIN); + assertFalse(set, "an unset URI still published"); + } + + function test_isHttps() public view { + assertTrue(harness.isHttps("https://a")); + assertFalse(harness.isHttps("http://a")); + assertFalse(harness.isHttps("https://"), "scheme alone is not a URI"); + assertFalse(harness.isHttps("bzz://a")); + assertFalse(harness.isHttps("")); + } +} + +/// @dev CREATE2 buys one address per contract across every chain. That only +/// holds while the initcode carries nothing chain-specific, so these pin +/// what does and does not reach an address. +contract DeployAddressTest is Test { + ValidateHarness internal harness; + + address internal constant SETTLEMENT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; + address internal constant ADMIN = address(0xA11CE); + + function setUp() public { + harness = new ValidateHarness(); + } + + /// @dev The reason for CREATE2 at all: same inputs, different chain, same + /// addresses. `predict` is `pure`, so no chain state can reach it. + function test_predict_IsTheSameOnEveryChain() public { + vm.chainId(1); + (address a1, address b1, address c1, address d1) = harness.predict(SETTLEMENT, ADMIN); + + vm.chainId(100); + (address a2, address b2, address c2, address d2) = harness.predict(SETTLEMENT, ADMIN); + + vm.chainId(8453); + (address a3, address b3, address c3, address d3) = harness.predict(SETTLEMENT, ADMIN); + + assertEq(a1, a2, "registry moved"); + assertEq(a1, a3, "registry moved"); + assertEq(b1, b2, "twap moved"); + assertEq(b1, b3, "twap moved"); + assertEq(c1, c2, "stopLoss moved"); + assertEq(d1, d2, "goodAfterTime moved"); + } + + /// @dev The TWAP address is derived from the registry's, not computed + /// beside it: a different registry has to move the handler too, or + /// the second deployment would point at the wrong one. + function test_predict_TwapFollowsTheRegistry() public view { + (address regA, address twapA,,) = harness.predict(SETTLEMENT, ADMIN); + (address regB, address twapB,,) = harness.predict(address(0xBEEF), ADMIN); + + assertTrue(regA != regB, "settlement does not reach the registry address"); + assertTrue(twapA != twapB, "registry does not reach the twap address"); + } + + /// @dev The precondition worth stating loudly: a different owner is a + /// different handler address, while the registry is unaffected. + function test_predict_AdminMovesHandlersOnly() public view { + (address regA, address twapA, address slA, address gatA) = harness.predict(SETTLEMENT, ADMIN); + (address regB, address twapB, address slB, address gatB) = harness.predict(SETTLEMENT, address(0xB0B)); + + assertEq(regA, regB, "admin should not reach the registry address"); + assertTrue(twapA != twapB, "admin does not reach the twap address"); + assertTrue(slA != slB, "admin does not reach the stopLoss address"); + assertTrue(gatA != gatB, "admin does not reach the goodAfterTime address"); + } + + /** + * @dev The chaining itself: the handler's initcode must carry the registry + * address, not the settlement one. Moving the settlement moves both, so + * a test that only varies the settlement cannot tell the two apart, and + * a mutation swapping them survives it. This pins the encoding. + */ + function test_predict_TwapEncodesTheRegistryNotTheSettlement() public view { + (address registry, address twap,,) = harness.predict(SETTLEMENT, ADMIN); + + assertEq( + twap, + vm.computeCreate2Address( + bytes32(0), keccak256(abi.encodePacked(type(OwnedTWAP).creationCode, abi.encode(registry, ADMIN))) + ), + "twap initcode does not carry the registry" + ); + + assertTrue( + twap + != vm.computeCreate2Address( + bytes32(0), keccak256(abi.encodePacked(type(OwnedTWAP).creationCode, abi.encode(SETTLEMENT, ADMIN))) + ), + "twap initcode carries the settlement" + ); + } + + /// @dev Checked against the cheatcode rather than a second copy of the + /// same derivation. + function test_predict_MatchesCreate2() public view { + (address registry,,,) = harness.predict(SETTLEMENT, ADMIN); + assertEq( + registry, + vm.computeCreate2Address( + bytes32(0), keccak256(abi.encodePacked(type(ComposableCow).creationCode, abi.encode(SETTLEMENT))) + ) + ); + } +} + +/** + * @dev The preflight checks run once, on a live network, with real funds behind + * them. Each is asserted to fire rather than assumed to. + */ +contract DeployStackTest is Test { + uint256 internal constant MAINNET = 1; + + ValidateHarness internal harness; + address internal settlement; + address internal admin = address(0xA11CE); + + function setUp() public { + harness = new ValidateHarness(); + // the check is only that the address has code, so any contract serves + settlement = address(new Bytecode()); + vm.chainId(MAINNET); + } + + function test_validate_AcceptsAValidConfig() public view { + harness.validate(MAINNET, settlement, admin); + } + + /// @dev The check that catches a stale `ETH_RPC_URL`. + function test_validate_RejectsWrongChain() public { + vm.chainId(100); + vm.expectRevert(abi.encodeWithSelector(DeployOwnedStack.WrongChain.selector, MAINNET, uint256(100))); + harness.validate(MAINNET, settlement, admin); + } + + /// @dev A settlement address with no code means the wrong address or the + /// wrong chain, and every order would be unsettleable. + function test_validate_RejectsCodelessSettlement() public { + vm.expectRevert(abi.encodeWithSelector(DeployOwnedStack.SettlementHasNoCode.selector, address(0xBEEF))); + harness.validate(MAINNET, address(0xBEEF), admin); + } + + /// @dev A zero owner strands both commitments: the handler deploys + /// uncommitted and no key can ever call `setDescriptor`. + function test_validate_RejectsZeroOwner() public { + vm.expectRevert(DeployOwnedStack.OwnerIsZero.selector); + harness.validate(MAINNET, settlement, address(0)); + } +} + +/// @dev Something with a non-empty code size, and nothing else. +contract Bytecode {}