├── .gitmodules ├── .solhint.json ├── src ├── interfaces │ ├── IValidator.sol │ ├── IExecutor.sol │ ├── IHook.sol │ ├── IValidation.sol │ ├── IStorage.sol │ └── IWalletCore.sol ├── test │ ├── MockERC20.sol │ ├── MockExecutor.sol │ ├── DeployFactory.sol │ └── MockHook.sol ├── Types.sol ├── base │ └── WalletCoreBase.sol ├── lib │ ├── Errors.sol │ └── WalletCoreLib.sol ├── ExecutionLogic.sol ├── validator │ └── ECDSAValidator.sol ├── FallbackHandler.sol ├── Storage.sol ├── ExecutorLogic.sol ├── ValidationLogic.sol └── WalletCore.sol ├── tsconfig.json ├── foundry.toml ├── .env.example ├── .gitignore ├── SECURITY.md ├── scripts ├── utils │ ├── EventTopics.sol │ └── InitializeTemplate.sol ├── CreateDeployFactory.sol ├── smoke_test │ ├── 2-sendTxs.sol │ ├── 3-sendTxsAsRelayer.sol │ └── 1-setCodeAndInitialize.ts ├── DeployInit.sol └── DeployInitHelper.sol ├── test ├── Factory.t.sol ├── Execution.t.sol ├── Storage.t.sol ├── FallbackHandler.t.sol ├── Hook.t.sol ├── Base.t.sol ├── Validator.t.sol ├── Validation.t.sol └── Executor.t.sol ├── hardhat.config.ts ├── .github └── workflows │ └── test.yml ├── CONTRIBUTING.md ├── package.json ├── README.md └── LICENSE /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "max-line-length": ["warn", 120], 5 | "payable-fallback": "warn" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/IValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | interface IValidator { 5 | function validate( 6 | bytes32 msgHash, 7 | bytes calldata validationData 8 | ) external view; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015", "dom"], 6 | "module": "commonjs", 7 | "target": "es2022", 8 | "esModuleInterop": true 9 | }, 10 | "files": ["./hardhat.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /src/test/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20 { 7 | constructor() ERC20("MockToken", "MTK") { 8 | _mint(msg.sender, 1000 ether); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/interfaces/IExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import {Session} from "src/Types.sol"; 5 | 6 | interface IExecutor { 7 | function getSessionTypedHash( 8 | Session calldata session 9 | ) external view returns (bytes32); 10 | 11 | function validateSession(Session calldata session) external view; 12 | } 13 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | gas_report = true 6 | optimizer = true 7 | optimizer_runs = 2000 8 | 9 | remappings = [ 10 | "@openzeppelin/=node_modules/@openzeppelin/", 11 | "forge-std/=lib/forge-std/src/" 12 | ] 13 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Deployer account 2 | DEPLOYER_PRIVATE_KEY= 3 | DEPLOYER_ADDRESS= 4 | 5 | # Factory configuration 6 | DEPLOY_FACTORY_ADDRESS=0xce0042B868300000d44A59004Da54A005ffdcf9f 7 | DEPLOY_FACTORY_SALT=0x0000000000000000000000000000000000000000000000000000000000000120 8 | 9 | # Contract addresses (will be populated after deployment) 10 | STORAGE_ADDRESS= 11 | ECDSA_VALIDATOR_ADDRESS= 12 | WALLET_CORE= -------------------------------------------------------------------------------- /src/Types.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | struct Call { 5 | address target; 6 | uint256 value; 7 | bytes data; 8 | } 9 | 10 | struct Session { 11 | uint256 id; 12 | address executor; 13 | address validator; 14 | uint256 validUntil; 15 | uint256 validAfter; 16 | bytes preHook; 17 | bytes postHook; 18 | bytes signature; 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 15 | 16 | #Foundry files 17 | broadcast 18 | lcov.info 19 | coverage_report 20 | 21 | # Hardhat files 22 | artifacts 23 | cache 24 | typechain-types 25 | 26 | #node Module 27 | node_modules 28 | 29 | # .env 30 | .env -------------------------------------------------------------------------------- /src/base/WalletCoreBase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import {IStorage} from "../interfaces/IStorage.sol"; 5 | 6 | abstract contract WalletCoreBase { 7 | function _hashTypedDataV4( 8 | bytes32 structHash 9 | ) internal view virtual returns (bytes32); 10 | 11 | function _walletImplementation() internal view virtual returns (address); 12 | 13 | function getMainStorage() public view virtual returns (IStorage); 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/IHook.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import {Call} from "src/Types.sol"; 5 | 6 | interface IHook { 7 | function preCheck( 8 | Call[] calldata calls, 9 | bytes calldata hookData, 10 | address executor 11 | ) external payable returns (bytes calldata preCheckRet); 12 | 13 | function postCheck( 14 | bytes calldata preCheckRet, 15 | bytes calldata hookData, 16 | address executor 17 | ) external payable; 18 | } 19 | -------------------------------------------------------------------------------- /src/interfaces/IValidation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import {Call} from "../Types.sol"; 5 | 6 | interface IValidation { 7 | event ValidatorAdded(address validator); 8 | 9 | function getValidationTypedHash( 10 | uint256 nonce, 11 | Call[] calldata calls 12 | ) external view returns (bytes32); 13 | 14 | function computeValidatorAddress( 15 | address validatorImpl, 16 | bytes calldata immutableArgs 17 | ) external view returns (address); 18 | } 19 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you believe you've found a security vulnerability in our project, we take your report seriously and appreciate your effort to responsibly disclose it. 6 | 7 | Please submit all vulnerability reports via our official security page: 8 | 👉 https://web3.okx.com/security 9 | 10 | This ensures your report is routed to the appropriate team for prompt review and response. Please do not open a public issue. We will triage and respond as quickly as possible. 11 | 12 | Thank you for helping us keep the ecosystem safe! 13 | -------------------------------------------------------------------------------- /scripts/utils/EventTopics.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "lib/forge-std/src/Script.sol"; 5 | import {IWalletCore} from "src/interfaces/IWalletCore.sol"; 6 | 7 | /// @title EventTopics 8 | /// @notice A script for printing the event topics of a contract 9 | contract EventTopics is Script { 10 | function run() external pure { 11 | console.log("Event topics StorageInitialized:"); 12 | console.logBytes32(IWalletCore.StorageInitialized.selector); 13 | 14 | console.log("Event topics StorageCreated:"); 15 | console.logBytes32(IWalletCore.StorageCreated.selector); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/Errors.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | library Errors { 5 | // Storage related 6 | error InvalidExecutor(); 7 | error InvalidSession(); 8 | error InvalidSessionId(); 9 | error InvalidOwner(); 10 | 11 | // Account related 12 | error NotFromSelf(); 13 | 14 | // Call related 15 | error CallFailed(uint256 index, bytes returnData); 16 | 17 | // ValidationLogic related 18 | error InvalidValidator(address validator); 19 | error InvalidValidatorImpl(address validatorImpl); 20 | 21 | // ECDSAValidator related 22 | error InvalidSignature(); 23 | 24 | // WalletCoreBase related 25 | error NameTooLong(); 26 | error VersionTooLong(); 27 | } 28 | -------------------------------------------------------------------------------- /scripts/utils/InitializeTemplate.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.12; 3 | 4 | import "lib/forge-std/src/Script.sol"; 5 | import "src/WalletCore.sol"; 6 | import "src/interfaces/IStorage.sol"; 7 | import "src/ValidationLogic.sol"; 8 | import "src/Types.sol"; 9 | 10 | /// @title CreateDeployFactory 11 | /// @notice A script for creating a deploy factory 12 | contract InitializeTemplate is Script { 13 | function run() external { 14 | uint256 senderPk = vm.envUint("DEPLOYER_PRIVATE_KEY"); 15 | vm.startBroadcast(senderPk); 16 | 17 | address walletCore = vm.envAddress("WALLET_CORE"); 18 | WalletCore(payable(walletCore)).initialize(); 19 | 20 | console.log("Completed InitializeTemplate script"); 21 | vm.stopBroadcast(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/MockExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import {IWalletCore} from "../interfaces/IWalletCore.sol"; 5 | import {IStorage} from "../interfaces/IStorage.sol"; 6 | import {IExecutor} from "../interfaces/IExecutor.sol"; 7 | import {Session, Call} from "../Types.sol"; 8 | 9 | contract MockExecutor { 10 | IWalletCore account; 11 | 12 | constructor(IWalletCore _account) { 13 | account = _account; 14 | } 15 | 16 | function execute( 17 | Call[] calldata calls, 18 | Session calldata session 19 | ) external payable { 20 | account.executeFromExecutor(calls, session); 21 | } 22 | 23 | function validateSession(Session calldata session) external view { 24 | IExecutor(address(account)).validateSession(session); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scripts/CreateDeployFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "lib/forge-std/src/Script.sol"; 5 | import {DeployFactory} from "src/test/DeployFactory.sol"; 6 | 7 | /// @title CreateDeployFactory 8 | /// @notice A script for creating a deploy factory 9 | contract CreateDeployFactory is Script { 10 | function run() external { 11 | vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY")); 12 | 13 | address deployOwner = vm.addr(vm.envUint("DEPLOYER_PRIVATE_KEY")); 14 | DeployFactory deployFactory = new DeployFactory(); 15 | 16 | console.log("Deploy owner: %s", deployOwner); 17 | console.log("Deploy factory address: %s", address(deployFactory)); 18 | console.log("Completed DeployFactory script"); 19 | vm.stopBroadcast(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/interfaces/IStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | interface IStorage { 5 | // EVENTS 6 | event NonceConsumed(uint256 utilisedNonce); 7 | event ValidatorStatusUpdated(address validator, bool status); 8 | event SessionRevoked(uint256 id); 9 | 10 | // FUNCTIONS 11 | function readAndUpdateNonce(address validator) external returns (uint256); 12 | 13 | function setValidatorStatus(address validator, bool isValid) external; 14 | 15 | function revokeSession(uint256 id) external; 16 | 17 | function getOwner() external view returns (address); 18 | 19 | function getNonce() external view returns (uint256); 20 | 21 | function validateValidator(address validator) external view; 22 | 23 | function validateSession(uint256 id, address validator) external view; 24 | } 25 | -------------------------------------------------------------------------------- /src/test/DeployFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | // EIP-2740 deploy factory to mimic the methods 5 | contract DeployFactory { 6 | /** 7 | * @notice Deploys `_initCode` using `_salt` for defining the deterministic address. 8 | * @param _initCode Initialization code. 9 | * @param _salt Arbitrary value to modify resulting address. 10 | * @return createdContract Created contract address. 11 | */ 12 | function deploy( 13 | bytes memory _initCode, 14 | bytes32 _salt 15 | ) public returns (address payable createdContract) { 16 | assembly { 17 | createdContract := create2( 18 | 0, 19 | add(_initCode, 0x20), 20 | mload(_initCode), 21 | _salt 22 | ) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/Factory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "./Base.t.sol"; 5 | import "src/lib/Errors.sol"; 6 | import {Storage} from "../src/Storage.sol"; 7 | 8 | contract FactoryTest is Base { 9 | function setUp() public override { 10 | super.setUp(); 11 | } 12 | 13 | function test_double_deploy() external { 14 | bytes32 deployFactorySalt = vm.envBytes32("DEPLOY_FACTORY_SALT"); 15 | ( 16 | address _storageAddr, 17 | address _ecdsaValidatorAddr, 18 | address _walletCoreAddr 19 | ) = DeployInitHelper.deployContracts( 20 | deployFactory, 21 | deployFactorySalt, 22 | NAME, 23 | VERSION 24 | ); 25 | 26 | assertEq(address(_storageAddr), address(_storageImpl)); 27 | assertEq(address(_ecdsaValidatorAddr), address(_ecdsaValidatorImpl)); 28 | assertEq(address(_walletCoreAddr), address(_walletCore)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ExecutionLogic.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.23; 3 | 4 | import {Call} from "./Types.sol"; 5 | import {Errors} from "./lib/Errors.sol"; 6 | 7 | abstract contract ExecutionLogic { 8 | /** 9 | * @notice Executes multiple contract calls in a single transaction 10 | * @dev Reverts if any of the calls fail 11 | * @param calls Array of Call structs containing destination address, value, and calldata 12 | * @return results Array of bytes containing the return data from each call 13 | */ 14 | function _batchCall( 15 | Call[] calldata calls 16 | ) internal returns (bytes[] memory results) { 17 | results = new bytes[](calls.length); 18 | for (uint256 i; i < calls.length; i++) { 19 | (bool success, bytes memory returnData) = calls[i].target.call{ 20 | value: calls[i].value 21 | }(calls[i].data); 22 | if (!success) revert Errors.CallFailed(i, returnData); 23 | results[i] = returnData; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | import "dotenv/config"; 4 | 5 | const config: HardhatUserConfig = { 6 | solidity: "0.8.23", 7 | paths: { 8 | sources: "./src", 9 | cache: "./cache", 10 | artifacts: "./artifacts" 11 | }, 12 | networks: { 13 | hardhat: { 14 | chainId: 31337 15 | }, 16 | devnet6: { 17 | url: "https://rpc.pectra-devnet-6.ethpandaops.io", 18 | chainId: 7072151312, 19 | accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""], 20 | }, 21 | holesky: { 22 | url: "https://1rpc.io/holesky", 23 | chainId: 17000, 24 | accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""], 25 | }, 26 | bsc: { 27 | url: "https://bsc-dataseed.binance.org/", 28 | chainId: 56, 29 | accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""], 30 | }, 31 | sepolia: { 32 | url: "https://ethereum-sepolia-rpc.publicnode.com", 33 | chainId: 11155111, 34 | accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""], 35 | }, 36 | }, 37 | }; 38 | 39 | export default config; -------------------------------------------------------------------------------- /scripts/smoke_test/2-sendTxs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.12; 3 | 4 | import "lib/forge-std/src/Script.sol"; 5 | import "src/interfaces/IWalletCore.sol"; 6 | import "src/Types.sol"; 7 | 8 | /// @title CreateDeployFactory 9 | /// @notice A script for creating a deploy factory 10 | contract SendTxs is Script { 11 | function run() external { 12 | vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY")); 13 | 14 | address sender = vm.addr(vm.envUint("DEPLOYER_PRIVATE_KEY")); 15 | address receiver = address(0xFeeCC911175C2B6D46BaE4fd357c995a4DC43C60); 16 | console.log("Sender: ", sender); 17 | console.log("Receiver: ", receiver); 18 | 19 | // Construct the call data for the WalletCore.execute() function 20 | Call[] memory calls = new Call[](1); 21 | calls[0] = Call({target: receiver, value: 0.00001 ether, data: ""}); 22 | // calls[1] = Call({target: receiver, value: 0.00002 ether, data: ""}); 23 | 24 | IWalletCore(sender).executeFromSelf(calls); 25 | 26 | console.log("Completed ExecuteFromSelf script"); 27 | vm.stopBroadcast(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/interfaces/IWalletCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 5 | import {IStorage} from "./IStorage.sol"; 6 | import {Call, Session} from "src/Types.sol"; 7 | 8 | interface IWalletCore is IERC165 { 9 | // EVENTS 10 | event StorageInitialized(); 11 | event StorageCreated(address storageAddress); 12 | 13 | function initialize() external; 14 | 15 | function executeFromSelf(Call[] calldata calls) external; 16 | 17 | function executeWithValidator( 18 | Call[] calldata calls, 19 | address validator, 20 | bytes calldata validationData 21 | ) external; 22 | 23 | function executeFromExecutor( 24 | Call[] calldata calls, 25 | Session calldata session 26 | ) external; 27 | 28 | function addValidator( 29 | address validatorImpl, 30 | bytes calldata immutableArgs 31 | ) external; 32 | 33 | function getMainStorage() external view returns (IStorage); 34 | 35 | function isValidSignature( 36 | bytes32 hash, 37 | bytes calldata signature 38 | ) external view returns (bytes4); 39 | } 40 | -------------------------------------------------------------------------------- /test/Execution.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "./Base.t.sol"; 5 | import "src/lib/Errors.sol"; 6 | 7 | contract ExecutionTest is Base { 8 | function setUp() public override { 9 | super.setUp(); 10 | } 11 | 12 | function test_executeFromSelf_succeeds_for_owner() public { 13 | vm.prank(_alice); 14 | Call[] memory calls = _construct_calls_data(); 15 | IWalletCore(_alice).executeFromSelf(calls); 16 | } 17 | 18 | function test_executeFromSelf_reverts_for_non_owner() public { 19 | vm.prank(_bob); 20 | Call[] memory calls = _construct_calls_data(); 21 | vm.expectRevert(abi.encodeWithSelector(Errors.NotFromSelf.selector)); 22 | IWalletCore(_alice).executeFromSelf(calls); 23 | } 24 | 25 | function test_execute_reverts_on_failed_call() public { 26 | vm.prank(_alice); 27 | Call[] memory calls = new Call[](2); 28 | calls[0] = Call({target: _bob, value: 1 ether, data: ""}); 29 | calls[1] = Call({target: _bob, value: 1000 ether, data: ""}); // will fail 30 | vm.expectRevert( 31 | abi.encodeWithSelector(Errors.CallFailed.selector, 1, "") 32 | ); 33 | IWalletCore(_alice).executeFromSelf(calls); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 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 | env: 19 | DEPLOY_FACTORY_SALT: "0x0000000000000000000000000000000000000000000000000000000000000000" 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 24 | 25 | - name: Install Foundry 26 | uses: foundry-rs/foundry-toolchain@v1 27 | with: 28 | version: nightly 29 | 30 | - name: Show Forge version 31 | run: | 32 | forge --version 33 | 34 | - name: Install Node.js and Prettier 35 | run: | 36 | curl -fsSL https://deb.nodesource.com/setup_16.x | bash - 37 | sudo apt-get install -y nodejs 38 | npm install --save-dev prettier prettier-plugin-solidity 39 | 40 | - name: Run Prettier Check 41 | run: | 42 | npx prettier --check --plugin=prettier-plugin-solidity 'src/**/*.sol' 'test/**/*.sol' 43 | 44 | - name: Run Forge build 45 | run: | 46 | forge build --sizes 47 | id: build 48 | 49 | - name: Run Forge tests 50 | run: | 51 | forge test -vvv 52 | id: test 53 | -------------------------------------------------------------------------------- /src/validator/ECDSAValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.23; 3 | 4 | import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; 5 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | 7 | import {IValidator} from "../interfaces/IValidator.sol"; 8 | import {Errors} from "../lib/Errors.sol"; 9 | 10 | contract ECDSAValidator is IValidator { 11 | using ECDSA for bytes32; 12 | 13 | /** 14 | * @notice Validates a signature against the stored signer address 15 | * @dev Uses ECDSA recovery to verify the signature matches the typed data hash 16 | * @param typedDataHash EIP-712 typed data hash to verify 17 | * @param signature ECDSA signature to validate 18 | */ 19 | function validate( 20 | bytes32 typedDataHash, 21 | bytes calldata signature 22 | ) external view { 23 | address recoveredSigner = typedDataHash.recover(signature); 24 | address signer = getSigner(); 25 | if (recoveredSigner != signer) revert Errors.InvalidSignature(); 26 | } 27 | 28 | /** 29 | * @notice Returns the signer address stored in this validator clone 30 | * @dev Retrieves and decodes the initialization arguments used when this clone was created 31 | * @return address The stored signer address that is authorized to sign transactions 32 | */ 33 | function getSigner() public view returns (address) { 34 | return abi.decode(Clones.fetchCloneArgs(address(this)), (address)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scripts/smoke_test/3-sendTxsAsRelayer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.12; 3 | 4 | import "lib/forge-std/src/Script.sol"; 5 | import "src/WalletCore.sol"; 6 | import "src/interfaces/IStorage.sol"; 7 | import "src/ValidationLogic.sol"; 8 | import "src/Types.sol"; 9 | 10 | /// @title CreateDeployFactory 11 | /// @notice A script for creating a deploy factory 12 | contract SendTxsAsRelayer is Script { 13 | function run() external { 14 | uint256 senderPk = vm.envUint("DEPLOYER_PRIVATE_KEY"); 15 | vm.startBroadcast(senderPk); 16 | 17 | address payable sender = payable(vm.addr(senderPk)); 18 | address receiver = address(0xFeeCC911175C2B6D46BaE4fd357c995a4DC43C60); 19 | console.log("Sender: ", sender); 20 | console.log("Receiver: ", receiver); 21 | 22 | // Construct the call data for the WalletCore.execute() function 23 | Call[] memory calls = new Call[](1); 24 | calls[0] = Call({target: receiver, value: 0.00001 ether, data: ""}); 25 | // calls[1] = Call({target: receiver, value: 0.00002 ether, data: ""}); 26 | 27 | uint256 nonce = IStorage(WalletCore(sender).getMainStorage()) 28 | .getNonce(); 29 | bytes32 hash = ValidationLogic(sender).getValidationTypedHash( 30 | nonce, 31 | calls 32 | ); 33 | 34 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(senderPk, hash); 35 | bytes memory signature = abi.encodePacked(r, s, v); 36 | 37 | address validator = address(1); 38 | IWalletCore(sender).executeWithValidator(calls, validator, signature); 39 | 40 | console.log("Completed ExecuteWithValidator script"); 41 | vm.stopBroadcast(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Wallet Core 2 | 3 | First off, thank you for taking the time to contribute to **Wallet Core** — a modular, secure, and gas-optimized implementation of EIP-7702. 4 | 5 | We welcome contributions from the community and are grateful for your support! 6 | 7 | --- 8 | 9 | ## 🧑‍💻 How to Contribute 10 | 11 | ### 1. Fork and Clone 12 | 13 | ```bash 14 | git clone https://github.com/okx/wallet-core.git 15 | cd wallet-core 16 | ``` 17 | 18 | ### 2. Create a Feature Branch 19 | 20 | ```bash 21 | git checkout -b feature/my-feature 22 | ``` 23 | 24 | ### 3. Make Your Changes 25 | 26 | - Follow the existing coding style (Solidity + TypeScript) 27 | - Write tests for your changes (Foundry/Hardhat) 28 | - Ensure the code compiles and passes all existing tests 29 | 30 | ### 4. Run Tests 31 | 32 | ```bash 33 | # Foundry 34 | forge test 35 | ``` 36 | 37 | ### 5. Commit and Push 38 | 39 | ```bash 40 | git commit -m "feat: " 41 | git push origin feature/my-feature 42 | ``` 43 | 44 | ### 6. Open a Pull Request 45 | 46 | - Go to GitHub and open a PR from your branch. 47 | - Fill out the PR template with context and checklist. 48 | 49 | --- 50 | 51 | ## ✅ Pull Request Requirements 52 | 53 | - Follow the [GPL-3.0 License](./LICENSE) 54 | - Write clear commit messages and PR descriptions 55 | - Link any related issues using `Fixes #123` 56 | - Keep PRs focused and minimal (no unrelated changes) 57 | 58 | --- 59 | 60 | ## 📂 Project Structure 61 | 62 | ``` 63 | src/ # EIP-7702 smart wallet contracts 64 | ├─ validator/ # Validator 65 | └─ interfaces/ # External/public interfaces 66 | test/ # Foundry tests 67 | scripts/ # Deployment and upgrade scripts 68 | ``` 69 | 70 | --- 71 | 72 | ## 🛡 Security Reporting 73 | 74 | If you discover a security issue, please **do not** open a public issue. 75 | Instead, report it to: [security](https://web3.okx.com/security) 76 | 77 | --- 78 | 79 | ## 💬 Questions or Feedback? 80 | 81 | Open a [GitHub Discussion](https://github.com/okx/wallet-core/discussions) or create an issue for bugs/requests. 82 | 83 | We appreciate your interest and contributions! 84 | -------------------------------------------------------------------------------- /src/FallbackHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.23; 3 | 4 | import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 5 | 6 | /** 7 | * @dev Contract that handles token receiving functionality, implementing both IERC165 and IModule interfaces. 8 | * Supports ERC721 and ERC1155 token receiving through standard interfaces. 9 | */ 10 | abstract contract FallbackHandler is IERC165 { 11 | /** 12 | * @dev Allows the contract to receive ETH 13 | */ 14 | receive() external payable virtual {} 15 | 16 | /** 17 | * @dev Fallback function that handles token receiving callbacks 18 | * Returns the function selector for ERC721 and ERC1155 token receiving functions 19 | */ 20 | fallback() external payable { 21 | assembly { 22 | let s := shr(224, calldataload(0)) 23 | // 0x150b7a02: `onERC721Received(address,address,uint256,bytes)`. 24 | // 0xf23a6e61: `onERC1155Received(address,address,uint256,uint256,bytes)`. 25 | // 0xbc197c81: `onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)`. 26 | if or(eq(s, 0x150b7a02), or(eq(s, 0xf23a6e61), eq(s, 0xbc197c81))) { 27 | mstore(0x20, s) // Store `msg.sig`. 28 | return(0x3c, 0x20) // Return `msg.sig`. 29 | } 30 | } 31 | 32 | revert(); 33 | } 34 | 35 | /** 36 | * @dev Implementation of IERC165 interface detection 37 | * @param interfaceId The interface identifier to check 38 | * @return bool True if the contract supports the interface 39 | */ 40 | function supportsInterface( 41 | bytes4 interfaceId 42 | ) external view virtual override returns (bool) { 43 | // 0x150b7a02: `type(IERC721Receiver).interfaceId`. 44 | // 0x4e2312e0: `type(IERC1155Receiver).interfaceId`. 45 | // 0x1626ba7e: `type(IERC1271).interfaceId`. 46 | // 0x01ffc9a7: `type(IERC165).interfaceId`. 47 | return 48 | interfaceId == 0x150b7a02 || 49 | interfaceId == 0x4e2312e0 || 50 | interfaceId == 0x1626ba7e || 51 | interfaceId == 0x01ffc9a7; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/MockHook.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import {Call} from "../Types.sol"; 6 | 7 | import "../interfaces/IHook.sol"; 8 | 9 | contract MockHook is IHook { 10 | bytes4 public constant TRANSFER_SELECTOR = 0xa9059cbb; 11 | 12 | function preCheck( 13 | Call[] calldata calls, 14 | bytes calldata hookData, 15 | address // executor 16 | ) external payable returns (bytes memory preCheckRet) { 17 | (address token, uint256 maxTotalAmount) = abi.decode( 18 | hookData, 19 | (address, uint256) 20 | ); 21 | 22 | uint256 initialBalance = IERC20(token).balanceOf(msg.sender); 23 | uint256 totalAmount = 0; 24 | 25 | for (uint256 i = 0; i < calls.length; i++) { 26 | require(calls[i].target == token, "Invalid token address"); 27 | 28 | bytes4 selector = bytes4(calls[i].data[:4]); 29 | require(selector == TRANSFER_SELECTOR, "Invalid operation"); 30 | 31 | (address recipientCalled, uint256 amount) = abi.decode( 32 | calls[i].data[4:], 33 | (address, uint256) 34 | ); 35 | 36 | require(recipientCalled != address(0), "Invalid recipient address"); 37 | totalAmount += amount; 38 | } 39 | 40 | require( 41 | totalAmount <= maxTotalAmount, 42 | "Total transfer amount exceeds limit" 43 | ); 44 | 45 | return abi.encode(token, initialBalance, totalAmount); 46 | } 47 | 48 | function postCheck( 49 | bytes calldata preHookRet, 50 | bytes calldata, // hookData 51 | address // executor 52 | ) external payable { 53 | (address token, uint256 initialBalance, uint256 totalAmount) = abi 54 | .decode(preHookRet, (address, uint256, uint256)); 55 | 56 | uint256 finalBalance = IERC20(token).balanceOf(msg.sender); 57 | require( 58 | initialBalance - finalBalance == totalAmount, 59 | "Balance mismatch: transfer amounts do not match" 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "7702-eoa-implementation", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "GPL-3.0", 6 | "scripts": { 7 | "prettier:sol": "prettier --write --plugin=prettier-plugin-solidity 'src/**/*.sol' 'test/**/*.sol'", 8 | "prettier:solhint": "solhint 'src/**/*.sol' 'test/**/*.sol' --fix --noPrompt", 9 | "prettier:check": "prettier --check --plugin=prettier-plugin-solidity 'src/**/*.sol' 'test/**/*.sol'", 10 | "cloc": "npx cloc src test --exclude-dir=mocks,test", 11 | "coverage": "forge coverage --report lcov && genhtml -o coverage_report lcov.info --ignore-errors inconsistent && open coverage_report/index.html", 12 | "test": "forge test", 13 | "deploy": "forge script scripts/DeployInit.sol --rpc-url ", 14 | "1-setCodeAndInitialize": "npx hardhat run scripts/smoke_test/1-setCodeAndInitialize.ts --network sepolia", 15 | "2-sendTxs": "forge script scripts/smoke_test/2-sendTxs.sol --rpc-url ", 16 | "3-sendTxsAsRelayer": "forge script scripts/smoke_test/3-sendTxsAsRelayer.sol --rpc-url ", 17 | "code": "cast code
--rpc-url " 18 | }, 19 | "pre-commit": [ 20 | "prettier:sol" 21 | ], 22 | "dependencies": { 23 | "@openzeppelin/contracts": "^5.2.0", 24 | "prettier": "^3.1.0", 25 | "prettier-plugin-solidity": "^1.2.0", 26 | "solhint": "^4.1.1" 27 | }, 28 | "devDependencies": { 29 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.8", 30 | "@nomicfoundation/hardhat-ethers": "^3.0.8", 31 | "@nomicfoundation/hardhat-ignition": "^0.15.10", 32 | "@nomicfoundation/hardhat-ignition-ethers": "^0.15.10", 33 | "@nomicfoundation/hardhat-network-helpers": "^1.0.12", 34 | "@nomicfoundation/hardhat-toolbox": "^5.0.0", 35 | "@nomicfoundation/hardhat-verify": "^2.0.13", 36 | "@typechain/ethers-v6": "^0.5.1", 37 | "@typechain/hardhat": "^9.1.0", 38 | "@types/mocha": "^10.0.10", 39 | "chai": "^4.5.0", 40 | "cloc": "^2.4.0-cloc", 41 | "hardhat": "^2.22.10", 42 | "hardhat-gas-reporter": "^2.2.2", 43 | "solidity-coverage": "^0.8.14", 44 | "ts-node": "^10.9.2", 45 | "typechain": "^8.3.2", 46 | "typescript": "^5.5.4", 47 | "typescript-eslint": "^8.25.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scripts/DeployInit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "lib/forge-std/src/Script.sol"; 5 | import {DeployInitHelper} from "./DeployInitHelper.sol"; 6 | import {DeployFactory} from "src/test/DeployFactory.sol"; 7 | import {Storage} from "src/Storage.sol"; 8 | import {ECDSAValidator} from "src/validator/ECDSAValidator.sol"; 9 | import {WalletCore} from "src/WalletCore.sol"; 10 | 11 | /// @title DeployInit 12 | /// @notice A script for deploying, initializing, and setting the access controls 13 | contract DeployInit is Script { 14 | function run() external { 15 | vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY")); 16 | 17 | address deployOwner = vm.addr(vm.envUint("DEPLOYER_PRIVATE_KEY")); 18 | console.log("Deploy owner: %s", deployOwner); 19 | 20 | DeployFactory deployFactory = DeployFactory( 21 | vm.envAddress("DEPLOY_FACTORY_ADDRESS") 22 | ); 23 | bytes32 deployFactorySalt = vm.envBytes32("DEPLOY_FACTORY_SALT"); 24 | console.log("Deploy factory address: %s", address(deployFactory)); 25 | console.log("Deploy factory salt:"); 26 | console.logBytes32(deployFactorySalt); 27 | 28 | string memory walletCoreName = "wallet-core"; 29 | string memory walletCoreVersion = "1.0.0"; 30 | console.log("WalletCore name: %s", walletCoreName); 31 | console.log("WalletCore version: %s", walletCoreVersion); 32 | 33 | address storage_; 34 | address ecdsaValidator_; 35 | address walletCore_; 36 | 37 | (storage_, ecdsaValidator_, walletCore_) = DeployInitHelper 38 | .deployContracts( 39 | deployFactory, 40 | deployFactorySalt, 41 | walletCoreName, 42 | walletCoreVersion 43 | ); 44 | 45 | console.log("WalletCore address: %s", walletCore_); 46 | console.log("Storage address: %s", storage_); 47 | console.log("ECDSAValidator address: %s", ecdsaValidator_); 48 | 49 | // Initialize the wallet core 50 | WalletCore(payable(walletCore_)).initialize(); 51 | console.log("WalletCore initialized"); 52 | 53 | console.log("Completed DeployInit script"); 54 | vm.stopBroadcast(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /scripts/DeployInitHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import {Storage} from "src/Storage.sol"; 5 | import {WalletCore} from "src/WalletCore.sol"; 6 | import {ECDSAValidator} from "src/validator/ECDSAValidator.sol"; 7 | import {DeployFactory} from "src/test/DeployFactory.sol"; 8 | import "lib/forge-std/src/Test.sol"; 9 | import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; 10 | 11 | library DeployInitHelper { 12 | function deployContracts( 13 | DeployFactory deployFactory, 14 | bytes32 deployFactorySalt, 15 | string memory walletCoreName, 16 | string memory walletCoreVersion 17 | ) 18 | internal 19 | returns ( 20 | address storageAddr, 21 | address ecdsaValidatorAddr, 22 | address walletCoreAddr 23 | ) 24 | { 25 | // Deploy Storage 26 | storageAddr = _deployIfNeeded( 27 | deployFactory, 28 | type(Storage).creationCode, 29 | deployFactorySalt 30 | ); 31 | 32 | // Deploy ECDSA Validator 33 | ecdsaValidatorAddr = _deployIfNeeded( 34 | deployFactory, 35 | type(ECDSAValidator).creationCode, 36 | deployFactorySalt 37 | ); 38 | 39 | // Deploy Wallet Core 40 | walletCoreAddr = _deployIfNeeded( 41 | deployFactory, 42 | abi.encodePacked( 43 | type(WalletCore).creationCode, 44 | abi.encode(storageAddr, walletCoreName, walletCoreVersion) // constructor args 45 | ), 46 | deployFactorySalt 47 | ); 48 | } 49 | 50 | function _deployIfNeeded( 51 | DeployFactory deployFactory, 52 | bytes memory bytecode, 53 | bytes32 salt 54 | ) internal returns (address) { 55 | address derivedAddress = Create2.computeAddress( 56 | salt, 57 | keccak256(bytecode), 58 | address(deployFactory) 59 | ); 60 | uint256 codeSize = derivedAddress.code.length; 61 | 62 | if (codeSize > 0) { 63 | console.log("Skipping deployment for: %s", derivedAddress); 64 | return payable(derivedAddress); 65 | } 66 | return deployFactory.deploy(bytecode, salt); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/Storage.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "./Base.t.sol"; 5 | import "src/lib/Errors.sol"; 6 | 7 | contract StorageTest is Base { 8 | event StorageInitialized(); 9 | 10 | function setUp() public override { 11 | super.setUp(); 12 | } 13 | 14 | function test_EIP712_name_too_long() public { 15 | vm.expectRevert(Errors.NameTooLong.selector); 16 | new WalletCore( 17 | address(_storageImpl), 18 | "wallet-core-with-a-very-long-name-that-exceeds-32-bytes", 19 | "1.0.0" 20 | ); 21 | } 22 | 23 | function test_EIP712_version_too_long() public { 24 | vm.expectRevert(Errors.VersionTooLong.selector); 25 | new WalletCore( 26 | address(_storageImpl), 27 | "wallet-core", 28 | "1.0.0-with-a-very-long-version-that-exceeds-32-bytes" 29 | ); 30 | } 31 | 32 | function test_initialize_emits_event_when_called_twice() public { 33 | vm.prank(_alice); 34 | vm.expectEmit(); 35 | emit StorageInitialized(); 36 | IWalletCore(_alice).initialize(); 37 | } 38 | 39 | function test_walletCore_does_not_modify_storage() public { 40 | // Start tracking storage access 41 | vm.record(); 42 | 43 | // Execute the function that should NOT modify storage 44 | vm.prank(_bob); 45 | _setCodeToEOA(address(_walletCore), _bob); 46 | 47 | // Bob initializes the account 48 | IWalletCore(_bob).initialize(); 49 | 50 | // Get accessed storage slots 51 | (, bytes32[] memory writes) = vm.accesses(_bob); 52 | 53 | // Verify that NO storage writes occurred 54 | assertEq(writes.length, 0, "Storage should not be modified!"); 55 | } 56 | 57 | function test_storage_returns_correct_owner() public { 58 | // Start tracking storage access 59 | vm.record(); 60 | 61 | // Execute the function that should NOT modify storage 62 | vm.prank(_bob); 63 | _setCodeToEOA(address(_walletCore), _bob); 64 | 65 | // Bob initializes the account 66 | IWalletCore(_bob).initialize(); 67 | 68 | // check owner in Storage 69 | vm.prank(_bob); 70 | address owner = IStorage(WalletCore(payable(_bob)).getMainStorage()) 71 | .getOwner(); 72 | assertEq(owner, _bob, "invalid owner"); 73 | } 74 | 75 | function test_readAndUpdateNonce_succeeds_for_owner() public { 76 | address storageAddress = address( 77 | WalletCore(payable(_alice)).getMainStorage() 78 | ); 79 | uint256 nonceBefore = IStorage(storageAddress).getNonce(); 80 | vm.prank(_alice); 81 | uint256 nonceUsed = IStorage(storageAddress).readAndUpdateNonce( 82 | address(1) 83 | ); 84 | uint256 nonceAfter = IStorage(storageAddress).getNonce(); 85 | assertEq(nonceBefore, nonceUsed); 86 | assertEq(nonceAfter, nonceBefore + 1); 87 | } 88 | 89 | function test_readAndUpdateNonce_reverts_for_non_owner() public { 90 | // Start tracking storage access 91 | vm.record(); 92 | 93 | // Execute the function that should NOT modify storage 94 | _setCodeToEOA(address(_walletCore), _bob); 95 | 96 | vm.prank(_bob); 97 | IStorage aliceStorage = IStorage( 98 | WalletCore(payable(_alice)).getMainStorage() 99 | ); 100 | vm.expectRevert(Errors.InvalidOwner.selector); 101 | aliceStorage.readAndUpdateNonce(address(1)); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /scripts/smoke_test/1-setCodeAndInitialize.ts: -------------------------------------------------------------------------------- 1 | const { ethers } = require('hardhat'); 2 | 3 | const main = async () => { 4 | const wallet = new ethers.Wallet(process.env.DEPLOYER_PRIVATE_KEY, ethers.provider); 5 | const chainId = (await ethers.provider.getNetwork()).chainId; 6 | 7 | // Get the contract instance 8 | const WALLET_CORE = process.env.WALLET_CORE; 9 | const WalletCore = await ethers.getContractAt("WalletCore", WALLET_CORE); 10 | 11 | console.log("Chain ID: ", chainId); 12 | console.log("EOA address: ", wallet.address); 13 | console.log("Setting code for EIP7702 account at: ", WALLET_CORE); 14 | 15 | // Encode the execute function call with WalletCore.initialize() 16 | const calldata = WalletCore.interface.encodeFunctionData("initialize"); 17 | 18 | const currentNonce = await ethers.provider.getTransactionCount(wallet.address); 19 | 20 | const authorizationData: { 21 | chainId: any; 22 | address: string | undefined; 23 | nonce: any; 24 | yParity?: string; 25 | r?: string; 26 | s?: string; 27 | } = { 28 | chainId: ethers.toBeHex(chainId.toString()), 29 | address: WALLET_CORE, 30 | nonce: ethers.toBeHex(currentNonce + 1), 31 | }; 32 | 33 | // Encode authorization data according to EIP-712 standard 34 | const encodedAuthorizationData = ethers.concat([ 35 | '0x05', // MAGIC code for EIP7702 36 | ethers.encodeRlp([ 37 | authorizationData.chainId, 38 | authorizationData.address, 39 | authorizationData.nonce, 40 | ]) 41 | ]); 42 | 43 | // Generate and sign authorization data hash 44 | const authorizationDataHash = ethers.keccak256(encodedAuthorizationData); 45 | const authorizationSignature = wallet.signingKey.sign(authorizationDataHash); 46 | 47 | // Store signature components 48 | authorizationData.yParity = authorizationSignature.yParity == 0 ? '0x' : '0x01'; 49 | authorizationData.r = authorizationSignature.r; 50 | authorizationData.s = authorizationSignature.s; 51 | 52 | // Get current gas fee data from the network 53 | const feeData = await ethers.provider.getFeeData(); 54 | 55 | // Prepare complete transaction data structure 56 | const txData = [ 57 | authorizationData.chainId, 58 | currentNonce == 0 ? "0x" : ethers.toBeHex(currentNonce), // Pass "0x" instead of "0x00" when currentNonce is 0 59 | ethers.toBeHex(feeData.maxPriorityFeePerGas), // Priority fee (tip) 60 | ethers.toBeHex(feeData.maxFeePerGas), // Maximum total fee willing to pay 61 | ethers.toBeHex(1000000), // Gas limit 62 | wallet.address, // Sender address 63 | '0x', // Value (in addition to batch transfers) 64 | calldata, // Encoded function call 65 | [], // Access list (empty for this transaction) 66 | [ 67 | [ 68 | authorizationData.chainId, 69 | authorizationData.address, 70 | authorizationData.nonce, 71 | authorizationData.yParity, 72 | authorizationData.r, 73 | authorizationData.s 74 | ] 75 | ] 76 | ]; 77 | 78 | // Encode final transaction data with version prefix 79 | const encodedTxData = ethers.concat([ 80 | '0x04', // Transaction type identifier 81 | ethers.encodeRlp(txData) 82 | ]); 83 | 84 | // Sign the complete transaction 85 | const txDataHash = ethers.keccak256(encodedTxData); 86 | const txSignature = wallet.signingKey.sign(txDataHash); 87 | 88 | // Construct the fully signed transaction 89 | const signedTx = ethers.hexlify(ethers.concat([ 90 | '0x04', 91 | ethers.encodeRlp([ 92 | ...txData, 93 | txSignature.yParity == 0 ? '0x' : '0x01', 94 | txSignature.r, 95 | txSignature.s 96 | ]) 97 | ])); 98 | 99 | // Send the raw transaction to the network 100 | const tx = await ethers.provider.send('eth_sendRawTransaction', [signedTx]); 101 | console.log('tx sent: ', tx); 102 | } 103 | 104 | main().then(() => { 105 | console.log('Execution completed'); 106 | process.exit(0); 107 | }).catch((error) => { 108 | console.error(error); 109 | process.exit(1); 110 | }); -------------------------------------------------------------------------------- /src/Storage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.23; 3 | 4 | import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; 5 | 6 | import {IStorage} from "./interfaces/IStorage.sol"; 7 | 8 | import {WalletCoreLib} from "./lib/WalletCoreLib.sol"; 9 | import {Errors} from "./lib/Errors.sol"; 10 | 11 | contract Storage is IStorage { 12 | //mutable storage 13 | uint256 private _nonce; 14 | mapping(address => bool) private _validValidator; 15 | mapping(uint256 => bool) private _invalidSessionId; 16 | 17 | /** 18 | * @notice Restricts function access to the wallet owner only 19 | * @dev Reverts with INVALID_OWNER if caller is not the owner 20 | */ 21 | modifier onlyOwner() { 22 | if (msg.sender != getOwner()) { 23 | revert Errors.InvalidOwner(); 24 | } 25 | _; 26 | } 27 | 28 | /** 29 | * @notice Reads the current nonce and increments it for the next transaction 30 | * @dev Only callable by wallet owner. Uses unchecked math for gas optimization 31 | * @param validator The address of the validator contract 32 | * @return uint256 The current nonce before increment 33 | */ 34 | function readAndUpdateNonce( 35 | address validator 36 | ) external onlyOwner returns (uint256) { 37 | validateValidator(validator); 38 | unchecked { 39 | uint256 currentNonce = _nonce++; 40 | emit NonceConsumed(currentNonce); 41 | return currentNonce; 42 | } 43 | } 44 | 45 | /** 46 | * @notice Sets a validator's whitelist status 47 | * @dev Only callable by wallet owner 48 | * @param validator Address of the validator 49 | * @param isValid True to whitelist, false to remove 50 | */ 51 | function setValidatorStatus( 52 | address validator, 53 | bool isValid 54 | ) external onlyOwner { 55 | _validValidator[validator] = isValid; 56 | emit ValidatorStatusUpdated(validator, isValid); 57 | } 58 | 59 | /** 60 | * @notice Revokes the specified session ID, marking it as invalid. 61 | * @dev Only callable by wallet owner 62 | * @param id The session ID to be revoked 63 | */ 64 | function revokeSession(uint256 id) external onlyOwner { 65 | _invalidSessionId[id] = true; 66 | emit SessionRevoked(id); 67 | } 68 | 69 | /** 70 | * @notice Returns the owner address of the wallet 71 | * @dev Decodes the owner address from the proxy contract's initialization data 72 | * @return address The owner address of the wallet 73 | */ 74 | function getOwner() public view returns (address) { 75 | return abi.decode(Clones.fetchCloneArgs(address(this)), (address)); 76 | } 77 | 78 | /** 79 | * @notice Returns the current nonce value 80 | * @dev Can be called by anyone 81 | * @return uint256 The current nonce value 82 | */ 83 | function getNonce() external view returns (uint256) { 84 | return _nonce; 85 | } 86 | 87 | /** 88 | * @notice Checks if a validator is whitelisted 89 | * @dev Reverts if validator is not whitelisted 90 | * @param validator Address of the validator to check 91 | */ 92 | function validateValidator(address validator) public view { 93 | if ( 94 | validator != WalletCoreLib.SELF_VALIDATION_ADDRESS && 95 | !_validValidator[validator] 96 | ) revert Errors.InvalidValidator(validator); 97 | } 98 | 99 | /** 100 | * @notice Validates a session and its associated validator 101 | * @dev Reverts if: 102 | * - Session is invalid (blacklisted) 103 | * - Validator is not activated 104 | * @param id The ID of the session to validate 105 | * @param validator The validator address (0x0 to skip validator check) 106 | */ 107 | function validateSession(uint256 id, address validator) external view { 108 | if (_invalidSessionId[id]) revert Errors.InvalidSessionId(); 109 | validateValidator(validator); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/FallbackHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "./Base.t.sol"; 5 | import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 6 | import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 7 | import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; 8 | import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; 9 | 10 | contract FallbackHandlerTest is Base { 11 | function test_receive_accepts_ether() public { 12 | uint256 initialBalance = _alice.balance; 13 | (bool success, ) = payable(_alice).call{value: 1 ether}(""); 14 | assertTrue(success); 15 | assertEq(_alice.balance, initialBalance + 1 ether); 16 | } 17 | 18 | function test_fallback_reverts_for_invalid_selector() public { 19 | // Test case 1: completely invalid selector 20 | bytes memory invalidData = abi.encodeWithSelector( 21 | bytes4(keccak256("invalidFunction()")), 22 | address(this) 23 | ); 24 | (bool success, ) = _alice.call(invalidData); 25 | assert(!success); 26 | } 27 | 28 | function test_fallback_handles_erc721_receive() public { 29 | // Create calldata for onERC721Received 30 | bytes memory data = abi.encodeWithSelector( 31 | 0x150b7a02, // onERC721Received selector 32 | address(this), 33 | address(this), 34 | 1, 35 | "" 36 | ); 37 | 38 | // Call fallback function 39 | (bool success, bytes memory returnData) = _alice.call(data); 40 | 41 | // Verify success and returned selector 42 | assertTrue(success); 43 | assertEq(bytes4(returnData), bytes4(0x150b7a02)); 44 | } 45 | 46 | function test_fallback_handles_erc1155_receive() public { 47 | // Create calldata for onERC1155Received 48 | bytes memory data = abi.encodeWithSelector( 49 | 0xf23a6e61, // onERC1155Received selector 50 | address(this), 51 | address(this), 52 | 1, 53 | 1, 54 | "" 55 | ); 56 | 57 | // Call fallback function 58 | (bool success, bytes memory returnData) = _alice.call(data); 59 | 60 | // Verify success and returned selector 61 | assertTrue(success); 62 | assertEq(bytes4(returnData), bytes4(0xf23a6e61)); 63 | } 64 | 65 | function test_fallback_handles_erc1155_batch_receive() public { 66 | // Create arrays for batch transfer 67 | uint256[] memory ids = new uint256[](2); 68 | uint256[] memory amounts = new uint256[](2); 69 | ids[0] = 1; 70 | ids[1] = 2; 71 | amounts[0] = 10; 72 | amounts[1] = 20; 73 | 74 | // Create calldata for onERC1155BatchReceived 75 | bytes memory data = abi.encodeWithSelector( 76 | 0xbc197c81, // onERC1155BatchReceived selector 77 | address(this), 78 | address(this), 79 | ids, 80 | amounts, 81 | "" 82 | ); 83 | 84 | // Call fallback function 85 | (bool success, bytes memory returnData) = _alice.call(data); 86 | 87 | // Verify success and returned selector 88 | assertTrue(success); 89 | assertEq(bytes4(returnData), bytes4(0xbc197c81)); 90 | } 91 | 92 | function test_supports_token_receive_interfaces() public view { 93 | assertEq( 94 | IERC165(_alice).supportsInterface( 95 | type(IERC721Receiver).interfaceId 96 | ), 97 | true 98 | ); 99 | assertEq( 100 | IERC165(_alice).supportsInterface( 101 | type(IERC1155Receiver).interfaceId 102 | ), 103 | true 104 | ); 105 | assertEq( 106 | IERC165(_alice).supportsInterface(type(IERC1271).interfaceId), 107 | true 108 | ); 109 | assertEq( 110 | IERC165(_alice).supportsInterface(type(IERC165).interfaceId), 111 | true 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/Hook.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "forge-std/Test.sol"; 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | import "./Base.t.sol"; 8 | import {IExecutor} from "src/interfaces/IExecutor.sol"; 9 | import {MockERC20} from "src/test/MockERC20.sol"; 10 | import {MockHook} from "src/test/MockHook.sol"; 11 | import {MockExecutor} from "src/test/MockExecutor.sol"; 12 | import {Call, Session} from "src/Types.sol"; 13 | 14 | contract HookTest is Base { 15 | MockERC20 mockToken; 16 | MockExecutor mockExecutor; 17 | MockHook mockHook; 18 | 19 | address receipt1; 20 | uint256 receipt1Pri; 21 | address receipt2; 22 | uint256 receipt2Pri; 23 | address sessionOwner; 24 | uint256 sessionOwnerPri; 25 | 26 | Session session; 27 | 28 | uint256 validAfter = 0; 29 | uint256 validUntil = type(uint256).max; 30 | 31 | function setUp() public override { 32 | super.setUp(); 33 | 34 | (sessionOwner, sessionOwnerPri) = makeAddrAndKey("sessionOwner"); 35 | (receipt1, receipt2Pri) = makeAddrAndKey("receipt1"); 36 | (receipt2, receipt2Pri) = makeAddrAndKey("receipt2"); 37 | 38 | vm.prank(_alice); 39 | mockToken = new MockERC20(); 40 | mockHook = new MockHook(); 41 | mockExecutor = new MockExecutor(IWalletCore(_alice)); 42 | 43 | session = Session({ 44 | id: 0, 45 | executor: address(mockExecutor), 46 | validator: address(1), 47 | validUntil: validUntil, 48 | validAfter: validAfter, 49 | preHook: bytes.concat( 50 | bytes20(address(mockHook)), 51 | abi.encode(address(mockToken), 50 ether) 52 | ), 53 | postHook: bytes.concat(bytes20(address(mockHook))), 54 | signature: "" 55 | }); 56 | 57 | bytes32 hash = IExecutor(_alice).getSessionTypedHash(session); 58 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_alicePk, hash); 59 | bytes memory sessionSignature = abi.encodePacked(r, s, v); 60 | session.signature = sessionSignature; 61 | } 62 | 63 | function test_hook_executes_transfers_within_limit() public { 64 | Call memory call1 = Call({ 65 | target: address(mockToken), 66 | value: 0, 67 | data: abi.encodeWithSignature( 68 | "transfer(address,uint256)", 69 | receipt1, 70 | 20 ether 71 | ) 72 | }); 73 | Call memory call2 = Call({ 74 | target: address(mockToken), 75 | value: 0, 76 | data: abi.encodeWithSignature( 77 | "transfer(address,uint256)", 78 | receipt2, 79 | 30 ether 80 | ) 81 | }); 82 | 83 | Call[] memory multiCallArray = new Call[](2); 84 | multiCallArray[0] = call1; 85 | multiCallArray[1] = call2; 86 | 87 | vm.prank(sessionOwner); 88 | mockExecutor.execute(multiCallArray, session); 89 | 90 | assertEq( 91 | mockToken.balanceOf(receipt1), 92 | 20 ether, 93 | "Invalid balance for receipt1" 94 | ); 95 | assertEq( 96 | mockToken.balanceOf(receipt2), 97 | 30 ether, 98 | "Invalid balance for receipt2" 99 | ); 100 | } 101 | 102 | function test_hook_reverts_when_exceeding_transfer_limit() public { 103 | Call memory call1 = Call({ 104 | target: address(mockToken), 105 | value: 0, 106 | data: abi.encodeWithSignature( 107 | "transfer(address,uint256)", 108 | receipt1, 109 | 30 ether 110 | ) 111 | }); 112 | Call memory call2 = Call({ 113 | target: address(mockToken), 114 | value: 0, 115 | data: abi.encodeWithSignature( 116 | "transfer(address,uint256)", 117 | receipt2, 118 | 30 ether 119 | ) 120 | }); 121 | 122 | Call[] memory multiCallArray = new Call[](2); 123 | multiCallArray[0] = call1; 124 | multiCallArray[1] = call2; 125 | 126 | vm.prank(sessionOwner); 127 | vm.expectRevert("Total transfer amount exceeds limit"); 128 | mockExecutor.execute(multiCallArray, session); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/Base.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "forge-std/Test.sol"; 5 | import {IStorage} from "src/interfaces/IStorage.sol"; 6 | import {IWalletCore} from "src/interfaces/IWalletCore.sol"; 7 | import {IValidation} from "src/interfaces/IValidation.sol"; 8 | import {ValidationLogic} from "src/ValidationLogic.sol"; 9 | import {WalletCore} from "src/WalletCore.sol"; 10 | import {ECDSAValidator} from "src/validator/ECDSAValidator.sol"; 11 | import {WalletCoreLib} from "src/lib/WalletCoreLib.sol"; 12 | import {Call} from "src/Types.sol"; 13 | import {Errors} from "src/lib/Errors.sol"; 14 | import {DeployInitHelper, DeployFactory} from "scripts/DeployInitHelper.sol"; 15 | 16 | contract Base is Test { 17 | string public constant NAME = "wallet-core"; 18 | string public constant VERSION = "1.0.0"; 19 | 20 | address internal _alice; 21 | uint256 internal _alicePk; 22 | address internal _bob; 23 | uint256 internal _bobPk; 24 | IStorage internal _storageImpl; 25 | ECDSAValidator internal _ecdsaValidatorImpl; 26 | WalletCore internal _walletCore; 27 | DeployFactory public deployFactory; 28 | bytes32 constant _STORAGE_SALT = WalletCoreLib.STORAGE_SALT; 29 | 30 | function setUp() public virtual { 31 | (_alice, _alicePk) = makeAddrAndKey("alice"); 32 | (_bob, _bobPk) = makeAddrAndKey("bob"); 33 | 34 | deployFactory = new DeployFactory(); 35 | bytes32 deployFactorySalt = vm.envBytes32("DEPLOY_FACTORY_SALT"); 36 | 37 | ( 38 | address _storageAddr, 39 | address _ecdsaValidatorAddr, 40 | address _walletCoreAddr 41 | ) = DeployInitHelper.deployContracts( 42 | deployFactory, 43 | deployFactorySalt, 44 | NAME, 45 | VERSION 46 | ); 47 | 48 | _storageImpl = IStorage(_storageAddr); 49 | _ecdsaValidatorImpl = ECDSAValidator(_ecdsaValidatorAddr); 50 | _walletCore = WalletCore(payable(_walletCoreAddr)); 51 | 52 | _setCodeToEOA(address(_walletCore), _alice); 53 | 54 | deal(_alice, 10 ether); 55 | 56 | // Alice initializes the account 57 | vm.prank(_alice); 58 | IWalletCore(_alice).initialize(); 59 | vm.stopPrank(); 60 | } 61 | 62 | function _getEdcsaValidatorAddress( 63 | address eoa, 64 | address signer, 65 | address validatorImpl 66 | ) internal view returns (address) { 67 | bytes memory initCode = abi.encode(signer); 68 | return 69 | IValidation(eoa).computeValidatorAddress(validatorImpl, initCode); 70 | } 71 | 72 | function _setCodeToEOA(address contractCode, address eoa) internal { 73 | bytes memory code = address(contractCode).code; 74 | vm.etch(eoa, code); 75 | } 76 | 77 | function _construct_signature( 78 | uint256 privateKey, 79 | uint256 nonce, 80 | Call[] memory calls 81 | ) public view returns (bytes memory) { 82 | bytes32 hash = _getValidationTypedHash(nonce, calls); 83 | return _signHash(privateKey, hash); 84 | } 85 | 86 | function _construct_calls_data() public view returns (Call[] memory) { 87 | Call[] memory calls = new Call[](1); 88 | calls[0] = Call({target: _bob, value: 1 ether, data: ""}); 89 | return calls; 90 | } 91 | 92 | function _getNonce(address account) internal view returns (uint256) { 93 | return 94 | IStorage(WalletCore(payable(account)).getMainStorage()).getNonce(); 95 | } 96 | 97 | function _getValidationTypedHash( 98 | uint256 nonce, 99 | Call[] memory calls 100 | ) internal view returns (bytes32) { 101 | return ValidationLogic(_alice).getValidationTypedHash(nonce, calls); 102 | } 103 | 104 | function _signHash( 105 | uint256 privateKey, 106 | bytes32 hash 107 | ) internal pure returns (bytes memory) { 108 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, hash); 109 | return abi.encodePacked(r, s, v); 110 | } 111 | 112 | function _addValidator(address signer) internal returns (address) { 113 | // Validator signer 114 | bytes memory initCode = abi.encode(signer); 115 | 116 | // Compute validator address 117 | address validator = _getEdcsaValidatorAddress( 118 | signer, 119 | signer, 120 | address(_ecdsaValidatorImpl) 121 | ); 122 | 123 | // Add validator 124 | vm.startPrank(signer); 125 | IWalletCore(signer).addValidator( 126 | address(_ecdsaValidatorImpl), 127 | initCode 128 | ); 129 | vm.stopPrank(); 130 | 131 | return validator; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/ExecutorLogic.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.23; 3 | 4 | import {IExecutor} from "./interfaces/IExecutor.sol"; 5 | import {IHook} from "./interfaces/IHook.sol"; 6 | import {IStorage} from "./interfaces/IStorage.sol"; 7 | 8 | import {WalletCoreBase} from "./base/WalletCoreBase.sol"; 9 | import {WalletCoreLib} from "./lib/WalletCoreLib.sol"; 10 | import {Call, Session} from "./Types.sol"; 11 | import {Errors} from "./lib/Errors.sol"; 12 | 13 | abstract contract ExecutorLogic is IExecutor, WalletCoreBase { 14 | bytes32 public constant SESSION_TYPEHASH = 15 | keccak256( 16 | "Session(address wallet,uint256 id,address executor,address validator,uint256 validUntil,uint256 validAfter,bytes preHook,bytes postHook)" 17 | ); 18 | 19 | /** 20 | * @notice Restricts function access to the authorized executor with a valid session and executes hooks 21 | * @dev Performs two checks: 22 | * 1. Caller must match the session's executor 23 | * 2. Session must be valid (not expired, not invalidated) 24 | * @dev Hook address is extracted from first 20 bytes of hook data 25 | * @dev Remaining bytes are passed as hook parameters 26 | * @param session The session data containing executor permissions and hook configurations 27 | * @param calls Array of calls to be executed 28 | * @custom:hooks PreHook runs before execution, PostHook runs after with preHook return data 29 | */ 30 | modifier onlyValidSession(Session calldata session, Call[] calldata calls) { 31 | validateSession(session); 32 | 33 | bytes memory ret; 34 | 35 | if (session.preHook.length >= 20) 36 | ret = IHook(address(bytes20(session.preHook[:20]))).preCheck( 37 | calls, 38 | session.preHook[20:], 39 | msg.sender 40 | ); 41 | 42 | _; 43 | 44 | if (session.postHook.length >= 20) 45 | IHook(address(bytes20(session.postHook[:20]))).postCheck( 46 | ret, 47 | session.postHook[20:], 48 | msg.sender 49 | ); 50 | } 51 | 52 | /** 53 | * @notice Validates a session's time bounds, status, and signature 54 | * @dev Checks three conditions: 55 | * 1. Current time is within session's time bounds 56 | * 2. Session is not invalidated in storage 57 | * 3. Session signature is valid using specified validator 58 | * @param session The session data to validate 59 | */ 60 | function validateSession(Session calldata session) public view { 61 | // Check executor authorization 62 | if (msg.sender != session.executor) revert Errors.InvalidExecutor(); 63 | 64 | // Check time bounds 65 | if ( 66 | session.validAfter > block.timestamp || 67 | block.timestamp > session.validUntil 68 | ) revert Errors.InvalidSession(); 69 | 70 | // Check invalidSessionId & validValidator in storage 71 | getMainStorage().validateSession(session.id, session.validator); 72 | 73 | // Validate signature 74 | bytes32 hash = getSessionTypedHash(session); 75 | bool isValid = WalletCoreLib.validate( 76 | session.validator, 77 | hash, 78 | session.signature 79 | ); 80 | if (!isValid) revert Errors.InvalidSignature(); 81 | } 82 | 83 | /** 84 | * @notice Creates an EIP-712 typed data hash for session validation 85 | * @dev Combines session data with domain separator using EIP-712 standard 86 | * @param session The session data containing ID, executor, validator, time bounds, and hooks 87 | * @return bytes32 The EIP-712 compliant hash for signature verification 88 | */ 89 | function getSessionTypedHash( 90 | Session calldata session 91 | ) public view returns (bytes32) { 92 | return _hashTypedDataV4(_getSessionHash(session)); 93 | } 94 | 95 | /** 96 | * @notice Creates a hash of session parameters for EIP-712 struct hashing 97 | * @dev Packs session data with SESSION_TYPEHASH using keccak256 98 | * @param session Session data 99 | * @return bytes32 The packed and hashed session data 100 | */ 101 | function _getSessionHash( 102 | Session calldata session 103 | ) internal view returns (bytes32) { 104 | return 105 | keccak256( 106 | abi.encode( 107 | SESSION_TYPEHASH, 108 | _walletImplementation(), 109 | session.id, 110 | session.executor, 111 | session.validator, 112 | session.validUntil, 113 | session.validAfter, 114 | keccak256(session.preHook), 115 | keccak256(session.postHook) 116 | ) 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wallet Core - EIP-7702 Smart Contract Wallet 2 | 3 | A modular and secure implementation of EIP-7702 smart contract wallet with multiple execution types and advanced security features. 4 | 5 | ## Prerequisites 6 | 7 | 1. Make sure you have Node.js installed 8 | 2. Run `npm install` in the project root to install dependencies 9 | 3. Install Foundry (which provides the `forge` command) by running: 10 | 11 | ```bash 12 | curl -L https://foundry.paradigm.xyz | bash 13 | ``` 14 | 15 | ## Overview 16 | 17 | This implementation provides a flexible smart contract wallet that supports: 18 | 19 | - EIP-7702 Type 4 initialization 20 | - Three distinct execution types 21 | - Advanced security features including replay protection and batched transactions 22 | - Modular architecture with separate storage and execution logic 23 | 24 | ## Core Features 25 | 26 | ### 1. Set Code & Initialize 27 | 28 | The wallet setup involves two main steps: 29 | 30 | 1. **Set Code**: 31 | 32 | - Submits an EIP-7702 Type 4 transaction 33 | - Assigns smart contract code to an EOA (Externally Owned Address) 34 | - Transforms the EOA into a smart contract wallet 35 | 36 | 2. **Initialize Contract**: 37 | - Calls the `initialize` function in Wallet Core 38 | - Sets up proper configuration and state 39 | - Creates and links Core Storage for nonce management 40 | 41 | ### 2. Execution Types 42 | 43 | #### Type 1: Execute From Self 44 | 45 | - Direct execution from the wallet itself 46 | - Uses `executeFromSelf` function 47 | - Verifies transaction through self-check 48 | - Supports batched transactions via `_batchCall` 49 | - Most gas-efficient execution type 50 | 51 | #### Type 2: Execute From Relayer 52 | 53 | 1. **Validator Setup**: 54 | - User adds validator to wallet core 55 | - Validator signs transaction off-chain with nonce 56 | 2. **Execution Flow**: 57 | - User provides off-chain signature 58 | - Relayer submits transaction via `executeWithValidation` 59 | - Core Storage manages nonce for replay protection 60 | - ECDSA validation ensures signature authenticity 61 | 62 | #### Type 3: Execute From Executor 63 | 64 | 1. **Session-Based Execution**: 65 | 66 | - No pre-encoded calls needed 67 | - Uses hook-based validation (`preHook` and `postHook`) 68 | - Single signature authorizes entire session 69 | 70 | 2. **Session Parameters**: 71 | - `session_id` 72 | - `validAfter` 73 | - `validUntil` 74 | - `executor` 75 | - `validator` 76 | - `preCheck` 77 | - `postCheck` 78 | - `signature` 79 | 80 | ## Architecture 81 | 82 | The implementation follows a modular design: 83 | 84 | - `WalletCore`: Main contract handling execution logic 85 | - `Core Storage`: Manages nonces and validation states 86 | - `ExecutionLogic`: Handles different execution types 87 | - `ValidationLogic`: Manages signature and session validation 88 | - `ExecutorLogic`: Implements session-based execution with hooks 89 | - `FallbackHandler`: Provides token receiving capabilities 90 | 91 | ## Deployed Contracts 92 | 93 | ### Ethereum Mainnet 94 | 95 | | Contract | Address | 96 | | ----------- | -------------------------------------------- | 97 | | WalletCore | `0x80296FF8D1ED46f8e3C7992664D13B833504c2Bb` | 98 | | CoreStorage | `0x7DAF91DFe55FcAb363416A6E3bceb3Da34ff1d30` | 99 | 100 | ### Sepolia Testnet 101 | 102 | | Contract | Address | 103 | | ----------- | -------------------------------------------- | 104 | | WalletCore | `0x80296FF8D1ED46f8e3C7992664D13B833504c2Bb` | 105 | | CoreStorage | `0x7DAF91DFe55FcAb363416A6E3bceb3Da34ff1d30` | 106 | 107 | ## Usage 108 | 109 | ### 1. Set Code & Initialize Wallet 110 | 111 | Deploy and initialize your ERC-7702 wallet: 112 | 113 | ```bash 114 | npx hardhat run scripts/smoke_test/1-setCodeAndInitialize.ts --network 115 | ``` 116 | 117 | This script: 118 | 119 | - Sets up the EOA as a smart contract wallet 120 | - Initializes core storage and configuration 121 | 122 | ### 2. Execute Direct Transactions 123 | 124 | Send transactions directly from the wallet: 125 | 126 | ```bash 127 | forge script scripts/smoke_test/2-sendTxs.sol --rpc-url --broadcast 128 | ``` 129 | 130 | This demonstrates: 131 | 132 | - Self-executed transactions 133 | - Batch call functionality 134 | - Direct interaction with external contracts 135 | 136 | ### 3. Execute via Relayer 137 | 138 | Send transactions through a relayer: 139 | 140 | ```bash 141 | forge script scripts/smoke_test/3-sendTxsAsRelayer.sol --rpc-url --broadcast 142 | ``` 143 | 144 | This shows: 145 | 146 | - Relayer-based transaction execution 147 | - Signature validation 148 | - Nonce management 149 | - Gas-efficient transaction batching 150 | 151 | ## Security Considerations 152 | 153 | - All execution types include proper validation 154 | - Nonce management prevents replay attacks 155 | - Session-based execution can be revoked 156 | - Hook-based validation provides additional security layers 157 | -------------------------------------------------------------------------------- /src/lib/WalletCoreLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; 5 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | 7 | import {IValidator} from "../interfaces/IValidator.sol"; 8 | 9 | library WalletCoreLib { 10 | using ECDSA for bytes32; 11 | using Clones for address; 12 | /** 13 | * @notice new storage should have a different salt 14 | */ 15 | bytes32 public constant STORAGE_SALT = 16 | keccak256(abi.encodePacked("storage")); 17 | 18 | bytes32 public constant VALIDATOR_SALT = 19 | keccak256(abi.encodePacked("validator")); 20 | 21 | address public constant SELF_VALIDATION_ADDRESS = address(1); 22 | 23 | /** 24 | * @notice Computes the deterministic address of the wallet's storage contract 25 | * @dev Uses OpenZeppelin's Clones library to predict the address before deployment 26 | * @param storageImpl The implementation address of the storage contract 27 | * @return address The deterministic address where the storage clone will be deployed 28 | * @custom:args The immutable arguments encoded are: 29 | * - address(this): The wallet address that owns this storage 30 | * @custom:salt A unique salt derived from STORAGE_SALT 31 | */ 32 | function _getStorage(address storageImpl) internal view returns (address) { 33 | return 34 | storageImpl.predictDeterministicAddressWithImmutableArgs( 35 | abi.encode(address(this)), 36 | STORAGE_SALT, 37 | address(this) 38 | ); 39 | } 40 | 41 | /** 42 | * @notice Validates a transaction or operation using either ECDSA signatures or an external validator contract 43 | * @dev Two validation methods are supported: 44 | * 1. ECDSA validation (when validator == address(1)): Recovers signer from signature and verifies it matches the wallet address 45 | * 2. External validator (any other address): Calls the validator contract and checks if it's authorized to validate 46 | * @param validator Address of the validator to use (address(1) for ECDSA signature validation) 47 | * @param typedDataHash EIP-712 typed data hash of the data to be validated 48 | * @param validationData For ECDSA: the 65-byte signature; For external validators: custom validation data 49 | * @return bool True if validation succeeds, false otherwise 50 | * @custom:security Ensure validator contracts are properly verified and authorized before use 51 | */ 52 | function validate( 53 | address validator, 54 | bytes32 typedDataHash, 55 | bytes calldata validationData 56 | ) internal view returns (bool) { 57 | if (validator == SELF_VALIDATION_ADDRESS) { 58 | return _validateSelf(typedDataHash, validationData); 59 | } else { 60 | try IValidator(validator).validate(typedDataHash, validationData) { 61 | return true; 62 | } catch { 63 | return false; 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * @notice Validates that a signature was signed by this contract 70 | * @param typedDataHash The hash of the data that was signed 71 | * @param signature The ECDSA signature to verify 72 | * @return bool True if the validation passes, false otherwise 73 | * @dev Reverts with INVALID_SIGNATURE if the signer is not account itself 74 | */ 75 | function _validateSelf( 76 | bytes32 typedDataHash, 77 | bytes calldata signature 78 | ) internal view returns (bool) { 79 | (address recoveredSigner, , ) = typedDataHash.tryRecover(signature); 80 | return recoveredSigner == address(this); 81 | } 82 | 83 | /** 84 | * @notice Creates a unique deployment salt by combining validator implementation and init code 85 | * @param validatorImpl The validator implementation address 86 | * @param initHash Hash of the validator's initialization code 87 | * @return bytes32 The computed salt for deterministic deployment 88 | */ 89 | function _computeCreationSalt( 90 | address validatorImpl, 91 | bytes32 initHash 92 | ) internal pure returns (bytes32) { 93 | return keccak256(abi.encode(validatorImpl, initHash)); 94 | } 95 | 96 | /** 97 | * @notice Computes the deterministic address of a validator contract before deployment 98 | * @param validatorImpl The implementation address of the validator 99 | * @param immutableArgs The initialization data for the validator 100 | * @param creationSalt A unique salt for deterministic deployment 101 | * @param deployer The address that will deploy the validator 102 | * @return The predicted address where the validator will be deployed 103 | */ 104 | function _computeValidatorAddress( 105 | address validatorImpl, 106 | bytes calldata immutableArgs, 107 | bytes32 creationSalt, 108 | address deployer 109 | ) internal pure returns (address) { 110 | return 111 | validatorImpl.predictDeterministicAddressWithImmutableArgs( 112 | immutableArgs, 113 | creationSalt, 114 | deployer 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /test/Validator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "./Base.t.sol"; 5 | import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; 6 | import {IValidation} from "src/interfaces/IValidation.sol"; 7 | import "src/lib/Errors.sol"; 8 | 9 | contract ValidatorTest is Base { 10 | using Clones for address; 11 | address internal _charlie; 12 | uint256 internal _charliePk; 13 | 14 | event ValidatorAdded(address validator); 15 | event ValidatorRemoved(address validator); 16 | event ValidatorStatusUpdated(address validator, bool status); 17 | error FailedDeployment(); 18 | 19 | function setUp() public override { 20 | (_charlie, _charliePk) = makeAddrAndKey("charlie"); 21 | super.setUp(); 22 | } 23 | 24 | function test_addValidator_reverts_for_non_owner() public { 25 | vm.prank(_bob); 26 | bytes memory initCode = abi.encode(_charlie); 27 | 28 | // Expect not from self revert 29 | vm.expectRevert(abi.encodeWithSelector(Errors.NotFromSelf.selector)); 30 | IWalletCore(_alice).addValidator( 31 | address(_ecdsaValidatorImpl), 32 | initCode 33 | ); 34 | } 35 | 36 | function test_addValidator_reverts_for_invalid_implementation() public { 37 | vm.prank(_alice); 38 | address dave = vm.addr(2); 39 | bytes memory initCode = abi.encode(_charlie); 40 | 41 | // Expect invalid validator implementation revert 42 | vm.expectRevert( 43 | abi.encodeWithSelector(Errors.InvalidValidatorImpl.selector, dave) 44 | ); 45 | IWalletCore(_alice).addValidator( 46 | address(dave), // Invalid validatorImpl 47 | initCode 48 | ); 49 | } 50 | 51 | function test_addValidator_reverts_for_duplicate() public { 52 | vm.prank(_alice); 53 | bytes memory initCode = abi.encode(_alice); // duplicate validator 54 | 55 | IWalletCore(_alice).addValidator( 56 | address(_ecdsaValidatorImpl), 57 | initCode 58 | ); 59 | 60 | // Expect failed if duplicate validator 61 | vm.prank(_alice); 62 | vm.expectRevert(abi.encodeWithSelector(FailedDeployment.selector)); 63 | IWalletCore(_alice).addValidator( 64 | address(_ecdsaValidatorImpl), 65 | initCode 66 | ); 67 | } 68 | 69 | function test_fraudulent_validator_reverts() public { 70 | // Create a fraudulent validator with impersonated signer 71 | bytes memory initCode = abi.encode(_alice); 72 | bytes32 salt = WalletCoreLib._computeCreationSalt( 73 | address(_ecdsaValidatorImpl), 74 | keccak256(initCode) 75 | ); 76 | address fraudulentValidator = address( 77 | address(_ecdsaValidatorImpl).cloneDeterministicWithImmutableArgs( 78 | initCode, 79 | salt 80 | ) 81 | ); 82 | 83 | uint256 nonce = _getNonce(_alice); 84 | Call[] memory calls = _construct_calls_data(); 85 | bytes memory signature = _construct_signature(_alicePk, nonce, calls); 86 | 87 | vm.prank(_alice); 88 | vm.expectRevert( 89 | abi.encodeWithSelector( 90 | Errors.InvalidValidator.selector, 91 | fraudulentValidator 92 | ) 93 | ); 94 | IWalletCore(_alice).executeWithValidator( 95 | calls, 96 | fraudulentValidator, 97 | signature 98 | ); 99 | } 100 | 101 | function test_validator_can_be_added() public { 102 | bytes memory initCode = abi.encode(_charlie); 103 | 104 | // Compute validator address 105 | address charlieValidator = _getEdcsaValidatorAddress( 106 | _alice, 107 | _charlie, 108 | address(_ecdsaValidatorImpl) 109 | ); 110 | 111 | // Expect validator added event 112 | vm.expectEmit(); 113 | emit ValidatorAdded(charlieValidator); 114 | 115 | // Add validator 116 | vm.prank(_alice); 117 | IWalletCore(_alice).addValidator( 118 | address(_ecdsaValidatorImpl), 119 | initCode 120 | ); 121 | 122 | assertEq(ECDSAValidator(charlieValidator).getSigner(), _charlie); 123 | } 124 | 125 | function test_new_validator_can_validate_transactions() public { 126 | vm.prank(_alice); 127 | bytes memory initCode = abi.encode(_charlie); 128 | 129 | // Add validator 130 | IWalletCore(_alice).addValidator( 131 | address(_ecdsaValidatorImpl), 132 | initCode 133 | ); 134 | 135 | uint256 nonce = _getNonce(_alice); 136 | Call[] memory calls = _construct_calls_data(); 137 | bytes memory signature = _construct_signature(_charliePk, nonce, calls); 138 | address charlieValidator = _getEdcsaValidatorAddress( 139 | _alice, 140 | _charlie, 141 | address(_ecdsaValidatorImpl) 142 | ); 143 | 144 | // Relayer executes with Charlie signature 145 | vm.prank(_bob); 146 | IWalletCore(_alice).executeWithValidator( 147 | calls, 148 | charlieValidator, 149 | signature 150 | ); 151 | 152 | assertEq(address(_bob).balance, 1 ether); 153 | } 154 | 155 | function test_pause_reverts_for_non_owner() public { 156 | IStorage storageContract = IStorage( 157 | WalletCore(payable(_alice)).getMainStorage() 158 | ); 159 | vm.prank(_bob); 160 | vm.expectRevert(abi.encodeWithSelector(Errors.InvalidOwner.selector)); 161 | storageContract.setValidatorStatus(address(0), false); 162 | } 163 | 164 | function test_pause_validator_succeeds() public { 165 | // First to add a valid validator 166 | address aliceECDSAValidator = _addValidator(_alice); 167 | 168 | vm.startPrank(_alice); 169 | vm.expectEmit(); 170 | emit ValidatorStatusUpdated(aliceECDSAValidator, true); 171 | IStorage(WalletCore(payable(_alice)).getMainStorage()) 172 | .setValidatorStatus(aliceECDSAValidator, true); 173 | 174 | emit ValidatorStatusUpdated(aliceECDSAValidator, false); 175 | IStorage(WalletCore(payable(_alice)).getMainStorage()) 176 | .setValidatorStatus(aliceECDSAValidator, false); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/ValidationLogic.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.23; 3 | 4 | import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; 5 | 6 | import {IValidation} from "./interfaces/IValidation.sol"; 7 | import {IStorage} from "./interfaces/IStorage.sol"; 8 | 9 | import {WalletCoreBase} from "./base/WalletCoreBase.sol"; 10 | import {WalletCoreLib} from "./lib/WalletCoreLib.sol"; 11 | import {Call} from "./Types.sol"; 12 | import {Errors} from "./lib/Errors.sol"; 13 | 14 | abstract contract ValidationLogic is IValidation, WalletCoreBase { 15 | using Clones for address; 16 | 17 | bytes32 private constant CALLS_TYPEHASH = 18 | keccak256("Calls(address wallet,uint256 nonce,bytes32[] calls)"); 19 | bytes32 private constant CALL_TYPEHASH = 20 | keccak256("Call(address target,uint256 value,bytes data)"); 21 | 22 | /** 23 | * @notice Modifier that validates a transaction using the specified validator 24 | * @dev Reads and updates the nonce from storage before validation 25 | * @param calls Array of calls to be validated 26 | * @param validator Address of the validator contract 27 | * @param validationData The validation data (signature for ECDSA, custom data for other validators) 28 | */ 29 | modifier onlyValidator( 30 | Call[] calldata calls, 31 | address validator, 32 | bytes calldata validationData 33 | ) { 34 | uint256 nonce = getMainStorage().readAndUpdateNonce(validator); 35 | _validateCall(nonce, calls, validator, validationData); 36 | _; 37 | } 38 | 39 | /** 40 | * @notice Adds a new validator contract to the wallet 41 | * @param validatorImpl The implementation address of the validator contract to be registered 42 | * @param immutableArgs Initialization data for the validator contract 43 | */ 44 | function _addValidator( 45 | address validatorImpl, 46 | bytes calldata immutableArgs 47 | ) internal { 48 | if (validatorImpl.code.length == 0) 49 | revert Errors.InvalidValidatorImpl(validatorImpl); 50 | 51 | // Fix creation salt 52 | bytes32 salt = WalletCoreLib.VALIDATOR_SALT; 53 | 54 | // Deploy using deterministic address 55 | address createdAddress = validatorImpl 56 | .cloneDeterministicWithImmutableArgs(immutableArgs, salt); 57 | 58 | getMainStorage().setValidatorStatus(createdAddress, true); 59 | 60 | // Initialize the validator 61 | emit ValidatorAdded(createdAddress); 62 | } 63 | 64 | /** 65 | * @notice Implements EIP-1271 signature validation standard 66 | * @dev Validates signatures by checking both the validator's signature and its authenticity 67 | * @dev The signature is bound to this wallet's address and the current chain ID 68 | * @param validator The address of the validator contract 69 | * @param _hash The hash of the data to be validated 70 | * @param signature ABI encoded (validator, signature) pair where: 71 | * - validator: address of the validator contract 72 | * - signature: the actual signature or validation data 73 | * @return bytes4 Returns MAGIC_VALUE (0x1626ba7e) if valid, INVALID_VALUE (0xffffffff) if invalid 74 | * @custom:security Verifies the validator is legitimate by checking its deterministic deployment 75 | */ 76 | function isValidSignature( 77 | address validator, 78 | bytes32 _hash, 79 | bytes calldata signature 80 | ) internal view returns (bool) { 81 | try getMainStorage().validateValidator(validator) {} catch { 82 | return false; 83 | } 84 | 85 | bytes32 boundHash = keccak256( 86 | abi.encode(bytes32(block.chainid), address(this), _hash) 87 | ); 88 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", boundHash)); 89 | 90 | return WalletCoreLib.validate(validator, digest, signature); 91 | } 92 | 93 | /** 94 | * @notice Generates an EIP-712 compliant typed data hash for transaction validation 95 | * @dev Combines the message hash with the domain separator using EIP-712 standard 96 | * @param nonce Current transaction nonce used to prevent replay attacks 97 | * @param calls Array of calls to be validated 98 | * @return bytes32 The EIP-712 typed data hash ready for signing 99 | */ 100 | function getValidationTypedHash( 101 | uint256 nonce, 102 | Call[] calldata calls 103 | ) public view returns (bytes32) { 104 | return _hashTypedDataV4(_getValidationHash(nonce, calls)); 105 | } 106 | 107 | /** 108 | * @notice Computes the deterministic address of a validator 109 | * @dev Uses the validator implementation and signer to calculate the expected address 110 | * @param validatorImpl The implementation contract address for the validator 111 | * @param immutableArgs The initialization code of the validator 112 | * @return address The predicted validator contract address 113 | */ 114 | function computeValidatorAddress( 115 | address validatorImpl, 116 | bytes calldata immutableArgs 117 | ) public view returns (address) { 118 | return 119 | WalletCoreLib._computeValidatorAddress( 120 | validatorImpl, 121 | immutableArgs, 122 | WalletCoreLib.VALIDATOR_SALT, 123 | address(this) 124 | ); 125 | } 126 | 127 | /** 128 | * @notice Internal function to validate a transaction using EIP-712 typed data 129 | * @dev Generates typed data hash and validates it using the specified validator 130 | * @param nonce Current transaction nonce from storage 131 | * @param calls Array of calls to be validated 132 | * @param validator Address of the validator contract 133 | * @param validationData The validation data (signature for ECDSA, custom data for other validators) 134 | */ 135 | function _validateCall( 136 | uint256 nonce, 137 | Call[] calldata calls, 138 | address validator, 139 | bytes calldata validationData 140 | ) internal view { 141 | bytes32 typedDataHash = getValidationTypedHash(nonce, calls); 142 | bool isValid = WalletCoreLib.validate( 143 | validator, 144 | typedDataHash, 145 | validationData 146 | ); 147 | if (!isValid) revert Errors.InvalidSignature(); 148 | } 149 | 150 | /** 151 | * @notice Creates a hash of the transaction data for validation 152 | * @dev Combines nonce and calls into a single hash using EIP-712 encoding 153 | * @param nonce Transaction nonce for replay protection 154 | * @param calls Array of calls to execute 155 | * @return bytes32 Hash of the transaction data 156 | */ 157 | function _getValidationHash( 158 | uint256 nonce, 159 | Call[] calldata calls 160 | ) internal view returns (bytes32) { 161 | bytes32[] memory callHashes = new bytes32[](calls.length); 162 | for (uint256 i = 0; i < calls.length; i++) { 163 | callHashes[i] = keccak256( 164 | abi.encode( 165 | CALL_TYPEHASH, 166 | calls[i].target, 167 | calls[i].value, 168 | keccak256(calls[i].data) 169 | ) 170 | ); 171 | } 172 | 173 | return 174 | keccak256( 175 | abi.encode( 176 | CALLS_TYPEHASH, 177 | _walletImplementation(), 178 | nonce, 179 | keccak256(abi.encode(callHashes)) 180 | ) 181 | ); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/WalletCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.23; 3 | 4 | import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; 5 | import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; 6 | 7 | import {IWalletCore} from "./interfaces/IWalletCore.sol"; 8 | import {IStorage} from "./interfaces/IStorage.sol"; 9 | 10 | import {WalletCoreBase} from "./base/WalletCoreBase.sol"; 11 | import {ECDSA, WalletCoreLib} from "./lib/WalletCoreLib.sol"; 12 | import {ValidationLogic} from "./ValidationLogic.sol"; 13 | import {ExecutionLogic} from "./ExecutionLogic.sol"; 14 | import {ExecutorLogic} from "./ExecutorLogic.sol"; 15 | import {FallbackHandler} from "./FallbackHandler.sol"; 16 | import {Call, Session} from "./Types.sol"; 17 | import {Errors} from "./lib/Errors.sol"; 18 | 19 | // Do not set any states in this contract 20 | contract WalletCore is 21 | IWalletCore, 22 | ValidationLogic, 23 | ExecutionLogic, 24 | ExecutorLogic, 25 | FallbackHandler, 26 | EIP712 27 | { 28 | using Clones for address; 29 | 30 | // EIP-1271 31 | bytes4 private constant MAGIC_VALUE = 0x1626ba7e; 32 | bytes4 private constant INVALID_VALUE = 0xffffffff; 33 | 34 | address public immutable ADDRESS_THIS; 35 | address public immutable MAIN_STORAGE_IMPL; 36 | 37 | constructor( 38 | address mainStorageImpl, 39 | string memory name, 40 | string memory version 41 | ) EIP712(name, version) { 42 | // Check name/version lengths, assure remain stateless 43 | if (bytes(name).length >= 32) { 44 | revert Errors.NameTooLong(); 45 | } 46 | if (bytes(version).length >= 32) { 47 | revert Errors.VersionTooLong(); 48 | } 49 | ADDRESS_THIS = address(this); 50 | MAIN_STORAGE_IMPL = mainStorageImpl; 51 | } 52 | 53 | /** 54 | * @dev Modifier to make a function callable by the account itself or EOA address under 7702 55 | */ 56 | modifier onlySelf() { 57 | if (msg.sender != address(this)) revert Errors.NotFromSelf(); 58 | _; 59 | } 60 | 61 | /** 62 | * @notice Initializes the wallet core 63 | * @dev Can only be called once during account creation with each storage version 64 | */ 65 | function initialize() external { 66 | if (WalletCoreLib._getStorage(MAIN_STORAGE_IMPL).code.length != 0) { 67 | emit StorageInitialized(); 68 | return; 69 | } 70 | 71 | // immutable args 72 | bytes memory owner = abi.encode(address(this)); 73 | 74 | address createdAddress = MAIN_STORAGE_IMPL 75 | .cloneDeterministicWithImmutableArgs( 76 | owner, 77 | WalletCoreLib.STORAGE_SALT 78 | ); 79 | 80 | emit StorageCreated(createdAddress); 81 | } 82 | 83 | /** 84 | * @notice Executes multiple contract calls in a single transaction 85 | * @dev Only callable by the account itself 86 | * @param calls Array of Call structs containing destination address, value, and calldata 87 | */ 88 | function executeFromSelf(Call[] calldata calls) external onlySelf { 89 | _batchCall(calls); 90 | } 91 | 92 | /** 93 | * @notice Executes a batch of calls after validation by a designated validator contract 94 | * @dev The validator must be previously registered and the validation data must be valid 95 | * @dev If validator address == 1, uses default built-in ECDSA ecrecover for signature verification 96 | * @param calls Array of Call structs to be executed, each containing destination address, value, and calldata 97 | * @param validator Address of the validator contract that will verify this transaction (use address(1) for ECDSA) 98 | * @param validationData Encoded data required by the validator for transaction verification. For ECDSA, this is the signature 99 | */ 100 | function executeWithValidator( 101 | Call[] calldata calls, 102 | address validator, 103 | bytes calldata validationData 104 | ) external onlyValidator(calls, validator, validationData) { 105 | _batchCall(calls); 106 | } 107 | 108 | /** 109 | * @notice Executes a batch of calls through a registered executor using a valid session 110 | * @dev Only callable by pre-signed sessions with valid signatures 111 | * @dev Executes hooks before and after the batch call if specified in the session 112 | * @param calls Array of Call structs to be executed, each containing destination address, value, and calldata 113 | * @param session Session struct containing executor details, permissions, and hook configurations 114 | */ 115 | function executeFromExecutor( 116 | Call[] calldata calls, 117 | Session calldata session 118 | ) external onlyValidSession(session, calls) { 119 | _batchCall(calls); 120 | } 121 | 122 | /** 123 | * @notice Registers a new validator contract for transaction validation 124 | * @dev Only callable by the wallet itself 125 | * @param validatorImpl The implementation address of the validator contract to be registered 126 | * @param immutableArgs Initialization data for the validator contract (can be empty) 127 | */ 128 | function addValidator( 129 | address validatorImpl, 130 | bytes calldata immutableArgs 131 | ) external onlySelf { 132 | _addValidator(validatorImpl, immutableArgs); 133 | } 134 | 135 | /** 136 | * @notice Implements EIP-1271 signature validation standard 137 | * @dev There are two types of signatures: 138 | * 1. 65 bytes: ECDSA signature 139 | * 2. >20 bytes: (validator, signature) pair 140 | * @param _hash The hash of the data to be validated 141 | * @param signature The signature to be validated 142 | * @return bytes4 Returns MAGIC_VALUE (0x1626ba7e) if valid, INVALID_VALUE (0xffffffff) if invalid 143 | */ 144 | function isValidSignature( 145 | bytes32 _hash, 146 | bytes calldata signature 147 | ) external view returns (bytes4) { 148 | // 7702 Post upgrade compatibility: try validate signature for EOA sigs 149 | // Make sure the _signature can be decoded 150 | if (signature.length == 65) { 151 | (address recovered, , ) = ECDSA.tryRecover(_hash, signature); 152 | if (recovered == address(this)) return MAGIC_VALUE; 153 | } 154 | 155 | if (signature.length < 20) return INVALID_VALUE; 156 | 157 | return 158 | isValidSignature( 159 | address(bytes20(signature[:20])), 160 | _hash, 161 | signature[20:] 162 | ) 163 | ? MAGIC_VALUE 164 | : INVALID_VALUE; 165 | } 166 | 167 | /** 168 | * @notice Returns the address of the wallet's storage contract 169 | * @dev Uses deterministic deployment to calculate the storage contract address. 170 | * This contract only stores core wallet states. For additional states, 171 | * create and query new dedicated storage contracts instead of modifying 172 | * this one. 173 | * @return address The deployed storage contract address for this wallet 174 | * @custom:architecture New features requiring additional storage should: 175 | * 1. Deploy a new dedicated storage contract 176 | * 2. Implement separate getter methods for the new storage 177 | */ 178 | function getMainStorage() 179 | public 180 | view 181 | override(IWalletCore, WalletCoreBase) 182 | returns (IStorage) 183 | { 184 | return IStorage(WalletCoreLib._getStorage(MAIN_STORAGE_IMPL)); 185 | } 186 | 187 | /** 188 | * @notice Creates a typed data hash following EIP-712 standard 189 | * @param structHash The hash of the struct data to be signed 190 | * @return The final EIP-712 typed data hash that can be signed by a wallet 191 | */ 192 | function _hashTypedDataV4( 193 | bytes32 structHash 194 | ) internal view override(EIP712, WalletCoreBase) returns (bytes32) { 195 | return EIP712._hashTypedDataV4(structHash); 196 | } 197 | 198 | /** 199 | * @notice Returns the address of the current wallet implementation 200 | * @dev This function is used in the proxy pattern to identify the implementation contract 201 | * @return ADDRESS_THIS The address of this contract, which serves as the implementation 202 | */ 203 | function _walletImplementation() internal view override returns (address) { 204 | return ADDRESS_THIS; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /test/Validation.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "./Base.t.sol"; 5 | 6 | contract ValidationTest is Base { 7 | event NonceConsumed(uint256 nonce); 8 | 9 | function setUp() public override { 10 | super.setUp(); 11 | } 12 | 13 | function test_executeWithValidator_succeeds_as_owner() public { 14 | vm.prank(_alice); 15 | uint256 nonce = _getNonce(_alice); 16 | Call[] memory calls = _construct_calls_data(); 17 | bytes memory signature = _construct_signature(_alicePk, nonce, calls); 18 | 19 | IWalletCore(_alice).executeWithValidator( 20 | calls, 21 | WalletCoreLib.SELF_VALIDATION_ADDRESS, 22 | signature 23 | ); 24 | 25 | assertEq(address(_bob).balance, 1 ether); 26 | } 27 | 28 | function test_executeWithValidator_succeeds_as_relayer() public { 29 | vm.prank(_bob); 30 | uint256 nonce = _getNonce(_alice); 31 | Call[] memory calls = _construct_calls_data(); 32 | bytes memory signature = _construct_signature(_alicePk, nonce, calls); 33 | 34 | IWalletCore(_alice).executeWithValidator( 35 | calls, 36 | WalletCoreLib.SELF_VALIDATION_ADDRESS, 37 | signature 38 | ); 39 | 40 | assertEq(address(_bob).balance, 1 ether); 41 | } 42 | 43 | function test_executeWithValidator_reverts_for_default_validator_invalid_signer() 44 | public 45 | { 46 | uint256 nonce = _getNonce(_alice); 47 | Call[] memory calls = _construct_calls_data(); 48 | bytes memory signature = _construct_signature(_bobPk, nonce, calls); 49 | 50 | vm.prank(_alice); 51 | vm.expectRevert( 52 | abi.encodeWithSelector(Errors.InvalidSignature.selector) 53 | ); 54 | IWalletCore(_alice).executeWithValidator( 55 | calls, 56 | WalletCoreLib.SELF_VALIDATION_ADDRESS, 57 | signature 58 | ); 59 | assertEq(address(_bob).balance, 0 ether); 60 | } 61 | 62 | function test_executeWithValidator_reverts_for_invalid_signature() public { 63 | vm.startPrank(_alice); 64 | uint256 nonce = _getNonce(_alice); 65 | Call[] memory calls = _construct_calls_data(); 66 | bytes memory signature = _construct_signature(_bobPk, nonce, calls); 67 | address validatorAddress = _addValidator(_alice); 68 | 69 | vm.expectRevert( 70 | abi.encodeWithSelector(Errors.InvalidSignature.selector) 71 | ); 72 | IWalletCore(_alice).executeWithValidator( 73 | calls, 74 | validatorAddress, 75 | signature 76 | ); 77 | 78 | assertEq(address(_bob).balance, 0 ether); 79 | } 80 | 81 | function test_executeWithValidator_reverts_for_invalid_nonce() public { 82 | vm.prank(_bob); 83 | uint256 nonce = _getNonce(_alice) + 99; 84 | Call[] memory calls = _construct_calls_data(); 85 | bytes memory signature = _construct_signature(_alicePk, nonce, calls); 86 | address validatorAddress = _getEdcsaValidatorAddress( 87 | _alice, 88 | _alice, 89 | address(_ecdsaValidatorImpl) 90 | ); 91 | 92 | vm.expectRevert(); 93 | IWalletCore(_alice).executeWithValidator( 94 | calls, 95 | validatorAddress, 96 | signature 97 | ); 98 | 99 | assertEq(address(_bob).balance, 0 ether); 100 | } 101 | 102 | function test_executeWithValidator_reverts_for_invalid_validator() public { 103 | vm.prank(_bob); 104 | uint256 nonce = _getNonce(_alice); 105 | Call[] memory calls = _construct_calls_data(); 106 | bytes memory signature = _construct_signature(_alicePk, nonce, calls); 107 | address validatorAddress = _bob; // invalid validator 108 | 109 | vm.expectRevert(); 110 | IWalletCore(_alice).executeWithValidator( 111 | calls, 112 | validatorAddress, 113 | signature 114 | ); 115 | 116 | assertEq(address(_bob).balance, 0 ether); 117 | } 118 | 119 | function test_executeWithValidator_reverts_for_validator_paused() public { 120 | vm.startPrank(_alice); 121 | uint256 nonce = _getNonce(_alice); 122 | Call[] memory calls = _construct_calls_data(); 123 | bytes memory signature = _construct_signature(_alicePk, nonce, calls); 124 | address validatorAddress = _addValidator(_alice); 125 | 126 | IStorage storageContract = IStorage( 127 | WalletCore(payable(_alice)).getMainStorage() 128 | ); 129 | vm.prank(_alice); 130 | storageContract.setValidatorStatus(validatorAddress, false); 131 | 132 | vm.expectRevert( 133 | abi.encodeWithSelector( 134 | Errors.InvalidValidator.selector, 135 | validatorAddress 136 | ) 137 | ); 138 | IWalletCore(_alice).executeWithValidator( 139 | calls, 140 | validatorAddress, 141 | signature 142 | ); 143 | 144 | assertEq(address(_bob).balance, 0 ether); 145 | } 146 | 147 | function test_executeWithValidator_emits_nonce_consumed() public { 148 | vm.prank(_alice); 149 | uint256 nonce = _getNonce(_alice); 150 | Call[] memory calls = _construct_calls_data(); 151 | bytes memory signature = _construct_signature(_alicePk, nonce, calls); 152 | 153 | vm.expectEmit(); 154 | emit NonceConsumed(nonce); 155 | 156 | IWalletCore(_alice).executeWithValidator( 157 | calls, 158 | WalletCoreLib.SELF_VALIDATION_ADDRESS, 159 | signature 160 | ); 161 | 162 | assertEq(address(_bob).balance, 1 ether); 163 | } 164 | 165 | function test_isValidSignature_fails_with_invalid_signer() public view { 166 | bytes32 hash = keccak256("test"); 167 | 168 | // Wrong signer 169 | bytes memory signature = abi.encodePacked( 170 | WalletCoreLib.SELF_VALIDATION_ADDRESS, 171 | _signDigest(hash, _bobPk) 172 | ); 173 | 174 | // Call isValidSignature 175 | bytes4 result = IWalletCore(_alice).isValidSignature(hash, signature); 176 | assertEq(result, bytes4(0xffffffff)); 177 | } 178 | 179 | function test_isValidSignature_fails_with_invalid_validator() public view { 180 | bytes32 hash = keccak256("test"); 181 | // Wrong validator 182 | bytes memory signature = abi.encodePacked( 183 | _bob, 184 | _signDigest(hash, _alicePk) 185 | ); 186 | 187 | // Call isValidSignature 188 | bytes4 result = IWalletCore(_alice).isValidSignature(hash, signature); 189 | assertEq(result, bytes4(0xffffffff)); 190 | } 191 | 192 | function test_isValidSignature_reverts_for_paused_validator() public { 193 | // Add validator 194 | address validator = _addValidator(_alice); 195 | 196 | // Pause validator 197 | vm.startPrank(_alice); 198 | IStorage(WalletCore(payable(_alice)).getMainStorage()) 199 | .setValidatorStatus(validator, false); 200 | vm.stopPrank(); 201 | 202 | bytes32 hash = keccak256("test"); 203 | bytes memory signature = abi.encodePacked( 204 | validator, 205 | _signDigest(hash, _alicePk) 206 | ); 207 | 208 | // Call isValidSignature 209 | bytes4 result = IWalletCore(_alice).isValidSignature(hash, signature); 210 | assertEq(result, bytes4(0xffffffff)); 211 | } 212 | 213 | function test_isValidSignature_fails_with_short_signature() public view { 214 | bytes32 hash = keccak256("test"); 215 | 216 | // signature shorter than 20 bytes 217 | bytes memory signature = bytes(""); 218 | 219 | // Call isValidSignature 220 | bytes4 result = IWalletCore(_alice).isValidSignature(hash, signature); 221 | assertEq(result, bytes4(0xffffffff)); 222 | } 223 | 224 | function test_isValidSignature_fails_with_longer_than_85_bytes_signature() 225 | public 226 | view 227 | { 228 | bytes32 hash = keccak256("test"); 229 | 230 | // signature have 100 bytes 231 | bytes memory signature = bytes(new bytes(100)); 232 | 233 | // Call isValidSignature 234 | bytes4 result = IWalletCore(_alice).isValidSignature(hash, signature); 235 | assertEq(result, bytes4(0xffffffff)); 236 | } 237 | 238 | function test_isValidSignature_succeeds_with_default_validator() 239 | public 240 | view 241 | { 242 | bytes32 hash = keccak256("test"); 243 | bytes memory signature = abi.encodePacked( 244 | WalletCoreLib.SELF_VALIDATION_ADDRESS, 245 | _signDigest(hash, _alicePk) 246 | ); 247 | 248 | // Call isValidSignature 249 | bytes4 result = IWalletCore(_alice).isValidSignature(hash, signature); 250 | assertEq(result, bytes4(0x1626ba7e)); 251 | } 252 | 253 | function test_isValidSignature_succeeds_with_valid_validator_signer() 254 | public 255 | { 256 | // Add validator 257 | address validator = _addValidator(_alice); 258 | 259 | bytes32 hash = keccak256("test"); 260 | bytes memory signature = abi.encodePacked( 261 | validator, 262 | _signDigest(hash, _alicePk) 263 | ); 264 | 265 | // Call isValidSignature 266 | bytes4 result = IWalletCore(_alice).isValidSignature(hash, signature); 267 | assertEq(result, bytes4(0x1626ba7e)); 268 | } 269 | 270 | function test_isValidSignature_for_premit() public view { 271 | bytes32 hash = keccak256("721 struct data"); 272 | // Sign the digest 273 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_alicePk, hash); 274 | 275 | // Signature 276 | bytes memory validationData = abi.encodePacked(r, s, v); 277 | 278 | // Call isValidSignature 279 | bytes4 result = IWalletCore(_alice).isValidSignature( 280 | hash, 281 | validationData 282 | ); 283 | assertEq(result, bytes4(0x1626ba7e)); 284 | } 285 | 286 | function _signDigest( 287 | bytes32 hash, 288 | uint256 signerPk 289 | ) internal view returns (bytes memory) { 290 | bytes32 boundHash = keccak256( 291 | abi.encode(bytes32(block.chainid), address(_alice), hash) 292 | ); 293 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", boundHash)); 294 | 295 | // Sign the digest 296 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, digest); 297 | 298 | return abi.encodePacked(r, s, v); 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /test/Executor.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.23; 3 | 4 | import "forge-std/Test.sol"; 5 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | 8 | import {IExecutor} from "src/interfaces/IExecutor.sol"; 9 | import {IStorage} from "src/interfaces/IStorage.sol"; 10 | import {MockERC20} from "src/test/MockERC20.sol"; 11 | import {MockExecutor} from "src/test/MockExecutor.sol"; 12 | import {Call, Session} from "src/Types.sol"; 13 | import {Errors} from "src/lib/Errors.sol"; 14 | import "./Base.t.sol"; 15 | 16 | contract ExecutorTest is Base { 17 | using ECDSA for bytes32; 18 | 19 | MockERC20 mockToken; 20 | MockExecutor mockExecutor; 21 | IStorage store; 22 | 23 | address receipt; 24 | uint256 receiptPri; 25 | address sessionOwner; 26 | uint256 sessionOwnerPri; 27 | 28 | Session session; 29 | 30 | uint256 validAfter = 0; 31 | uint256 validUntil = block.timestamp + 1000; 32 | 33 | function setUp() public override { 34 | super.setUp(); 35 | 36 | (sessionOwner, sessionOwnerPri) = makeAddrAndKey("sessionOwner"); 37 | (receipt, receiptPri) = makeAddrAndKey("receipt"); 38 | 39 | vm.prank(_alice); 40 | mockToken = new MockERC20(); 41 | mockExecutor = new MockExecutor(IWalletCore(_alice)); 42 | 43 | session = Session({ 44 | id: 0, 45 | executor: address(mockExecutor), 46 | validator: address(1), 47 | validUntil: validUntil, 48 | validAfter: validAfter, 49 | preHook: "", 50 | postHook: "", 51 | signature: "" 52 | }); 53 | 54 | bytes32 hash = IExecutor(_alice).getSessionTypedHash(session); 55 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_alicePk, hash); 56 | bytes memory sessionSignature = abi.encodePacked(r, s, v); 57 | session.signature = sessionSignature; 58 | 59 | store = IStorage(WalletCore(payable(_alice)).getMainStorage()); 60 | } 61 | 62 | function test_execute_reverts_for_invalid_session() public { 63 | vm.prank(_bob); 64 | 65 | _walletCore = new WalletCore(address(_storageImpl), NAME, VERSION); 66 | _setCodeToEOA(address(_walletCore), _bob); 67 | 68 | IWalletCore(_bob).initialize(); 69 | 70 | Session memory bobSession = Session({ 71 | id: 0, 72 | executor: address(mockExecutor), 73 | validator: address(1), 74 | validUntil: validUntil, 75 | validAfter: validAfter, 76 | preHook: "", 77 | postHook: "", 78 | signature: "" 79 | }); 80 | 81 | bytes32 hash = IExecutor(_bob).getSessionTypedHash(bobSession); 82 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_bobPk, hash); 83 | bytes memory sessionSignature = abi.encodePacked(r, s, v); 84 | session.signature = sessionSignature; 85 | 86 | Call memory call = Call({ 87 | target: address(mockToken), 88 | value: 0, 89 | data: abi.encodeWithSignature( 90 | "transfer(address,uint256)", 91 | receipt, 92 | 10 93 | ) 94 | }); 95 | Call[] memory singleCallArray = new Call[](1); 96 | singleCallArray[0] = call; 97 | 98 | vm.prank(sessionOwner); 99 | vm.expectRevert(); 100 | mockExecutor.execute(singleCallArray, session); 101 | 102 | assertEq(mockToken.balanceOf(receipt), 0); 103 | } 104 | 105 | function test_session_validation_fails_for_wrong_wallet() public { 106 | vm.prank(_bob); 107 | 108 | _walletCore = new WalletCore(address(_storageImpl), NAME, VERSION); 109 | _setCodeToEOA(address(_walletCore), _bob); 110 | 111 | IWalletCore(_bob).initialize(); 112 | 113 | Session memory bobSession = Session({ 114 | id: 0, 115 | executor: address(mockExecutor), 116 | validator: address(1), 117 | validUntil: validUntil, 118 | validAfter: validAfter, 119 | preHook: "", 120 | postHook: "", 121 | signature: "" 122 | }); 123 | 124 | bytes32 hash = IExecutor(_bob).getSessionTypedHash(bobSession); 125 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_bobPk, hash); 126 | bytes memory sessionSignature = abi.encodePacked(r, s, v); 127 | bobSession.signature = sessionSignature; 128 | 129 | vm.prank(_alice); 130 | vm.expectRevert(); 131 | IExecutor(_alice).validateSession(bobSession); 132 | } 133 | 134 | function test_session_validates_successfully() public { 135 | vm.prank(_alice); 136 | IExecutor(session.executor).validateSession(session); 137 | } 138 | 139 | function test_execute_reverts_for_invalid_validator() public { 140 | address validatorAddress = _getEdcsaValidatorAddress( 141 | _alice, 142 | _alice, 143 | address(_ecdsaValidatorImpl) 144 | ); 145 | 146 | session = Session({ 147 | id: 0, 148 | executor: address(mockExecutor), 149 | validator: validatorAddress, 150 | validUntil: validUntil, 151 | validAfter: validAfter, 152 | preHook: "", 153 | postHook: "", 154 | signature: "" 155 | }); 156 | 157 | bytes32 hash = IExecutor(_alice).getSessionTypedHash(session); 158 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_alicePk, hash); 159 | bytes memory sessionSignature = abi.encodePacked(r, s, v); 160 | session.signature = sessionSignature; 161 | 162 | Call memory call = Call({ 163 | target: address(mockToken), 164 | value: 0, 165 | data: abi.encodeWithSignature( 166 | "transfer(address,uint256)", 167 | receipt, 168 | 10 169 | ) 170 | }); 171 | 172 | Call[] memory singleCallArray = new Call[](1); 173 | singleCallArray[0] = call; 174 | 175 | vm.prank(_alice); 176 | IWalletCore(_alice).addValidator( 177 | address(_ecdsaValidatorImpl), 178 | abi.encode(_alice) 179 | ); 180 | 181 | IStorage storageContract = IStorage( 182 | WalletCore(payable(_alice)).getMainStorage() 183 | ); 184 | 185 | vm.prank(_alice); 186 | storageContract.setValidatorStatus(validatorAddress, false); 187 | 188 | vm.expectRevert( 189 | abi.encodeWithSelector( 190 | Errors.InvalidValidator.selector, 191 | validatorAddress 192 | ) 193 | ); 194 | vm.prank(sessionOwner); 195 | mockExecutor.execute(singleCallArray, session); 196 | 197 | vm.prank(_alice); 198 | storageContract.setValidatorStatus(validatorAddress, true); 199 | 200 | vm.prank(sessionOwner); 201 | mockExecutor.execute(singleCallArray, session); 202 | } 203 | 204 | function test_execute_reverts_for_invalid_executor() public { 205 | vm.prank(_alice); 206 | MockExecutor mockExecutor2 = new MockExecutor(IWalletCore(_alice)); 207 | 208 | Call memory call = Call({ 209 | target: address(mockToken), 210 | value: 0, 211 | data: abi.encodeWithSignature( 212 | "transfer(address,uint256)", 213 | receipt, 214 | 10 215 | ) 216 | }); 217 | 218 | Call[] memory singleCallArray = new Call[](1); 219 | singleCallArray[0] = call; 220 | 221 | vm.prank(sessionOwner); 222 | vm.expectRevert(Errors.InvalidExecutor.selector); 223 | mockExecutor2.execute(singleCallArray, session); 224 | 225 | assertEq(mockToken.balanceOf(receipt), 0); 226 | } 227 | 228 | function test_session_executes_with_valid_signature() public { 229 | Call memory call = Call({ 230 | target: address(mockToken), 231 | value: 0, 232 | data: abi.encodeWithSignature( 233 | "transfer(address,uint256)", 234 | receipt, 235 | 10 236 | ) 237 | }); 238 | Call[] memory singleCallArray = new Call[](1); 239 | singleCallArray[0] = call; 240 | 241 | vm.prank(sessionOwner); 242 | mockExecutor.execute(singleCallArray, session); 243 | 244 | assertEq(mockToken.balanceOf(receipt), 10); 245 | } 246 | 247 | function test_execute_reverts_for_expired_session() public { 248 | Call memory call = Call({ 249 | target: address(mockToken), 250 | value: 0, 251 | data: abi.encodeWithSignature( 252 | "transfer(address,uint256)", 253 | receipt, 254 | 10 255 | ) 256 | }); 257 | 258 | vm.warp(session.validUntil + 1000); 259 | 260 | Call[] memory singleCallArray = new Call[](1); 261 | singleCallArray[0] = call; 262 | 263 | vm.prank(sessionOwner); 264 | vm.expectRevert(Errors.InvalidSession.selector); 265 | mockExecutor.execute(singleCallArray, session); 266 | 267 | assertEq(mockToken.balanceOf(receipt), 0); 268 | } 269 | 270 | function test_session_can_be_revoked() public { 271 | Call memory call = Call({ 272 | target: address(mockToken), 273 | value: 0, 274 | data: abi.encodeWithSignature( 275 | "transfer(address,uint256)", 276 | receipt, 277 | 10 278 | ) 279 | }); 280 | Call[] memory singleCallArray = new Call[](1); 281 | singleCallArray[0] = call; 282 | 283 | vm.prank(sessionOwner); 284 | mockExecutor.execute(singleCallArray, session); 285 | 286 | assertEq(mockToken.balanceOf(receipt), 10); 287 | 288 | vm.prank(_alice); 289 | store.revokeSession(session.id); 290 | 291 | vm.prank(sessionOwner); 292 | vm.expectRevert(Errors.InvalidSessionId.selector); 293 | mockExecutor.execute(singleCallArray, session); 294 | } 295 | 296 | function test_session_can_be_recovered_after_invalidation() public { 297 | Call memory call = Call({ 298 | target: address(mockToken), 299 | value: 0, 300 | data: abi.encodeWithSignature( 301 | "transfer(address,uint256)", 302 | receipt, 303 | 10 304 | ) 305 | }); 306 | Call[] memory singleCallArray = new Call[](1); 307 | singleCallArray[0] = call; 308 | 309 | vm.prank(sessionOwner); 310 | mockExecutor.execute(singleCallArray, session); 311 | 312 | assertEq(mockToken.balanceOf(receipt), 10); 313 | 314 | vm.prank(_alice); 315 | store.revokeSession(session.id); 316 | 317 | vm.prank(sessionOwner); 318 | vm.expectRevert(Errors.InvalidSessionId.selector); 319 | mockExecutor.execute(singleCallArray, session); 320 | } 321 | 322 | function test_session_revocation_reverts_for_non_owner() public { 323 | Call memory call = Call({ 324 | target: address(mockToken), 325 | value: 0, 326 | data: abi.encodeWithSignature( 327 | "transfer(address,uint256)", 328 | receipt, 329 | 10 330 | ) 331 | }); 332 | Call[] memory singleCallArray = new Call[](1); 333 | singleCallArray[0] = call; 334 | 335 | vm.prank(sessionOwner); 336 | mockExecutor.execute(singleCallArray, session); 337 | 338 | assertEq(mockToken.balanceOf(receipt), 10); 339 | vm.prank(_bob); 340 | 341 | vm.expectRevert(Errors.InvalidOwner.selector); 342 | store.revokeSession(session.id); 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------