diff --git a/test/concrete/OrderBook.addOrder.mock.t.sol b/test/concrete/OrderBook.addOrder.mock.t.sol new file mode 100644 index 000000000..3a5f41ef1 --- /dev/null +++ b/test/concrete/OrderBook.addOrder.mock.t.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.19; + +import "rain.metadata/LibMeta.sol"; + +import "test/util/abstract/OrderBookExternalMockTest.sol"; + +/// @title OrderBookAddOrderMockTest +/// @notice Tests the addOrder function of the OrderBook contract. +contract OrderBookAddOrderMockTest is OrderBookExternalMockTest, IMetaV1 { + /// A little boilerplate to make it easier to build the order that we expect + /// for a given order config. + function expectedOrder(address owner, OrderConfig memory config, address expression) + internal + view + returns (Order memory, bytes32) + { + Evaluable memory expectedEvaluable = Evaluable(iInterpreter, iStore, expression); + Order memory order = Order( + owner, + config.evaluableConfig.sources.length > 1 + && config.evaluableConfig.sources[SourceIndex.unwrap(HANDLE_IO_ENTRYPOINT)].length > 0, + expectedEvaluable, + config.validInputs, + config.validOutputs + ); + return (order, LibOrder.hash(order)); + } + + /// Boilerplate to add an order with a mocked deployer and checks events and + /// storage accesses. + function addOrderWithChecks(address owner, OrderConfig memory config, address expression) + internal + returns (Order memory, bytes32) + { + config.evaluableConfig.deployer = iDeployer; + (Order memory order, bytes32 orderHash) = expectedOrder(owner, config, expression); + assertTrue(!iOrderbook.orderExists(orderHash)); + vm.mockCall( + address(iDeployer), + abi.encodeWithSelector(IExpressionDeployerV1.deployExpression.selector), + abi.encode(iInterpreter, iStore, expression) + ); + vm.expectEmit(false, false, false, true); + emit AddOrder(owner, iDeployer, order, orderHash); + if (config.meta.length > 0) { + vm.expectEmit(false, false, true, false); + // The subject of the meta is the order hash. + emit MetaV1(owner, uint256(orderHash), config.meta); + } + vm.record(); + vm.recordLogs(); + vm.prank(owner); + iOrderbook.addOrder(config); + // MetaV1 is NOT emitted if the meta is empty. + assertEq(vm.getRecordedLogs().length, config.meta.length > 0 ? 2 : 1); + (bytes32[] memory reads, bytes32[] memory writes) = vm.accesses(address(iOrderbook)); + // 3x for reentrancy guard, 1x for dead order check, 1x for live write. + assertEq(reads.length, 5); + // 2x for reentrancy guard, 1x for live write. + assertEq(writes.length, 3); + assertTrue(iOrderbook.orderExists(orderHash)); + + // Adding the same order again MUST revert. This MAY be impossible to + // encounter for a real expression deployer, as the deployer MAY NOT + // return the same address twice, but it is possible to mock. + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(OrderExists.selector, owner, orderHash)); + vm.mockCall( + address(iDeployer), + abi.encodeWithSelector(IExpressionDeployerV1.deployExpression.selector), + abi.encode(iInterpreter, iStore, expression) + ); + iOrderbook.addOrder(config); + return (order, orderHash); + } + + /// Valid config has a few requirements, this boilerplate makes it easier to + /// get the fuzzer to meet them. + function assumeValidConfig(OrderConfig memory config) internal pure { + vm.assume(config.evaluableConfig.sources.length >= 2); + vm.assume(config.validInputs.length > 0); + vm.assume(config.validOutputs.length > 0); + if (config.meta.length > 0) { + // This is a bit of a hack, but it's the easiest way to get a valid + // meta document. + config.meta = abi.encodePacked(META_MAGIC_NUMBER_V1, config.meta); + } + } + + /// Adding an order without calculations MUST revert. + function testAddOrderWithoutCalculationsReverts(address owner, OrderConfig memory config) public { + vm.prank(owner); + config.evaluableConfig.sources = new bytes[](0); + vm.expectRevert(abi.encodeWithSelector(OrderNoSources.selector, owner)); + iOrderbook.addOrder(config); + (Order memory order, bytes32 orderHash) = expectedOrder(owner, config, address(0)); + (order); + assertTrue(!iOrderbook.orderExists(orderHash)); + } + + /// Adding an order without inputs MUST revert. + function testAddOrderWithoutInputsReverts(address owner, OrderConfig memory config) public { + vm.prank(owner); + vm.assume(config.evaluableConfig.sources.length > 1); + config.validInputs = new IO[](0); + vm.expectRevert(abi.encodeWithSelector(OrderNoInputs.selector, owner)); + iOrderbook.addOrder(config); + (Order memory order, bytes32 orderHash) = expectedOrder(owner, config, address(0)); + (order); + assertTrue(!iOrderbook.orderExists(orderHash)); + } + + /// Adding an order without outputs MUST revert. + function testAddOrderWithoutOutputsReverts(address owner, OrderConfig memory config) public { + vm.prank(owner); + vm.assume(config.evaluableConfig.sources.length > 1); + vm.assume(config.validInputs.length > 0); + config.validOutputs = new IO[](0); + vm.expectRevert(abi.encodeWithSelector(OrderNoOutputs.selector, owner)); + iOrderbook.addOrder(config); + (Order memory order, bytes32 orderHash) = expectedOrder(owner, config, address(0)); + (order); + assertTrue(!iOrderbook.orderExists(orderHash)); + } + + /// Adding an order with calculations, inputs and outputs will succeed if + /// the expression is valid according to the deployer. The resulting order + /// MUST be emitted. This test assumes empty meta. + function testAddOrderWithCalculationsInputsAndOutputsSucceeds( + address owner, + OrderConfig memory config, + address expression + ) public { + vm.assume(config.evaluableConfig.sources.length >= 2); + vm.assume(config.validInputs.length > 0); + vm.assume(config.validOutputs.length > 0); + config.meta = new bytes(0); + (Order memory order, bytes32 orderhash) = addOrderWithChecks(owner, config, expression); + (order); + (orderhash); + } + + /// Adding a valid order with a non-empty meta MUST revert if the meta is + /// not self describing as a rain meta document. + function testAddOrderWithNonEmptyMetaReverts(address owner, OrderConfig memory config, address expression) public { + vm.prank(owner); + vm.assume(config.evaluableConfig.sources.length >= 2); + vm.assume(config.validInputs.length > 0); + vm.assume(config.validOutputs.length > 0); + vm.assume(!LibMeta.isRainMetaV1(config.meta)); + vm.assume(config.meta.length > 0); + + config.evaluableConfig.deployer = iDeployer; + vm.mockCall( + address(iDeployer), + abi.encodeWithSelector(IExpressionDeployerV1.deployExpression.selector), + abi.encode(iInterpreter, iStore, expression) + ); + vm.expectRevert(abi.encodeWithSelector(NotRainMetaV1.selector, config.meta)); + iOrderbook.addOrder(config); + + (Order memory order, bytes32 orderHash) = expectedOrder(owner, config, expression); + (order); + assertTrue(!iOrderbook.orderExists(orderHash)); + } + + /// Adding a valid order with a non-empty meta MUST emit MetaV1 if the meta + /// is self describing as a rain meta document. + function testAddOrderWithNonEmptyMetaEmitsMetaV1(address owner, OrderConfig memory config, address expression) + public + { + vm.assume(config.evaluableConfig.sources.length >= 2); + vm.assume(config.validInputs.length > 0); + vm.assume(config.validOutputs.length > 0); + vm.assume(config.meta.length > 0); + + // This is a bit of a hack, but it's the easiest way to get a valid + // meta document. + config.meta = abi.encodePacked(META_MAGIC_NUMBER_V1, config.meta); + + (Order memory order, bytes32 orderHash) = addOrderWithChecks(owner, config, expression); + (order); + (orderHash); + } + + /// Alice and Bob can add orders with the same config. The resulting orders + /// MUST be different. + function testAddOrderTwoAccountsWithSameConfig( + address alice, + address bob, + OrderConfig memory config, + address expression + ) public { + vm.assume(alice != bob); + assumeValidConfig(config); + (Order memory aliceOrder, bytes32 aliceOrderHash) = addOrderWithChecks(alice, config, expression); + (Order memory bobOrder, bytes32 bobOrderHash) = addOrderWithChecks(bob, config, expression); + (aliceOrder); + (bobOrder); + assertTrue(aliceOrderHash != bobOrderHash); + } + + /// Alice and Bob can add orders with different configs. The resulting orders + /// MUST be different. + function testAddOrderTwoAccountsWithDifferentConfig( + address alice, + address bob, + OrderConfig memory aliceConfig, + OrderConfig memory bobConfig, + address aliceExpression, + address bobExpression + ) public { + vm.assume(alice != bob); + assumeValidConfig(aliceConfig); + assumeValidConfig(bobConfig); + (Order memory aliceOrder, bytes32 aliceOrderHash) = addOrderWithChecks(alice, aliceConfig, aliceExpression); + (Order memory bobOrder, bytes32 bobOrderHash) = addOrderWithChecks(bob, bobConfig, bobExpression); + (aliceOrder); + (bobOrder); + assertTrue(aliceOrderHash != bobOrderHash); + } + + /// Alice can add orders with different configs. The resulting orders MUST + /// be different. + function testAddOrderSameAccountWithDifferentConfig( + address alice, + OrderConfig memory configOne, + OrderConfig memory configTwo, + address expressionOne, + address expressionTwo + ) public { + assumeValidConfig(configOne); + assumeValidConfig(configTwo); + (Order memory expectedOrderOne, bytes32 expectedOrderOneHash) = expectedOrder(alice, configOne, expressionOne); + (Order memory expectedOrderTwo, bytes32 expectedOrderTwoHash) = expectedOrder(alice, configTwo, expressionTwo); + (expectedOrderOne); + (expectedOrderTwo); + assertTrue(expectedOrderOneHash != expectedOrderTwoHash); + (Order memory orderOne, bytes32 orderOneHash) = addOrderWithChecks(alice, configOne, expressionOne); + (Order memory orderTwo, bytes32 orderTwoHash) = addOrderWithChecks(alice, configTwo, expressionTwo); + (orderOne); + (orderTwo); + assertTrue(orderOneHash != orderTwoHash); + } +} diff --git a/test/concrete/OrderBook.addOrder.t.sol b/test/concrete/OrderBook.addOrder.t.sol index 636776c06..bfe61481d 100644 --- a/test/concrete/OrderBook.addOrder.t.sol +++ b/test/concrete/OrderBook.addOrder.t.sol @@ -1,246 +1,14 @@ // SPDX-License-Identifier: CAL pragma solidity =0.8.19; -import "rain.metadata/LibMeta.sol"; - -import "test/util/abstract/OrderBookExternalMockTest.sol"; +import "test/util/abstract/OrderBookExternalRealTest.sol"; /// @title OrderBookAddOrderTest -/// @notice Tests the addOrder function of the OrderBook contract. -contract OrderBookAddOrderTest is OrderBookExternalMockTest, IMetaV1 { - /// A little boilerplate to make it easier to build the order that we expect - /// for a given order config. - function expectedOrder(address owner, OrderConfig memory config, address expression) - internal - view - returns (Order memory, bytes32) - { - Evaluable memory expectedEvaluable = Evaluable(iInterpreter, iStore, expression); - Order memory order = Order( - owner, - config.evaluableConfig.sources.length > 1 - && config.evaluableConfig.sources[SourceIndex.unwrap(HANDLE_IO_ENTRYPOINT)].length > 0, - expectedEvaluable, - config.validInputs, - config.validOutputs - ); - return (order, LibOrder.hash(order)); - } - - /// Boilerplate to add an order with a mocked deployer and checks events and - /// storage accesses. - function addOrderWithChecks(address owner, OrderConfig memory config, address expression) - internal - returns (Order memory, bytes32) - { - config.evaluableConfig.deployer = iDeployer; - (Order memory order, bytes32 orderHash) = expectedOrder(owner, config, expression); - assertTrue(!iOrderbook.orderExists(orderHash)); - vm.mockCall( - address(iDeployer), - abi.encodeWithSelector(IExpressionDeployerV1.deployExpression.selector), - abi.encode(iInterpreter, iStore, expression) - ); - vm.expectEmit(false, false, false, true); - emit AddOrder(owner, iDeployer, order, orderHash); - if (config.meta.length > 0) { - vm.expectEmit(false, false, true, false); - // The subject of the meta is the order hash. - emit MetaV1(owner, uint256(orderHash), config.meta); - } - vm.record(); - vm.recordLogs(); - vm.prank(owner); - iOrderbook.addOrder(config); - // MetaV1 is NOT emitted if the meta is empty. - assertEq(vm.getRecordedLogs().length, config.meta.length > 0 ? 2 : 1); - (bytes32[] memory reads, bytes32[] memory writes) = vm.accesses(address(iOrderbook)); - // 3x for reentrancy guard, 1x for dead order check, 1x for live write. - assertEq(reads.length, 5); - // 2x for reentrancy guard, 1x for live write. - assertEq(writes.length, 3); - assertTrue(iOrderbook.orderExists(orderHash)); - - // Adding the same order again MUST revert. This MAY be impossible to - // encounter for a real expression deployer, as the deployer MAY NOT - // return the same address twice, but it is possible to mock. - vm.prank(owner); - vm.expectRevert(abi.encodeWithSelector(OrderExists.selector, owner, orderHash)); - vm.mockCall( - address(iDeployer), - abi.encodeWithSelector(IExpressionDeployerV1.deployExpression.selector), - abi.encode(iInterpreter, iStore, expression) - ); - iOrderbook.addOrder(config); - return (order, orderHash); - } - - /// Valid config has a few requirements, this boilerplate makes it easier to - /// get the fuzzer to meet them. - function assumeValidConfig(OrderConfig memory config) internal pure { - vm.assume(config.evaluableConfig.sources.length >= 2); - vm.assume(config.validInputs.length > 0); - vm.assume(config.validOutputs.length > 0); - if (config.meta.length > 0) { - // This is a bit of a hack, but it's the easiest way to get a valid - // meta document. - config.meta = abi.encodePacked(META_MAGIC_NUMBER_V1, config.meta); - } - } - - /// Adding an order without calculations MUST revert. - function testAddOrderWithoutCalculationsReverts(address owner, OrderConfig memory config) public { - vm.prank(owner); - config.evaluableConfig.sources = new bytes[](0); - vm.expectRevert(abi.encodeWithSelector(OrderNoSources.selector, owner)); - iOrderbook.addOrder(config); - (Order memory order, bytes32 orderHash) = expectedOrder(owner, config, address(0)); - (order); - assertTrue(!iOrderbook.orderExists(orderHash)); - } - - /// Adding an order without inputs MUST revert. - function testAddOrderWithoutInputsReverts(address owner, OrderConfig memory config) public { - vm.prank(owner); - vm.assume(config.evaluableConfig.sources.length > 1); - config.validInputs = new IO[](0); - vm.expectRevert(abi.encodeWithSelector(OrderNoInputs.selector, owner)); - iOrderbook.addOrder(config); - (Order memory order, bytes32 orderHash) = expectedOrder(owner, config, address(0)); - (order); - assertTrue(!iOrderbook.orderExists(orderHash)); - } - - /// Adding an order without outputs MUST revert. - function testAddOrderWithoutOutputsReverts(address owner, OrderConfig memory config) public { - vm.prank(owner); - vm.assume(config.evaluableConfig.sources.length > 1); - vm.assume(config.validInputs.length > 0); - config.validOutputs = new IO[](0); - vm.expectRevert(abi.encodeWithSelector(OrderNoOutputs.selector, owner)); - iOrderbook.addOrder(config); - (Order memory order, bytes32 orderHash) = expectedOrder(owner, config, address(0)); - (order); - assertTrue(!iOrderbook.orderExists(orderHash)); - } - - /// Adding an order with calculations, inputs and outputs will succeed if - /// the expression is valid according to the deployer. The resulting order - /// MUST be emitted. This test assumes empty meta. - function testAddOrderWithCalculationsInputsAndOutputsSucceeds( - address owner, - OrderConfig memory config, - address expression - ) public { - vm.assume(config.evaluableConfig.sources.length >= 2); - vm.assume(config.validInputs.length > 0); - vm.assume(config.validOutputs.length > 0); - config.meta = new bytes(0); - (Order memory order, bytes32 orderhash) = addOrderWithChecks(owner, config, expression); - (order); - (orderhash); - } - - /// Adding a valid order with a non-empty meta MUST revert if the meta is - /// not self describing as a rain meta document. - function testAddOrderWithNonEmptyMetaReverts(address owner, OrderConfig memory config, address expression) public { - vm.prank(owner); - vm.assume(config.evaluableConfig.sources.length >= 2); - vm.assume(config.validInputs.length > 0); - vm.assume(config.validOutputs.length > 0); - vm.assume(!LibMeta.isRainMetaV1(config.meta)); - vm.assume(config.meta.length > 0); - - config.evaluableConfig.deployer = iDeployer; - vm.mockCall( - address(iDeployer), - abi.encodeWithSelector(IExpressionDeployerV1.deployExpression.selector), - abi.encode(iInterpreter, iStore, expression) - ); - vm.expectRevert(abi.encodeWithSelector(NotRainMetaV1.selector, config.meta)); - iOrderbook.addOrder(config); - - (Order memory order, bytes32 orderHash) = expectedOrder(owner, config, expression); - (order); - assertTrue(!iOrderbook.orderExists(orderHash)); - } - - /// Adding a valid order with a non-empty meta MUST emit MetaV1 if the meta - /// is self describing as a rain meta document. - function testAddOrderWithNonEmptyMetaEmitsMetaV1(address owner, OrderConfig memory config, address expression) - public - { - vm.assume(config.evaluableConfig.sources.length >= 2); - vm.assume(config.validInputs.length > 0); - vm.assume(config.validOutputs.length > 0); - vm.assume(config.meta.length > 0); - - // This is a bit of a hack, but it's the easiest way to get a valid - // meta document. - config.meta = abi.encodePacked(META_MAGIC_NUMBER_V1, config.meta); - - (Order memory order, bytes32 orderHash) = addOrderWithChecks(owner, config, expression); - (order); - (orderHash); - } - - /// Alice and Bob can add orders with the same config. The resulting orders - /// MUST be different. - function testAddOrderTwoAccountsWithSameConfig( - address alice, - address bob, - OrderConfig memory config, - address expression - ) public { - vm.assume(alice != bob); - assumeValidConfig(config); - (Order memory aliceOrder, bytes32 aliceOrderHash) = addOrderWithChecks(alice, config, expression); - (Order memory bobOrder, bytes32 bobOrderHash) = addOrderWithChecks(bob, config, expression); - (aliceOrder); - (bobOrder); - assertTrue(aliceOrderHash != bobOrderHash); - } - - /// Alice and Bob can add orders with different configs. The resulting orders - /// MUST be different. - function testAddOrderTwoAccountsWithDifferentConfig( - address alice, - address bob, - OrderConfig memory aliceConfig, - OrderConfig memory bobConfig, - address aliceExpression, - address bobExpression - ) public { - vm.assume(alice != bob); - assumeValidConfig(aliceConfig); - assumeValidConfig(bobConfig); - (Order memory aliceOrder, bytes32 aliceOrderHash) = addOrderWithChecks(alice, aliceConfig, aliceExpression); - (Order memory bobOrder, bytes32 bobOrderHash) = addOrderWithChecks(bob, bobConfig, bobExpression); - (aliceOrder); - (bobOrder); - assertTrue(aliceOrderHash != bobOrderHash); - } +/// @notice A test harness for testing the OrderBook addOrder function. +contract OrderBookAddOrderTest is OrderBookExternalRealTest { - /// Alice can add orders with different configs. The resulting orders MUST - /// be different. - function testAddOrderSameAccountWithDifferentConfig( - address alice, - OrderConfig memory configOne, - OrderConfig memory configTwo, - address expressionOne, - address expressionTwo - ) public { - assumeValidConfig(configOne); - assumeValidConfig(configTwo); - (Order memory expectedOrderOne, bytes32 expectedOrderOneHash) = expectedOrder(alice, configOne, expressionOne); - (Order memory expectedOrderTwo, bytes32 expectedOrderTwoHash) = expectedOrder(alice, configTwo, expressionTwo); - (expectedOrderOne); - (expectedOrderTwo); - assertTrue(expectedOrderOneHash != expectedOrderTwoHash); - (Order memory orderOne, bytes32 orderOneHash) = addOrderWithChecks(alice, configOne, expressionOne); - (Order memory orderTwo, bytes32 orderTwoHash) = addOrderWithChecks(alice, configTwo, expressionTwo); - (orderOne); - (orderTwo); - assertTrue(orderOneHash != orderTwoHash); + /// No sources reverts as we need at least a calculate expression. + function testAddOrderRealNoSourcesReverts(OrderConfig memory config) public { + // iOrderbook.addOrder(config); } -} +} \ No newline at end of file diff --git a/test/util/abstract/OrderBookExternalRealTest.sol b/test/util/abstract/OrderBookExternalRealTest.sol index cd76b3c1f..a828952f8 100644 --- a/test/util/abstract/OrderBookExternalRealTest.sol +++ b/test/util/abstract/OrderBookExternalRealTest.sol @@ -13,36 +13,33 @@ import "test/util/abstract/IOrderBookV3Stub.sol"; import "src/concrete/OrderBook.sol"; abstract contract OrderBookExternalRealTest is Test, IOrderBookV3Stub { - IInterpreterV1 immutable iInterpreter; - IInterpreterStoreV1 immutable iStore; - IExpressionDeployerV1 immutable iDeployer; - IOrderBookV3 immutable iOrderbook; - IERC20 immutable iToken0; - IERC20 immutable iToken1; + IInterpreterV1 internal immutable iInterpreter; + IInterpreterStoreV1 internal immutable iStore; + IExpressionDeployerV1 internal immutable iDeployer; + IOrderBookV3 internal immutable iOrderbook; + IERC20 internal immutable iToken0; + IERC20 internal immutable iToken1; constructor() { - vm.pauseGasMetering(); iInterpreter = IInterpreterV1(new RainterpreterNP()); iStore = IInterpreterStoreV1(new RainterpreterStore()); // Deploy the expression deployer. + vm.etch(address(IERC1820_REGISTRY), REVERTING_MOCK_BYTECODE); + vm.mockCall( + address(IERC1820_REGISTRY), + abi.encodeWithSelector(IERC1820Registry.interfaceHash.selector), + abi.encode(bytes32(uint256(5)))); + vm.mockCall(address(IERC1820_REGISTRY), abi.encodeWithSelector(IERC1820Registry.setInterfaceImplementer.selector), ""); bytes memory deployerMeta = LibRainterpreterExpressionDeployerNPMeta.authoringMeta(); - iDeployer = IExpressionDeployerV1( - address( + console2.log("current deployer meta hash:"); + console2.logBytes32(keccak256(deployerMeta)); + iDeployer = IExpressionDeployerV1(address( new RainterpreterExpressionDeployerNP(RainterpreterExpressionDeployerConstructionConfig( address(iInterpreter), address(iStore), deployerMeta - )) - ) - ); - // All non-mocked calls will revert. - vm.etch(address(iDeployer), REVERTING_MOCK_BYTECODE); - vm.mockCall( - address(iDeployer), - abi.encodeWithSelector(IExpressionDeployerV1.deployExpression.selector), - abi.encode(iInterpreter, iStore, address(0)) - ); + )))); bytes memory orderbookMeta = vm.readFileBinary(ORDER_BOOK_META_PATH); console2.log("orderbook meta hash:"); console2.logBytes(abi.encodePacked(keccak256(orderbookMeta))); @@ -54,6 +51,5 @@ abstract contract OrderBookExternalRealTest is Test, IOrderBookV3Stub { vm.etch(address(iToken0), REVERTING_MOCK_BYTECODE); iToken1 = IERC20(address(uint160(uint256(keccak256("token1.rain.test"))))); vm.etch(address(iToken1), REVERTING_MOCK_BYTECODE); - vm.resumeGasMetering(); } }