Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 132 additions & 2 deletions apps/eip-7702-gas-sponsorship/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ head:

This quickstart gives you everything you need to simulate gas sponsorship on an Anvil fork using EIP-7702. A comprehensive guide with context on EIP-7702 and gas sponsorship can be found within our [docs](https://docs.berachain.com/developers/).

There are two parts to this guide:
There are three parts to this guide:

- **Part A**: Use `cast` to simulate a minimal EIP-7702 sponsorship flow with empty calldata.
- **Part B**: Use a full Solidity script to simulate delegation, signer validation, calldata execution, and sponsor reimbursement.
- **Part C**: Demonstrate ERC20-based gas reimbursement from EOA to Sponsor as part of the transaction.

---

Expand Down Expand Up @@ -271,7 +272,7 @@ Using a Foundry Solidity script gives you a lot:
The file is `SimpleDelegatePart2.s.sol`, and the main entry point is the `SimpleDelegate2Script` contract. You can run it using:

```bash
source .env && forge script script/SimpleDelegatePart2.s.sol:SimpleDelegate2Script \
source .env && forge script script/SimpleDelegatePart3.s.sol:SimpleDelegate2Script \
--rpc-url $TEST_RPC_URL \
--broadcast -vvvv
```
Expand Down Expand Up @@ -399,3 +400,132 @@ The below snippit from the output shows chat the EOA successfully burns a small
```

Finally, we have walked through the second part of gas sponsorship. Congrats! In the next gas-sponsorship guide expansion, we will walk through support for ERC20 payment flows.

### Part C — Sponsorship with ERC20 Token Reimbursement

This section showcases how an EOA can reimburse its sponsor in ERC20 tokens while the sponsor pays native gas. The flow combines EIP-7702 delegation, signed execution, and an ERC20 `transfer()` made within the implementation logic.

The changes in the solidity code, within `SimpleDelegatePart3.sol` are outlined below.

```solidity
function execute(
Call memory userCall,
address sponsor,
uint256 nonce,
bytes calldata signature,
address _token,
uint256 _tokenAmount
) external payable {

...
if(_tokenAmount > 0) {
bool tokenTransfer = IERC20(_token).transfer(sponsor, _tokenAmount);
if (!tokenTransfer) revert ERC20TokenPaymentFailed();
}

}
```

We have prepared a script to run that carried out all other steps mentioned in Parts B, and does so around this new implementation logic with a test token $TTKN, to showcase ERC20 transference within gas sponsored EIP-7702 transactions.

#### Step 1 — Run Script

```bash-vue
# FROM ./
source .env && forge script script/SimpleDelegatePart3.s.sol:SimpleDelegate3Script \
--rpc-url $TEST_RPC_URL \
--broadcast -vvvv
```

This script will:

- Deploy a test ERC20 token with `mint()` support
- Mint tokens to the EOA so it can transfer some to the Sponsor later
- Deploy the EIP-7702 implementation (`SimpleDelegatePart3`)
- Prepare a `burnNative()` call from EOA to Sponsor as the userCall
- Sign the call digest, and broadcast from the SPONSOR
- Observe gas reimbursement and ERC20 token payment

#### Step 2 - Check Results

Output includes:

```bash
source .env && forge script script/SimpleDelegatePart3.s.sol:SimpleDelegate3Script --rpc-url $TEST_RPC_URL --broadcast -vv
b4f2846fcf5e0785c863bb05ef86dc03784beaee71d64f5ad195c0364b61461c, 0x0000000000000000000000000000000000000060, 570334188278029218423514361678308014246523497324 [5.703e47]) └─ ← [Revert] EvmError: Revert

Error: Simulated execution failed.
Ichiraku-Macbook-Air:eip-7702-gas-sponsorship ichiraku$ source
.env && forge script script/SimpleDelegatePart3.s.sol:SimpleDelegate3Script --rpc-url $TEST_RPC_URL --broadcast -vv [⠊] Compiling...
No files changed, compilation skipped
Warning: EIP-3855 is not supported in one or more of the RPCs
used. Unsupported Chain IDs: 80069.
Contracts deployed with a Solidity version equal or higher tha
n 0.8.20 might not work properly. For more information, please see https://eips.ethereum.org/EIP
S/eip-3855 Script ran successfully.

