From 79fbf704336c6a139a65370087ad4a529a0179b3 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Tue, 5 May 2026 15:52:52 +0400 Subject: [PATCH 1/2] test token-revert bubble-up paths in LibFlow transfers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new fuzz tests, one per token standard: - testFlowERC20TokenRevertBubblesUp — TOKEN_A.transferFrom reverts - testFlowERC721TokenRevertBubblesUp — TOKEN_B.safeTransferFrom reverts - testFlowERC1155TokenRevertBubblesUp — TOKEN_C.safeTransferFrom reverts Each pins that the underlying token revert propagates out of flow() rather than being caught and swallowed by SafeERC20 / the interface calls in LibFlow. Closes #331. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/src/concrete/Flow.transfer.t.sol | 85 +++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/test/src/concrete/Flow.transfer.t.sol b/test/src/concrete/Flow.transfer.t.sol index 50c6e581..4b0a8e5e 100644 --- a/test/src/concrete/Flow.transfer.t.sol +++ b/test/src/concrete/Flow.transfer.t.sol @@ -428,4 +428,89 @@ contract FlowTransferTest is FlowTest { flow.flow(evaluable, new uint256[](0), new SignedContextV1[](0)); vm.stopPrank(); } + + /// A token revert during ERC20 `transferFrom` MUST bubble up out of + /// `flow()`. Pins that `SafeERC20` does not silently swallow the + /// underlying token revert. + /// forge-config: default.fuzz.runs = 100 + function testFlowERC20TokenRevertBubblesUp(address alice, uint256 amount) external { + vm.assume(alice != address(0)); + vm.assume(Sentinel.unwrap(RAIN_FLOW_SENTINEL) != amount); + vm.label(alice, "Alice"); + + (IFlowV5 flow, EvaluableV2 memory evaluable) = deployFlow(); + assumeEtchable(alice, address(flow)); + + ERC20Transfer[] memory erc20Transfers = new ERC20Transfer[](1); + erc20Transfers[0] = ERC20Transfer({token: TOKEN_A, from: alice, to: address(flow), amount: amount}); + + uint256[] memory stack = LibStackGeneration.generateFlowStack( + Sentinel.unwrap(RAIN_FLOW_SENTINEL), + FlowTransferV1(erc20Transfers, new ERC721Transfer[](0), new ERC1155Transfer[](0)) + ); + interpreterEval2MockCall(stack, new uint256[](0)); + + vm.mockCallRevert(TOKEN_A, abi.encodeWithSelector(IERC20.transferFrom.selector), bytes("TOKEN_REVERT")); + + vm.startPrank(alice); + vm.expectRevert(bytes("TOKEN_REVERT")); + flow.flow(evaluable, new uint256[](0), new SignedContextV1[](0)); + vm.stopPrank(); + } + + /// A token revert during ERC721 `safeTransferFrom` MUST bubble up. + /// forge-config: default.fuzz.runs = 100 + function testFlowERC721TokenRevertBubblesUp(address alice, uint256 tokenId) external { + vm.assume(alice != address(0)); + vm.assume(Sentinel.unwrap(RAIN_FLOW_SENTINEL) != tokenId); + vm.label(alice, "Alice"); + + (IFlowV5 flow, EvaluableV2 memory evaluable) = deployFlow(); + assumeEtchable(alice, address(flow)); + + ERC721Transfer[] memory erc721Transfers = new ERC721Transfer[](1); + erc721Transfers[0] = ERC721Transfer({token: TOKEN_B, from: alice, to: address(flow), id: tokenId}); + + uint256[] memory stack = LibStackGeneration.generateFlowStack( + Sentinel.unwrap(RAIN_FLOW_SENTINEL), + FlowTransferV1(new ERC20Transfer[](0), erc721Transfers, new ERC1155Transfer[](0)) + ); + interpreterEval2MockCall(stack, new uint256[](0)); + + vm.mockCallRevert(TOKEN_B, abi.encodeWithSelector(ERC721_SAFE_TRANSFER_FROM_3), bytes("ERC721_REVERT")); + + vm.startPrank(alice); + vm.expectRevert(bytes("ERC721_REVERT")); + flow.flow(evaluable, new uint256[](0), new SignedContextV1[](0)); + vm.stopPrank(); + } + + /// A token revert during ERC1155 `safeTransferFrom` MUST bubble up. + /// forge-config: default.fuzz.runs = 100 + function testFlowERC1155TokenRevertBubblesUp(address alice, uint256 tokenId, uint256 amount) external { + vm.assume(alice != address(0)); + vm.assume(Sentinel.unwrap(RAIN_FLOW_SENTINEL) != tokenId); + vm.assume(Sentinel.unwrap(RAIN_FLOW_SENTINEL) != amount); + vm.label(alice, "Alice"); + + (IFlowV5 flow, EvaluableV2 memory evaluable) = deployFlow(); + assumeEtchable(alice, address(flow)); + + ERC1155Transfer[] memory erc1155Transfers = new ERC1155Transfer[](1); + erc1155Transfers[0] = + ERC1155Transfer({token: TOKEN_C, from: alice, to: address(flow), id: tokenId, amount: amount}); + + uint256[] memory stack = LibStackGeneration.generateFlowStack( + Sentinel.unwrap(RAIN_FLOW_SENTINEL), + FlowTransferV1(new ERC20Transfer[](0), new ERC721Transfer[](0), erc1155Transfers) + ); + interpreterEval2MockCall(stack, new uint256[](0)); + + vm.mockCallRevert(TOKEN_C, abi.encodeWithSelector(IERC1155.safeTransferFrom.selector), bytes("ERC1155_REVERT")); + + vm.startPrank(alice); + vm.expectRevert(bytes("ERC1155_REVERT")); + flow.flow(evaluable, new uint256[](0), new SignedContextV1[](0)); + vm.stopPrank(); + } } From 2921b9d40d7f9ac2cd0eba0b2c8c9c84ddbac9ea Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Sat, 9 May 2026 16:07:22 +0400 Subject: [PATCH 2/2] forge fmt sweep Co-Authored-By: Claude Opus 4.7 (1M context) --- test/src/concrete/Flow.construction.t.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/test/src/concrete/Flow.construction.t.sol b/test/src/concrete/Flow.construction.t.sol index ade81acc..2812cda0 100644 --- a/test/src/concrete/Flow.construction.t.sol +++ b/test/src/concrete/Flow.construction.t.sol @@ -80,7 +80,6 @@ contract FlowConstructionTest is FlowTest { I_CLONE_FACTORY.clone(impl, abi.encode(flowConfig)); } - function testFlowConstructionInitialize(address expression, bytes memory bytecode, uint256[] memory constants) external {