Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions test/src/concrete/Flow.preview.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Loading