Skip to content

Commit

Permalink
First version finished
Browse files Browse the repository at this point in the history
Signed-off-by: Francesco <[email protected]>
  • Loading branch information
fulminmaxi committed Dec 2, 2024
1 parent c07e59d commit d88b72d
Show file tree
Hide file tree
Showing 12 changed files with 585 additions and 95 deletions.
162 changes: 110 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

<div class="mermaid">
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
</div>

[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 <your_rpc_url> --private-key <your_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 <subcommand>
```
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
4 changes: 3 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 4 additions & 4 deletions script/Counter.s.sol → script/TokenFlow.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
14 changes: 0 additions & 14 deletions src/Counter.sol

This file was deleted.

56 changes: 56 additions & 0 deletions src/ITokenFlow.sol
Original file line number Diff line number Diff line change
@@ -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);



}
82 changes: 82 additions & 0 deletions src/TokenFlow.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}


Loading

0 comments on commit d88b72d

Please sign in to comment.