Skip to content
Open
Show file tree
Hide file tree
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
36 changes: 36 additions & 0 deletions test/concrete/MaliciousReenteringToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {IFlowV5} from "src/interface/IFlowV5.sol";
import {EvaluableV2} from "rain.interpreter.interface/lib/caller/LibEvaluable.sol";
import {SignedContextV1} from "rain.interpreter.interface/interface/IInterpreterCallerV2.sol";

/// An ERC20-shaped token whose `transferFrom` callback re-enters
/// `flow.flow(...)` on the same flow contract. Used to exercise the
/// `nonReentrant` guard on `Flow.flow`.
contract MaliciousReenteringToken {
IFlowV5 internal immutable I_FLOW;
EvaluableV2 internal evaluable;

constructor(IFlowV5 flow_) {
I_FLOW = flow_;
}

function setEvaluable(EvaluableV2 memory ev) external {
evaluable = ev;
}

/// Re-enters `flow.flow` from inside the ERC20 `transferFrom` path. If
/// `Flow.flow`'s `nonReentrant` guard is in place the inner call will
/// revert with `"ReentrancyGuard: reentrant call"` and bubble up.
//forge-lint: disable-next-line(mixed-case-function)
function transferFrom(address, address, uint256) external returns (bool) {
I_FLOW.flow(evaluable, new uint256[](0), new SignedContextV1[](0));
return true;
}

function transfer(address, uint256) external pure returns (bool) {
return true;
}
}
33 changes: 33 additions & 0 deletions test/src/concrete/Flow.transfer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {LibContextWrapper} from "test/lib/LibContextWrapper.sol";
import {IERC721} from "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {IERC1155} from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol";
import {LibStackGeneration} from "test/lib/LibStackGeneration.sol";
import {MaliciousReenteringToken} from "test/concrete/MaliciousReenteringToken.sol";

/// `IERC721.safeTransferFrom` is overloaded (3-arg + 4-arg). Pin the 3-arg
/// selector via a single-overload wrapper interface so the disambiguation
Expand Down Expand Up @@ -428,4 +429,36 @@ contract FlowTransferTest is FlowTest {
flow.flow(evaluable, new uint256[](0), new SignedContextV1[](0));
vm.stopPrank();
}

/// `Flow.flow` carries `nonReentrant`. A malicious ERC20 token whose
/// `transferFrom` re-enters `flow.flow(...)` MUST cause the inner call
/// to revert with the OZ ReentrancyGuardUpgradeable v4 string. This
/// pins the guard against a future change that drops `nonReentrant`.
/// forge-config: default.fuzz.runs = 100
function testFlowReentrancyGuardFiresOnTokenCallback(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));

MaliciousReenteringToken malToken = new MaliciousReenteringToken(flow);
malToken.setEvaluable(evaluable);

ERC20Transfer[] memory erc20Transfers = new ERC20Transfer[](1);
erc20Transfers[0] =
ERC20Transfer({token: address(malToken), 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.startPrank(alice);
vm.expectRevert(bytes("ReentrancyGuard: reentrant call"));
flow.flow(evaluable, new uint256[](0), new SignedContextV1[](0));
vm.stopPrank();
}
}
Loading