== Logs ==
Sponsor balance (wei): 19997231719999031102
EOA token balance before: 1000000000000000000000
Sponsor token balance before: 0
Sponsor native balance before: 19997231719999031102
EOA token balance after: 995000000000000000000
Sponsor token balance after: 5000000000000000000
Sponsor native balance after: 19997156892998507313
---- Execution Summary ----
Sponsor Gas Spent (wei): 74827000523789
EOA Delta (wei): 0
Amount reimbursed to Sponsor (wei): 29925172999476211
---- Test Case 1: Replay with Same Nonce ----

---- Test Case 2: Replay with Wrong ChainID ----
Cross-chain replay failed as expected (invalid chainId in si
gnature).
## Setting up 1 EVM.
[45] 0x63E6ab65010C695805a3049546EF71e4A242EB6C::execute{val
ue: 30000000000000000}(Call({ data: 0xfbc7c433, to: 0x63E6ab65010C695805a3049546EF71e4A242EB6C, value: 10000000000000000 [1e16] }), 0x00195EFB66D39809EcE9AaBDa38172A5e603C0dE, 24, 0xee88108053564eb1d1d93025ca580847bdb0267102f71efb7ab19632a67508cf4707d6b5262a72bec4dbec17bc6ceb7808046e1051959db5acf508a195bd57731b, TestToken: [0xb7046b684d1D45B144964e5F12D85C60B65d864E], 5000000000000000000 [5e18]) └─ ← [Revert] EvmError: Revert

[45] 0x63E6ab65010C695805a3049546EF71e4A242EB6C::execute{val
ue: 30000000000000000}(Call({ data: 0xfbc7c433, to: 0x63E6ab65010C695805a3049546EF71e4A242EB6C, value: 10000000000000000 [1e16] }), 0x00195EFB66D39809EcE9AaBDa38172A5e603C0dE, 24, 0xee88108053564eb1d1d93025ca580847bdb0267102f71efb7ab19632a67508cf4707d6b5262a72bec4dbec17bc6ceb7808046e1051959db5acf508a195bd57731b, 0x0000000000000000000000000000000000000060, 570334188278029218423514361678308014246523497324 [5.703e47]) └─ ← [Revert] EvmError: Revert

