Skip to content

Commit 75ac733

Browse files
committed
first commit
0 parents  commit 75ac733

File tree

9 files changed

+400
-0
lines changed

9 files changed

+400
-0
lines changed

.github/workflows/test.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
8+
env:
9+
FOUNDRY_PROFILE: ci
10+
11+
jobs:
12+
check:
13+
strategy:
14+
fail-fast: true
15+
16+
name: Foundry project
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
submodules: recursive
22+
23+
- name: Install Foundry
24+
uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de
25+
26+
- name: Show Forge version
27+
run: |
28+
forge --version
29+
30+
- name: Run Forge fmt
31+
run: |
32+
forge fmt --check
33+
id: fmt
34+
35+
- name: Run Forge build
36+
run: |
37+
forge build --sizes
38+
id: build
39+
40+
- name: Run Forge tests
41+
run: |
42+
forge test -vvv
43+
id: test

.gitignore

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Compiler files
2+
cache/
3+
out/
4+
5+
# Ignores development broadcast logs
6+
!/broadcast
7+
/broadcast/*/31337/
8+
/broadcast/**/dry-run/
9+
broadcast/
10+
11+
# Docs
12+
docs/
13+
14+
# Dotenv file
15+
.env

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "lib/forge-std"]
2+
path = lib/forge-std
3+
url = https://github.com/foundry-rs/forge-std

