-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Francesco <[email protected]>
- Loading branch information
1 parent
c07e59d
commit d88b72d
Showing
12 changed files
with
585 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
|
||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
|
||
|
Oops, something went wrong.