diff --git a/README.md b/README.md
index 1e37ffb..c6c406c 100644
--- a/README.md
+++ b/README.md
@@ -1,67 +1,125 @@
-## Foundry
-
-**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
-
-Foundry consists of:
-
-- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
-- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
-- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
-- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
-
-## Documentation
-
-https://book.getfoundry.sh/
-
-## Usage
-
-### Build
-
-```shell
-$ forge build
+# TokenFlow
+
+TokenFlow is a primitive that enables arbitrary token movements within a scope while enforcing constraints on the final state.
+
+Handling token approvals and transfers often requires careful tracking of every operation and extensive safety checks.
+Traditional approaches either rely on multiple approvals degrading UX or introduce unnecessary intermediary hops.
+While `Permit2` solves the multiple approvals problem and safely holds approvals, it's mostly used with signatures, thus requiring users to sign and then send their transaction, or delegate transaction execution to third parties. Moreover, since signatures are fully spent it can only be used for a single transfer, and not for complex operations like a multiplexing swap.
+
+Thanks to transient storage, TokenFlow allows users to specify constraints on the final token state, without tying it to a specific action or contract.
+
+The core idea is simple:
+1. Define constraints on the final token state
+2. Within an internal scope, allow _any_ contract to move _any_ user token around, in _any_ amount
+3. Verify that the user's final token balance meets the constraints. If a token was not specified in the constraints but was moved, the default constraint is that the token was not spent, i.e. the balance must be greater or equal to the initial balance.
+
+In spirit, this is similar to a flash loan from the user's balance, with the key difference that user-specified constraints are enforced.
+
+## How It Works
+
+
+sequenceDiagram
+ participant User
+ participant TokenFlow
+ participant Scope
+ participant Token
+
+ User->>TokenFlow: main(constraints, scope)
+ activate TokenFlow
+ Note over TokenFlow: Initialize netflows tracking
+ TokenFlow->>Scope: enter()
+ activate Scope
+ Note over Scope: Can freely move tokens
+ Scope->>Token: transferFrom(user, to)
+ Scope->>Token: transferFrom(from, user)
+ Scope-->>TokenFlow: return
+ deactivate Scope
+ Note over TokenFlow: Verify netflow constraints
+ TokenFlow-->>User: return
+ deactivate TokenFlow
+
+
+[View diagram](./docs/flow-diagram.svg)
+
+## Examples
+
+```solidity
+// Example 1: Token Swap
+function swapTokens(
+ address tokenIn,
+ address tokenOut,
+ uint amountIn,
+ uint minAmountOut
+) external {
+ Constraint[] memory constraints = new Constraint[](2);
+ constraints[0] = Constraint(tokenIn, int256(amountIn)); // Max outflow
+ constraints[1] = Constraint(tokenOut, -int256(minAmountOut)); // Min inflow
+ // This will revert if the constraints are not met
+ tokenFlow.main(constraints, swapContract, "");
+}
+
+// Example 2: Simple Approve
+function simpleApprove(
+ address token,
+ address spender,
+ uint amount
+) external {
+ Constraint[] memory constraints = new Constraint[](1);
+ constraints[0] = Constraint(token, int256(amount));
+ tokenFlow.main(constraints, contractToApprove, "");
+}
+
+// Example 3: Batch Operations
+function batchedOperations(
+ address[] calldata tokens,
+ uint[] calldata maxOutflows,
+ address batchProcessor
+) external {
+ Constraint[] memory constraints = new Constraint[](tokens.length);
+ for (uint i = 0; i < tokens.length; i++) {
+ constraints[i] = Constraint(tokens[i], int256(maxOutflows[i]));
+ }
+ tokenFlow.main(constraints, batchProcessor, "");
+}
```
-### Test
+## Implementation
-```shell
-$ forge test
-```
+TokenFlow combines two key mechanisms:
-### Format
+1. **Netflow Accounting**: Rather than transferring a predetermined amount, we allow arbitrary token movements within a scope. The system tracks the net flow of tokens (inflows minus outflows) and ensures it satisfies the user's constraints. This enables complex operations while maintaining simple safety invariants.
-```shell
-$ forge fmt
-```
+2. **Transient Storage + Scoping**: A scoped execution environment using transient storage ensures all state is properly isolated and cleaned up between transactions. This prevents any state leakage between different flows and provides clean composition.
-### Gas Snapshots
+## Properties
-```shell
-$ forge snapshot
-```
+TokenFlow enables several key optimizations and use cases:
-### Anvil
+1. **Optimized Token Movements**
+ - DEX aggregators can transfer directly from users to pools
+ - No intermediate router hops needed
+ - Eliminates the need for a contract holding approvals
-```shell
-$ anvil
-```
+2. **Intent Settlement**
+ - Users specify constraints (min/max amounts) without tying them to specific actions
+ - The user constraints are separated from the calldata of the operation, meaning they can be settled by any solver or protocol.
-### Deploy
+3. **Composability**
+ - Clean composition with other protocols
+ - No shared state between flows
+ - Simple safety invariants
-```shell
-$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key
-```
+## Security Considerations
-### Cast
+1. **Scope Trust**
+ - Scope contracts have full control over user funds during execution
+ - Must be carefully audited and verified
+ - Consider using scope allowlists for additional safety
-```shell
-$ cast
-```
+2. **Token Compatibility**
+ - Works with standard ERC20 tokens
+ - Works with fee-on-transfer tokens
-### Help
+## License
-```shell
-$ forge --help
-$ anvil --help
-$ cast --help
-```
-# token-flow
+UNLICENSED
diff --git a/foundry.toml b/foundry.toml
index 25b918f..10d09d7 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -2,5 +2,7 @@
src = "src"
out = "out"
libs = ["lib"]
+solc_version = "0.8.28"
-# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
+[invariant]
+fail_on_revert = true
diff --git a/script/Counter.s.sol b/script/TokenFlow.s.sol
similarity index 62%
rename from script/Counter.s.sol
rename to script/TokenFlow.s.sol
index cdc1fe9..8079843 100644
--- a/script/Counter.s.sol
+++ b/script/TokenFlow.s.sol
@@ -2,17 +2,17 @@
pragma solidity ^0.8.13;
import {Script, console} from "forge-std/Script.sol";
-import {Counter} from "../src/Counter.sol";
+import {TokenFlow} from "src/TokenFlow.sol";
-contract CounterScript is Script {
- Counter public counter;
+contract TokenFlowScript is Script {
+ TokenFlow public tokenFlow;
function setUp() public {}
function run() public {
vm.startBroadcast();
- counter = new Counter();
+ tokenFlow = new TokenFlow();
vm.stopBroadcast();
}
diff --git a/src/Counter.sol b/src/Counter.sol
deleted file mode 100644
index aded799..0000000
--- a/src/Counter.sol
+++ /dev/null
@@ -1,14 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-contract Counter {
- uint256 public number;
-
- function setNumber(uint256 newNumber) public {
- number = newNumber;
- }
-
- function increment() public {
- number++;
- }
-}
diff --git a/src/ITokenFlow.sol b/src/ITokenFlow.sol
new file mode 100644
index 0000000..97cb7b9
--- /dev/null
+++ b/src/ITokenFlow.sol
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0;
+
+
+struct Constraint {
+ address token;
+ int256 value;
+}
+
+
+/// @title IFlowScope
+/// @notice A flow scope is a contract that is called by the TokenFlow contract to execute a transaction.
+/// @dev The flow scope is free to transfer any token of the payer, as long as they're repaid by the end of the flow.
+interface IFlowScope {
+ /// @notice Enter a token flow. During the token flow, any token of the payer can be transferred, as long as they're back by the end of the flow.
+ /// @param selectorExtension A safety measure to prevent conflicts with ERC20 selectors.
+ /// @param constraints The netflows constraints of the token flow.
+ /// @param payer The payer of the token flow. Whoever is paying for the token flow.
+ /// @param data Data to be passed to the entrypoint.
+ function enter(bytes28 selectorExtension, Constraint[] calldata constraints, address payer, bytes calldata data) external;
+}
+
+/// @notice Reverts when the netflows constraints are violated.
+error BadNetflows();
+
+/// @notice Reverts when the function is called outside the proper scope (external or internal).
+error InvalidScope();
+
+/// @title ITokenFlow
+/// A token flow is a set of netflows that must be respected by the internal scope. The internal scope is free to spend any token the user has approved to the token flow.
+interface ITokenFlow {
+ /// @notice Entrypoint into a token flow.
+ /// @param constraints The netflows constraints of the token flow.
+ /// @param scope The contract to be called to execute the token flow.
+ /// @param data Data to be passed to the internal scope.
+ function main(Constraint[] calldata constraints, IFlowScope scope, bytes calldata data) external;
+
+ /// @notice Move tokens out of the current flow to the payer.
+ /// @dev Calling this function outside of a flow scope will revert.
+ /// @param token The token to move.
+ /// @param amount The amount of tokens to move.
+ function moveOut(address token, uint128 amount) external;
+
+ /// @notice Move tokens from the current flow payer into the specified address.
+ /// @dev Calling this function outside of a flow scope will revert.
+ /// @param token The token to move.
+ /// @param amount The amount of tokens to move.
+ /// @param to The address to move the tokens to.
+ function moveIn(address token, uint128 amount, address to) external;
+
+ /// @notice A helper function to get the current flow payer.
+ function payer() external view returns (address);
+
+
+
+}
diff --git a/src/TokenFlow.sol b/src/TokenFlow.sol
new file mode 100644
index 0000000..f5a3637
--- /dev/null
+++ b/src/TokenFlow.sol
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.28;
+
+import {ITokenFlow, Constraint, BadNetflows, InvalidScope, IFlowScope} from "src/ITokenFlow.sol";
+import {TransientNetflows} from "src/TransientNetflows.sol";
+import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
+
+
+contract TokenFlow is ITokenFlow {
+ using SafeTransferLib for address;
+
+ uint constant EXTERNAL_SCOPE = 0;
+ uint constant INTERNAL_SCOPE = 1;
+ /// @notice The selector extension to append to the operator call.
+ /// @dev For security reasons, there must never be clash between the IFlowScope.enter selector and any ERC20 selector that can transfer funds (today this is only transferFrom).
+ /// @dev The selector extension is used to avoid such clashes.
+ bytes28 public constant SELECTOR_EXTENSION = bytes28(keccak256("IFlowScope.enter(bytes28,Constraint[],address,bytes)"));
+
+
+ modifier requireScope(uint required) {
+ if (scope != required) revert InvalidScope();
+ _;
+ }
+
+ /// @inheritdoc ITokenFlow
+ address public transient payer;
+ uint private transient scope;
+
+
+
+ /// @inheritdoc ITokenFlow
+ function main(Constraint[] calldata constraints, IFlowScope internalScope, bytes calldata data) external requireScope(EXTERNAL_SCOPE) {
+ initTransientState(constraints, msg.sender);
+
+ (bool ok, bytes memory err) = address(internalScope).call(abi.encodeCall(IFlowScope.enter, (SELECTOR_EXTENSION, constraints, msg.sender, data)));
+
+ if (!ok) {
+ clearTransientState();
+ // bubble up the error
+ assembly {
+ revert(add(err, 0x20), mload(err))
+ }
+ }
+
+ bool netflowsPositive = TransientNetflows.arePositive();
+ clearTransientState();
+
+ if (!netflowsPositive) {
+ revert BadNetflows();
+ }
+ }
+
+ /// @inheritdoc ITokenFlow
+ function moveOut(address token, uint128 amount) external requireScope(INTERNAL_SCOPE) {
+ TransientNetflows.add(token, int256(uint256(amount)));
+
+ token.safeTransferFrom(msg.sender, payer, amount);
+ }
+
+ /// @inheritdoc ITokenFlow
+ function moveIn(address token, uint128 amount, address to) external requireScope(INTERNAL_SCOPE) {
+ TransientNetflows.add(token, -int256(uint256(amount)));
+
+ token.safeTransferFrom(payer, to, amount);
+ }
+
+ function initTransientState(Constraint[] calldata constraints, address payer_) private {
+ for (uint256 i = 0; i < constraints.length; i++) {
+ TransientNetflows.insert(constraints[i].token, constraints[i].value);
+ }
+ payer = payer_;
+ scope = INTERNAL_SCOPE;
+ }
+
+ function clearTransientState() private {
+ payer = address(0);
+ scope = EXTERNAL_SCOPE;
+ TransientNetflows.clear();
+ }
+}
+
+
diff --git a/src/TransientNetflows.sol b/src/TransientNetflows.sol
new file mode 100644
index 0000000..c4d1b09
--- /dev/null
+++ b/src/TransientNetflows.sol
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.28;
+
+import {EfficientHashLib} from "solady/utils/EfficientHashLib.sol";
+
+
+/// @notice An helper library to manage netflows in a transient storage.
+/// @dev the netflows are stored as an array of (token, value) pairs, with length in the first slot of the netflows.
+library TransientNetflows {
+ /// @notice The slot where the nonce for this set of netflows is stored.
+ /// Equivalent to bytes32(uint256(keccak256("TokenFlow.netflows")) - 1)
+ bytes32 internal constant NETFLOWS_SLOT = 0xb8ea23bb4fe1252fa49dff7d6168221ebfea7b5c55753f63740c76a259eb8f88;
+
+
+ /// @notice The slot where the counter of negative netflows is stored.
+ /// Equivalent to bytes32(uint256(keccak256("TokenFlow.negativeNetflowsCounter")) - 1)
+ bytes32 internal constant NEGATIVE_NETFLOWS_COUNTER_SLOT = 0x14f6a9c5e25725efcb69b4d15bdae41110c6a38bf78cda4b45b3539514d3fc55;
+
+ /// @notice Sets the netflow for a token. If the netflow is not present, it is created.
+ /// @param token The token to set the netflow for.
+ /// @param value The value to set the netflow to.
+ function insert(address token, int256 value) internal {
+ // load the nonce from the netflows slot
+ bytes32 slot = deriveAddressSlot(token);
+ assembly ("memory-safe") {
+ let previousValue := tload(slot)
+ // If the previous value was >= 0 and the new value is < 0, we increment the negative netflows counter.
+ if and(iszero(slt(previousValue, 0)), slt(value, 0)) {
+ tstore(NEGATIVE_NETFLOWS_COUNTER_SLOT, add(tload(NEGATIVE_NETFLOWS_COUNTER_SLOT), 1))
+ }
+ // If the previous value was < 0 and the new value is >= 0, we decrement the negative netflows counter.
+ if and(slt(previousValue, 0), iszero(slt(value, 0))) {
+ tstore(NEGATIVE_NETFLOWS_COUNTER_SLOT, sub(tload(NEGATIVE_NETFLOWS_COUNTER_SLOT), 1))
+ }
+ tstore(slot, value)
+ }
+ }
+
+ /// @notice Adds a value to the netflow for a token. If the token is not present, it is created with the value.
+ /// @param token The token to add the value to.
+ /// @param delta The value to add to the netflow.
+ function add(address token, int256 delta) internal {
+ insert(token, get(token) + delta);
+ }
+
+ /// @notice Gets the netflow for a token.
+ /// @param token The token to get the netflow for.
+ /// @return value The netflow value, or 0 if not present.
+ function get(address token) internal view returns (int256 value) {
+ bytes32 slot = deriveAddressSlot(token);
+ assembly ("memory-safe") {
+ value := tload(slot)
+ }
+ }
+
+ /// @notice Checks if all the netflows are positive.
+ /// @dev We keep a running counter of how many netflows are negative. See `insert`, so we just need to check if the counter is 0.
+ /// @return result True if all the netflows are positive, false otherwise.
+ function arePositive() internal view returns (bool result) {
+ assembly ("memory-safe") {
+ result := iszero(tload(NEGATIVE_NETFLOWS_COUNTER_SLOT))
+ }
+ }
+
+ /// @notice Clears the netflows by incrementing the nonce. This ensure all the current netflows are not accessible anymore.
+ function clear() internal {
+ assembly ("memory-safe") {
+ tstore(NETFLOWS_SLOT, add(tload(NETFLOWS_SLOT), 1))
+ tstore(NEGATIVE_NETFLOWS_COUNTER_SLOT, 0)
+ }
+ }
+
+
+ function deriveAddressSlot(address token) internal view returns (bytes32 ) {
+ uint slot;
+ assembly ("memory-safe") {
+ slot := tload(NETFLOWS_SLOT)
+ }
+ return EfficientHashLib.hash(slot, uint256(uint160(token)));
+ }
+}
diff --git a/test/Counter.t.sol b/test/Counter.t.sol
deleted file mode 100644
index 54b724f..0000000
--- a/test/Counter.t.sol
+++ /dev/null
@@ -1,24 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import {Test, console} from "forge-std/Test.sol";
-import {Counter} from "../src/Counter.sol";
-
-contract CounterTest is Test {
- Counter public counter;
-
- function setUp() public {
- counter = new Counter();
- counter.setNumber(0);
- }
-
- function test_Increment() public {
- counter.increment();
- assertEq(counter.number(), 1);
- }
-
- function testFuzz_SetNumber(uint256 x) public {
- counter.setNumber(x);
- assertEq(counter.number(), x);
- }
-}
diff --git a/test/TokenFlow.t.sol b/test/TokenFlow.t.sol
new file mode 100644
index 0000000..e9f1c2f
--- /dev/null
+++ b/test/TokenFlow.t.sol
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.28;
+
+import {Test, console} from "forge-std/Test.sol";
+import {ERC20} from "solady/tokens/ERC20.sol";
+import {TokenFlow} from "src/TokenFlow.sol";
+import {ITokenFlow, Constraint, BadNetflows, InvalidScope, IFlowScope} from "src/ITokenFlow.sol";
+import {MockERC20} from "./mocks/MockERC20.sol";
+import {MockFlowScope} from "./mocks/MockFlowScope.sol";
+
+contract TokenFlowTest is Test {
+ TokenFlow tokenFlow;
+ MockERC20 token1;
+ MockERC20 token2;
+ MockFlowScope flowScope;
+ address alice = makeAddr("alice");
+
+ function setUp() public {
+ tokenFlow = new TokenFlow();
+ token1 = new MockERC20("Token1", "TK1", 18);
+ token2 = new MockERC20("Token2", "TK2", 18);
+ flowScope = new MockFlowScope(tokenFlow);
+
+ token1.approve(address(tokenFlow), type(uint256).max);
+ token2.approve(address(tokenFlow), type(uint256).max);
+ }
+
+ function test_avoidCollisionWithERC20() public {
+ Constraint[] memory constraints = new Constraint[](0);
+
+ bytes memory data = abi.encodeCall(
+ ERC20.transferFrom,
+ (address(alice), address(this), 1 ether)
+ );
+
+ vm.expectRevert();
+ tokenFlow.main(constraints, IFlowScope(address(token1)), data);
+ }
+
+ // Scope access
+
+ function test_revertCallingMoveInFromExternalScope() public {
+ vm.expectRevert(InvalidScope.selector);
+ tokenFlow.moveIn(address(token1), 1 ether, address(this));
+ }
+
+ function test_revertCallingMoveOutFromExternalScope() public {
+ vm.expectRevert(InvalidScope.selector);
+ tokenFlow.moveOut(address(token1), 1 ether);
+ }
+
+ function test_revertCallingMainFromInternalScope() public {
+ Constraint[] memory constraints = new Constraint[](0);
+
+ flowScope.addReentry(flowScope, "");
+
+ vm.expectRevert(InvalidScope.selector);
+ // FlowScope will re-enter the tokenFlow contract, which will revert
+ tokenFlow.main(constraints, flowScope, "");
+ }
+}
diff --git a/test/TransientNetflows.t.sol b/test/TransientNetflows.t.sol
new file mode 100644
index 0000000..aa666fb
--- /dev/null
+++ b/test/TransientNetflows.t.sol
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.28;
+
+import {Test, console2 as console} from "forge-std/Test.sol";
+import {TransientNetflows} from "src/TransientNetflows.sol";
+
+
+
+
+contract TransientNetflowsTest is Test {
+
+ function test_insert(address token, int256 amount) public {
+ TransientNetflows.insert(token, amount);
+
+ bytes32 slot = TransientNetflows.deriveAddressSlot(token);
+ int value;
+ assembly {
+ value := tload(slot)
+ }
+ assertEq(value, amount, "incorrect amount");
+ }
+
+
+ function test_get(address token, int256 amount) public {
+ TransientNetflows.insert(token, amount);
+ int256 value = TransientNetflows.get(token);
+ assertEq(value, amount, "incorrect amount");
+ }
+
+ function test_clear() public {
+ TransientNetflows.insert(address(1), 1 ether);
+ TransientNetflows.insert(address(2), 2 ether);
+ TransientNetflows.clear();
+ assertEq(TransientNetflows.get(address(1)), 0, "incorrect amount");
+ assertEq(TransientNetflows.get(address(2)), 0, "incorrect amount");
+ }
+
+
+ function test_are_positive(address token1, address token2, int256 amount1, int256 amount2) public {
+ vm.assume(token1 != token2);
+ vm.assume(amount1 > 0);
+ vm.assume(amount2 < 0);
+
+ TransientNetflows.insert(token1, amount1);
+ assertTrue(TransientNetflows.arePositive(), "should be positive with single positive amount");
+
+ TransientNetflows.insert(token2, amount2);
+ assertFalse(TransientNetflows.arePositive(), "should be negative with one negative amount");
+
+ TransientNetflows.clear();
+ assertTrue(TransientNetflows.arePositive(), "should be positive after clear");
+
+ TransientNetflows.insert(token1, amount2);
+ assertFalse(TransientNetflows.arePositive(), "should be negative after reinserting negative");
+ }
+}
diff --git a/test/mocks/MockERC20.sol b/test/mocks/MockERC20.sol
new file mode 100644
index 0000000..f24f4be
--- /dev/null
+++ b/test/mocks/MockERC20.sol
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.28;
+
+import {ERC20} from "solady/tokens/ERC20.sol";
+
+contract MockERC20 is ERC20 {
+ string public _name;
+ string public _symbol;
+ uint8 public _decimals;
+
+ constructor(string memory name_, string memory symbol_, uint8 decimals_) {
+ _name = name_;
+ _symbol = symbol_;
+ _decimals = decimals_;
+
+ }
+
+ function name() public view override returns (string memory) {
+ return _name;
+ }
+
+ function symbol() public view override returns (string memory) {
+ return _symbol;
+ }
+
+ function decimals() public view override returns (uint8) {
+ return _decimals;
+ }
+}
diff --git a/test/mocks/MockFlowScope.sol b/test/mocks/MockFlowScope.sol
new file mode 100644
index 0000000..26aa66e
--- /dev/null
+++ b/test/mocks/MockFlowScope.sol
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.28;
+
+import {IFlowScope, Constraint} from "src/ITokenFlow.sol";
+import {ITokenFlow} from "src/ITokenFlow.sol";
+
+struct MoveIn {
+ address token;
+ uint128 amount;
+ address recipient;
+}
+
+struct MoveOut {
+ address token;
+ uint128 amount;
+}
+
+struct Reentry {
+ IFlowScope flowScope;
+ bytes data;
+}
+
+struct Revert {
+ string reason;
+}
+
+enum InstructionType {
+ MoveIn,
+ MoveOut,
+ Reentry,
+ Revert
+}
+
+contract MockFlowScope is IFlowScope {
+ ITokenFlow tokenFlow;
+
+ uint instructionIndex;
+ InstructionType[] instructionTypes;
+
+ uint moveInIndex;
+ uint moveOutIndex;
+ uint reentryIndex;
+ uint revertIndex;
+
+ MoveIn[] moveInInstructions;
+ MoveOut[] moveOutInstructions;
+ Reentry[] reentryInstructions;
+ Revert[] revertInstructions;
+
+ bool shouldRevert;
+ bool shouldReenter;
+
+ constructor(ITokenFlow _tokenFlow) {
+ tokenFlow = _tokenFlow;
+ }
+
+ function addMoveIn(address token, uint128 amount, address recipient) external {
+ moveInInstructions.push(MoveIn({token: token, amount: amount, recipient: recipient}));
+ instructionTypes.push(InstructionType.MoveIn);
+ }
+
+ function addMoveOut(address token, uint128 amount) external {
+ moveOutInstructions.push(MoveOut({token: token, amount: amount}));
+ instructionTypes.push(InstructionType.MoveOut);
+ }
+
+ function addReentry(IFlowScope flowScope, bytes calldata data) external {
+ reentryInstructions.push(Reentry({flowScope: flowScope, data: data}));
+ instructionTypes.push(InstructionType.Reentry);
+ }
+
+ function addRevert(string calldata reason) external {
+ revertInstructions.push(Revert({reason: reason}));
+ instructionTypes.push(InstructionType.Revert);
+ }
+
+ function enter(
+ bytes28 /* selectorExtension */,
+ Constraint[] calldata constraints,
+ address /* payer */,
+ bytes calldata /* data */
+ ) external {
+ // Execute current instruction
+ if (instructionIndex < instructionTypes.length) {
+ InstructionType iType = instructionTypes[instructionIndex];
+
+ if (iType == InstructionType.MoveIn) {
+ tokenFlow.moveIn(moveInInstructions[moveInIndex].token, moveInInstructions[moveInIndex].amount, moveInInstructions[moveInIndex].recipient);
+ moveInIndex++;
+ } else if (iType == InstructionType.MoveOut) {
+ tokenFlow.moveOut(moveOutInstructions[moveOutIndex].token, moveOutInstructions[moveOutIndex].amount);
+ moveOutIndex++;
+ } else if (iType == InstructionType.Reentry) {
+ ITokenFlow(msg.sender).main(constraints, reentryInstructions[reentryIndex].flowScope, reentryInstructions[reentryIndex].data);
+ reentryIndex++;
+ } else if (iType == InstructionType.Revert) {
+ revert(revertInstructions[revertIndex].reason);
+ }
+
+ instructionIndex++;
+ }
+ }
+}
\ No newline at end of file