Skip to content

Commit 56ed4bd

Browse files
committed
first commit
0 parents  commit 56ed4bd

File tree

8 files changed

+343
-0
lines changed

8 files changed

+343
-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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Compiler files
2+
cache/
3+
out/
4+
5+
# Ignores development broadcast logs
6+
!/broadcast
7+
/broadcast/*/31337/
8+
/broadcast/**/dry-run/
9+
10+
# Docs
11+
docs/
12+
13+
# Dotenv file
14+
.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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
| Ethereum Mainnet | [0xbd77a32e628e69d8b168d3813f019e51d787b569](https://etherscan.io/address/0xbd77a32e628e69d8b168d3813f019e51d787b569#code) |
14+
| Ethereum Sepolia | [0xbd77a32e628e69d8b168d3813f019e51d787b569](https://sepolia.etherscan.io/address/0xbd77a32e628e69d8b168d3813f019e51d787b569#code) |
15+
| Ethereum Holesky | [0xbd77a32e628e69d8b168d3813f019e51d787b569](https://holesky.etherscan.io/address/0xbd77a32e628e69d8b168d3813f019e51d787b569#code) |
16+
| Base | [0xbd77a32e628e69d8b168d3813f019e51d787b569](https://basescan.org/address/0xbd77a32e628e69d8b168d3813f019e51d787b569#code) |
17+
| Base Sepolia | [0xbd77a32e628e69d8b168d3813f019e51d787b569](https://sepolia.basescan.org/address/0xbd77a32e628e69d8b168d3813f019e51d787b569#code) |
18+
| Binance Smart Chain | [0xbd77a32e628e69d8b168d3813f019e51d787b569](https://bscscan.com/address/0xbd77a32e628e69d8b168d3813f019e51d787b569#code) |
19+
| Binance Testnet | [0xbd77a32e628e69d8b168d3813f019e51d787b569](https://testnet.bscscan.com/address/0xbd77a32e628e69d8b168d3813f019e51d787b569#code) |
20+
| Optimism | [0xbd77a32e628e69d8b168d3813f019e51d787b569](https://optimistic.etherscan.io/address/0xbd77a32e628e69d8b168d3813f019e51d787b569#code) |
21+
| Optimism Sepolia | [0xbd77a32e628e69d8b168d3813f019e51d787b569](https://sepolia-optimism.etherscan.io/address/0xbd77a32e628e69d8b168d3813f019e51d787b569#code) |
22+
23+
## Foundry
24+
25+
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
26+
27+
Foundry consists of:
28+
29+
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
30+
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
31+
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
32+
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
33+
34+
## Documentation
35+
36+
https://book.getfoundry.sh/
37+
38+
## Usage
39+
40+
### Build
41+
42+
```shell
43+
$ forge build
44+
```
45+
46+
### Test
47+
48+
```shell
49+
$ forge test
50+
```
51+
52+
### Format
53+
54+
```shell
55+
$ forge fmt
56+
```
57+
58+
### Gas Snapshots
59+
60+
```shell
61+
$ forge snapshot
62+
```
63+
64+
### Anvil
65+
66+
```shell
67+
$ anvil
68+
```
69+
70+
### Deploy
71+
72+
```shell
73+
$ forge create script/BasicSmartAccount.sol:BasicSmartAccount --rpc-url <your_rpc_url> --private-key <your_private_key>
74+
```
75+
76+
```shell
77+
forge verify-contract 0xbd77a32e628e69d8b168d3813f019e51d787b569 ./src/BasicSmartAccount.sol:BasicSmartAccount --verifier-url https://api-holesky.etherscan.io/api --etherscan-api-key <etherscan-private-key> --watch
78+
```
79+
80+
### Cast
81+
82+
```shell
83+
$ cast <subcommand>
84+
```
85+
86+
### Help
87+
88+
```shell
89+
$ forge --help
90+
$ anvil --help
91+
$ cast --help
92+
```

foundry.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[profile.default]
2+
src = "src"
3+
out = "out"
4+
libs = ["lib"]
5+
6+
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-License-Identifier : MIT
2+
pragma solidity ^0.8.18;
3+
4+
import {Script} from "forge-std/Script.sol";
5+
import {BasicSmartAccount} from "../src/BasicSmartAccount.sol";
6+
7+
contract DeployBasicSmartAccount is Script {
8+
function run() external {
9+
vm.startBroadcast();
10+
new BasicSmartAccount();
11+
vm.stopBroadcast();
12+
}
13+
}

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.29;
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+
}

test/BasicSmartAccount.t.sol

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.29;
3+
4+
import {Test, console} from "forge-std/Test.sol";
5+
import {BasicSmartAccount} from "../src/BasicSmartAccount.sol";
6+
7+
error InvalidSignature();
8+
9+
contract BasicSmartAccountTest is Test {
10+
BasicSmartAccount public BasicSmartAccount;
11+
12+
// test vector from holesky
13+
bytes userOps =
14+
0x91325d5b27a4895bfaca49f50eed2a364127b4ba00000000000000000000000000000000000000000000000000000000000003e80000000000000000000000000000000000000000000000000000000000000000685ce6742351ae9b618f383883d6d1e0c5a31b4b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000091325d5b27a4895bfaca49f50eed2a364127b4ba0000000000000000000000000000000000000000000000000000000000000064685ce6742351ae9b618f383883d6d1e0c5a31b4b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000091325d5b27a4895bfaca49f50eed2a364127b4ba00000000000000000000000000000000000000000000000000000000000000ea685ce6742351ae9b618f383883d6d1e0c5a31b4b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000091325d5b27a4895bfaca49f50eed2a364127b4ba0000000000000000000000000000000000000000000000000000000000000237;
15+
uint256 r =
16+
0x00e51e31e24314ee13f992f062fc8670fb8c237114c11c2ea5915032fd2e8f6c;
17+
uint256 vs =
18+
0x93d2cd1867dd53ce2de74b570fda2950f14f727d10ddf4be102328092d4598f1;
19+
20+
function setUp() public {
21+
// Holesky chain id
22+
vm.chainId(17000);
23+
address contractAddress = 0x1365105c3DbF6F434809E10328115ECc352D60E7;
24+
deployCodeTo("BasicSmartAccount.sol", contractAddress);
25+
dfnsSmartAccount = DfnsSmartAccount(contractAddress);
26+
}
27+
28+
function test_handleOps() public {
29+
assertEq(dfnsSmartAccount.getNonce(), 0);
30+
dfnsSmartAccount.handleOps(userOps, r, vs);
31+
assertEq(dfnsSmartAccount.getNonce(), 1);
32+
}
33+
34+
function test_handleOpsWrongSignature() public {
35+
assertEq(dfnsSmartAccount.getNonce(), 0);
36+
vm.expectRevert(InvalidSignature.selector);
37+
dfnsSmartAccount.handleOps(userOps, r, vs + 1);
38+
assertEq(dfnsSmartAccount.getNonce(), 0);
39+
}
40+
41+
function test_handleOpsReplayProtection() public {
42+
assertEq(dfnsSmartAccount.getNonce(), 0);
43+
dfnsSmartAccount.handleOps(userOps, r, vs);
44+
vm.expectRevert(InvalidSignature.selector);
45+
dfnsSmartAccount.handleOps(userOps, r, vs);
46+
assertEq(dfnsSmartAccount.getNonce(), 1);
47+
}
48+
}

0 commit comments

Comments
 (0)