[45] 0x63E6ab65010C695805a3049546EF71e4A242EB6C::execute{val
ue: 30000000000000000}(Call({ data: 0xfbc7c433, to: 0x63E6ab65

```

This proves the EOA reimbursed the SPONSOR in ERC20 while gas was paid in native currency by the SPONSOR. A transfer of 5e18 $TTKN was made from the EOA to the SPONSOR.

---

#### Step 3 - A Word on Batch and Inner Calls for Token Transferrence

Not only singular, but multiple token transfers can be carried out with EIP-7702 using batched inner calls. One could actually carry out ERC20 gas sponsorship via batched transactions. This guide does not walk through that execution path, but will touch on how it could be carried out. For more information on batched transactions, make sure to check out our other guide [here](https://docs.berachain.com/developers/guides/eip7702-batch-transactions).


**Example**

If `userCall.data` contains logic to execute multiple transfers or call other internal functions, they all run within the EOA context:

```solidity
Call[] memory calls = new Call[](2);
calls[0] = Call({
to: address(token1),
value: 0,
data: abi.encodeWithSelector(IERC20.transfer.selector, sponsor1, 1e18)
});
calls[1] = Call({
to: address(token2),
value: 0,
data: abi.encodeWithSelector(IERC20.transfer.selector, sponsor2, 2e18)
});

// Implementation logic loops through and executes each
for (uint256 i = 0; i < calls.length; i++) {
(bool ok, ) = calls[i].to.call{value: calls[i].value}(calls[i].data);
require(ok, "Inner call failed");
}
```

These patterns unlock multi-party sponsorship, ERC20 + native splits, and more, all enforceable inside the 7702 authorized contract code.

Congrats on getting through all three parts of the EIP-7702 Gas Sponsorship Guide! As always, make sure to provide any feedback to better improve this and all other guides.
2 changes: 1 addition & 1 deletion apps/eip-7702-gas-sponsorship/lib/forge-std
Submodule forge-std updated 3 files
+1 −1 README.md
+71 −1 src/Vm.sol
+1 −1 test/Vm.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ contract SimpleDelegate2Script is Script {
console.log("---- Test Case 1: Replay with Same Nonce ----");

vm.startBroadcast(SPONSOR_PK);
// vm.attachDelegation(signedDelegation);

(bool replaySuccess,) = EOA.call{value: transferAmount}(
abi.encodeWithSelector(SimpleDelegatePart2.execute.selector, call, SPONSOR, nonce, signature)
Expand All @@ -90,7 +89,7 @@ contract SimpleDelegate2Script is Script {
if (replaySuccess) {
console.log("Replay succeeded unexpectedly (should have failed due to nonce reuse).");
} else {
console.log("");
console.log("Replay failed as expected (Same Nonce).");
}

vm.stopBroadcast();
Expand All @@ -106,7 +105,6 @@ contract SimpleDelegate2Script is Script {
bytes memory forgedSignature = abi.encodePacked(fr, fs, fv);

vm.startBroadcast(SPONSOR_PK);
// vm.attachDelegation(signedDelegation);

(bool forgedSuccess,) = EOA.call{value: transferAmount}(
abi.encodeWithSelector(SimpleDelegatePart2.execute.selector, call, SPONSOR, nonce, forgedSignature)
Expand Down
156 changes: 156 additions & 0 deletions apps/eip-7702-gas-sponsorship/script/SimpleDelegatePart3.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {SimpleDelegatePart3} from "../src/SimpleDelegatePart3.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {Vm} from "forge-std/Vm.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/// @notice TestTokenContract
contract TestToken is ERC20 {
constructor() ERC20("TestToken", "TTKN") {
_mint(msg.sender, 1000e18);
}
}

/// @dev Run script on Bepolia: `source .env && forge script script/SimpleDelegatePart3.s.sol:SimpleDelegate3Script --rpc-url $BEPOLIA_RPC_URL --broadcast -vvvv`
/// @dev Run script on anvil fork: `source .env && forge script script/SimpleDelegatePart3.s.sol:SimpleDelegate3Script --rpc-url $TEST_RPC_URL --broadcast -vvvv`
contract SimpleDelegate3Script is Script {
address payable EOA;
uint256 EOA_PK;
address payable SPONSOR;
uint256 SPONSOR_PK;
SimpleDelegatePart3 public simpleDelegate;
address TOKEN;
uint256 constant TOKEN_TRANSFER_AMOUNT = 5e18;
ERC20 public testToken;

function run() public {
EOA = payable(vm.envAddress("EOA_WALLET1_ADDRESS"));
EOA_PK = vm.envUint("EOA_WALLET1_PK");
SPONSOR = payable(vm.envAddress("SPONSOR_WALLET2_ADDRESS"));
SPONSOR_PK = vm.envUint("SPONSOR_WALLET2_PK");

vm.startBroadcast(EOA_PK);
simpleDelegate = new SimpleDelegatePart3();
testToken = new TestToken();
vm.stopBroadcast();

TOKEN = address(testToken);

Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(simpleDelegate), EOA_PK);

uint256 burnAmount = 0.01 ether;
uint256 transferAmount = burnAmount + 0.02 ether; // extra buffer for gas reimbursement
uint256 nonce = simpleDelegate.getNonceToUse(vm.getNonce(EOA));

console.log("Sponsor balance (wei):", SPONSOR.balance);
require(SPONSOR.balance >= transferAmount, "Sponsor too poor for tx + value");

SimpleDelegatePart3.Call memory call = SimpleDelegatePart3.Call({
to: EOA,
value: burnAmount,
data: abi.encodeWithSelector(simpleDelegate.burnNative.selector)
});

bytes32 digest =
keccak256(abi.encodePacked(block.chainid, call.to, call.value, keccak256(call.data), SPONSOR, nonce));
bytes32 ethSigned = MessageHashUtils.toEthSignedMessageHash(digest);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(EOA_PK, ethSigned);
bytes memory signature = abi.encodePacked(r, s, v);

uint256 sponsorBefore = SPONSOR.balance;
uint256 eoaBefore = EOA.balance;

uint256 eoaTokenBefore = IERC20(TOKEN).balanceOf(EOA);
uint256 sponsorTokenBefore = IERC20(TOKEN).balanceOf(SPONSOR);
uint256 sponsorBalanceBefore = SPONSOR.balance;

console.log("EOA token balance before:", eoaTokenBefore);
console.log("Sponsor token balance before:", sponsorTokenBefore);
console.log("Sponsor native balance before:", sponsorBalanceBefore);

vm.startBroadcast(SPONSOR_PK);
vm.attachDelegation(signedDelegation);

bytes memory code = address(EOA).code;
require(code.length > 0, "no code written to EOA");

(bool success,) = EOA.call{value: transferAmount}(
abi.encodeWithSelector(
SimpleDelegatePart3.execute.selector, call, SPONSOR, nonce, signature, TOKEN, TOKEN_TRANSFER_AMOUNT
)
);
require(success, "Call to EOA smart account failed");

vm.stopBroadcast();

uint256 eoaTokenAfter = IERC20(TOKEN).balanceOf(EOA);
uint256 sponsorTokenAfter = IERC20(TOKEN).balanceOf(SPONSOR);
uint256 sponsorBalanceAfter = SPONSOR.balance;

console.log("EOA token balance after:", eoaTokenAfter);
console.log("Sponsor token balance after:", sponsorTokenAfter);
console.log("Sponsor native balance after:", sponsorBalanceAfter);

uint256 sponsorAfter = SPONSOR.balance;
uint256 eoaAfter = EOA.balance;

uint256 sponsorDelta = sponsorBefore > sponsorAfter ? sponsorBefore - sponsorAfter : 0;

uint256 actualReimbursement =
sponsorAfter > (sponsorBefore - transferAmount) ? sponsorAfter - (sponsorBefore - transferAmount) : 0;

uint256 eoaDelta = eoaAfter > eoaBefore ? eoaAfter - eoaBefore : 0;

console.log("---- Execution Summary ----");
console.log("Sponsor Gas Spent (wei):", sponsorDelta);
console.log("EOA Delta (wei):", eoaDelta);
console.log("Amount reimbursed to Sponsor (wei):", actualReimbursement);

console.log("---- Test Case 1: Replay with Same Nonce ----");

vm.startBroadcast(SPONSOR_PK);
// vm.attachDelegation(signedDelegation);

(bool replaySuccess,) = EOA.call{value: transferAmount}(
abi.encodeWithSelector(SimpleDelegatePart3.execute.selector, call, SPONSOR, nonce, signature)
);

if (replaySuccess) {
console.log("Replay succeeded unexpectedly (should have failed due to nonce reuse).");
} else {
console.log("");
}

vm.stopBroadcast();

console.log("---- Test Case 2: Replay with Wrong ChainID ----");

uint256 fakeChainId = 1; // Ethereum Mainnet

bytes32 forgedDigest =
keccak256(abi.encodePacked(fakeChainId, call.to, call.value, keccak256(call.data), SPONSOR, nonce));
bytes32 forgedEthSigned = MessageHashUtils.toEthSignedMessageHash(forgedDigest);
(uint8 fv, bytes32 fr, bytes32 fs) = vm.sign(EOA_PK, forgedEthSigned);
bytes memory forgedSignature = abi.encodePacked(fr, fs, fv);

vm.startBroadcast(SPONSOR_PK);
// vm.attachDelegation(signedDelegation);

(bool forgedSuccess,) = EOA.call{value: transferAmount}(
abi.encodeWithSelector(SimpleDelegatePart3.execute.selector, call, SPONSOR, nonce, forgedSignature)
);

if (forgedSuccess) {
console.log("Cross-chain replay succeeded unexpectedly (should have failed due to signature mismatch).");
} else {
console.log("Cross-chain replay failed as expected (invalid chainId in signature).");
}

vm.stopBroadcast();
}
}
Loading
Loading