├── .github ├── assets │ └── logo.png └── workflows │ └── rust.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── contracts ├── executor.sol └── proxy.sol └── src ├── flow_builder.rs ├── lib.rs ├── opcodes.rs └── test.rs /.github/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitFinding/multiplexer/8e37a1882114028c35b8c3b19c88cd5557c67c83/.github/assets/logo.png -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Check 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install Solc 19 | run: | 20 | sudo add-apt-repository ppa:ethereum/ethereum 21 | sudo apt-get update 22 | sudo apt-get install solc 23 | 24 | - name: Cargo Check 25 | run: cargo check --verbose 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust / Cargo build artifacts 2 | target/ 3 | Cargo.lock 4 | 5 | # Solidity build artifacts 6 | contracts_output/ 7 | 8 | # IDE specific files 9 | .idea/ 10 | .vscode/ 11 | 12 | # Environment variables 13 | .env 14 | 15 | # OS specific files 16 | .DS_Store 17 | Thumbs.db 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Executor Contract 2 | 3 | We love your input! We want to make contributing to Executor Contract as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## We Develop with Github 12 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 13 | 14 | ## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html) 15 | Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 16 | 17 | 1. Fork the repo and create your branch from `main`. 18 | 2. If you've added code that should be tested, add tests. 19 | 3. If you've changed APIs, update the documentation. 20 | 4. Ensure the test suite passes. 21 | 5. Make sure your code lints. 22 | 6. Issue that pull request! 23 | 24 | ## Any contributions you make will be under the MIT Software License 25 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 26 | 27 | ## Report bugs using Github's [issue tracker](https://github.com/yourusername/executor-contract/issues) 28 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/yourusername/executor-contract/issues/new); it's that easy! 29 | 30 | ## Write bug reports with detail, background, and sample code 31 | 32 | **Great Bug Reports** tend to have: 33 | 34 | - A quick summary and/or background 35 | - Steps to reproduce 36 | - Be specific! 37 | - Give sample code if you can. 38 | - What you expected would happen 39 | - What actually happens 40 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 41 | 42 | ## License 43 | By contributing, you agree that your contributions will be licensed under its MIT License. 44 | 45 | ## References 46 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md). 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multiplexer-evm" 3 | version = "0.1.1" 4 | edition = "2021" 5 | build = "build.rs" # Build the solidity contracts 6 | authors = ["BitFinding"] 7 | description = "A Rust library and Solidity contracts for building and executing complex EVM transaction sequences, including flash loans." 8 | license = "MIT OR Apache-2.0" 9 | homepage = "https://github.com/BitFinding/multiplexer" 10 | repository = "https://github.com/BitFinding/multiplexer" 11 | keywords = ["ethereum", "solidity", "evm", "mev", "flashloan"] 12 | categories = ["cryptography::cryptocurrencies", "development-tools::build-utils"] 13 | readme = "README.md" 14 | 15 | [dependencies] 16 | alloy-primitives = { version = "1.0" } 17 | 18 | [dev-dependencies] 19 | tokio = { version = "1.44", features = ["rt", "macros"] } 20 | alloy = { version = "0.14", features = ["full", "node-bindings"] } 21 | 22 | [build-dependencies] 23 | hex = "0.4.3" 24 | serde = {version = "1.0", features = ["derive"]} 25 | serde_json = "1.0" 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 BitFinding 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Executor Contract Logo 3 | 4 | # Multiplexer 5 | 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | [![Crates.io](https://img.shields.io/crates/v/multiplexer-evm.svg)](https://crates.io/crates/multiplexer-evm) 8 | [![docs.rs](https://img.shields.io/docsrs/multiplexer-evm)](https://docs.rs/multiplexer-evm) 9 | [![Rust](https://github.com/BitFinding/multiplexer/actions/workflows/rust.yml/badge.svg)](https://github.com/BitFinding/multiplexer/actions/workflows/rust.yml) 10 | 11 | Originally developed as an internal MEV launchpad, Multiplexer is now open-sourced. It provides a flexible smart contract system for executing complex transaction sequences, featuring a Solidity executor contract and a Rust library for building execution flows. 12 | 13 |
14 | 15 | > ⚠️ **WARNING**: This contract has not been audited. Using this contract with real assets could result in permanent loss of funds. Use at your own risk. 16 | 17 | ## 🚀 Quick Start 18 | 19 | ```bash 20 | # Clone the repository 21 | git clone https://github.com/BitFinding/multiplexer.git 22 | cd multiplexer 23 | 24 | # Install dependencies and compile contracts 25 | # (This uses build.rs to compile contracts/executor.sol and contracts/proxy.sol) 26 | cargo build 27 | 28 | # Run tests (requires a mainnet fork RPC URL) 29 | ETH_RPC_URL=https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY cargo test 30 | ``` 31 | 32 | ## Architecture 33 | 34 | The system consists of: 35 | 36 | 1. **`executor.sol`**: The core contract that executes sequences of operations based on provided bytecode. It manages memory (`txData`) and handles callbacks. 37 | 2. **`proxy.sol`**: A simple immutable proxy contract used to deploy the executor logic, allowing for potential future upgrades (though the current proxy is basic). 38 | 3. **Rust Library (`src/`)**: Provides a `FlowBuilder` utility to easily construct the bytecode sequences for the executor, abstracting away the low-level opcode details. 39 | 40 | ## Example Usage 41 | 42 | ### Morpho Flash Loan Example (Rust FlowBuilder) 43 | 44 | This example demonstrates how to construct the bytecode for a Morpho flash loan using the Rust `FlowBuilder`. The goal is to borrow 100 WETH, and the callback data (`inner_flow_bytes`) will contain the instructions to approve the repayment to Morpho. 45 | 46 | ```rust 47 | use alloy::{ 48 | primitives::{address, uint, Address, Bytes, U256, hex}, 49 | sol, 50 | sol_types::{SolCall}, 51 | }; 52 | use multiplexer::FlowBuilder; 53 | 54 | // Define necessary constants 55 | const ONEHUNDRED_ETH: U256 = uint!(100000000000000000000_U256); // 100e18 56 | const WETH9: Address = address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); 57 | const MORPHO: Address = address!("BBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb"); 58 | 59 | // Simplified Sol definitions for the example 60 | sol! { 61 | interface IERC20 { 62 | function approve(address spender, uint256 value) external returns (bool); 63 | } 64 | interface IMorpho { 65 | function flashLoan(address token, uint256 assets, bytes calldata data) external; 66 | } 67 | } 68 | 69 | /// Generates the bytecode for a Morpho flash loan flow. 70 | /// The flow borrows 100 WETH and sets up the callback to approve repayment. 71 | fn generate_morpho_flashloan_flow() -> Vec { 72 | // 1. Prepare the inner flow (callback data): Approve WETH repayment. 73 | // This flow will be executed by the executor when Morpho calls back into it. 74 | // It needs to ensure Morpho can pull the funds back. 75 | let approve_calldata = IERC20::approveCall { 76 | spender: MORPHO, 77 | value: ONEHUNDRED_ETH, // Approve the exact loan amount. 78 | // Note: A real flash loan requires approving amount + fee. 79 | // The executor must hold sufficient WETH *before* this approval runs. 80 | }.abi_encode(); 81 | 82 | let inner_flow_bytes = FlowBuilder::empty() 83 | .call(WETH9, &approve_calldata, U256::ZERO) // Call WETH9.approve(MORPHO, amount) 84 | .optimize() 85 | .build_raw(); // Get the raw bytecode for the inner flow. 86 | // `build_raw()` produces *only* the sequence of action opcodes. 87 | 88 | // 2. Prepare the outer flow: Initiate the flash loan. 89 | // This is the main flow sent to the executor contract transaction. 90 | let flashloan_calldata = IMorpho::flashLoanCall { 91 | token: WETH9, // Asset to borrow 92 | assets: ONEHUNDRED_ETH, // Amount to borrow 93 | data: inner_flow_bytes.into(), // Pass the repayment flow as callback data 94 | }.abi_encode(); 95 | 96 | let main_flow_bytes = FlowBuilder::empty() 97 | .set_fail() // Revert the entire transaction if any subsequent call fails (including the callback) 98 | .set_callback(MORPHO) // Set Morpho as the expected callback address. The executor will only 99 | // execute the callback data if msg.sender matches this address. 100 | .call(MORPHO, &flashloan_calldata, U256::ZERO) // Call Morpho.flashLoan(...) 101 | .optimize() // Apply peephole optimizations 102 | .build(); // Build the final bytecode sequence. 103 | // `build()` prepends the `executeActions()` function selector (0xc94f554d) 104 | // to the raw action bytecode generated by `build_raw()`. 105 | 106 | main_flow_bytes 107 | } 108 | 109 | // --- How to use the generated bytecode --- 110 | fn main() { 111 | let flow_bytecode = generate_morpho_flashloan_flow(); 112 | 113 | // `flow_bytecode` now contains the sequence: 114 | // SETFAIL -> SETCALLBACK(MORPHO) -> CALL(MORPHO, flashLoan(...)) 115 | 116 | // This `flow_bytecode` would be used as the `data` field in an Ethereum transaction 117 | // sent to your deployed Executor contract instance. 118 | 119 | // Important Considerations for Execution: 120 | // 1. Funding: The Executor contract must possess enough WETH *after* the flash loan 121 | // is granted but *before* the callback completes to successfully execute the 122 | // `inner_flow_bytes` (the WETH approval) and allow Morpho to reclaim the funds + fee. 123 | // This usually means the Executor needs some initial WETH or the operations *within* 124 | // the flash loan (not shown in this basic example) must generate the required WETH. 125 | // 2. Gas: Ensure sufficient gas is provided for the main transaction and the callback execution. 126 | // 3. Permissions: The transaction sender must be the owner of the Executor contract. 127 | 128 | println!("Generated Flow Bytecode: 0x{}", hex::encode(&flow_bytecode)); 129 | } 130 | ``` 131 | 132 | ### Low-Level Bytecode Example 133 | 134 | Here's an example sequence that performs a basic contract call using the raw opcodes: 135 | 136 | ```text 137 | # Assume target_addr = 0x... target contract address 138 | # Assume eth_value = 1 ether (in wei) 139 | # Assume function selector = 0xaabbccdd 140 | 141 | # Bytecode Sequence: 142 | 0x01 0x0040 # CLEARDATA: Allocate 64 bytes for calldata 143 | 0x03 # SETADDR: Set target contract address 144 | 0x04 # SETVALUE: Set ETH value to send (1 ether) 145 | 0x02 0x0000 0x0004 # SETDATA: Set function selector at offset 0, length 4 146 | aabbccdd # ↳ Function selector bytes 147 | 0x0A # SETFAIL: Enable revert on failure 148 | 0x06 # CALL: Execute the call 149 | 0x00 # EOF: End sequence 150 | ``` 151 | 152 | ## Core Features 153 | 154 | - Sequential execution of multiple operations in a single transaction 155 | - Support for flash loans from multiple protocols (Morpho, Aave) 156 | - Low-level operation support (calls, creates, delegate calls) 157 | - Memory management for transaction data 158 | - Fail-safe mechanisms with configurable error handling 159 | 160 | ## Operations 161 | 162 | The contract supports the following operations, encoded as single-byte opcodes: 163 | 164 | | Opcode | Operation | Description | Encoding Format | 165 | | ------ | ------------ | ------------------------------------ | --------------------------------------------------------------------------------------- | 166 | | 0x00 | EOF | End of flow marker | `0x00` | 167 | | 0x01 | CLEARDATA | Clear transaction data buffer | `0x01 + [size: uint16]` | 168 | | 0x02 | SETDATA | Set data at specific offset | `0x02 + [offset: uint16] + [size: uint16] + [ bytes]` | 169 | | 0x03 | SETADDR | Set target address | `0x03 + [address: bytes20]` | 170 | | 0x04 | SETVALUE | Set ETH value for calls | `0x04 + [value: uint256]` | 171 | | 0x05 | EXTCODECOPY | Copy external contract code | `0x05 + [addr: bytes20] + [dataOffset: uint16] + [codeOffset: uint16] + [size: uint16]` | 172 | | 0x06 | CALL | Perform external call | `0x06` | 173 | | 0x07 | CREATE | Deploy new contract | `0x07` | 174 | | 0x08 | DELEGATECALL | Perform delegate call | `0x08` | 175 | | 0x09 | SETCALLBACK | Set callback address for flash loans | `0x09 + [address: bytes20]` | 176 | | 0x0A | SETFAIL | Enable revert on call failure | `0x0A` | 177 | | 0x0B | CLEARFAIL | Disable revert on call failure | `0x0B` | 178 | 179 | ## Memory Management 180 | 181 | The contract maintains a dynamic bytes array (`txData`) as a working buffer for all operations: 182 | 183 | - Memory Layout: 184 | - 0x00-0x20: Length of array (32 bytes) 185 | - 0x20-onwards: Actual data bytes 186 | 187 | Operations that interact with this buffer: 188 | 189 | - CLEARDATA: Clears and resizes the buffer 190 | - SETDATA: Writes data at specific offsets 191 | - EXTCODECOPY: Copies external contract code into the buffer 192 | - CALL/DELEGATECALL/CREATE: Read from the buffer for execution 193 | 194 | ## Flash Loan Support 195 | 196 | The contract implements callbacks for multiple flash loan protocols: 197 | 198 | ### Morpho Flash Loan Callback 199 | 200 | ```solidity 201 | function onMorphoFlashLoan(uint256 amount, bytes calldata data) 202 | ``` 203 | 204 | When Morpho calls this function on the executor, the executor will execute the bytecode passed in `data`. 205 | 206 | ### Aave Flash Loan Callback 207 | 208 | ```solidity 209 | function executeOperation( 210 | address asset, 211 | uint256 amount, 212 | uint256 premium, 213 | address initiator, 214 | bytes calldata params 215 | ) 216 | ``` 217 | 218 | ## Security Considerations 219 | 220 | - Owner-only access control 221 | - Callback address validation for flash loans (`SETCALLBACK`) 222 | - Automatic callback address clearing after use (prevents re-entrancy with old callback data) 223 | - Optional failure handling with `SETFAIL`/`CLEARFAIL` 224 | - Memory bounds checking for all operations 225 | 226 | ## Development 227 | 228 | The contract is developed in Solidity and includes a comprehensive test suite written in Rust. The tests use Anvil for local blockchain simulation. 229 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use hex; 2 | use serde::Deserialize; 3 | use std::{collections::HashMap, env, fs::File, io::Write, path::PathBuf, process::Command}; 4 | 5 | #[derive(Debug, Deserialize)] 6 | struct SolSrcInfo { 7 | #[serde(rename = "bin-runtime")] 8 | bin_runtime: String, 9 | bin: String, 10 | } 11 | 12 | #[derive(Debug, Deserialize)] 13 | struct SolJsonOut { 14 | contracts: HashMap, 15 | } 16 | 17 | fn build_get_json(source: &str) -> SolJsonOut { 18 | let output_execute = Command::new("sh") 19 | .args([ 20 | "-c", 21 | &format!( 22 | "solc {} --via-ir --optimize --optimize-runs 2000 --combined-json=bin,bin-runtime", 23 | source 24 | ), 25 | ]) 26 | .output() 27 | .unwrap(); 28 | let sol_output: SolJsonOut = 29 | serde_json::from_slice(&output_execute.stdout).expect("failed to load solc output"); 30 | sol_output 31 | } 32 | 33 | // Example custom build script. 34 | fn main() { 35 | // Abort if inside docs.rs 36 | if env::var("DOCS_RS").is_ok() { 37 | return; 38 | } 39 | 40 | // Tell Cargo that if the given file changes, to rerun this build script. 41 | println!("cargo::rerun-if-changed=contracts/executor.sol"); 42 | println!("cargo::rerun-if-changed=contracts/proxy.sol"); 43 | 44 | // Get the OUT_DIR environment variable 45 | let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR environment variable not set")); 46 | 47 | let executor_file = "contracts/executor.sol"; 48 | let executor_contract = "contracts/executor.sol:executor"; 49 | let executor_solc = build_get_json(executor_file); 50 | let executor_outs = executor_solc 51 | .contracts 52 | .get(executor_contract) 53 | .expect("solc output didn't generate the executor file"); 54 | let executor_binruntime = &executor_outs.bin_runtime; 55 | let executor_bin = &executor_outs.bin; 56 | 57 | let executor_bin_path = out_dir.join("executor.bin"); 58 | let mut file = File::create(&executor_bin_path).unwrap(); 59 | file.write_all( 60 | &hex::decode(executor_bin).expect("failed to decode hex binary from solc output"), 61 | ) 62 | .unwrap(); 63 | 64 | let executor_runtime_bin_path = out_dir.join("executor_runtime.bin"); 65 | let mut file = File::create(&executor_runtime_bin_path).unwrap(); 66 | file.write_all( 67 | &hex::decode(executor_binruntime).expect("failed to decode hex binary from solc output"), 68 | ) 69 | .unwrap(); 70 | 71 | let proxy_file = "contracts/proxy.sol"; 72 | let proxy_contract = "contracts/proxy.sol:proxy"; 73 | let proxy_solc = build_get_json(proxy_file); 74 | let proxy_outs = proxy_solc 75 | .contracts 76 | .get(proxy_contract) 77 | .expect("solc output didn't generate the proxy file"); 78 | let proxy_binruntime = &proxy_outs.bin_runtime; 79 | let proxy_bin = &proxy_outs.bin; 80 | 81 | let proxy_bin_path = out_dir.join("proxy.bin"); 82 | let mut file = File::create(&proxy_bin_path).unwrap(); 83 | file.write_all(&hex::decode(proxy_bin).expect("failed to decode hex binary from solc output")) 84 | .unwrap(); 85 | 86 | let proxy_runtime_bin_path = out_dir.join("proxy_runtime.bin"); 87 | let mut file = File::create(&proxy_runtime_bin_path).unwrap(); 88 | file.write_all( 89 | &hex::decode(proxy_binruntime).expect("failed to decode hex binary from solc output"), 90 | ) 91 | .unwrap(); 92 | } 93 | -------------------------------------------------------------------------------- /contracts/executor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @title Executor Contract 6 | * @notice A flexible contract that can execute a series of actions including flash loans 7 | * @dev This contract supports multiple flash loan protocols (Morpho, Aave, ERC3156) and 8 | * allows for complex transaction execution with various low-level operations 9 | */ 10 | contract executor { 11 | /// @notice The address that can initiate actions (set to tx.origin on deployment via proxy) 12 | address owner; 13 | /// @notice Address allowed to trigger callback functions 14 | address callbackAddress; 15 | 16 | /** 17 | * @notice Creates a new executor instance 18 | * @dev Sets the contract owner to the transaction origin 19 | */ 20 | constructor() payable { 21 | owner = tx.origin; // Note: tx.origin used intentionally for specific use case 22 | } 23 | 24 | receive() external payable {} 25 | fallback() external payable {} 26 | 27 | /** 28 | * @notice Supported operation types for the executor 29 | * @dev Each action corresponds to a specific operation in the execution flow 30 | */ 31 | enum Action { 32 | EOF, // End of flow marker 33 | CLEARDATA, // Clear the transaction data buffer 34 | SETDATA, // Set data at specific offset 35 | SETADDR, // Set target address 36 | SETVALUE, // Set ETH value for calls 37 | EXTCODECOPY, // Copy external contract code 38 | CALL, // Perform external call 39 | CREATE, // Deploy new contract 40 | DELEGATECALL, // Perform delegate call 41 | SETCALLBACK, // Set callback address 42 | SETFAIL, // Enable revert on call failure 43 | CLEARFAIL // Disable revert on call failure 44 | } 45 | 46 | /** 47 | * @dev Internal callback handler for flash loan protocols 48 | * @param calldata_offset The offset in calldata where execution instructions begin 49 | * @notice Validates callback sender and executes the provided instructions 50 | */ 51 | function _onCallback(uint256 calldata_offset) internal { 52 | require(msg.sender == callbackAddress, "Invalid callback sender"); 53 | callbackAddress = address(0); // Reset callback address for security 54 | _executeActions(calldata_offset); 55 | } 56 | 57 | /** 58 | * @notice Morpho flash loan callback handler 59 | * @dev Calldata offset calculation simplified to constant 100 60 | * Original calculation: 4 (function selector) + 32 + calldataload(4 + 32) 61 | * @param amount The amount of tokens borrowed 62 | * @param data Additional call parameters 63 | */ 64 | function onMorphoFlashLoan(uint256 amount, bytes calldata data) external { 65 | // Simplified from dynamic calculation to fixed offset 66 | // assembly { 67 | // calldata_offset := add(4, add(32, calldataload(add(4, mul(1, 32))))) 68 | // } 69 | _onCallback(100); // Fixed offset based on Morpho's calldata layout 70 | } 71 | 72 | /** 73 | * @notice Aave flash loan callback handler 74 | * @dev Calldata offset calculation simplified to constant 196 75 | * Original calculation: 4 (selector) + 32 + calldataload(4 + 4*32) 76 | * @param asset The address of the flash-borrowed asset 77 | * @param amount The amount of the flash-borrowed asset 78 | * @param premium The fee of the flash-borrowed asset 79 | * @param initiator The address initiating the flash loan 80 | * @param params Arbitrary packed params to pass to the receiver as extra information 81 | * @return true if the flash loan was successful 82 | */ 83 | function executeOperation( 84 | address asset, 85 | uint256 amount, 86 | uint256 premium, 87 | address initiator, 88 | bytes calldata params 89 | ) external returns (bool) { 90 | // Simplified from dynamic calculation to fixed offset 91 | // assembly { 92 | // calldata_offset := add(4, add(32, calldataload(add(4, mul(4, 32))))) 93 | // } 94 | _onCallback(196); // Fixed offset based on Aave's calldata layout 95 | return true; 96 | } 97 | 98 | 99 | /** 100 | * @notice Main entry point for executing a series of actions 101 | * @dev Payable to allow receiving ETH for operations 102 | */ 103 | function executeActions() external payable { 104 | _executeActions(4); // Skip function selector (4 bytes) 105 | } 106 | 107 | /** 108 | * @notice Internal function to execute a series of actions 109 | * @dev Processes a byte stream of actions with their parameters 110 | * 111 | * Memory Management: 112 | * The contract maintains a single dynamic bytes array (txData) that serves as a 113 | * working buffer for all operations. This buffer is: 114 | * - Cleared and resized by CLEARDATA 115 | * - Written to by SETDATA and EXTCODECOPY 116 | * - Read from by CALL, DELEGATECALL, and CREATE 117 | * 118 | * Memory Layout: 119 | * txData (bytes array): 120 | * - 0x00-0x20: Length of array (32 bytes) 121 | * - 0x20-onwards: Actual data bytes 122 | * 123 | * All operations that write to txData must respect: 124 | * - Array bounds 125 | * - Proper offset calculation 126 | * - Word alignment for 32-byte operations 127 | * 128 | * @param calldata_offset Starting position in calldata to read actions from 129 | * @custom:security Uses tx.origin intentionally for specific authorization model 130 | */ 131 | function _executeActions(uint256 calldata_offset) internal { 132 | // --- Authorization --- 133 | require(tx.origin == owner, "Unauthorized"); 134 | 135 | // --- Execution Setup --- 136 | bytes calldata data = msg.data[calldata_offset:]; 137 | uint256 offset = 0; 138 | address target; // Target address for calls 139 | uint256 value; // ETH value for calls 140 | bool fail = false; // Fail flag for call operations 141 | bytes memory txData; // Transaction data buffer 142 | 143 | // --- Action Loop --- 144 | unchecked{ 145 | while (offset < data.length) { 146 | Action op = Action(uint8(data[offset])); 147 | offset += 1; 148 | 149 | if (op == Action.EOF) { 150 | break; 151 | } 152 | else if (op == Action.CLEARDATA) { 153 | uint256 size; 154 | (size, offset) = _parseUint16(data, offset); 155 | txData = new bytes(size); 156 | } 157 | else if (op == Action.SETDATA) { 158 | uint256 data_offset; 159 | uint256 data_size; 160 | (data_offset, offset) = _parseUint16(data, offset); 161 | (data_size, offset) = _parseUint16(data, offset); 162 | uint256 i; 163 | // First loop: Copy full 32-byte words efficiently using assembly 164 | for (i = 0; i < data_size/32; i++) { 165 | uint256 value_i; 166 | (value_i, offset) = _parseUint256(data, offset); 167 | assembly{ 168 | // Memory layout for txData: 169 | // txData : points to array struct 170 | // +0x20 : skips length prefix 171 | // +offset : moves to target position 172 | // +i*0x20 : moves to current 32-byte word 173 | mstore(add(add(add(txData, 0x20), data_offset), mul(i, 0x20)), value_i) 174 | } 175 | } 176 | // Second loop: Copy remaining bytes one by one 177 | for (i = ((data_size/32) * 32); i < data_size; i++) { 178 | txData[data_offset + i] = data[offset]; 179 | offset+=1; 180 | } 181 | } else if (op == Action.SETADDR) { 182 | (target, offset) = _parseAddress(data, offset); 183 | } else if (op == Action.SETVALUE) { 184 | (value, offset) = _parseUint256(data, offset); 185 | } else if (op == Action.EXTCODECOPY) { 186 | // Parameters for extcodecopy: 187 | // 1. address: 20-byte address of the contract to query 188 | // 2. destOffset: memory position where code will be copied 189 | // 3. offset: position in contract code to start copying 190 | // 4. size: number of bytes to copy 191 | address code_contract; 192 | uint256 data_offset; 193 | uint256 code_offset; 194 | uint256 size; 195 | (code_contract, offset) = _parseAddress(data, offset); 196 | (data_offset, offset) = _parseUint16(data, offset); 197 | (code_offset, offset) = _parseUint16(data, offset); 198 | (size, offset) = _parseUint16(data, offset); 199 | assembly { 200 | // Memory layout for destination: 201 | // txData : array pointer 202 | // +0x20 : skip length prefix 203 | // +offset : target position in array 204 | extcodecopy( 205 | code_contract, // source contract 206 | add(txData, add(data_offset, 0x20)), // destination in memory 207 | code_offset, // start position in source 208 | size // number of bytes 209 | ) 210 | } 211 | } else if (op == Action.CALL) { 212 | // Perform external call with current txData buffer 213 | // txData contains the complete calldata including: 214 | // - function selector (4 bytes) 215 | // - encoded parameters (remaining bytes) 216 | bool success; 217 | (success, ) = target.call{value: value}(txData); 218 | if (fail) { 219 | require(success, "CALL_FAILED"); 220 | } 221 | value = 0; // Reset value for safety 222 | } else if (op == Action.CREATE) { 223 | assembly { 224 | // Memory layout for contract creation: 225 | // txData : points to array struct 226 | // mload(txData): gets the length of the initialization code 227 | // add(txData, 0x20): points to the actual initialization code 228 | // 229 | // create(value, offset, size): 230 | // - value: amount of ETH to send 231 | // - offset: memory position of init code 232 | // - size: length of init code 233 | target := create( 234 | value, // ETH value for new contract 235 | add(txData, 0x20), // Skip array length word 236 | mload(txData) // Size of initialization code 237 | ) 238 | } 239 | value = 0; // Reset value after use 240 | } else if (op == Action.DELEGATECALL) { 241 | // Perform delegatecall using current txData buffer 242 | // Note: delegatecall runs code in the context of THIS contract: 243 | // - uses this contract's storage 244 | // - uses this contract's ETH balance 245 | // - msg.sender remains the original caller 246 | bool success; 247 | (success, ) = target.delegatecall(txData); 248 | if (fail) { 249 | require(success, "DELCALL_FAILED"); 250 | } 251 | } else if (op == Action.SETCALLBACK) { 252 | (callbackAddress, offset) = _parseAddress(data, offset); 253 | } else if (op == Action.SETFAIL) { 254 | fail = true; 255 | } else if (op == Action.CLEARFAIL) { 256 | fail = false; 257 | } 258 | 259 | } 260 | } 261 | } 262 | 263 | /** 264 | * @notice Parse a function selector from byte array 265 | * @dev Memory layout for bytes array: 266 | * - 0x00-0x20: length of array (32 bytes) 267 | * - 0x20+: actual bytes data 268 | * The assembly loads 32 bytes starting at data[offset] 269 | * @param data Source byte array 270 | * @param offset Starting position in the array 271 | * @return bytes4 The parsed function selector 272 | * @return uint256 The new offset after parsing 273 | */ 274 | function _parseFuncId(bytes memory data, uint256 offset) internal pure returns (bytes4, uint256) { 275 | bytes4 funcId; 276 | assembly { 277 | // data points to the bytes array in memory 278 | // add(data, 0x20) skips the length field 279 | // add(..., offset) moves to the desired position 280 | funcId := mload(add(add(data, offset), 0x20)) 281 | } 282 | return (funcId, offset + 4); 283 | } 284 | 285 | /** 286 | * @notice Parse an Ethereum address from byte array 287 | * @dev Memory layout handling: 288 | * 1. data points to the bytes array struct in memory 289 | * 2. First 32 bytes at data contain the array length 290 | * 3. Actual bytes start at data + 0x20 291 | * 4. We load 32 bytes but only want last 20 bytes for address 292 | * 293 | * @param data Source byte array 294 | * @param offset Starting position in the array 295 | * @return address The parsed address 296 | * @return uint256 The new offset after parsing 297 | */ 298 | function _parseAddress(bytes memory data, uint256 offset) internal pure returns (address, uint256) { 299 | uint256 addr; 300 | assembly { 301 | // Load 32 bytes from position (data + 0x20 + offset) 302 | // - pointer to bytes array struct 303 | // - 0x20: skip array length field 304 | // - offset: position in actual data 305 | addr := mload(add(add(data, offset), 0x20)) 306 | } 307 | // Shift right by 96 bits (12 bytes) to get only the last 20 bytes 308 | // This aligns the address to the least significant bits 309 | addr = addr >> 96; 310 | return (address(uint160(addr)), offset + 20); 311 | } 312 | 313 | /** 314 | * @notice Parse a uint256 from byte array 315 | * @dev Uses assembly for efficient memory operations 316 | * @param data Source byte array 317 | * @param offset Starting position in the array 318 | * @return uint256 The parsed value 319 | * @return uint256 The new offset after parsing 320 | */ 321 | function _parseUint256(bytes memory data, uint256 offset) internal pure returns (uint256, uint256) { 322 | uint256 value; 323 | assembly { 324 | value := mload(add(add(data, offset),0x20)) 325 | } 326 | return (value, offset + 32); 327 | } 328 | 329 | /** 330 | * @notice Parse a uint16 from byte array 331 | * @dev Combines two bytes into a uint16 332 | * @param data Source byte array 333 | * @param offset Starting position in the array 334 | * @return uint256 The parsed value 335 | * @return uint256 The new offset after parsing 336 | */ 337 | function _parseUint16(bytes memory data, uint256 offset) internal pure returns (uint256, uint256) { 338 | uint256 value = uint256(uint8(data[offset])) << 8 | uint256(uint8(data[offset + 1])); 339 | return (value, offset + 2); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /contracts/proxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @title Simple Immutable Proxy 6 | * @notice A basic proxy contract that delegates all calls to a fixed target implementation. 7 | * @dev Uses an immutable target address set during deployment. 8 | * Ownership is assigned to tx.origin and checked on subsequent calls. 9 | */ 10 | contract proxy { 11 | /// @notice The address that deployed the proxy and is allowed to interact with it. 12 | address public owner; 13 | /// @notice The immutable address of the implementation contract. 14 | address immutable target; 15 | 16 | /** 17 | * @notice Deploys the proxy and the initial implementation logic. 18 | * @param _target The address of the implementation contract. 19 | * @param constructorData The ABI-encoded data for the implementation's constructor. 20 | */ 21 | constructor(address _target, bytes memory constructorData) payable { 22 | owner = tx.origin; // Owner is the EOA that initiated the deployment transaction. 23 | target = _target; 24 | (bool success,) = target.delegatecall(constructorData); // Executes implementation's constructor logic 25 | require(success, "PROXY_CONSTRUCTOR_DELEGATECALL_FAILED"); 26 | } 27 | 28 | /** 29 | * @notice Fallback function to delegate calls to the target implementation. 30 | * @dev Requires that the transaction origin matches the owner set during deployment. 31 | * Forwards all ETH sent with the call. 32 | */ 33 | fallback() external payable { 34 | require(tx.origin == owner, "PROXY_UNAUTHORIZED"); // Ensures only the original deployer EOA can call. 35 | (bool success,) = target.delegatecall(msg.data); 36 | require(success, "PROXY_FALLBACK_DELEGATECALL_FAILED"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/flow_builder.rs: -------------------------------------------------------------------------------- 1 | use alloy_primitives::{Address, U256}; 2 | 3 | use crate::opcodes::{ 4 | Call, ClearData, Create, DelegateCall, ExtCodeCopy, SetAddr, SetData, SetValue, SetCallback, SetFail, ClearFail, 5 | }; 6 | 7 | // Enum for all opcode actions 8 | enum Action { 9 | ClearData(ClearData), 10 | SetData(SetData), 11 | SetAddr(SetAddr), 12 | SetValue(SetValue), 13 | ExtCodeCopy(ExtCodeCopy), 14 | Call(Call), 15 | Create(Create), 16 | DelegateCall(DelegateCall), 17 | SetFail(SetFail), 18 | ClearFail(ClearFail), 19 | SetCallback(SetCallback) 20 | } 21 | 22 | impl Action { 23 | fn encode(&self) -> Vec { 24 | match self { 25 | Action::ClearData(cd) => cd.encode(), 26 | Action::SetData(sd) => sd.encode(), 27 | Action::SetAddr(sa) => sa.encode(), 28 | Action::SetValue(sv) => sv.encode(), 29 | Action::ExtCodeCopy(ecc) => ecc.encode(), 30 | Action::Call(c) => c.encode(), 31 | Action::Create(c) => c.encode(), 32 | Action::DelegateCall(dc) => dc.encode(), 33 | Action::SetFail(sf) => sf.encode(), 34 | Action::ClearFail(cf) => cf.encode(), 35 | Action::SetCallback(scb) => scb.encode(), 36 | } 37 | } 38 | } 39 | 40 | // FlowBuilder to manage the actions 41 | #[derive(Default)] 42 | pub struct FlowBuilder { 43 | actions: Vec, 44 | } 45 | 46 | impl FlowBuilder { 47 | /// Creates an empty `FlowBuilder` with no actions. 48 | pub fn empty() -> Self { 49 | Self::default() 50 | } 51 | 52 | /// A simple optimizer that will remove redundant sets 53 | fn peephole_opt(&mut self) { 54 | let mut ops_to_remove = Vec::new(); 55 | let mut last_value = U256::ZERO; 56 | let mut last_target = Address::ZERO; 57 | let mut last_data: Vec = Vec::new(); 58 | let mut last_fail = false; 59 | 60 | for (idx, action) in self.actions.iter().enumerate() { 61 | let to_remove = match action { 62 | Action::SetFail(_) => { 63 | if last_fail { 64 | true 65 | } else { 66 | last_fail = true; 67 | false 68 | } 69 | } 70 | Action::ClearFail(_) => { 71 | if last_fail { 72 | last_fail = false; 73 | false 74 | } else { 75 | true 76 | } 77 | } 78 | Action::Call(_) => { 79 | last_value = U256::ZERO; 80 | false 81 | } 82 | Action::Create(Create { created_address }) => { 83 | last_target = *created_address; 84 | last_value = U256::ZERO; 85 | false 86 | } 87 | Action::SetAddr(SetAddr { addr }) => { 88 | let res = last_target == *addr; 89 | last_target = *addr; 90 | res 91 | } 92 | Action::SetValue(SetValue { value }) => { 93 | let res = last_value == *value; 94 | last_value = *value; 95 | res 96 | } 97 | Action::ClearData(ClearData { size }) => { 98 | let res = last_data.len() == *size as usize; 99 | last_data = vec![0; *size as usize]; 100 | res 101 | } 102 | Action::SetData(SetData { offset, data }) => { 103 | let offset_uz = *offset as usize; 104 | let mut new_data = last_data.clone(); 105 | new_data.splice(offset_uz..offset_uz + data.len(), data.to_owned()); 106 | let res = last_data == new_data; 107 | last_data = new_data; 108 | res 109 | } 110 | _ => false, 111 | }; 112 | if to_remove { 113 | ops_to_remove.push(idx); 114 | } 115 | } 116 | 117 | for idx in ops_to_remove.into_iter().rev() { 118 | self.actions.remove(idx); 119 | } 120 | } 121 | 122 | /// Adds an `EXTCODECOPY` operation to the action list. 123 | pub fn set_extcodecopy_op(&mut self, source: Address, data_offset: u16, code_offset: u16, size: u16) -> &mut Self { 124 | self.actions.push(Action::ExtCodeCopy(ExtCodeCopy { 125 | source, 126 | data_offset, 127 | code_offset, 128 | size, 129 | })); 130 | self 131 | } 132 | 133 | /// Adds a `SETADDR` operation to the action list. 134 | pub fn set_addr_op(&mut self, addr: Address) -> &mut Self { 135 | self.actions.push(Action::SetAddr(SetAddr { addr })); 136 | self 137 | } 138 | 139 | /// Adds a `SETVALUE` operation to the action list. 140 | pub fn set_value_op(&mut self, value: U256) -> &mut Self { 141 | self.actions.push(Action::SetValue(SetValue { value })); 142 | self 143 | } 144 | 145 | /// Adds a `SETDATA` operation to the action list. 146 | pub fn set_data_op(&mut self, offset: u16, data: &[u8]) -> &mut Self { 147 | self.actions.push(Action::SetData(SetData { 148 | offset, 149 | data: data.to_owned(), 150 | })); 151 | self 152 | } 153 | 154 | /// Adds a `CLEARDATA` operation to the action list. 155 | pub fn set_cleardata_op(&mut self, size: u16) -> &mut Self { 156 | self.actions.push(Action::ClearData(ClearData { size })); 157 | self 158 | } 159 | 160 | /// Adds a `CALL` operation to the action list. 161 | pub fn call_op(&mut self) -> &mut Self { 162 | self.actions.push(Action::Call(Call::new())); 163 | self 164 | } 165 | 166 | /// Adds a `CREATE` operation to the action list. 167 | pub fn create_op(&mut self, created_address: Address) -> &mut Self { 168 | self.actions.push(Action::Create(Create { created_address })); 169 | self 170 | } 171 | 172 | /// Adds a `DELEGATECALL` operation to the action list. 173 | pub fn delegatecall_op(&mut self) -> &mut Self { 174 | self.actions.push(Action::DelegateCall(DelegateCall::new())); 175 | self 176 | } 177 | 178 | /// Prepares a `CALL` operation with the specified target, data, and value. 179 | pub fn call(&mut self, target: Address, data: &[u8], value: U256) -> &mut Self { 180 | assert!(data.len() < u16::MAX as usize, "datalen exceeds 0xffff"); 181 | 182 | self.set_addr_op(target) 183 | .set_value_op(value) 184 | .set_cleardata_op(data.len() as u16) 185 | .set_data_op(0, data) 186 | .call_op() 187 | } 188 | 189 | /// Prepares a `DELEGATECALL` operation with the specified target and data. 190 | pub fn delegatecall(&mut self, target: Address, data: &[u8]) -> &mut Self { 191 | self.set_addr_op(target) 192 | .set_cleardata_op(data.len() as u16) 193 | .set_data_op(0, data) 194 | .delegatecall_op() 195 | } 196 | 197 | /// Prepares a `CREATE` operation with the specified address, data, and value. 198 | pub fn create(&mut self, created_address: Address, data: &[u8], value: U256) -> &mut Self { 199 | self.set_value_op(value) 200 | .set_cleardata_op(data.len() as u16) 201 | .set_data_op(0, data) 202 | .create_op(created_address) 203 | } 204 | 205 | /// prepare set callback 206 | pub fn set_callback(&mut self, callback_address: Address) -> &mut Self { 207 | self.actions.push(Action::SetCallback(SetCallback::new(callback_address))); 208 | self 209 | } 210 | 211 | /// Prepares a `SETFAIL` operation. 212 | pub fn set_fail(&mut self) -> &mut Self { 213 | self.actions.push(Action::SetFail(SetFail::new())); 214 | self 215 | } 216 | 217 | /// Prepares a `CLEARFAIL` operation. 218 | pub fn clear_fail(&mut self) -> &mut Self { 219 | self.actions.push(Action::ClearFail(ClearFail::new())); 220 | self 221 | } 222 | 223 | /// Optimizes the sequence of operations. 224 | pub fn optimize(&mut self) -> &mut Self { 225 | self.peephole_opt(); 226 | self 227 | } 228 | 229 | /// Builds the sequence of operations into a byte vector, optionally optimizing it. 230 | pub fn build_raw(&mut self) -> Vec { 231 | let mut res = Vec::new(); 232 | for action in &self.actions { 233 | res.extend(&action.encode()); 234 | } 235 | res 236 | } 237 | 238 | 239 | /// Builds the sequence of operations into a byte vector, optionally optimizing it. 240 | pub fn build(&mut self) -> Vec { 241 | // ======= executor.sol:executor ======= 242 | // Function signatures: 243 | // c94f554d: executeActions() 244 | let mut res = vec![0xc9, 0x4f, 0x55, 0x4d]; 245 | res.extend(self.build_raw()); 246 | res 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Contract initcode & runtime code 2 | 3 | // Use include_bytes! when not building on docs.rs 4 | #[cfg(not(docsrs))] 5 | pub const EXECUTOR_INIT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/executor.bin")); 6 | #[cfg(not(docsrs))] 7 | pub const DELEGATE_PROXY_INIT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/proxy.bin")); 8 | #[cfg(not(docsrs))] 9 | pub const EXECUTOR_RUNTIME: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/executor_runtime.bin")); 10 | #[cfg(not(docsrs))] 11 | pub const DELEGATE_PROXY_RUNTIME: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/proxy_runtime.bin")); 12 | 13 | // Use empty slices when building on docs.rs 14 | #[cfg(docsrs)] 15 | pub const EXECUTOR_INIT: &[u8] = &[]; 16 | #[cfg(docsrs)] 17 | pub const DELEGATE_PROXY_INIT: &[u8] = &[]; 18 | #[cfg(docsrs)] 19 | pub const EXECUTOR_RUNTIME: &[u8] = &[]; 20 | #[cfg(docsrs)] 21 | pub const DELEGATE_PROXY_RUNTIME: &[u8] = &[]; 22 | 23 | 24 | pub mod flow_builder; 25 | pub mod opcodes; 26 | 27 | // Re-export Flowbuilder 28 | pub use flow_builder::FlowBuilder; 29 | 30 | #[cfg(test)] 31 | mod test; 32 | -------------------------------------------------------------------------------- /src/opcodes.rs: -------------------------------------------------------------------------------- 1 | use alloy_primitives::{Address, U256}; 2 | 3 | // Operation opcodes as constants 4 | pub const OP_EOF: u8 = 0x00; 5 | pub const OP_CLEARDATA: u8 = 0x01; 6 | pub const OP_SETDATA: u8 = 0x02; 7 | pub const OP_SETADDR: u8 = 0x03; 8 | pub const OP_SETVALUE: u8 = 0x04; 9 | pub const OP_EXTCODECOPY: u8 = 0x05; 10 | pub const OP_CALL: u8 = 0x06; 11 | pub const OP_CREATE: u8 = 0x07; 12 | pub const OP_DELEGATECALL: u8 = 0x08; 13 | pub const OP_SETCALLBACK: u8 = 0x09; 14 | pub const OP_SETFAIL: u8 = 0x0a; 15 | pub const OP_CLEARFAIL: u8 = 0x0b; 16 | 17 | 18 | // Struct for the CLEARDATA operation 19 | pub struct ClearData { 20 | pub size: u16, 21 | } 22 | 23 | impl ClearData { 24 | pub fn new(size: u16) -> Self { 25 | ClearData { size } 26 | } 27 | 28 | pub fn encode(&self) -> Vec { 29 | let mut encoded = Vec::new(); 30 | encoded.push(OP_CLEARDATA); // Opcode 31 | encoded.extend(&self.size.to_be_bytes()); // Size 32 | encoded 33 | } 34 | } 35 | 36 | // Struct for the SETDATA operation 37 | pub struct SetData { 38 | pub offset: u16, 39 | pub data: Vec, 40 | } 41 | 42 | impl SetData { 43 | pub fn new(offset: u16, data: Vec) -> Self { 44 | SetData { offset, data } 45 | } 46 | 47 | pub fn encode(&self) -> Vec { 48 | let data_size = self.data.len() as u16; 49 | let mut encoded = Vec::new(); 50 | encoded.push(OP_SETDATA); // Opcode 51 | encoded.extend(&self.offset.to_be_bytes()); // Offset 52 | encoded.extend(&data_size.to_be_bytes()); // Data Size 53 | encoded.extend(&self.data); // Data 54 | 55 | encoded 56 | } 57 | } 58 | 59 | // Struct for the SETADDR operation 60 | pub struct SetAddr { 61 | pub addr: Address, // 20-byte address 62 | } 63 | 64 | impl SetAddr { 65 | pub fn new(addr: Address) -> Self { 66 | SetAddr { addr } 67 | } 68 | 69 | pub fn encode(&self) -> Vec { 70 | let mut encoded = Vec::new(); 71 | encoded.push(OP_SETADDR); // Opcode 72 | encoded.extend(&self.addr); // Address 73 | encoded 74 | } 75 | } 76 | 77 | // Struct for the SETVALUE operation 78 | #[derive(Clone, Debug)] 79 | pub struct SetValue { 80 | pub value: U256, 81 | } 82 | 83 | impl SetValue { 84 | pub fn new(value: U256) -> Self { 85 | SetValue { value } 86 | } 87 | 88 | pub fn encode(&self) -> Vec { 89 | let mut encoded = Vec::new(); 90 | encoded.push(OP_SETVALUE); // Opcode 91 | encoded.extend(&self.value.to_be_bytes::<32>()); // Value 92 | encoded 93 | } 94 | } 95 | 96 | // Struct for the EXTCODECOPY operation 97 | pub struct ExtCodeCopy { 98 | pub source: Address, // Address of contract to copy code from 99 | pub data_offset: u16, // Offset in the data to copy the code to 100 | pub code_offset: u16, // Offset in the code to copy from 101 | pub size: u16, // Size of the code to copy 102 | } 103 | 104 | impl ExtCodeCopy { 105 | pub fn new(source: Address, data_offset: u16, code_offset: u16, size: u16) -> Self { 106 | ExtCodeCopy { 107 | source, 108 | data_offset, 109 | code_offset, 110 | size, 111 | } 112 | } 113 | 114 | pub fn encode(&self) -> Vec { 115 | let mut encoded = Vec::new(); 116 | encoded.push(OP_EXTCODECOPY); // Opcode 117 | encoded.extend(&self.source); // Source address 118 | encoded.extend(&self.data_offset.to_be_bytes()); // Offset 119 | encoded.extend(&self.code_offset.to_be_bytes()); // Offset 120 | encoded.extend(&self.size.to_be_bytes()); // Size 121 | encoded 122 | } 123 | } 124 | 125 | // Struct for the CALL operation 126 | #[derive(Default)] 127 | pub struct Call {} 128 | 129 | impl Call { 130 | pub fn new() -> Self { 131 | Call {} 132 | } 133 | 134 | pub fn encode(&self) -> Vec { 135 | vec![OP_CALL] // Opcode 136 | } 137 | } 138 | 139 | // Struct for the CREATE operation 140 | #[derive(Default)] 141 | pub struct Create { 142 | pub created_address: Address, 143 | } 144 | 145 | impl Create { 146 | pub fn new(created_address: Address) -> Self { 147 | Self { created_address } 148 | } 149 | 150 | pub fn encode(&self) -> Vec { 151 | vec![OP_CREATE] // Opcode 152 | } 153 | } 154 | 155 | // Struct for the DELEGATECALL operation 156 | #[derive(Default)] 157 | pub struct DelegateCall {} 158 | 159 | impl DelegateCall { 160 | pub fn new() -> Self { 161 | DelegateCall {} 162 | } 163 | 164 | pub fn encode(&self) -> Vec { 165 | vec![OP_DELEGATECALL] // Opcode 166 | } 167 | } 168 | 169 | // Struct for the SETCALLBACK operation 170 | #[derive(Default)] 171 | pub struct SetCallback { 172 | pub callback_address: Address 173 | } 174 | 175 | impl SetCallback { 176 | pub fn new(callback_address: Address) -> Self { 177 | SetCallback {callback_address} 178 | } 179 | 180 | pub fn encode(&self) -> Vec { 181 | let mut encoded = vec![OP_SETCALLBACK]; // Opcode 182 | encoded.extend(&self.callback_address); // Offset 183 | encoded 184 | } 185 | } 186 | 187 | // Struct for the SETFAIL operation 188 | #[derive(Default)] 189 | pub struct SetFail {} 190 | 191 | impl SetFail { 192 | pub fn new() -> Self { 193 | SetFail {} 194 | } 195 | 196 | pub fn encode(&self) -> Vec { 197 | vec![OP_SETFAIL] // Opcode 198 | } 199 | } 200 | 201 | // Struct for the CLEARFAIL operation 202 | #[derive(Default)] 203 | pub struct ClearFail {} 204 | 205 | impl ClearFail { 206 | pub fn new() -> Self { 207 | ClearFail {} 208 | } 209 | 210 | pub fn encode(&self) -> Vec { 211 | vec![OP_CLEARFAIL] // Opcode 212 | } 213 | } 214 | 215 | -------------------------------------------------------------------------------- /src/test.rs: -------------------------------------------------------------------------------- 1 | 2 | use crate::{FlowBuilder, DELEGATE_PROXY_INIT, EXECUTOR_INIT}; 3 | use alloy::{ 4 | hex, 5 | network::TransactionBuilder, // Added Ethereum 6 | primitives::{address, bytes, uint, Address, U256}, 7 | // Removed EthereumSigner, node_bindings::Anvil 8 | providers::{ 9 | ext::AnvilApi, 10 | fillers::{BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller}, // Corrected filler paths 11 | layers::AnvilProvider, 12 | Identity, Provider, ProviderBuilder, RootProvider, 13 | }, 14 | rpc::types::TransactionRequest, 15 | sol, 16 | sol_types::{SolCall, SolConstructor}, 17 | }; 18 | use core::str; 19 | 20 | // Type alias for the complex provider type using LocalWallet as signer 21 | type AnvilTestProvider = FillProvider< 22 | JoinFill< 23 | Identity, 24 | JoinFill>> 25 | >, 26 | AnvilProvider, 27 | >; 28 | 29 | // Constants 30 | const BUDGET: U256 = uint!(1000000000000000000000_U256); // 1000e18 31 | const TWO_ETH: U256 = uint!(2000000000000000000_U256); // 2e18 32 | const ONEHUNDRED_ETH: U256 = uint!(10000000000000000000_U256); 33 | const WALLET: Address = Address::repeat_byte(0x41); 34 | const BOB: Address = Address::repeat_byte(0x42); 35 | const WETH9: Address = address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); 36 | const MORPHO: Address = address!("BBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb"); 37 | 38 | // Test helpers 39 | async fn setup_provider() -> AnvilTestProvider { 40 | let provider = get_provider(); 41 | provider 42 | .anvil_set_balance(WALLET, BUDGET + U256::from(10u64.pow(18))) 43 | .await 44 | .unwrap(); 45 | provider 46 | .anvil_set_balance(BOB, BUDGET + U256::from(10u64.pow(18))) 47 | .await 48 | .unwrap(); 49 | provider 50 | } 51 | 52 | async fn deploy_executor(provider: &AnvilTestProvider) -> Address { 53 | let tx = TransactionRequest::default() 54 | .with_from(WALLET) 55 | .with_deploy_code(EXECUTOR_INIT) 56 | .with_nonce(0); 57 | 58 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 59 | provider.evm_mine(None).await.unwrap(); 60 | let receipt = provider 61 | .get_transaction_receipt(tx_hash) 62 | .await 63 | .unwrap() 64 | .unwrap(); 65 | assert!(receipt.status()); 66 | receipt.contract_address.unwrap() 67 | } 68 | fn get_provider() -> AnvilTestProvider { 69 | ProviderBuilder::new().on_anvil_with_config(|anvil| { 70 | anvil 71 | .fork(std::env::var("ETH_RPC_URL").expect("failed to retrieve ETH_RPC_URL url from env")) 72 | .fork_block_number(20_000_000) 73 | }) 74 | } 75 | 76 | sol! { 77 | #[sol(rpc)] 78 | interface IERC20 { 79 | event Transfer(address indexed from, address indexed to, uint256 value); 80 | event Approval(address indexed owner, address indexed spender, uint256 value); 81 | function totalSupply() external view returns (uint256); 82 | function balanceOf(address account) external view returns (uint256); 83 | function transfer(address to, uint256 value) external returns (bool); 84 | function allowance(address owner, address spender) external view returns (uint256); 85 | function approve(address spender, uint256 value) external returns (bool); 86 | function transferFrom(address from, address to, uint256 value) external returns (bool); 87 | } 88 | } 89 | 90 | sol! { 91 | #[sol(rpc)] 92 | contract IProxy { 93 | constructor(address _target, bytes memory constructorData) payable; 94 | } 95 | } 96 | 97 | sol! { 98 | #[sol(rpc)] 99 | interface IWETH { 100 | function deposit() external payable; 101 | function transfer(address to, uint value) external returns (bool); 102 | function withdraw(uint amount) external; 103 | } 104 | } 105 | 106 | sol! { 107 | interface IMorpho { 108 | function flashLoan(address token, uint256 assets, bytes calldata data) external; 109 | } 110 | } 111 | 112 | #[test] 113 | fn test_flow_builder_create() { 114 | let calldata = FlowBuilder::empty() 115 | .create(Address::ZERO, "LALA".as_bytes(), U256::from(10)) 116 | .optimize() 117 | .build(); 118 | assert_eq!( 119 | calldata, 120 | hex!("c94f554d04000000000000000000000000000000000000000000000000000000000000000a01000402000000044c414c4107") 121 | ); 122 | } 123 | 124 | #[test] 125 | fn test_flow_builder_call() { 126 | let addr_a = Address::repeat_byte(0x41); 127 | let calldata = FlowBuilder::empty() 128 | .call(addr_a, &vec![98, 99], U256::ZERO) 129 | .optimize() 130 | .build(); 131 | assert_eq!( 132 | calldata, 133 | hex!("c94f554d0341414141414141414141414141414141414141410100020200000002626306") 134 | ); 135 | } 136 | 137 | #[test] 138 | fn test_flow_builder_delegatecall() { 139 | let addr_b = Address::repeat_byte(0x42); 140 | let calldata = FlowBuilder::empty() 141 | .delegatecall(addr_b, &vec![70, 71]) 142 | .optimize() 143 | .build(); 144 | assert_eq!( 145 | calldata, 146 | hex!("c94f554d0342424242424242424242424242424242424242420100020200000002464708") 147 | ); 148 | } 149 | 150 | #[test] 151 | fn test_flow_builder_combined_operations() { 152 | let addr_a = Address::repeat_byte(0x41); 153 | let addr_b = Address::repeat_byte(0x42); 154 | let calldata = FlowBuilder::empty() 155 | .create(Address::ZERO, "LALA".as_bytes(), U256::from(10)) 156 | .call(addr_a, &vec![98, 99], U256::ZERO) 157 | .delegatecall(addr_b, &vec![70, 71]) 158 | .optimize() 159 | .build(); 160 | assert_eq!( 161 | calldata, 162 | hex!("c94f554d04000000000000000000000000000000000000000000000000000000000000000a01000402000000044c414c410703414141414141414141414141414141414141414101000202000000026263060342424242424242424242424242424242424242420200000002464708") 163 | ); 164 | } 165 | 166 | #[tokio::test] 167 | async fn test_bob_cannot_interact() { 168 | // A random account can not interact with multiplexer 169 | let provider = ProviderBuilder::new().on_anvil(); 170 | provider 171 | .anvil_set_balance(WALLET, BUDGET + U256::from(10u64.pow(18))) 172 | .await 173 | .unwrap(); 174 | provider 175 | .anvil_set_balance(BOB, BUDGET + U256::from(10u64.pow(18))) 176 | .await 177 | .unwrap(); 178 | 179 | let tx = TransactionRequest::default() 180 | .with_from(WALLET) 181 | .with_deploy_code(EXECUTOR_INIT) 182 | .with_nonce(0); 183 | 184 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 185 | 186 | provider.evm_mine(None).await.unwrap(); 187 | 188 | let receipt = provider 189 | .get_transaction_receipt(tx_hash) 190 | .await 191 | .unwrap() 192 | .unwrap(); 193 | assert!(receipt.status()); 194 | let executor_wallet = receipt.contract_address.unwrap(); 195 | 196 | // Executor address is deterministic because we use always same WALLET and nonce. 197 | assert_eq!( 198 | address!("c088f75b5733d097f266010c1502399a53bdfdbd"), 199 | executor_wallet 200 | ); 201 | 202 | let tx = TransactionRequest::default() 203 | .with_from(BOB) 204 | .with_to(executor_wallet) 205 | .with_nonce(1) 206 | .with_value(BUDGET); 207 | 208 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 209 | 210 | provider.evm_mine(None).await.unwrap(); 211 | 212 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap(); 213 | 214 | assert!(receipt.is_none()); // Tx can not be send from bob 215 | } 216 | 217 | #[tokio::test] 218 | async fn test_wallet_can_interact() { 219 | let provider = ProviderBuilder::new().on_anvil(); 220 | provider 221 | .anvil_set_balance(WALLET, BUDGET + U256::from(10u64.pow(18))) 222 | .await 223 | .unwrap(); 224 | provider 225 | .anvil_set_balance(BOB, BUDGET + U256::from(10u64.pow(18))) 226 | .await 227 | .unwrap(); 228 | 229 | let tx = TransactionRequest::default() 230 | .with_from(WALLET) 231 | .with_deploy_code(EXECUTOR_INIT) 232 | .with_nonce(0); 233 | 234 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 235 | provider.evm_mine(None).await.unwrap(); 236 | 237 | let receipt = provider 238 | .get_transaction_receipt(tx_hash) 239 | .await 240 | .unwrap() 241 | .unwrap(); 242 | let executor_wallet = receipt.contract_address.unwrap(); 243 | // Executor address is deterministic because we use always same WALLET and nonce. 244 | assert_eq!( 245 | address!("c088f75b5733d097f266010c1502399a53bdfdbd"), 246 | executor_wallet 247 | ); 248 | 249 | let tx = TransactionRequest::default() 250 | .with_from(WALLET) 251 | .with_to(executor_wallet) 252 | .with_nonce(1) 253 | .with_value(BUDGET); 254 | 255 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 256 | 257 | provider.evm_mine(None).await.unwrap(); 258 | 259 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap(); 260 | 261 | assert!(receipt.is_some()); // Tx succeed from WALLET 262 | 263 | let account_balance = provider.get_balance(executor_wallet).await.unwrap(); 264 | assert_eq!(account_balance, BUDGET); // executor has the money sent in empty tx 265 | } 266 | 267 | #[tokio::test] 268 | async fn test_weth_deposit_through_executor() { 269 | let provider = setup_provider().await; 270 | let executor = deploy_executor(&provider).await; 271 | 272 | // Initial balance check 273 | let executor_balance = provider.get_balance(executor).await.unwrap(); 274 | let weth9_contract = IERC20::new(WETH9, provider.clone()); 275 | let executor_weth_balance = weth9_contract.balanceOf(executor).call().await.unwrap(); 276 | assert_eq!(executor_balance, U256::ZERO); 277 | assert_eq!(executor_weth_balance, U256::ZERO); 278 | 279 | // Deposit ETH to get WETH 280 | let fb = FlowBuilder::empty().call(WETH9, &bytes!(""), TWO_ETH).optimize().build(); 281 | let tx = TransactionRequest::default() 282 | .with_from(WALLET) 283 | .with_to(executor) 284 | .with_value(TWO_ETH) 285 | .with_input(fb); 286 | 287 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 288 | provider.evm_mine(None).await.unwrap(); 289 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap().unwrap(); 290 | assert!(receipt.status()); 291 | 292 | // Verify balances after deposit 293 | let executor_balance = provider.get_balance(executor).await.unwrap(); 294 | let executor_weth_balance = weth9_contract.balanceOf(executor).call().await.unwrap(); 295 | assert_eq!(executor_balance, U256::ZERO); 296 | assert_eq!(executor_weth_balance, TWO_ETH); 297 | } 298 | 299 | #[tokio::test] 300 | async fn test_weth_withdraw_through_executor() { 301 | let provider = setup_provider().await; 302 | let executor = deploy_executor(&provider).await; 303 | 304 | // First deposit WETH 305 | let fb = FlowBuilder::empty().call(WETH9, &bytes!(""), TWO_ETH).optimize().build(); 306 | let tx = TransactionRequest::default() 307 | .with_from(WALLET) 308 | .with_to(executor) 309 | .with_value(TWO_ETH) 310 | .with_input(fb); 311 | let _tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 312 | provider.evm_mine(None).await.unwrap(); 313 | 314 | // Then withdraw it back to ETH 315 | let withdraw_calldata = IWETH::withdrawCall { amount: TWO_ETH }.abi_encode(); 316 | let fb = FlowBuilder::empty().call(WETH9, &withdraw_calldata, U256::ZERO).optimize().build(); 317 | let tx = TransactionRequest::default() 318 | .with_from(WALLET) 319 | .with_to(executor) 320 | .with_input(fb); 321 | 322 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 323 | provider.evm_mine(None).await.unwrap(); 324 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap().unwrap(); 325 | assert!(receipt.status()); 326 | 327 | // Verify final balances 328 | let executor_balance = provider.get_balance(executor).await.unwrap(); 329 | let weth9_contract = IERC20::new(WETH9, provider.clone()); 330 | let executor_weth_balance = weth9_contract.balanceOf(executor).call().await.unwrap(); 331 | assert_eq!(executor_balance, TWO_ETH); 332 | assert_eq!(executor_weth_balance, U256::ZERO); 333 | } 334 | 335 | #[tokio::test] 336 | async fn test_wallet_can_proxy_create_small() { 337 | let provider = get_provider(); 338 | 339 | // reality check 340 | let weth_balance = provider.get_balance(WETH9).await.unwrap(); 341 | assert_eq!(format!("{}", weth_balance), "2933633723194923479377016"); 342 | 343 | // test WALLETs 344 | // 0x4141414141..4141414141 with 1001 eth 345 | // 0x4242424242..4242424242 with 1001 eth 346 | provider 347 | .anvil_set_balance(WALLET, BUDGET + U256::from(1e18 as u64)) 348 | .await 349 | .unwrap(); 350 | 351 | let wallet_balance = provider.get_balance(WALLET).await.unwrap(); 352 | assert_eq!(wallet_balance, BUDGET + U256::from(1e18 as u64)); // executor shoud shave sent the value to WETH9 353 | 354 | provider 355 | .anvil_set_balance(BOB, BUDGET + U256::from(1e18 as u64)) 356 | .await 357 | .unwrap(); 358 | // Make the Executor contract (WALLET is the owner) 359 | let tx = TransactionRequest::default() 360 | .with_from(WALLET) 361 | .with_deploy_code(EXECUTOR_INIT) 362 | .with_nonce(0); 363 | 364 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 365 | provider.evm_mine(None).await.unwrap(); 366 | let receipt = provider 367 | .get_transaction_receipt(tx_hash) 368 | .await 369 | .unwrap() 370 | .unwrap(); 371 | assert!(receipt.status()); 372 | let executor = receipt.contract_address.unwrap(); 373 | 374 | // Create dellegate proxy 375 | // let mut calldata = DELEGATE_PROXY_INIT; 376 | // calldata.extend(hex!("00").repeat(12)); 377 | // calldata.extend(executor.as_slice()); 378 | let mut calldata = DELEGATE_PROXY_INIT.to_vec(); 379 | calldata.extend( 380 | IProxy::constructorCall { 381 | _target: executor, 382 | constructorData: "".into(), 383 | } 384 | .abi_encode(), 385 | ); 386 | 387 | let fb = FlowBuilder::empty() 388 | .create(executor.create(1), &calldata, U256::ZERO) 389 | .call( 390 | executor.create(1), 391 | &FlowBuilder::empty() 392 | .call(WETH9, &vec![], TWO_ETH) 393 | .optimize().build(), 394 | TWO_ETH, 395 | ) 396 | .optimize().build(); 397 | 398 | let tx = TransactionRequest::default() 399 | .with_from(WALLET) 400 | .with_to(executor) 401 | .with_value(TWO_ETH) 402 | .with_input(fb); 403 | 404 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 405 | 406 | provider.evm_mine(None).await.unwrap(); 407 | 408 | let receipt = provider 409 | .get_transaction_receipt(tx_hash) 410 | .await 411 | .unwrap() 412 | .unwrap(); 413 | 414 | assert!(receipt.status()); 415 | 416 | let executor_balance = provider.get_balance(executor).await.unwrap(); 417 | assert_eq!(executor_balance, U256::ZERO); // executor shoud shave sent the value to WETH9 418 | assert_eq!( 419 | address!("c84f9705070281e8c800c57d92dbab053a80a2d0"), 420 | executor.create(1) 421 | ); 422 | 423 | // Executor has 424 | // 0 eth 425 | // 0 weth 426 | let executor_balance = provider.get_balance(executor).await.unwrap(); 427 | assert_eq!(executor_balance, U256::ZERO); // executor shoud shave sent the value to WETH9 428 | 429 | let weth9_contract = IERC20::new(WETH9, provider.clone()); 430 | let executor_weth_balance = weth9_contract.balanceOf(executor).call().await.unwrap(); 431 | assert_eq!(executor_weth_balance, U256::ZERO); // executor should have 2 eth worth of weth 432 | 433 | // Proxy created via executor that points to the executor ?? ?AHHH 434 | // 0 eth 435 | // 2 weth 436 | 437 | let proxy_balance = provider.get_balance(executor.create(1)).await.unwrap(); 438 | assert_eq!(proxy_balance, U256::ZERO); // executor shoud shave sent the value to WETH9 439 | 440 | let weth9_contract = IERC20::new(WETH9, provider.clone()); 441 | let proxy_weth_balance = weth9_contract 442 | .balanceOf(executor.create(1)) 443 | .call() 444 | .await 445 | .unwrap(); 446 | assert_eq!(proxy_weth_balance, TWO_ETH); // executor should have 2 eth worth of weth 447 | 448 | // Test ownership in the created proxy 449 | // WALLET -> executor -> proxy mint some weth 450 | 451 | let withdraw_calldata = IWETH::withdrawCall { amount: TWO_ETH }.abi_encode(); 452 | let multiplexed_withdraw_calldata = FlowBuilder::empty() 453 | .call(WETH9, &withdraw_calldata, U256::ZERO) 454 | .optimize().build(); // multiplexed withdraw from weth 455 | 456 | let fb = FlowBuilder::empty().call( 457 | executor.create(1), 458 | &multiplexed_withdraw_calldata, 459 | U256::ZERO, 460 | ).optimize().build(); // this should send 2 eth to weth and assign the same weth value to the executor 461 | 462 | let tx = TransactionRequest::default() 463 | .with_from(WALLET) 464 | .with_to(executor) 465 | .with_value(U256::ZERO) 466 | .with_input(fb); 467 | 468 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 469 | 470 | provider.evm_mine(None).await.unwrap(); 471 | 472 | let receipt = provider 473 | .get_transaction_receipt(tx_hash) 474 | .await 475 | .unwrap() 476 | .unwrap(); 477 | 478 | assert!(receipt.status()); 479 | 480 | // Executor has 481 | // 0 eth 482 | // 0 weth 483 | let executor_balance = provider.get_balance(executor).await.unwrap(); 484 | assert_eq!(executor_balance, U256::ZERO); // executor shoud shave sent the value to WETH9 485 | 486 | let weth9_contract = IERC20::new(WETH9, provider.clone()); 487 | let executor_weth_balance = weth9_contract.balanceOf(executor).call().await.unwrap(); 488 | assert_eq!(executor_weth_balance, U256::ZERO); 489 | 490 | // Proxy created via executor that points to the executor ?? ?AHHH 491 | // 2 eth 492 | // 0 weth 493 | 494 | let proxy_balance = provider.get_balance(executor.create(1)).await.unwrap(); 495 | assert_eq!(proxy_balance, TWO_ETH); // executor shoud shave sent the value to WETH9 496 | 497 | let weth9_contract = IERC20::new(WETH9, provider.clone()); 498 | let proxy_weth_balance = weth9_contract 499 | .balanceOf(executor.create(1)) 500 | .call() 501 | .await 502 | .unwrap(); 503 | assert_eq!(proxy_weth_balance, U256::ZERO); 504 | 505 | // bob -> executor -> ?? :fail: 506 | // bob -> proxy :fail: 507 | } 508 | 509 | #[tokio::test] 510 | async fn test_wallet_can_proxy_create_ultimate() { 511 | let provider = get_provider(); 512 | 513 | // reality check 514 | let weth9_contract = IERC20::new(WETH9, provider.clone()); 515 | let weth_balance = provider.get_balance(WETH9).await.unwrap(); 516 | assert_eq!(format!("{}", weth_balance), "2933633723194923479377016"); 517 | 518 | // test WALLETs 519 | // 0x4141414141..4141414141 with 1001 eth 520 | // 0x4242424242..4242424242 with 1001 eth 521 | provider 522 | .anvil_set_balance(WALLET, BUDGET + U256::from(1e18 as u64)) 523 | .await 524 | .unwrap(); 525 | provider 526 | .anvil_set_balance(BOB, BUDGET + U256::from(1e18 as u64)) 527 | .await 528 | .unwrap(); 529 | 530 | //////////////////////////////////////////////////////////// 531 | // Make the Executor contract (WALLET is the owner) 532 | let tx = TransactionRequest::default() 533 | .with_from(WALLET) 534 | .with_deploy_code(EXECUTOR_INIT) 535 | .with_nonce(0); 536 | 537 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 538 | provider.evm_mine(None).await.unwrap(); 539 | let receipt = provider 540 | .get_transaction_receipt(tx_hash) 541 | .await 542 | .unwrap() 543 | .unwrap(); 544 | 545 | assert!(receipt.status()); 546 | let executor = receipt.contract_address.unwrap(); 547 | 548 | //////////////////////////////////////////////////////////// 549 | // Make the Proxy(Executor) contract (WALLET is the owner) 550 | // Link the proxy to the executor but do not use the delegatecall in the constructor 551 | 552 | let mut deploy_proxy_executor = DELEGATE_PROXY_INIT.to_vec(); 553 | deploy_proxy_executor.extend( 554 | IProxy::constructorCall { 555 | _target: executor, 556 | constructorData: "".into(), 557 | } 558 | .abi_encode(), 559 | ); 560 | 561 | let tx = TransactionRequest::default() 562 | .with_from(WALLET) 563 | .with_deploy_code(deploy_proxy_executor) 564 | .with_nonce(1); 565 | 566 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 567 | provider.evm_mine(None).await.unwrap(); 568 | let receipt = provider 569 | .get_transaction_receipt(tx_hash) 570 | .await 571 | .unwrap() 572 | .unwrap(); 573 | assert!(receipt.status()); 574 | let proxy_executor = receipt.contract_address.unwrap(); 575 | assert_eq!(proxy_executor, WALLET.create(1)); 576 | 577 | //////////////////////////////////////////////////////////// 578 | // Deposit weth in the proxy account 579 | // Use the deployed Proxy(Executor) contract (WALLET is the owner) to deposit weth 580 | let deposit_calldata = []; 581 | let fb = FlowBuilder::empty().call(WETH9, &deposit_calldata, TWO_ETH).optimize().build(); 582 | 583 | let tx = TransactionRequest::default() 584 | .with_from(WALLET) 585 | .with_to(proxy_executor) 586 | .with_value(TWO_ETH) 587 | .with_input(fb); 588 | 589 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 590 | provider.evm_mine(None).await.unwrap(); 591 | 592 | let receipt = provider 593 | .get_transaction_receipt(tx_hash) 594 | .await 595 | .unwrap() 596 | .unwrap(); 597 | assert!(receipt.status()); 598 | 599 | // Executor account has no assets 600 | // 0 eth 601 | // 0 weth 602 | let executor_balance = provider.get_balance(executor).await.unwrap(); 603 | assert_eq!(executor_balance, U256::ZERO); // executor shoud shave sent the value to WETH9 604 | let executor_weth_balance = weth9_contract.balanceOf(executor).call().await.unwrap(); 605 | assert_eq!(executor_weth_balance, U256::ZERO); // executor should have 2 eth worth of weth 606 | 607 | // Proxy(Executor) account has 2 weth 608 | // 0 eth 609 | // 2 weth 610 | let proxy_executor_balance = provider.get_balance(proxy_executor).await.unwrap(); 611 | assert_eq!(proxy_executor_balance, U256::ZERO); // executor shoud shave sent the value to WETH9 612 | let proxy_executor_weth_balance = weth9_contract 613 | .balanceOf(proxy_executor) 614 | .call() 615 | .await 616 | .unwrap(); 617 | assert_eq!(proxy_executor_weth_balance, TWO_ETH); // executor should have 2 eth worth of weth 618 | 619 | //////////////////////////////////////////////////////////// 620 | // Whithdraw weth from the proxy account 621 | // Use the deployed Proxy(Executor) contract (WALLET is the owner) to deposit weth 622 | let withdraw_calldata = IWETH::withdrawCall { amount: TWO_ETH }.abi_encode(); 623 | let fb = FlowBuilder::empty().call(WETH9, &withdraw_calldata, U256::ZERO).optimize().build(); 624 | 625 | let tx = TransactionRequest::default() 626 | .with_from(WALLET) 627 | .with_to(proxy_executor) 628 | .with_value(U256::ZERO) 629 | .with_input(fb); 630 | 631 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 632 | provider.evm_mine(None).await.unwrap(); 633 | 634 | let receipt = provider 635 | .get_transaction_receipt(tx_hash) 636 | .await 637 | .unwrap() 638 | .unwrap(); 639 | assert!(receipt.status()); 640 | 641 | // Executor account has no assets 642 | // 0 eth 643 | // 0 weth 644 | let executor_balance = provider.get_balance(executor).await.unwrap(); 645 | assert_eq!(executor_balance, U256::ZERO); // executor shoud shave sent the value to WETH9 646 | let executor_weth_balance = weth9_contract.balanceOf(executor).call().await.unwrap(); 647 | assert_eq!(executor_weth_balance, U256::ZERO); // executor should have 2 eth worth of weth 648 | 649 | // Proxy(Executor) account has 2 weth 650 | // 2 eth 651 | // 0 weth 652 | let proxy_executor_balance = provider.get_balance(proxy_executor).await.unwrap(); 653 | assert_eq!(proxy_executor_balance, TWO_ETH); 654 | let proxy_executor_weth_balance = weth9_contract 655 | .balanceOf(proxy_executor) 656 | .call() 657 | .await 658 | .unwrap(); 659 | assert_eq!(proxy_executor_weth_balance, U256::ZERO); 660 | } 661 | 662 | #[tokio::test] 663 | async fn test_extcodecopy() { 664 | // Flipper.sol:: 665 | // contract proxy { 666 | // bool flag; 667 | // function flip() external { 668 | // flag = !flag; 669 | // } 670 | // } 671 | 672 | let flipper_init = hex!("6080604052348015600e575f80fd5b50608f80601a5f395ff3fe6080604052348015600e575f80fd5b50600436106026575f3560e01c8063cde4efa914602a575b5f80fd5b60306032565b005b5f8054906101000a900460ff16155f806101000a81548160ff02191690831515021790555056fea264697066735822122054836815366ebd9b068e7694d59a986fb0267bc2cc7c9ec20ffdccea97c00a3b64736f6c634300081a0033"); 673 | let flipper_runtime = hex!("6080604052348015600e575f80fd5b50600436106026575f3560e01c8063cde4efa914602a575b5f80fd5b60306032565b005b5f8054906101000a900460ff16155f806101000a81548160ff02191690831515021790555056fea264697066735822122054836815366ebd9b068e7694d59a986fb0267bc2cc7c9ec20ffdccea97c00a3b64736f6c634300081a0033"); 674 | let flipper_prolog = hex!("6080604052348015600e575f80fd5b50608f80601a5f395ff3fe"); 675 | let provider = get_provider(); 676 | 677 | // reality check 678 | let weth_balance = provider.get_balance(WETH9).await.unwrap(); 679 | assert_eq!(format!("{}", weth_balance), "2933633723194923479377016"); 680 | 681 | // test WALLETs 682 | // 0x4141414141..4141414141 with 1001 eth 683 | // 0x4242424242..4242424242 with 1001 eth 684 | provider 685 | .anvil_set_balance(WALLET, BUDGET + U256::from(1e18 as u64)) 686 | .await 687 | .unwrap(); 688 | provider 689 | .anvil_set_balance(BOB, BUDGET + U256::from(1e18 as u64)) 690 | .await 691 | .unwrap(); 692 | // Make the Executor contract (WALLET is the owner) 693 | let tx = TransactionRequest::default() 694 | .with_from(WALLET) 695 | .with_deploy_code(EXECUTOR_INIT) 696 | .with_nonce(0); 697 | 698 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 699 | provider.evm_mine(None).await.unwrap(); 700 | let receipt = provider 701 | .get_transaction_receipt(tx_hash) 702 | .await 703 | .unwrap() 704 | .unwrap(); 705 | assert!(receipt.status()); 706 | let executor = receipt.contract_address.unwrap(); 707 | 708 | // create normal flipper account. 709 | let tx = TransactionRequest::default() 710 | .with_from(WALLET) 711 | .with_deploy_code(flipper_init); 712 | 713 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 714 | provider.evm_mine(None).await.unwrap(); 715 | let receipt = provider 716 | .get_transaction_receipt(tx_hash) 717 | .await 718 | .unwrap() 719 | .unwrap(); 720 | assert!(receipt.status()); 721 | let flipper = receipt.contract_address.unwrap(); 722 | 723 | let created_flipper_runtime = provider.get_code_at(flipper).await.unwrap(); 724 | 725 | // The created flipper has the expected runtime bytecode. duh 726 | assert_eq!(created_flipper_runtime.to_vec(), flipper_runtime.to_vec()); 727 | 728 | let flipper1 = executor.create(1); 729 | let fb = FlowBuilder::empty() 730 | .set_cleardata_op(flipper_init.len() as u16) 731 | .set_data_op(0, &flipper_init) 732 | .create_op(flipper1) 733 | .optimize().build(); 734 | 735 | // create normal flipper account. Using data ops 736 | let tx = TransactionRequest::default() 737 | .with_from(WALLET) 738 | .with_to(executor) 739 | .with_input(fb); 740 | 741 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 742 | provider.evm_mine(None).await.unwrap(); 743 | let receipt = provider 744 | .get_transaction_receipt(tx_hash) 745 | .await 746 | .unwrap() 747 | .unwrap(); 748 | assert!(receipt.status()); 749 | println!("NO excodecopy gas used {:?}", receipt.gas_used); 750 | 751 | assert_eq!( 752 | address!("c84f9705070281e8c800c57d92dbab053a80a2d0"), 753 | flipper1 754 | ); 755 | 756 | let created_flipper1_runtime = provider.get_code_at(flipper1).await.unwrap(); 757 | assert_eq!(created_flipper1_runtime, created_flipper_runtime); 758 | 759 | // create normal flipper account. Using data Extcodecopy 760 | let flipper2 = executor.create(2); 761 | let fb = FlowBuilder::empty() 762 | .set_cleardata_op(flipper_init.len() as u16) 763 | .set_data_op(0, &flipper_prolog) 764 | // .set_data_op(flipper_prolog.len() as u16, &flipper_runtime) 765 | //.set_data_op(0, &flipper_init) 766 | .set_extcodecopy_op( 767 | flipper1, 768 | flipper_prolog.len() as u16, 769 | 0, 770 | created_flipper_runtime.len() as u16, 771 | ) 772 | .create_op(flipper2) 773 | .optimize().build(); 774 | 775 | let tx = TransactionRequest::default() 776 | .with_from(WALLET) 777 | .with_to(executor) 778 | .with_input(fb); 779 | 780 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 781 | provider.evm_mine(None).await.unwrap(); 782 | let receipt = provider 783 | .get_transaction_receipt(tx_hash) 784 | .await 785 | .unwrap() 786 | .unwrap(); 787 | assert!(receipt.status()); 788 | println!("excodecopy gas used {:?}", receipt.gas_used); 789 | assert_eq!( 790 | address!("6266c8947cb0834202f2a3be9e0b5f97e0089fda"), 791 | flipper2 792 | ); 793 | 794 | let created_flipper2_runtime = provider.get_code_at(flipper2).await.unwrap(); 795 | assert_eq!(created_flipper2_runtime, created_flipper_runtime); 796 | } 797 | 798 | // This test test a simple flashloan with morpho 799 | #[tokio::test] 800 | async fn test_flashloan_success_with_callback() { 801 | let provider = setup_provider().await; 802 | let executor = deploy_executor(&provider).await; 803 | 804 | let approve_calldata = IERC20::approveCall { 805 | spender: MORPHO, 806 | value: ONEHUNDRED_ETH, 807 | }.abi_encode(); 808 | 809 | let fb = FlowBuilder::empty() 810 | .call(WETH9, &approve_calldata, U256::ZERO) 811 | .optimize() 812 | .build_raw(); 813 | 814 | let flashloan_calldata = IMorpho::flashLoanCall { 815 | token: WETH9, 816 | assets: ONEHUNDRED_ETH, 817 | data: fb.into(), 818 | }.abi_encode(); 819 | 820 | let fb = FlowBuilder::empty() 821 | .set_fail() 822 | .set_callback(MORPHO) 823 | .call(MORPHO, &flashloan_calldata, U256::ZERO) 824 | .optimize() 825 | .build(); 826 | 827 | let tx = TransactionRequest::default() 828 | .with_from(WALLET) 829 | .with_to(executor) 830 | .with_value(U256::ZERO) 831 | .with_input(fb); 832 | 833 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 834 | provider.evm_mine(None).await.unwrap(); 835 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap().unwrap(); 836 | assert!(receipt.status()); 837 | } 838 | 839 | #[tokio::test] 840 | async fn test_flashloan_fails_without_callback() { 841 | let provider = setup_provider().await; 842 | let executor = deploy_executor(&provider).await; 843 | 844 | let approve_calldata = IERC20::approveCall { 845 | spender: MORPHO, 846 | value: ONEHUNDRED_ETH, 847 | }.abi_encode(); 848 | 849 | let fb = FlowBuilder::empty() 850 | .call(WETH9, &approve_calldata, U256::ZERO) 851 | .optimize() 852 | .build_raw(); 853 | 854 | let flashloan_calldata = IMorpho::flashLoanCall { 855 | token: WETH9, 856 | assets: ONEHUNDRED_ETH, 857 | data: fb.into(), 858 | }.abi_encode(); 859 | 860 | let fb = FlowBuilder::empty() 861 | .set_fail() 862 | .call(MORPHO, &flashloan_calldata, U256::ZERO) 863 | .optimize() 864 | .build(); 865 | 866 | let tx = TransactionRequest::default() 867 | .with_from(WALLET) 868 | .with_to(executor) 869 | .with_value(U256::ZERO) 870 | .with_input(fb); 871 | 872 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 873 | provider.evm_mine(None).await.unwrap(); 874 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap().unwrap(); 875 | assert!(!receipt.status()); 876 | } 877 | 878 | #[tokio::test] 879 | async fn test_multiple_flashloans_with_callback_reset() { 880 | let provider = setup_provider().await; 881 | let executor = deploy_executor(&provider).await; 882 | 883 | let approve_calldata = IERC20::approveCall { 884 | spender: MORPHO, 885 | value: ONEHUNDRED_ETH, 886 | }.abi_encode(); 887 | 888 | let fb = FlowBuilder::empty() 889 | .call(WETH9, &approve_calldata, U256::ZERO) 890 | .optimize() 891 | .build_raw(); 892 | 893 | let flashloan_calldata = IMorpho::flashLoanCall { 894 | token: WETH9, 895 | assets: ONEHUNDRED_ETH, 896 | data: fb.into(), 897 | }.abi_encode(); 898 | 899 | let fb = FlowBuilder::empty() 900 | .set_fail() 901 | .set_callback(MORPHO) 902 | .call(MORPHO, &flashloan_calldata, U256::ZERO) 903 | .set_callback(MORPHO) 904 | .call(MORPHO, &flashloan_calldata, U256::ZERO) 905 | .optimize() 906 | .build(); 907 | 908 | let tx = TransactionRequest::default() 909 | .with_from(WALLET) 910 | .with_to(executor) 911 | .with_value(U256::ZERO) 912 | .with_input(fb); 913 | 914 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 915 | provider.evm_mine(None).await.unwrap(); 916 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap().unwrap(); 917 | assert!(receipt.status()); 918 | } 919 | 920 | #[tokio::test] 921 | async fn test_multiple_flashloans_fails_without_callback_reset() { 922 | let provider = setup_provider().await; 923 | let executor = deploy_executor(&provider).await; 924 | 925 | let approve_calldata = IERC20::approveCall { 926 | spender: MORPHO, 927 | value: ONEHUNDRED_ETH, 928 | }.abi_encode(); 929 | 930 | let fb = FlowBuilder::empty() 931 | .call(WETH9, &approve_calldata, U256::ZERO) 932 | .optimize() 933 | .build_raw(); 934 | 935 | let flashloan_calldata = IMorpho::flashLoanCall { 936 | token: WETH9, 937 | assets: ONEHUNDRED_ETH, 938 | data: fb.into(), 939 | }.abi_encode(); 940 | 941 | let fb = FlowBuilder::empty() 942 | .set_fail() 943 | .set_callback(MORPHO) 944 | .call(MORPHO, &flashloan_calldata, U256::ZERO) 945 | .call(MORPHO, &flashloan_calldata, U256::ZERO) 946 | .optimize() 947 | .build(); 948 | 949 | let tx = TransactionRequest::default() 950 | .with_from(WALLET) 951 | .with_to(executor) 952 | .with_value(U256::ZERO) 953 | .with_input(fb); 954 | 955 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 956 | provider.evm_mine(None).await.unwrap(); 957 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap().unwrap(); 958 | assert!(!receipt.status()); 959 | } 960 | 961 | #[tokio::test] 962 | async fn test_nested_flashloan_success() { 963 | let provider = setup_provider().await; 964 | let executor = deploy_executor(&provider).await; 965 | 966 | let approve_calldata = IERC20::approveCall { 967 | spender: MORPHO, 968 | value: ONEHUNDRED_ETH, 969 | }.abi_encode(); 970 | 971 | let fb_approve = FlowBuilder::empty() 972 | .call(WETH9, &approve_calldata, U256::ZERO) 973 | .optimize() 974 | .build_raw(); 975 | 976 | let flashloan_calldata_inner = IMorpho::flashLoanCall { 977 | token: WETH9, 978 | assets: ONEHUNDRED_ETH, 979 | data: fb_approve.into(), 980 | }.abi_encode(); 981 | 982 | let fb_inner = FlowBuilder::empty() 983 | .set_fail() 984 | .set_callback(MORPHO) 985 | .call(MORPHO, &flashloan_calldata_inner, U256::ZERO) 986 | .call(WETH9, &approve_calldata, U256::ZERO) 987 | .optimize() 988 | .build_raw(); 989 | 990 | let flashloan_calldata_outer = IMorpho::flashLoanCall { 991 | token: WETH9, 992 | assets: ONEHUNDRED_ETH, 993 | data: fb_inner.into(), 994 | }.abi_encode(); 995 | 996 | let fb = FlowBuilder::empty() 997 | .set_fail() 998 | .set_callback(MORPHO) 999 | .call(MORPHO, &flashloan_calldata_outer, U256::ZERO) 1000 | .optimize() 1001 | .build(); 1002 | 1003 | let tx = TransactionRequest::default() 1004 | .with_from(WALLET) 1005 | .with_to(executor) 1006 | .with_value(U256::ZERO) 1007 | .with_input(fb); 1008 | 1009 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 1010 | provider.evm_mine(None).await.unwrap(); 1011 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap().unwrap(); 1012 | assert!(receipt.status()); 1013 | } 1014 | 1015 | #[tokio::test] 1016 | async fn test_nested_flashloan_fails_without_callback() { 1017 | let provider = setup_provider().await; 1018 | let executor = deploy_executor(&provider).await; 1019 | 1020 | let approve_calldata = IERC20::approveCall { 1021 | spender: MORPHO, 1022 | value: ONEHUNDRED_ETH, 1023 | }.abi_encode(); 1024 | 1025 | let fb_approve = FlowBuilder::empty() 1026 | .call(WETH9, &approve_calldata, U256::ZERO) 1027 | .optimize() 1028 | .build_raw(); 1029 | 1030 | let flashloan_calldata_inner = IMorpho::flashLoanCall { 1031 | token: WETH9, 1032 | assets: ONEHUNDRED_ETH, 1033 | data: fb_approve.into(), 1034 | }.abi_encode(); 1035 | 1036 | let fb_inner = FlowBuilder::empty() 1037 | .set_fail() 1038 | .call(MORPHO, &flashloan_calldata_inner, U256::ZERO) 1039 | .call(WETH9, &approve_calldata, U256::ZERO) 1040 | .optimize() 1041 | .build_raw(); 1042 | 1043 | let flashloan_calldata_outer = IMorpho::flashLoanCall { 1044 | token: WETH9, 1045 | assets: ONEHUNDRED_ETH, 1046 | data: fb_inner.into(), 1047 | }.abi_encode(); 1048 | 1049 | let fb = FlowBuilder::empty() 1050 | .set_fail() 1051 | .set_callback(MORPHO) 1052 | .call(MORPHO, &flashloan_calldata_outer, U256::ZERO) 1053 | .optimize() 1054 | .build(); 1055 | 1056 | let tx = TransactionRequest::default() 1057 | .with_from(WALLET) 1058 | .with_to(executor) 1059 | .with_value(U256::ZERO) 1060 | .with_input(fb); 1061 | 1062 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 1063 | provider.evm_mine(None).await.unwrap(); 1064 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap().unwrap(); 1065 | assert!(!receipt.status()); 1066 | } 1067 | 1068 | #[tokio::test] 1069 | /// Tests Aave V3 flash loan functionality with callback 1070 | /// 1071 | /// This test verifies: 1072 | /// 1. Flash loan execution with proper callback setup 1073 | /// 2. Premium calculation and payment (0.05%) 1074 | /// 3. WETH wrapping/unwrapping during the process 1075 | /// 4. Successful transaction completion and balance verification 1076 | async fn test_flashloan_aave3_success_with_callback() { 1077 | // Aave V3 Pool contract on mainnet 1078 | const AAVE3_POOL: Address = address!("87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"); 1079 | // Flash loan premium rate (0.05%) 1080 | const PREMIUM_FACTOR: U256 = uint!(500000000000000_U256); 1081 | 1082 | let provider = setup_provider().await; 1083 | let executor = deploy_executor(&provider).await; 1084 | 1085 | // Calculate premium for 100 ETH flash loan 1086 | let premium = ONEHUNDRED_ETH * PREMIUM_FACTOR / uint!(1000000000000000000_U256); 1087 | 1088 | // First send premium amount to executor so it can repay the flash loan 1089 | let tx = TransactionRequest::default() 1090 | .with_from(WALLET) 1091 | .with_to(executor) 1092 | .with_value(premium); 1093 | 1094 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 1095 | provider.evm_mine(None).await.unwrap(); 1096 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap().unwrap(); 1097 | assert!(receipt.status()); 1098 | 1099 | sol! { 1100 | interface IAavePool { 1101 | function flashLoanSimple( 1102 | address receiverAddress, 1103 | address asset, 1104 | uint256 amount, 1105 | bytes calldata params, 1106 | uint16 referralCode 1107 | ) external; 1108 | } 1109 | } 1110 | 1111 | // Build the repayment flow that will be executed in the callback 1112 | let repay_fb = FlowBuilder::empty() 1113 | // First deposit ETH to get WETH for the premium 1114 | .call(WETH9, &[], premium) 1115 | // Then transfer full amount back to Aave 1116 | .call(WETH9, &IERC20::approveCall { 1117 | spender: AAVE3_POOL, 1118 | value: ONEHUNDRED_ETH + premium, 1119 | }.abi_encode(), U256::ZERO) 1120 | .optimize() 1121 | .build_raw(); 1122 | 1123 | // Build flash loan call using proper ABI encoding 1124 | let flashloan_calldata = IAavePool::flashLoanSimpleCall { 1125 | receiverAddress: executor, 1126 | asset: WETH9, 1127 | amount: ONEHUNDRED_ETH, 1128 | params: repay_fb.into(), 1129 | referralCode: 0 1130 | }.abi_encode(); 1131 | 1132 | let fb = FlowBuilder::empty() 1133 | .set_fail() 1134 | .set_callback(AAVE3_POOL) 1135 | .call(AAVE3_POOL, &flashloan_calldata, U256::ZERO) 1136 | .optimize() 1137 | .build(); 1138 | 1139 | let tx = TransactionRequest::default() 1140 | .with_from(WALLET) 1141 | .with_to(executor) 1142 | .with_value(U256::ZERO) 1143 | .with_input(fb); 1144 | 1145 | let tx_hash = provider.eth_send_unsigned_transaction(tx).await.unwrap(); 1146 | provider.evm_mine(None).await.unwrap(); 1147 | let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap().unwrap(); 1148 | assert!(receipt.status()); 1149 | } 1150 | --------------------------------------------------------------------------------