diff --git a/flake.lock b/flake.lock index 6d95c8b4..6c780a52 100644 --- a/flake.lock +++ b/flake.lock @@ -250,11 +250,11 @@ "solc": "solc" }, "locked": { - "lastModified": 1776019532, - "narHash": "sha256-0aMnHCZ2fR0+ZwuRHFouYYUQqYL/dSp5cOgka0m6vE4=", + "lastModified": 1777887852, + "narHash": "sha256-1AXVZpY1okCogZKt0CslqP06FG5HldcY+RmjEfct1rc=", "owner": "rainprotocol", "repo": "rainix", - "rev": "9babaac787d1e1119609b0d89c33a6b42249c066", + "rev": "620c4d3604ba7967004afce6ffacd2ae9ef2c1a1", "type": "github" }, "original": { diff --git a/src/lib/LibFlow.sol b/src/lib/LibFlow.sol index bb5ef753..3bfe7ce9 100644 --- a/src/lib/LibFlow.sol +++ b/src/lib/LibFlow.sol @@ -48,17 +48,23 @@ library LibFlow { ERC721Transfer[] memory erc721; ERC1155Transfer[] memory erc1155; Pointer tuplesPointer; - // erc20 + // erc20: each tuple is 4 stack words read top-down as + // (token, from, to, amount). The cast below reinterprets the + // tuple memory as `ERC20Transfer[]`; this is only correct so + // long as `ERC20Transfer` declares fields in exactly that order. (stackTop, tuplesPointer) = stackBottom.consumeSentinelTuples(stackTop, RAIN_FLOW_SENTINEL, 4); assembly ("memory-safe") { erc20 := tuplesPointer } - // erc721 + // erc721: each tuple is 4 stack words read top-down as + // (token, from, to, id). `ERC721Transfer` field order must match. (stackTop, tuplesPointer) = stackBottom.consumeSentinelTuples(stackTop, RAIN_FLOW_SENTINEL, 4); assembly ("memory-safe") { erc721 := tuplesPointer } - // erc1155 + // erc1155: each tuple is 5 stack words read top-down as + // (token, from, to, id, amount). `ERC1155Transfer` field order + // must match. (stackTop, tuplesPointer) = stackBottom.consumeSentinelTuples(stackTop, RAIN_FLOW_SENTINEL, 5); assembly ("memory-safe") { erc1155 := tuplesPointer diff --git a/test/src/concrete/Flow.preview.t.sol b/test/src/concrete/Flow.preview.t.sol index b50a647b..fd4d127d 100644 --- a/test/src/concrete/Flow.preview.t.sol +++ b/test/src/concrete/Flow.preview.t.sol @@ -3,7 +3,15 @@ pragma solidity =0.8.25; import {FlowTest} from "test/abstract/FlowTest.sol"; -import {IFlowV5, FlowTransferV1, ERC20Transfer, ERC721Transfer, ERC1155Transfer} from "src/interface/IFlowV5.sol"; +import { + IFlowV5, + FlowTransferV1, + ERC20Transfer, + ERC721Transfer, + ERC1155Transfer, + RAIN_FLOW_SENTINEL, + Sentinel +} from "src/interface/IFlowV5.sol"; import {EvaluableV2} from "rain.interpreter.interface/lib/caller/LibEvaluable.sol"; import {LibEvaluable} from "rain.interpreter.interface/lib/caller/LibEvaluable.sol"; @@ -175,4 +183,57 @@ contract FlowPreviewTest is FlowTest { keccak256(abi.encode(flowTransfer)), keccak256(abi.encode(flow.stackToFlow(stack))), "wrong compare Structs" ); } + + /// Pins the positional layout of stack tuples against the named fields of + /// `ERC20Transfer` / `ERC721Transfer` / `ERC1155Transfer`. The stack is + /// authored by hand with distinct markers per field — exactly what a + /// rainlang author writes — so that any future reorder of the struct + /// fields produces field-named assertions that fail. + function testFlowStackToFlowFieldOrderPinned() external { + (IFlowV5 flow,) = deployFlow(); + + // Stack layout (low index = bottom of stack, high index = top; top is + // consumed first by stackToFlow): + // [sentinel, erc1155 tuple (5), sentinel, erc721 tuple (4), sentinel, erc20 tuple (4)] + // Markers chosen so each field is unique: top nibble = token type + // (A=erc20, B=erc721, C=erc1155), next nibble = field index in tuple. + uint256[] memory stack = new uint256[](16); + stack[0] = Sentinel.unwrap(RAIN_FLOW_SENTINEL); + stack[1] = uint256(uint160(0xC0)); // erc1155 token + stack[2] = uint256(uint160(0xC1)); // erc1155 from + stack[3] = uint256(uint160(0xC2)); // erc1155 to + stack[4] = 0xC3C3; // erc1155 id + stack[5] = 0xC4C4; // erc1155 amount + stack[6] = Sentinel.unwrap(RAIN_FLOW_SENTINEL); + stack[7] = uint256(uint160(0xB0)); // erc721 token + stack[8] = uint256(uint160(0xB1)); // erc721 from + stack[9] = uint256(uint160(0xB2)); // erc721 to + stack[10] = 0xB3B3; // erc721 id + stack[11] = Sentinel.unwrap(RAIN_FLOW_SENTINEL); + stack[12] = uint256(uint160(0xA0)); // erc20 token + stack[13] = uint256(uint160(0xA1)); // erc20 from + stack[14] = uint256(uint160(0xA2)); // erc20 to + stack[15] = 0xA3A3; // erc20 amount + + FlowTransferV1 memory result = flow.stackToFlow(stack); + + assertEq(result.erc20.length, 1, "erc20 length"); + assertEq(result.erc20[0].token, address(uint160(0xA0)), "erc20 token"); + assertEq(result.erc20[0].from, address(uint160(0xA1)), "erc20 from"); + assertEq(result.erc20[0].to, address(uint160(0xA2)), "erc20 to"); + assertEq(result.erc20[0].amount, 0xA3A3, "erc20 amount"); + + assertEq(result.erc721.length, 1, "erc721 length"); + assertEq(result.erc721[0].token, address(uint160(0xB0)), "erc721 token"); + assertEq(result.erc721[0].from, address(uint160(0xB1)), "erc721 from"); + assertEq(result.erc721[0].to, address(uint160(0xB2)), "erc721 to"); + assertEq(result.erc721[0].id, 0xB3B3, "erc721 id"); + + assertEq(result.erc1155.length, 1, "erc1155 length"); + assertEq(result.erc1155[0].token, address(uint160(0xC0)), "erc1155 token"); + assertEq(result.erc1155[0].from, address(uint160(0xC1)), "erc1155 from"); + assertEq(result.erc1155[0].to, address(uint160(0xC2)), "erc1155 to"); + assertEq(result.erc1155[0].id, 0xC3C3, "erc1155 id"); + assertEq(result.erc1155[0].amount, 0xC4C4, "erc1155 amount"); + } }