├── .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 |

3 |
4 | # Multiplexer
5 |
6 | [](https://opensource.org/licenses/MIT)
7 | [](https://crates.io/crates/multiplexer-evm)
8 | [](https://docs.rs/multiplexer-evm)
9 | [](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 |
--------------------------------------------------------------------------------