Skip to content

Commit

Permalink
feat: Splits smart vaults (#51)
Browse files Browse the repository at this point in the history
* feat: create smart vaults package

* feat: add simple account and factory from eth-infitism/aa

* feat: restructure smart vault

* feat: restructure smart vault factory

* feat: multisig support with passkey and eoa signers

* feat: add root owner tests

* feat: add multi signer tests

* fix: signer cap

* feat: add factory tests

* feat: validateUserOp tests

* feat: execute tests

* feat: create functionality

* feat: avoid signing over gas userop until last signature

* chore: update docs

* Light state sync and multi user op (#28)

* feat: light state sync first pass

* feat: 1271 light state sync support

* fix: tests

* chore: light state sync tests

* chore: erc1271 tests

* feat: multiChain user op

* lil refactoring

* minor changes

* feat: light multi user op

* fix: rename to getX()

* fix: set nonce if and only if nonce is greater than storage

* fix: revert when trying to remove empty signer

* fix: minor fixes

* fix: capitalize immutables

* feat: rootOwner -> solay/Ownable

* fix: _nonce to _salt

* feat: add styling guide

* chore: gas report

* feat: don't return early in signature verification flows

* feat: add missing event in initialize flow

* fix: pass light hash directly

* feat: optimize isValidSignature flow

* fix: use constants

* fix: add constants to multiSignerLib

* chore: add more docs

* chore: add more initialization tests

* chore: add more user op signature tests

* chore: add more erc1271 signature tests

* feat: strip down light sync (#30)

* feat: strip down light sync

* fix: clean up assembly blocks

* fix: docs

* fix: address comments

* fix: address comment

* feat: remove onlySelfOrOwner

* docs: update signature scheme

* feat: add fallback module (#31)

* feat: add fallback module

* feat: add custom receive fn

* fix: remove code check when validating signer

* chore: rename bundle to merkelized

* fix: remove redundant brackets

* chore: remove via-ir for default profile

* feat: redo signature for user op and erc1271 (#33)

* feat: redo signature for user op and erc1271

* minor changes

* minor changes

* Refactor SmartVault (#34)

* fix compiler warning

* create AccountSigner and PasskeySigner

* move nonce logic to LightSync module

* use standard capitalization

* fix enum variant

* redesign signature of _validate functions

* deduplicate merkle logic

* remove redundant code

* deduplicate multisig logic

* reuse validation function

* merge multisig logic with and without in-memory updates

* feat: rip light sync

---------

Co-authored-by: Francisco <[email protected]>

* feat: operator manager (#32)

* feat: operator manager first pass

* feat: operator manager

* docs: minor update

* fix: address comments

* chore: operator -> module

* fix: tests

* fix: name

* chore: merge multiSignerSignatureLib to multiSignerLib

* fix: add comments in user op validation

* fix: minor doc updates

* chore: minor doc updates

* feat: update storage layout of signer (#36)

* feat: update storage layout of signer

* fix: minor fixes

* fix: minor fixes

* fix: address comments

* fix: address comments

* fix: using  MultiSignerLib

* fix: use global and remove unneeded private functions

* fix: use $.isValidSignature

* fix: update isPasskey logic

* fix: address will's comments

* docs: add security contact

* fix: add storage location fallback and module manager

* fix: change updateFallbackHandler modifier public -> external

* chore: add slither and remove dead code

* feat: multi signer auth (#38)

* feat: multi signer auth

* fix: docs:
:

* fix: add suggested doc change

* chore: update readme (#39)

* chore: update readme

* fix: address comments

* chore: add dummy passkey signature test

* fix: clean up multi signer auth (#40)

* fix: clean up multi signer auth

* feat: mapping to array

* fix: address comment

* feat: update _checkOwner() (#41)

* feat: add fallback handler called event (#42)

* chore: doc updates

* chore: doc updates

* test: add missing fallback test

* feat: add upper limit to gas price in light user op hash (#43)

* feat: add upper limit to gas price in light user op hash

* fix: update comment

* feat: add preVerification and callGas limits

* feat: add verification gas limit

* fix: docs

* feat: add paymaster limits

* fix: use paymasterVerificationGasLimit

* fix: ordering of params

* fix: prevent empty signers validating signature (#45)

* fix: revert when duplicate signer is used (#46)

* fix: revert when duplicate signer is used

* fix: move check one level out

* fix: verify light merkle root when threshold is > 1 (#47)

* fix: verify light merkle root when threshold is > 1

* fix: update doc

* feat: deployment script (#44)

* feat: custom webauthn (#50)

* chore: add audit report

* fix: lint

---------

Co-authored-by: Francisco <[email protected]>
  • Loading branch information
r0ohafza and frangio authored Sep 4, 2024
1 parent 3a66c93 commit 1c7fdc4
Show file tree
Hide file tree
Showing 40 changed files with 8,134 additions and 297 deletions.
8 changes: 8 additions & 0 deletions Styling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

# Styling Guide

* Comments must have periods after every sentence.
* $ is reserved for storage variables in contracts. If it a private or internal storage variable, prefix it with $_.
* Underscore prefix are reserved for private and internal functions and variables in contracts.
* Underscore suffix are reserved for function params.
* Constant and Immutable must be in ALL_CAPS. If it is a private or internal constant in a contract, prefix it with an underscore.
Binary file added audits/splits-smartVaults.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "splits-v2",
"name": "splits-monorepo",
"private": true,
"scripts": {
"build": "turbo build",
Expand Down
6 changes: 6 additions & 0 deletions packages/smart-vaults/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
PRIVATE_KEY=

MAINNET_RPC_URL=
SEPOLIA_RPC_URL=

ETHERSCAN_API_KEY=
21 changes: 21 additions & 0 deletions packages/smart-vaults/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# directories
cache
coverage
node_modules
out

# files
*.env
*.log
.DS_Store
.pnp.*
lcov.info
yarn.lock
.gas-snapshot

# broadcasts
!broadcast
broadcast/*
broadcast/*/31337/

*.verify.json
18 changes: 18 additions & 0 deletions packages/smart-vaults/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# directories
broadcast
cache
coverage
lib
node_modules
out
out-optimized

# files
*.env
*.log
.DS_Store
.pnp.*
lcov.info
package-lock.json
pnpm-lock.yaml
yarn.lock
87 changes: 87 additions & 0 deletions packages/smart-vaults/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Smart Vaults

Splits 4337 smart accounts are engineered to provide users with a seamless and secure way to manage a tiered system of
multi-chain multi-sigs. This means minimizing user friction to synchronize state (e.g. implementation, signer set)
across networks without degrading the underlying security model.

![architecture](./docs/smart%20account%20architecture.png)

Currently, these smart accounts are compatible with entry point
[v0.7](https://github.com/eth-infinitism/account-abstraction/tree/releases/v0.7).

## Feature set

- **m-of-n signers**: Supports both [**Passkeys**](https://splits.org/blog/passkeys-developers/) and **EOAs**.
- **ERC1271 Support**: Can verify [**ERC1271**](https://eips.ethereum.org/EIPS/eip-1271) signatures.
- **Token Support**: Accepts **ERC721** and **ERC1155**(single and batch) tokens.
- **Fallback Manager**: Allows users to extend their smart accounts to handle future callback-based interactions, such
as those involving **ERC721** tokens, ensuring future-proofing.
- **Module Manager**: Provides users the ability to add trusted modules that can interact on behalf of the smart
account.
- **Contract Deployment**: Enables the deployment of new contracts using `create` from within the smart account during a
UserOp.
- **[Merkelized User Operations](#merkelized-user-operations)**: Supports signing once for multiple user operations
across different networks and accounts using Merkle trees.
- **[Light User Operation](#light-user-operation)**: When multiple signatures are required, allows the last signer to
set gas according to current market conditions.

### Merkelized User Operations

Merkelized User Operations utilize **Merkle trees** to generate a root of all intended user operations. The user signs
the Merkle root once, and when submitting a user operation, the signature includes the Merkle proof for verification.
There is no strict limit on the number of operations or which parameters should remain constant, allowing operations
across multiple networks and smart accounts with a single signature.

### Light User Operation

When the **threshold** (the number of unique and valid signatures required for a valid user operation) is greater than
**1**, the first **threshold - 1** signers sign over a reduced set of properties from the user operation. This gives the
final signer flexibility to price gas for the user operation inline with current market conditions. In the case of
Merkelized User Operations, the initial signatures are verified against a **light** Merkle root, constructed using these
reduced light user operations.

Properties included in light UserOp:

- **sender**
- **nonce**
- **calldata**

Properties excluded:

- **initCode** - This has been excluded because of `sender` since `sender` is calculated deterministically from
`initCode` making it redundant.
- **accountGasLimits**
- **preVerificationGas**
- **gasFees**
- **paymasterAndData**
- **signature**

## Build

`pnpm build`

## Test

`pnpm test`

### Coverage

`pnpm test:coverage`

### Coverage Report

`pnpm test:coverage:report`

## Lint

`pnpm lint`

### Format

`pnpm format`

## Developers/Integrators

### Foundry

`forge install 0xSplits/splits-contracts-monorepo`
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions packages/smart-vaults/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[profile.default]
auto_detect_solc = false
gas_reports = ["SmartVault", "MultiSigner", "FallbackManager"]
optimizer = true
optimizer_runs = 5_000_000
out = "out"
script = "script"
solc = "0.8.23"
src = "src"
test = "test"
allow_paths = ['../../node_modules']
fs_permissions = [{ access = "read-write", path = "./" }]
evm_version = "shanghai"

[profile.default.fuzz]
max_test_rejects = 1_000_000
runs = 1000

[profile.default.invariant]
call_override = false
depth = 20
fail_on_revert = true
runs = 20

[profile.optimized]
via_ir = true
out = "out-optimized"

[profile.test-optimized]
ffi = true
src = "test"

[doc]
ignore = ["**/*.t.sol"]
out = "docs"

[profile.ci]
fuzz = { runs = 10_000 }
verbosity = 4

[fmt]
bracket_spacing = true
int_types = "long"
line_length = 120
multiline_func_header = "all"
number_underscore = "thousands"
quote_style = "double"
tab_width = 4
wrap_comments = true
sort_imports = true
37 changes: 37 additions & 0 deletions packages/smart-vaults/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "smart-vaults",
"version": "1.0.0",
"description": "Splits smart vaults",
"scripts": {
"build": "forge build",
"build:optimized": "FOUNDRY_PROFILE=optimized forge build",
"clean": "rm -rf cache out out-optimized",
"lint": "pnpm lint:sol && pnpm run prettier:check",
"lint:sol": "forge fmt --check && pnpm solhint {script,src}/**/*.sol",
"prettier:check": "prettier --check \"**/*.{json,md,svg,yml}\"",
"prettier:write": "prettier --write \"**/*.{json,md,svg,yml}\"",
"test": "forge test -vvv",
"test:optimized": "pnpm run build:optimized && FOUNDRY_PROFILE=test-optimized forge test -vvv",
"test:coverage": "forge coverage",
"test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage",
"deploy:VaultFactory:test": "export DRY_RUN=true && source .env && FOUNDRY_PROFILE=optimized forge script script/VaultFactory.s.sol:VaultFactoryScript --account SPLITS_DEPLOYER -vvvvv --rpc-url ",
"deploy:VaultFactory": "export DRY_RUN=false && source .env && FOUNDRY_PROFILE=optimized forge script script/VaultFactory.s.sol:VaultFactoryScript --account SPLITS_DEPLOYER --broadcast --verify -vvvvv --rpc-url "
},
"keywords": [],
"author": "@splits",
"license": "ISC",
"private": true,
"devDependencies": {
"@prb/test": "^0.6.4",
"forge-std": "github:foundry-rs/forge-std#v1",
"prettier": "^3.1.0"
},
"dependencies": {
"@openzeppelin/contracts": "github:openzeppelin/openzeppelin-contracts#5705e8208bc92cd82c7bcdfeac8dbc7377767d96",
"solady": "^0.0.156",
"web-authn": "github:base-org/webauthn-sol#619f20ab0f074fef41066ee4ab24849a913263b2",
"FreshCryptoLib": "github:rdubois-crypto/FreshCryptoLib#76f3f135b7b27d2aa519f265b56bfc49a2573ab5",
"account-abstraction": "github:eth-infinitism/account-abstraction#7af70c8993a6f42973f520ae0752386a5032abe7",
"solhint": "^5.0.3"
}
}
10 changes: 10 additions & 0 deletions packages/smart-vaults/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@prb/test/=node_modules/@prb/test/src
forge-std/=node_modules/forge-std/src
@openzeppelin/=node_modules/@openzeppelin/contracts/
@account-abstraction/=node_modules/accountabstraction/contracts/
solady/=node_modules/solady/src
@web-authn/=node_modules/web-authn/src
openzeppelin-contracts/=node_modules/@openzeppelin/contracts/
FreshCryptoLib/=node_modules/FreshCryptoLib/solidity/src/
src/=src/
account-abstraction/=node_modules/account-abstraction/contracts/
76 changes: 76 additions & 0 deletions packages/smart-vaults/script/Base.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.23;

import { ICreateX } from "./ICreateX.sol";

import { Script } from "forge-std/Script.sol";
import { stdJson } from "forge-std/StdJson.sol";
import { LibString } from "solady/utils/LibString.sol";

contract BaseScript is Script {
using stdJson for string;

ICreateX private immutable CREATEX = ICreateX(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed);

function getConfig() internal view returns (string memory) {
string memory dir = string.concat(vm.projectRoot(), "/script/config/");
string memory file = string.concat(vm.toString(block.chainid), ".json");
return vm.readFile(string.concat(dir, file));
}

function getAddressFromConfig(string memory _key) internal view returns (address) {
return getConfig().readAddress(string.concat(".", _key));
}

function getStringFromConfig(string memory _key) internal view returns (string memory) {
return getConfig().readString(string.concat(".", _key));
}

function getUintFromConfig(string memory _key) internal view returns (uint256) {
return getConfig().readUint(string.concat(".", _key));
}

function create3(bytes32 salt, bytes memory initCode) internal returns (address) {
return CREATEX.deployCreate3(salt, initCode);
}

function computeSalt(address deployer, bytes11 _salt) internal pure returns (bytes32) {
// keccak256(abi.encodePacked(deployer, hex"00", _salt))
return bytes32(abi.encodePacked(deployer, hex"00", _salt));
}

function updateDeployment(address _contract, string memory _name) internal {
if (isDryRun()) {
return;
}
string memory directory = string.concat(vm.projectRoot(), "/deployments/");
if (!vm.exists(directory)) {
vm.createDir(directory, true);
}

string memory file = string.concat(vm.projectRoot(), "/deployments/", vm.toString(block.chainid), ".json");
bool exists = vm.exists(file);
if (!exists) {
vm.writeFile(file, "{}");
}

string memory json = vm.readFile(file);
if (vm.keyExists(json, string.concat(".", _name))) {
vm.writeJson(LibString.toHexStringChecksummed(_contract), file, string.concat(".", _name));
} else {
string memory root = "root";
vm.serializeJson(root, json);
vm.writeJson(vm.serializeAddress(root, _name, _contract), file);
}
}

function computeCreate3Address(bytes32 salt, address deployer) public view returns (address) {
bytes32 guardedSalt = keccak256(abi.encode(deployer, block.chainid, salt));

return CREATEX.computeCreate3Address(guardedSalt);
}

function isDryRun() internal view returns (bool) {
return vm.envBool("DRY_RUN");
}
}
Loading

0 comments on commit 1c7fdc4

Please sign in to comment.