diff --git a/test/src/concrete/Flow.transfer.t.sol b/test/src/concrete/Flow.transfer.t.sol index 89aebcb7..9c799d5f 100644 --- a/test/src/concrete/Flow.transfer.t.sol +++ b/test/src/concrete/Flow.transfer.t.sol @@ -430,6 +430,55 @@ contract FlowTransferTest is FlowTest { vm.stopPrank(); } + /// `IFlowV5.flow()` MUST process the flow atomically. When a later + /// transfer fails, the entire flow MUST revert and earlier transfers + /// must NOT have observable side effects. With mocks, we observe this + /// by asserting the outer revert (transaction revert rolls back any + /// state) and that the failing transfer's selector is the revert + /// reason — i.e. the inner failure was not caught and squashed. + /// forge-config: default.fuzz.runs = 100 + function testFlowAtomicRollbackOnLaterTransferFailure( + address alice, + address bob, + uint256 erc20Amount, + uint256 erc1155TokenId, + uint256 erc1155Amount + ) external { + vm.assume(alice != address(0)); + vm.assume(bob != alice); + vm.assume(Sentinel.unwrap(RAIN_FLOW_SENTINEL) != erc20Amount); + vm.assume(Sentinel.unwrap(RAIN_FLOW_SENTINEL) != erc1155TokenId); + vm.assume(Sentinel.unwrap(RAIN_FLOW_SENTINEL) != erc1155Amount); + vm.label(alice, "Alice"); + vm.label(bob, "Bob"); + + (IFlowV5 flow, EvaluableV2 memory evaluable) = deployFlow(); + assumeEtchable(alice, address(flow)); + assumeEtchable(bob, address(flow)); + + // ERC20 from alice → flow (would succeed); ERC1155 from bob → alice + // (will revert because `bob` is neither `msg.sender` nor `flow`). + ERC20Transfer[] memory erc20Transfers = new ERC20Transfer[](1); + erc20Transfers[0] = ERC20Transfer({token: TOKEN_A, from: alice, to: address(flow), amount: erc20Amount}); + + ERC1155Transfer[] memory erc1155Transfers = new ERC1155Transfer[](1); + erc1155Transfers[0] = + ERC1155Transfer({token: TOKEN_C, from: bob, to: alice, id: erc1155TokenId, amount: erc1155Amount}); + + uint256[] memory stack = LibStackGeneration.generateFlowStack( + Sentinel.unwrap(RAIN_FLOW_SENTINEL), + FlowTransferV1(erc20Transfers, new ERC721Transfer[](0), erc1155Transfers) + ); + interpreterEval2MockCall(stack, new uint256[](0)); + + vm.mockCall(TOKEN_A, abi.encodeWithSelector(IERC20.transferFrom.selector), abi.encode(true)); + + vm.startPrank(alice); + vm.expectRevert(UnsupportedERC1155Flow.selector); + flow.flow(evaluable, new uint256[](0), new SignedContextV1[](0)); + vm.stopPrank(); + } + /// `IFlowV5.flow()` MUST revert if the evaluable returns a malformed /// stack. The observable revert is `MissingSentinel(RAIN_FLOW_SENTINEL)` /// from `LibStackSentinel.consumeSentinelTuples`.