Gap
flowInit writes registeredFlows[evaluable.hash()] = FLOW_IS_REGISTERED in a loop with no dedup check (src/concrete/Flow.sol:220). If two EvaluableConfigV3 entries deploy to identical (interpreter, store, expression) tuples — i.e. identical hashes — the second iteration silently overwrites the same slot and emits FlowInitialized a second time for the same evaluable.
This may be intentional (deployers are trusted), but no test pins the behaviour either way.
Source
src/concrete/Flow.sol:220 (mapping write)
src/concrete/Flow.sol:225 (event emit, unconditional)
Existing related tests
None — no test passes duplicate evaluable configs.
Proposed test
Add to test/src/concrete/Flow.construction.t.sol:
import {LibEvaluable, EvaluableV2} from "rain.interpreter.interface/lib/caller/LibEvaluable.sol";
import {Vm} from "forge-std/Test.sol";
import {LibLogHelper} from "test/lib/LibLogHelper.sol";
import {IFlowV5} from "src/interface/IFlowV5.sol";
import {SignedContextV1} from "rain.interpreter.interface/interface/IInterpreterCallerV2.sol";
using LibEvaluable for EvaluableV2;
using LibLogHelper for Vm.Log[];
function testFlowConstructionDuplicateEvaluables(
address expression,
bytes memory bytecode,
uint256[] memory constants
) external {
expressionDeployerDeployExpression2MockCall(bytecode, constants, expression, bytes(hex"0007"));
EvaluableConfigV3[] memory flowConfig = new EvaluableConfigV3[](2);
flowConfig[0] = EvaluableConfigV3(DEPLOYER, bytecode, constants);
flowConfig[1] = EvaluableConfigV3(DEPLOYER, bytecode, constants);
vm.recordLogs();
address clone = I_CLONE_FACTORY.clone(deployFlowImplementation(), abi.encode(flowConfig));
Vm.Log[] memory logs = vm.getRecordedLogs();
Vm.Log[] memory initLogs =
logs.findEvents(keccak256("FlowInitialized(address,(address,address,address))"));
// Pin current behaviour: one event per config, even when duplicated.
assertEq(initLogs.length, 2, "expected one FlowInitialized per config");
// The (single) registered evaluable should still be callable.
EvaluableV2 memory evaluable = EvaluableV2(INTERPRETER, STORE, expression);
interpreterEval2MockCall(generateFlowStack(transferEmpty()), new uint256[](0));
IFlowV5(clone).flow(evaluable, new uint256[](0), new SignedContextV1[](0));
}
Gap
flowInitwritesregisteredFlows[evaluable.hash()] = FLOW_IS_REGISTEREDin a loop with no dedup check (src/concrete/Flow.sol:220). If twoEvaluableConfigV3entries deploy to identical(interpreter, store, expression)tuples — i.e. identical hashes — the second iteration silently overwrites the same slot and emitsFlowInitializeda second time for the same evaluable.This may be intentional (deployers are trusted), but no test pins the behaviour either way.
Source
src/concrete/Flow.sol:220(mapping write)src/concrete/Flow.sol:225(event emit, unconditional)Existing related tests
None — no test passes duplicate evaluable configs.
Proposed test
Add to
test/src/concrete/Flow.construction.t.sol: