diff --git a/test/src/concrete/Flow.preview.t.sol b/test/src/concrete/Flow.preview.t.sol index 856e749b..c133d03a 100644 --- a/test/src/concrete/Flow.preview.t.sol +++ b/test/src/concrete/Flow.preview.t.sol @@ -266,4 +266,81 @@ contract FlowPreviewTest is FlowTest { vm.expectRevert(abi.encodeWithSelector(MissingSentinel.selector, RAIN_FLOW_SENTINEL)); flow.stackToFlow(stack); } + + /// A stack whose top section is not aligned to the ERC20 tuple size (4). + /// `consumeSentinelTuples` walks the cursor down from the top of the stack + /// in strides of `4 * 0x20` bytes, testing the word below each stride for + /// the sentinel. Here three plain words sit above the intended ERC20 + /// sentinel, so the stride never lands on the intended sentinel: the ERC20 + /// pass instead matches the ERC721 sentinel one stride lower and consumes + /// it, the ERC721 pass then matches the ERC1155 sentinel and consumes it, + /// and the ERC1155 pass finds no sentinel left below it. The parse reverts + /// with `MissingSentinel(RAIN_FLOW_SENTINEL)` rather than mis-parsing the + /// misaligned stack into a `FlowTransferV1`. + function testStackToFlowRevertsOnMalformedTupleCount() external { + (IFlowV5 flow,) = deployFlow(); + uint256 sentinel = Sentinel.unwrap(RAIN_FLOW_SENTINEL); + uint256[] memory stack = new uint256[](6); + stack[0] = sentinel; // erc1155 sentinel + stack[1] = sentinel; // erc721 sentinel + stack[2] = sentinel; // erc20 sentinel + // Three trailing words above the ERC20 sentinel; 3 is not a multiple of + // the ERC20 tuple size 4, so the section is misaligned. + stack[3] = 1; + stack[4] = 2; + stack[5] = 3; + vm.expectRevert(abi.encodeWithSelector(MissingSentinel.selector, RAIN_FLOW_SENTINEL)); + flow.stackToFlow(stack); + } + + /// A `RAIN_FLOW_SENTINEL` planted inside a tuple field is indistinguishable + /// from a real section boundary to `consumeSentinelTuples`, which scans for + /// the first matching word from the top of the stack down. The planted + /// sentinel in the top word is matched by the ERC20 pass as the ERC20 + /// boundary, so the ERC20 section is read as empty and the genuine ERC20 + /// sentinel lower in the stack is then matched by the ERC721 pass. The + /// genuine ERC20 tuple is therefore consumed as an ERC721 tuple whose + /// `token` field is the (truncated) sentinel value. `stackToFlow` does not + /// revert: it returns a `FlowTransferV1` with the sections shifted and + /// corrupted. + function testStackToFlowSentinelInsideTupleCorrupts() external { + (IFlowV5 flow,) = deployFlow(); + uint256 sentinel = Sentinel.unwrap(RAIN_FLOW_SENTINEL); + + // Stack layout (low index = bottom of stack, high index = top; top is + // consumed first by stackToFlow): + // stack[0] sentinel (erc1155 boundary) + // stack[1] sentinel (erc721 boundary) + // stack[2] sentinel (intended erc20 boundary) + // stack[3..5] a (token, from, to) triple + // stack[6] sentinel planted where the erc20 `amount` field would be + uint256[] memory stack = new uint256[](7); + stack[0] = sentinel; + stack[1] = sentinel; + stack[2] = sentinel; + stack[3] = uint256(uint160(address(0xAAAA))); + stack[4] = uint256(uint160(address(this))); + stack[5] = uint256(uint160(address(0xBBBB))); + stack[6] = sentinel; + + FlowTransferV1 memory result = flow.stackToFlow(stack); + + // The planted sentinel is read as the erc20 boundary, so the erc20 + // section is empty. + assertEq(result.erc20.length, 0, "erc20 section shifted away by planted sentinel"); + + // The intended erc20 sentinel is consumed as the erc721 boundary, so + // the (token, from, to) triple plus the intended erc20 sentinel are + // read as a single erc721 tuple. The intended erc20 sentinel becomes + // the erc721 `token` field, truncated to an address. + assertEq(result.erc721.length, 1, "intended erc20 tuple misread as erc721"); + assertEq(result.erc721[0].token, address(uint160(sentinel)), "erc721 token is the truncated sentinel"); + assertEq(result.erc721[0].from, address(0xAAAA), "erc721 from"); + assertEq(result.erc721[0].to, address(this), "erc721 to"); + assertEq(result.erc721[0].id, uint256(uint160(address(0xBBBB))), "erc721 id"); + + // Only the bottom sentinel remains for the erc1155 pass, so its section + // is empty. + assertEq(result.erc1155.length, 0, "erc1155 section empty"); + } }