Skip to content

Commit ac85ca6

Browse files
committed
Batch Call & Sponsor
1 parent 9a129e1 commit ac85ca6

File tree

9 files changed

+544
-94
lines changed

9 files changed

+544
-94
lines changed

README-Foundry.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
## Foundry
2+
3+
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
4+
5+
Foundry consists of:
6+
7+
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
8+
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
9+
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
10+
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
11+
12+
## Documentation
13+
14+
https://book.getfoundry.sh/
15+
16+
## Usage
17+
18+
### Build
19+
20+
```shell
21+
$ forge build
22+
```
23+
24+
### Test
25+
26+
```shell
27+
$ forge test
28+
```
29+
30+
### Format
31+
32+
```shell
33+
$ forge fmt
34+
```
35+
36+
### Gas Snapshots
37+
38+
```shell
39+
$ forge snapshot
40+
```
41+
42+
### Anvil
43+
44+
```shell
45+
$ anvil
46+
```
47+
48+
### Deploy
49+
50+
```shell
51+
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
52+
```
53+
54+
### Cast
55+
56+
```shell
57+
$ cast <subcommand>
58+
```
59+
60+
### Help
61+
62+
```shell
63+
$ forge --help
64+
$ anvil --help
65+
$ cast --help
66+
```

README.md

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,101 @@
1-
## Foundry
1+
# BatchCallAndSponsor
22

3-
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
3+
An educational project demonstrating account abstraction and sponsored transaction execution using EIP-7702. This project uses Foundry for deployment, scripting, and testing.
44

5-
Foundry consists of:
5+
## Overview
66

7-
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
8-
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
9-
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
10-
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
7+
The `BatchCallAndSponsor` contract enables batch execution of calls by verifying signatures over a nonce and batched call data. It supports:
8+
- **Direct execution**: by the smart account itself.
9+
- **Sponsored execution**: via an off-chain signature (by a sponsor).
1110

12-
## Documentation
11+
Replay protection is provided by an internal nonce that increments after each batch execution.
1312

14-
https://book.getfoundry.sh/
13+
## Features
1514

16-
## Usage
15+
- Batch transaction execution
16+
- Off-chain signature verification using ECDSA
17+
- Replay protection through nonce incrementation
18+
- Support for both ETH and ERC-20 token transfers
1719

18-
### Build
20+
## Prerequisites
1921