README.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Basic Smart Account
2+
3+
This smart contract uses the SafeLite example: https://github.com/5afe/safe-eip7702/blob/main/safe-eip7702-contracts/contracts/experimental/SafeLite.sol
4+
It was stripped from all unecessary logic to only keep the batch functionality.
5+
It uses no dependencies and relies on some assembly to save on gas usage.
6+
7+
DFNS is also using a modfied [SafeLite example and completed an audit for it](https://github.com/dfns/dfns-smart-account).
8+
9+
It is deployed on the following chains:
10+
11+
| Blockchain | Contract Address |
12+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
13+
| Arbitrum | [0x19A6c30398c5276466Bf13AF2Ba568ef75026B86](https://arbiscan.io/address/0x19A6c30398c5276466Bf13AF2Ba568ef75026B86#code) |
14+
| Base | [0x19A6c30398c5276466Bf13AF2Ba568ef75026B86](https://basescan.org/address/0x19A6c30398c5276466Bf13AF2Ba568ef75026B86#code) |
15+
| Berachain | [0x19A6c30398c5276466Bf13AF2Ba568ef75026B86](https://berascan.org/address/0x19A6c30398c5276466Bf13AF2Ba568ef75026B86#code) |
16+
| Binance Smart Chain | [0x19A6c30398c5276466Bf13AF2Ba568ef75026B86](https://bscscan.com/address/0x19A6c30398c5276466Bf13AF2Ba568ef75026B86#code) |
17+
| Ethereum Mainnet | [0x19A6c30398c5276466Bf13AF2Ba568ef75026B86](https://etherscan.io/address/0x19A6c30398c5276466Bf13AF2Ba568ef75026B86#code) |
18+
| Ethereum Holesky | [0x19A6c30398c5276466Bf13AF2Ba568ef75026B86](https://holesky.etherscan.io/address/0x19A6c30398c5276466Bf13AF2Ba568ef75026B86#code) |
19+
| Optimism | [0x19A6c30398c5276466Bf13AF2Ba568ef75026B86](https://optimistic.etherscan.io/address/0x19A6c30398c5276466Bf13AF2Ba568ef75026B86#code) |
20+
21+
## EIP 7702
22+
23+
### Overview
24+
25+
EIP7702 allows EOA addresses to be treated as contract addresses. This is introduced as a new EVM transaction type which adds
26+
a new field called "authorization list".
27+
28+
Each authorization in the list makes a temporary delegation to a contract address for some EOA. This authorization must be signed by that EOA.
29+
It is temporary in that the EOA can be delegated to this contract only in transactions containing a proper authorization for it.
30+
31+
Note that the transaction itself does not need to be signed by the EOA's being delegated. So a "fee-payer" address can craft a transaction
32+
containing these authorizations for other EOA's and thus sponsor actions for them.
33+
34+
### EIP 7702 contract
35+
36+
For the authorized EOA, this contract is executed as if EOA is the contract. Meaning the contract can move funds, call other contracts for the EOA.
37+
Thus it's important that the EOA additionally signs the actions done by the contract (stored in the `data` of the transaction). This protects the EOA
38+
from the "fee-payer" address inserting arbitrary actions.
39+
40+
The input to `BasicSmartAccount` is a list of `(to address, value uint256, data bytes)` tuples, all of which are signed by the EOA. The
41+
input is simply verified and executed.
42+
43+
In summary:
44+
45+
- Any EOA can delegate to `BasicSmartAccount` contract by signing an authorization.
46+
- By delegating to `BasicSmartAccount` in a transaction, an EOA is trusting `BasicSmartAccount` to correctly execute any signed `(address, value, data)` tuples included in that same transaction.
47+
- The transaction containing the authorization list and `(address, value, data)` data itself can be signed by a separate address. This address pays the fees.
48+
49+
## Foundry
50+
51+
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
52+
53+
Foundry consists of:
54+
55+
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
56+
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
57+
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
58+
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
59+
60+
## Documentation
61+
62+
https://book.getfoundry.sh/
63+
64+
## Usage
65+
66+
### Build
67+
68+
```shell
69+
forge build
70+
```
71+
72+
### Test
73+
74+
```shell
75+
forge test
76+
```
77+
78+
### Format
79+
80+
```shell
81+
forge fmt
82+
```
83+
84+
### Gas Snapshots
85+
86+
```shell
87+
forge snapshot
88+
```
89+
90+
### Anvil
91+
92+
```shell
93+
anvil
94+
```
95+
96+
### Deploy
97+
98+
Deployment is done using deterministic `CREATE2` instruction. Meaning any address can deploy this contract to a chain (for the given `EVM_SALT`),
99+
and it will have the expected contract address. Note that if any part of the contract changes, then the contract address will also be different.
100+
101+
```shell
102+
# keccak256("BasicSmartAccount")
103+
export EVM_SALT=bdfee0231e0903cde9ca6fd75d08a500062dc3d87718f712bc6958ed697617c3
104+
forge script ./script/DeployBasicSmartAccount.s.sol --rpc-url <your_rpc_url> --private-key <your_private_key>
105+
```
106+
107+
Add `--broadcast` when you are ready to transmit for real.
108+
109+
### Cast
110+
111+
```shell
112+
$ cast <subcommand>
113+
```
114+
115+
### Help
116+
117+
```shell
118+
$ forge --help
119+
$ anvil --help
120+
$ cast --help
121+
```

foundry.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[profile.default]
2+
src = "src"
3+
out = "out"
4+
libs = ["lib"]
5+
6+
# Pin these settings to ensure deterministic contract address
7+
solc = "0.8.30"
8+
9+
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

lib/forge-std

Submodule forge-std added at 77041d2
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier : MIT
2+
pragma solidity ^0.8.18;
3+
4+
import {Script} from "forge-std/Script.sol";
5+
import {console} from "forge-std/console.sol";
6+
import {BasicSmartAccount} from "../src/BasicSmartAccount.sol";
7+
8+
contract DeployBasicSmartAccount is Script {
9+
function run() external {
10+
bytes32 salt = vm.envBytes32("EVM_SALT");
11+
12+
vm.startBroadcast();
13+
BasicSmartAccount impl = new BasicSmartAccount{salt: salt}();
14+
vm.stopBroadcast();
15+
16+
console.log("contract address: ", address(impl));
17+
}
18+
}

src/BasicSmartAccount.sol

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// SPDX-License-Identifier: LGPL-3.0-only
2+
pragma solidity ^0.8.30;
3+
4+
/**
5+
* @title BasicSmartAccount - This contract support batch execution of transactions.
6+
* The only storage is a nonce to prevent replay attacks.
7+
* The contract is intended to be used with EIP-7702 where EOA delegates to this contract.
8+
*/
9+
contract BasicSmartAccount {
10+
struct Storage {
11+
uint256 nonce;
12+
}
13+
14+
// Reserve a unique storage slot for the nonce.
15+
// * keccak256("BasicSmartAccount") & (~0xff)
16+
bytes32 private constant _STORAGE =
17+
0xbdfee0231e0903cde9ca6fd75d08a500062dc3d87718f712bc6958ed69761700;
18+
19+
// Domain typehash for EIP712 message.
20+
// * keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");
21+
bytes32 private constant _DOMAIN_TYPEHASH =
22+
0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218;
23+
24+
// The struct typehash for the EIP712 message.
25+
// * keccak256("HandleOps(bytes32 data,uint256 nonce)")
26+
bytes32 private constant _HANDLEOPS_TYPEHASH =
27+
0x4f8bb4631e6552ac29b9d6bacf60ff8b5481e2af7c2104fe0261045fa6988111;
28+
29+
address private immutable ENTRY_POINT;
30+
31+
error InvalidSignature();
32+
33+
/**
34+
* @dev Sends multiple transactions with signature validation and reverts all if one fails.
35+
* @param userOps Encoded User Ops.
36+
* @param r The r part of the signature.
37+
* @param vs The v and s part of the signature.
38+
*/
39+
function handleOps(
40+
bytes memory userOps,
41+
uint256 r,
42+
uint256 vs
43+
) public payable {
44+
Storage storage $ = _storage();
45+
uint256 nonce = $.nonce;
46+
47+
// Calculate the hash of transactions data and nonce for signature verification
48+
bytes32 domainSeparator = keccak256(
49+
abi.encode(_DOMAIN_TYPEHASH, block.chainid, address(this))
50+
);
51+
52+
bytes32 structHash = keccak256(
53+
abi.encode(_HANDLEOPS_TYPEHASH, keccak256(userOps), nonce)
54+
);
55+
bytes32 digest = keccak256(
56+
abi.encodePacked("\x19\x01", domainSeparator, structHash)
57+
);
58+
59+
// Verify the signature of EIP712 message
60+
require(_isValidSignature(digest, r, vs), InvalidSignature());
61+
62+
// Update nonce for the sender to prevent replay attacks
63+
unchecked {
64+
$.nonce = nonce + 1;
65+
}
66+
67+
/* solhint-disable no-inline-assembly */
68+
assembly ("memory-safe") {
69+
let length := mload(userOps)
70+
let i := 0x20
71+
for {
72+
73+
} lt(i, length) {
74+
75+
} {
76+
let to := shr(0x60, mload(add(userOps, i)))
77+
let value := mload(add(userOps, add(i, 0x14)))
78+
let dataLength := mload(add(userOps, add(i, 0x34)))
79+
let data := add(userOps, add(i, 0x54))
80+
let success := call(gas(), to, value, data, dataLength, 0, 0)
81+
82+
if eq(success, 0) {
83+
returndatacopy(0, 0, returndatasize())
84+
revert(0, returndatasize())
85+
}
86+
i := add(i, add(0x54, dataLength))
87+
}
88+
}
89+
/* solhint-enable no-inline-assembly */
90+
}
91+
92+
/**
93+
* @dev Validates the signature by extracting `v` and `s` from `vs` and using `ecrecover`.
94+
* @param hash The hash of the signed data.
95+
* @param r The r part of the signature.
96+
* @param vs The v and s part of the signature combined.
97+
* @return bool True if the signature is valid, false otherwise.
98+
*/
99+
function _isValidSignature(
100+
bytes32 hash,
101+
uint256 r,
102+
uint256 vs
103+
) internal view returns (bool) {
104+
unchecked {
105+
uint256 v = (vs >> 255) + 27;
106+
uint256 s = vs &
107+
0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
108+
109+
return
110+
address(this) ==
111+
ecrecover(hash, uint8(v), bytes32(r), bytes32(s));
112+
}
113+
}
114+
115+
function _storage() private pure returns (Storage storage $) {
116+
assembly ("memory-safe") {
117+
$.slot := _STORAGE
118+
}
119+
}
120+
121+
function getNonce() external view returns (uint256) {
122+
return _storage().nonce;
123+
}
124+
}

0 commit comments

Comments
 (0)