20-
```shell
21-
$ forge build
22-
```
22+
- [Foundry](https://github.com/foundry-rs/foundry)
23+
- Solidity ^0.8.20
24+
25+
## Running the Project
2326

24-
### Test
27+
### Step 1: Install Foundry
2528

26-
```shell
27-
$ forge test
29+
```sh
30+
curl -L https://foundry.paradigm.xyz | bash
31+
git clone https://github.com/quiknode-labs/qn-guide-examples.git
32+
cd qn-guide-examples/ethereum/eip-7702
2833
```
2934

30-
### Format
35+
### Step 2: Install Packages and Create the Remappings File
3136

32-
```shell
33-
$ forge fmt
37+
```sh
38+
forge install OpenZeppelin/openzeppelin-contracts
39+
forge install foundry-rs/forge-std
40+
forge remappings > remappings.txt
3441
```
3542

36-
### Gas Snapshots
43+
### Step 3: Run a Local Network
44+
45+
Run the following command on your terminal to start a local network with the Prague hardfork.
3746

38-
```shell
39-
$ forge snapshot
47+
```bash
48+
anvil --hardfork prague
4049
```
4150

42-
### Anvil
51+
### Step 4: Build the Contract
4352

44-
```shell
45-
$ anvil
53+
On another terminal, run the following command to build the contract.
54+
55+
```bash
56+
forge build
4657
```
4758

48-
### Deploy
59+
### Step 5: Run the Test Cases
60+
61+
After building the contract, run the following command to run the test cases. If you want to display stack traces for all tests, use `-vvvv` flag instead of `-vvv`.
4962

50-
```shell
51-
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
63+
```bash
64+
forge test -vvv
5265
```
5366

54-
### Cast
67+
The output should look like this:
68+
69+
```bash
70+
Ran 4 tests for test/BatchCallAndSponsor.t.sol:BatchCallAndSponsorTest
71+
[PASS] testDirectExecution() (gas: 128386)
72+
Logs:
73+
Sending 1 ETH from Alice to Bob and transferring 100 tokens to Bob in a single transaction
5574

56-
```shell
57-
$ cast <subcommand>
75+
[PASS] testReplayAttack() (gas: 114337)
76+
Logs:
77+
Test replay attack: Reusing the same signature should revert.
78+
79+
[PASS] testSponsoredExecution() (gas: 110461)
80+
Logs:
81+
Sending 1 ETH from Alice to a random address while the transaction is sponsored by Bob
82+
83+
[PASS] testWrongSignature() (gas: 37077)
84+
Logs:
85+
Test wrong signature: Execution should revert with 'Invalid signature'.
86+
87+
Suite result: ok. 4 passed; 0 failed; 0 skipped;
5888
```
5989
60-
### Help
90+
#### Step 6: Run the Script
91+
92+
Now that you’ve set up the project, it’s time to run the deployment script. This script deploys the contract, mints tokens, and tests both batch execution and sponsored execution features.
93+
94+
We use the following command:
95+
- **`--broadcast`**: Broadcasts the transactions to your local network.
96+
- **`--rpc-url 127.0.0.1:8545`**: Connects to your local network.
97+
- **`--tc BatchCallAndSponsorScript`**: Specifies the target contract for the script.
6198
62-
```shell
63-
$ forge --help
64-
$ anvil --help
65-
$ cast --help
99+
```bash
100+
forge script ./script/BatchCallAndSponsor.s.sol --tc BatchCallAndSponsorScript --broadcast --rpc-url 127.0.0.1:8545
66101
```

remappings.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
2+
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
3+
forge-std/=lib/forge-std/src/
4+
halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/
5+
openzeppelin-contracts/=lib/openzeppelin-contracts/

script/BatchCallAndSponsor.s.sol

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import "forge-std/Script.sol";
5+
import {Vm} from "forge-std/Vm.sol";
6+
import {BatchCallAndSponsor} from "../src/BatchCallAndSponsor.sol";
7+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
8+
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
9+
10+
contract MockERC20 is ERC20 {
11+
constructor() ERC20("Mock Token", "MOCK") {}
12+
13+
function mint(address to, uint256 amount) external {
14+
_mint(to, amount);
15+
}
16+
}
17+
18+
contract BatchCallAndSponsorScript is Script {
19+
// Alice's address and private key (EOA with no initial contract code).
20+
address payable ALICE_ADDRESS = payable(0x70997970C51812dc3A010C7d01b50e0d17dc79C8);
21+
uint256 constant ALICE_PK = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;
22+
23+
// Bob's address and private key (Bob will execute transactions on Alice's behalf).
24+
address constant BOB_ADDRESS = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;
25+
uint256 constant BOB_PK = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a;
26+
27+
// The contract that Alice will delegate execution to.
28+
BatchCallAndSponsor public implementation;
29+
30+
// ERC-20 token contract for minting test tokens.
31+
MockERC20 public token;
32+
33+
function run() external {
34+
// Start broadcasting transactions with Alice's private key.
35+
vm.startBroadcast(ALICE_PK);
36+
37+
// Deploy the delegation contract (Alice will delegate calls to this contract).
38+
implementation = new BatchCallAndSponsor();
39+
40+
// Deploy an ERC-20 token contract where Alice is the minter.
41+
token = new MockERC20();
42+
43+
// // Fund accounts
44+
token.mint(ALICE_ADDRESS, 1000e18);
45+
46+
vm.stopBroadcast();
47+
48+
// Perform direct execution
49+
performDirectExecution();
50+
51+
// Perform sponsored execution
52+
performSponsoredExecution();
53+
}
54+
55+
function performDirectExecution() internal {
56+
BatchCallAndSponsor.Call[] memory calls = new BatchCallAndSponsor.Call[](2);
57+
58+
// ETH transfer
59+
calls[0] = BatchCallAndSponsor.Call({to: BOB_ADDRESS, value: 1 ether, data: ""});
60+
61+
// Token transfer
62+
calls[1] = BatchCallAndSponsor.Call({
63+
to: address(token),
64+
value: 0,
65+
data: abi.encodeCall(ERC20.transfer, (BOB_ADDRESS, 100e18))
66+
});
67+
68+
vm.signAndAttachDelegation(address(implementation), ALICE_PK);
69+
vm.startPrank(ALICE_ADDRESS);
70+
BatchCallAndSponsor(ALICE_ADDRESS).execute(calls);
71+
vm.stopPrank();
72+
73+
console.log("Bob's balance after direct execution:", BOB_ADDRESS.balance);
74+
console.log("Bob's token balance after direct execution:", token.balanceOf(BOB_ADDRESS));
75+
}
76+
77+
function performSponsoredExecution() internal {
78+
console.log("Sending 1 ETH from Alice to a random address, the transaction is sponsored by Bob");
79+
80+
BatchCallAndSponsor.Call[] memory calls = new BatchCallAndSponsor.Call[](1);
81+
address recipient = makeAddr("recipient");
82+
calls[0] = BatchCallAndSponsor.Call({to: recipient, value: 1 ether, data: ""});
83+
84+
// Alice signs a delegation allowing `implementation` to execute transactions on her behalf.
85+
Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), ALICE_PK);
86+
87+
// Bob attaches the signed delegation from Alice and broadcasts it.
88+
vm.startBroadcast(BOB_PK);
89+
vm.attachDelegation(signedDelegation);
90+
91+
// Verify that Alice's account now temporarily behaves as a smart contract.
92+
bytes memory code = address(ALICE_ADDRESS).code;
93+
require(code.length > 0, "no code written to Alice");
94+
// console.log("Code on Alice's account:", vm.toString(code));
95+
96+
bytes memory encodedCalls = "";
97+
for (uint256 i = 0; i < calls.length; i++) {
98+
encodedCalls = abi.encodePacked(encodedCalls, calls[i].to, calls[i].value, calls[i].data);
99+
}
100+
bytes32 digest = keccak256(abi.encodePacked(BatchCallAndSponsor(ALICE_ADDRESS).nonce(), encodedCalls));
101+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ALICE_PK, MessageHashUtils.toEthSignedMessageHash(digest));
102+
bytes memory signature = abi.encodePacked(r, s, v);
103+
104+
// As Bob, execute the transaction via Alice's temporarily assigned contract.
105+
BatchCallAndSponsor(ALICE_ADDRESS).execute(calls, signature);
106+
107+
vm.stopBroadcast();
108+
109+
console.log("Recipient balance after sponsored execution:", recipient.balance);
110+
}
111+
}

script/Counter.s.sol

Lines changed: 0 additions & 19 deletions
This file was deleted.

0 commit comments

Comments
 (0)