├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── README.md ├── audits └── SlowMist Audit Report - SoulWallet.pdf ├── bug-bounty.md ├── contracts ├── SoulWallet.sol ├── abstract │ ├── DefaultCallbackHandler.sol │ ├── ERC1271Handler.sol │ ├── SoulWalletHookManager.sol │ ├── SoulWalletModuleManager.sol │ ├── SoulWalletOwnerManager.sol │ ├── SoulWalletUpgradeManager.sol │ └── SoulWalletValidatorManager.sol ├── automation │ ├── AaveUsdcSaveAutomation.sol │ └── ClaimInterest.sol ├── dev │ ├── EIP1271Wallet.sol │ ├── HelloWorld.sol │ ├── NewImplementation.sol │ ├── ReceivePayment.sol │ ├── TestOracle.sol │ └── tokens │ │ └── TokenERC20.sol ├── factory │ └── SoulWalletFactory.sol ├── hooks │ └── 2fa │ │ └── Crypto2FAHook.sol ├── interfaces │ ├── ISoulWallet.sol │ ├── ISoulWalletHookManager.sol │ ├── ISoulWalletModuleManager.sol │ ├── ISoulWalletOwnerManager.sol │ ├── ISoulWalletValidatorManager.sol │ └── IUpgradable.sol ├── libraries │ ├── Base64Url.sol │ ├── DecodeCalldata.sol │ ├── Errors.sol │ ├── TypeConversion.sol │ └── WebAuthn.sol ├── modules │ ├── BaseModule.sol │ ├── README.md │ ├── interfaces │ │ └── ISoulWalletModule.sol │ ├── readmepic │ │ └── module.png │ ├── socialRecovery │ │ ├── README.md │ │ ├── SocialRecoveryModule.sol │ │ ├── base │ │ │ └── BaseSocialRecovery.sol │ │ ├── interfaces │ │ │ └── ISocialRecovery.sol │ │ └── socialReoceryFlow.png │ └── upgrade │ │ ├── IUpgrade.sol │ │ └── UpgradeModule.sol ├── paymaster │ ├── ERC20Paymaster.sol │ └── interfaces │ │ └── IOracle.sol └── validator │ ├── README.md │ ├── SoulWalletDefaultValidator.sol │ ├── libraries │ └── ValidatorSigDecoder.sol │ └── validation_flow.png ├── doc ├── anonymous_guardian.md ├── contract_architecture.md └── images │ ├── add_guardian_diagram.png │ ├── contract_architecture.png │ └── recovery_diagram.png ├── foundry.toml ├── lib └── solenv │ ├── .github │ └── workflows │ │ └── test.yml │ ├── .gitignore │ ├── .gitmodules │ ├── .prettierrc.json │ ├── LICENSE │ ├── README.md │ ├── foundry.toml │ ├── lib │ ├── forge-std │ │ ├── .github │ │ │ └── workflows │ │ │ │ └── tests.yml │ │ ├── .gitignore │ │ ├── .gitmodules │ │ ├── LICENSE-APACHE │ │ ├── LICENSE-MIT │ │ ├── README.md │ │ ├── lib │ │ │ └── ds-test │ │ │ │ ├── .gitignore │ │ │ │ ├── LICENSE │ │ │ │ ├── Makefile │ │ │ │ ├── default.nix │ │ │ │ ├── demo │ │ │ │ └── demo.sol │ │ │ │ └── src │ │ │ │ └── test.sol │ │ └── src │ │ │ ├── Script.sol │ │ │ ├── Test.sol │ │ │ ├── Vm.sol │ │ │ ├── console.sol │ │ │ ├── console2.sol │ │ │ └── test │ │ │ ├── StdAssertions.t.sol │ │ │ ├── StdCheats.t.sol │ │ │ ├── StdError.t.sol │ │ │ ├── StdMath.t.sol │ │ │ └── StdStorage.t.sol │ └── solidity-stringutils │ │ ├── .gitattributes │ │ ├── .github │ │ └── workflows │ │ │ └── ci.yml │ │ ├── .gitignore │ │ ├── .gitmodules │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README │ │ ├── README.md │ │ ├── dappfile │ │ ├── lib │ │ └── ds-test │ │ │ ├── .gitignore │ │ │ ├── LICENSE │ │ │ ├── Makefile │ │ │ ├── default.nix │ │ │ ├── demo │ │ │ └── demo.sol │ │ │ └── src │ │ │ └── test.sol │ │ ├── src │ │ ├── strings.sol │ │ └── strings.t.sol │ │ └── strings.sol │ ├── remappings.txt │ ├── script │ └── Solenv.s.sol │ ├── src │ └── Solenv.sol │ └── test │ └── Solenv.t.sol ├── package-lock.json ├── package.json ├── remappings.txt ├── script ├── AutomationDeployer.s.sol ├── CreateWalletDirect.s.sol ├── CreateWalletEntryPoint.s.sol ├── CreateWalletEntryPointPaymaster.s.sol ├── DeployHelper.sol ├── PaymasterDeployer.s.sol ├── ReceivePayment.s.sol ├── SingletonFactory.s.sol ├── SocialRecoveryDeployer.s.sol ├── WalletDeployer.s.sol └── ffi │ ├── save_to_env.js │ └── save_to_env_backend.js ├── test ├── automation │ └── ClaimInterest.t.sol ├── helper │ ├── Bundler.t.sol │ ├── BytesLib.t.sol │ └── UserOpHelper.t.sol ├── hooks │ └── 2fa │ │ └── Crypto2FAHook.t.sol ├── modules │ ├── socialRecovery │ │ └── SocialRecoveryModule.t.sol │ └── upgrade │ │ └── UpgradeModule.t.sol ├── paymaster │ ├── ERC20Paymaster.t.sol │ └── ERC20PaymasterActiveWallet.t.sol ├── soulwallet │ ├── 1271 │ │ └── SoulWallet1271.t.sol │ ├── base │ │ ├── SoulWalletInstence.sol │ │ └── SoulWalletLogicInstence.sol │ ├── deploy │ │ ├── direct.t.sol │ │ └── protocol.t.sol │ └── factory │ │ └── SoulWalletFactory.t.sol └── validator │ └── SoulWalletDefaultValidator.t.sol └── tsconfig.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | 10 | env: 11 | FOUNDRY_PROFILE: ci 12 | 13 | jobs: 14 | check: 15 | strategy: 16 | fail-fast: true 17 | 18 | name: SoulWallet Contract 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | submodules: recursive 24 | 25 | - name: Install Foundry 26 | uses: foundry-rs/foundry-toolchain@v1.2.0 27 | with: 28 | version: nightly 29 | 30 | - name: Run Forge build 31 | run: | 32 | forge --version 33 | forge build --sizes 34 | id: build 35 | 36 | - name: Run Forge tests 37 | run: | 38 | forge test -vvv 39 | id: test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler and build files 2 | cache/ 3 | artifacts/ 4 | out/ 5 | compiler_config.json 6 | 7 | # Node modules 8 | node_modules/ 9 | 10 | # Coverage files 11 | coverage/ 12 | coverage.json 13 | 14 | # Environment files 15 | .env 16 | .env* 17 | 18 | # MacOS file system 19 | .DS_Store 20 | 21 | # Development broadcast logs 22 | broadcast/ 23 | !/broadcast 24 | /broadcast/*/31337/ 25 | /broadcast/**/dry-run/ 26 | broadcast/* 27 | 28 | # Documentation 29 | docs/ 30 | .vscode/ 31 | lcov.info 32 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts-upgradeable"] 5 | path = lib/openzeppelin-contracts-upgradeable 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 7 | branch = release-v5.0 8 | [submodule "lib/nitro-contracts"] 9 | path = lib/nitro-contracts 10 | url = https://github.com/OffchainLabs/nitro-contracts 11 | [submodule "lib/solenv"] 12 | path = lib/solenv 13 | url = https://github.com/memester-xyz/solenv 14 | [submodule "lib/solady"] 15 | path = lib/solady 16 | url = https://github.com/vectorized/solady 17 | [submodule "lib/openzeppelin-contracts"] 18 | path = lib/openzeppelin-contracts 19 | url = https://github.com/Openzeppelin/openzeppelin-contracts 20 | branch = release-v5.1 21 | [submodule "lib/account-abstraction"] 22 | path = lib/account-abstraction 23 | url = https://github.com/eth-infinitism/account-abstraction 24 | branch = releases/v0.7 25 | [submodule "lib/soulwallet-core"] 26 | path = lib/soulwallet-core 27 | url = https://github.com/SoulWallet/soulwallet-core 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.packageDefaultDependenciesContractsDirectory": "contracts", 3 | "solidity.packageDefaultDependenciesDirectory": "lib", 4 | "editor.formatOnSave": true, 5 | "solidity.formatter": "forge", 6 | "solidity.remappings": [ 7 | "@account-abstraction/=lib/account-abstraction/", 8 | "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", 9 | "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", 10 | "@source/=contracts/", 11 | "forge-std/=lib/forge-std/src/", 12 | "@solmate/=lib/solmate/src/" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

SoulWallet Contracts [draft version]

3 |
4 | 5 |
6 | 7 |
8 | 9 | ## Features 10 | 11 | - Support [ERC-4337: Account Abstraction](https://eips.ethereum.org/EIPS/eip-4337) 12 | - [Modular design ](https://hackmd.io/3gbndH7tSl2J1EbNePJ3Yg) 13 | - Upgradability: The smart contract for this wallet can be upgraded in a secure way to add new features or fix vulnerabilities in the future. 14 | - Stablecoin pay gas: Users can pay transaction gas fees with stablecoins such as USDC, USDT, DAI, etc. 15 | 16 | ## Architecutre 17 | 18 | The smart contract comprises three main logic components: 19 | 20 | 1. SoulWallet Core: 21 | 22 | - This is the primary wallet logic. 23 | - Supports the ERC4337 interface. 24 | - Manages modules and hooks. 25 | 26 | 2. Modules: 27 | 28 | - Modules provide extended functionality. 29 | - A module is a whitelisted contract capable of executing transactions on behalf of the smart contract wallet. 30 | - Modules enhance the functionality of the contracts by adding extra access logic for transaction execution. 31 | 32 | 3. Hooks: 33 | 34 | - A hook is essentially a function or a set of functions that are called at specific points within a contract's execution flow. 35 | - Hooks can be set up to perform additional checks on transactions before they're executed. 36 | 37 | ## Repository Structure 38 | 39 | All contracts are held within the `soul-wallet-contract/contracts` folder. 40 | 41 | ``` 42 | contracts 43 | ├── abstract 44 | ├── automation 45 | ├── dev 46 | │ └── tokens 47 | ├── factory 48 | ├── hooks 49 | │ └── 2fa 50 | ├── interfaces 51 | ├── libraries 52 | ├── modules 53 | │ ├── interfaces 54 | │ ├── socialRecovery 55 | │ │ ├── base 56 | │ │ └── interfaces 57 | │ └── upgrade 58 | ├── paymaster 59 | │ └── interfaces 60 | └── validator 61 | └── libraries 62 | ``` 63 | 64 | ## Test 65 | 66 | ```shell 67 | npm run test 68 | ``` 69 | 70 | ## Integration 71 | 72 | Third parties can build new modules/plugins on top of SoulWallet to add additional functionality. 73 | 74 | ### Module 75 | 76 | To add a new module, the contract should inherit from BaseModule. BaseModule is an abstract base contract that provides a foundation for other modules. It ensures the initialization, de-initialization, and proper authorization of modules. 77 | 78 | ```solidity 79 | import "./BaseModule.sol"; 80 | 81 | contract NewModule is BaseModule { 82 | function requiredFunctions() external pure override returns (bytes4[] memory) 83 | { 84 | // return wallet functions that modules need access to 85 | } 86 | 87 | function inited(address wallet) internal view virtual override returns (bool) { 88 | // Implement your checking logic 89 | } 90 | 91 | function _init(bytes calldata data) internal virtual override { 92 | // Implement initialization logic 93 | } 94 | 95 | function _deInit() internal virtual override { 96 | // Implement de-initialization logic 97 | } 98 | } 99 | 100 | ``` 101 | 102 | ### Hook 103 | 104 | To integrate a new hook, your contract should inherit `IHook` interface. This interface will define the standard structure and functionalities for your hooks. 105 | 106 | ```solidity 107 | 108 | import {IHook} from "@soulwallet-core/contracts/interface/IHook.sol"; 109 | 110 | contract NewHook is IHook { 111 | function preIsValidSignatureHook(bytes32 hash, bytes calldata hookSignature) external view { 112 | // Implement hook logic 113 | } 114 | 115 | 116 | function preUserOpValidationHook( 117 | UserOperation calldata userOp, 118 | bytes32 userOpHash, 119 | uint256 missingAccountFunds, 120 | bytes calldata hookSignature 121 | ) external { 122 | // Implement your hook-specific logic here 123 | } 124 | } 125 | ``` 126 | 127 | ## Disclaimer 128 | 129 | This project is provided "as is" with no warranties or guarantees of any kind, express or implied. The developers make no claims about the suitability, reliability, availability, timeliness, security or accuracy of the software or its related documentation. The use of this software is at your own risk. 130 | 131 | The developers will not be liable for any damages or losses, whether direct, indirect, incidental or consequential, arising from the use of or inability to use this software or its related documentation, even if advised of the possibility of such damages. 132 | 133 | ## Acknowledgments 134 | 135 | - ERC-4337: Account Abstraction Using Alt Mempool 136 | - Infinitism account abstraction contract 137 | - Gnosis Safe Contracts 138 | -------------------------------------------------------------------------------- /audits/SlowMist Audit Report - SoulWallet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elytro-eth/soul-wallet-contract/fc7cc084563ad1bda870df841b77caa9ee3a3661/audits/SlowMist Audit Report - SoulWallet.pdf -------------------------------------------------------------------------------- /bug-bounty.md: -------------------------------------------------------------------------------- 1 | # Soul Wallet Bug Bounty Program 2 | 3 | ## Overview 4 | 5 | As of August 20th, 2024, the [soulwallet-core](https://github.com/Soulwallet/soulwallet-core) and [soul-wallet-contract](https://github.com/SoulWallet/soul-wallet-contract) repositories are included in the Soulwallet Bug Bounty Program (the “Program”) to encourage the responsible disclosure of vulnerabilities. 6 | 7 | The Program is focused exclusively on critical and high-severity bugs, with rewards of up to $50,000. Good luck and happy hunting! 8 | 9 | ## Scope 10 | 11 | The Program is limited to bugs that fall under the following categories: 12 | 13 | **Critical** 14 | 15 | - Direct theft of any user funds 16 | - Permanent freezing of funds 17 | - Permanent freezing of NFTs 18 | - Unauthorized spending of user funds without access to user keys 19 | 20 | **High** 21 | 22 | - Temporary freezing of funds 23 | - Temporary freezing of NFTs 24 | 25 | **Temporary Exclusion** 26 | 27 | - We are aware of issues related to `FCL_elliptic.sol` and are currently working on modifications. Until these modifications are completed, we will not be accepting bug submissions related to `FCL_elliptic.sol`. Thank you for your understanding. 28 | 29 | The following items are not covered under this Program: 30 | 31 | - Any contract found under the `contracts/test` directory. 32 | - Bugs in any third-party contracts or platforms interacting with Soulwallet. 33 | - Issues already reported or discovered in contracts created by third parties on Soulwallet. 34 | - Previously reported vulnerabilities. 35 | 36 | Additionally, vulnerabilities depending on any of the following are also excluded from this Program: 37 | 38 | - Frontend issues 39 | - Denial-of-Service (DoS) attacks 40 | - Spam attacks 41 | - Phishing 42 | - Automated tools (e.g., GitHub Actions, AWS, etc.) 43 | - Compromise or misuse of third-party systems or services 44 | 45 | ## Rewards 46 | 47 | Rewards will be determined based on the severity of the reported bug and will be assessed and allocated at the discretion of the Soulwallet team. For critical vulnerabilities that could result in user fund losses, rewards of up to $50,000 may be awarded. Lower severity issues will be rewarded at the team's discretion. 48 | 49 | ## Reward Calculation for High-Level Reports 50 | 51 | High-severity vulnerabilities related to the theft or permanent freezing of unclaimed yield or royalties are evaluated based on the total amount of funds at risk, up to the maximum high-severity reward. This approach is designed to motivate security researchers to identify and report vulnerabilities that may not have significant monetary value at present but could pose a serious threat to the project if left unresolved. 52 | 53 | For critical bugs, a reward of USD $50,000 will be granted, but only if the impact results in: 54 | 55 | - A loss of funds through an attack that does not require any user intervention 56 | - The leakage of private keys or the exposure of key generation processes leading to unauthorized access to user funds 57 | 58 | All other impacts classified as Critical will receive a flat reward of USD $5,000. The remaining severity levels will be compensated according to the Impact in Scope table. 59 | 60 | ## Disclosure 61 | 62 | All discovered vulnerabilities must be reported exclusively to the following email: [security@soulwallet.io](mailto:security@soulwallet.io). 63 | 64 | The vulnerability must not be publicly disclosed or shared with anyone else until Soulwallet has been informed, the issue has been resolved, and permission for public disclosure has been granted. Furthermore, disclosure must occur within 24 hours of discovering the vulnerability. 65 | 66 | A detailed report of the vulnerability increases the likelihood of receiving a reward and may lead to a higher reward amount. Please include as much information as possible about the vulnerability, such as: 67 | 68 | - The conditions under which the bug can be reproduced. 69 | - Steps required to reproduce the bug, or even better, a proof of concept. 70 | - The potential impact if the vulnerability were to be exploited. 71 | 72 | Anyone who reports a unique, previously undisclosed vulnerability that results in a change to the code or a configuration change, and who keeps the vulnerability confidential until it has been resolved by our engineers, will be publicly acknowledged for their contribution if they wish. 73 | 74 | ## Eligibility 75 | 76 | To qualify for a reward under this Program, you must: 77 | 78 | - Be the first to disclose the unique vulnerability to [security@soulwallet.io](mailto:security@soulwallet.io), in accordance with the disclosure requirements above. If similar vulnerabilities are reported within the same 24-hour period, rewards will be divided at Soulwallet's discretion. 79 | - Provide sufficient information for our engineers to reproduce and fix the vulnerability. 80 | - Not engage in any unlawful conduct when disclosing the bug, including through threats, demands, or other coercive tactics. 81 | - Not exploit the vulnerability in any way, including making it public or profiting from it (other than receiving a reward under this Program). 82 | - Make a good faith effort to avoid privacy violations, data destruction, or interruption or degradation of Soulwallet. 83 | - Submit only one vulnerability per report, unless it is necessary to chain vulnerabilities to demonstrate impact. 84 | - Not submit a vulnerability caused by an underlying issue that has already been rewarded under this Program. 85 | - Not be a current or former employee, vendor, or contractor of Soulwallet, or an employee of any of our vendors or contractors. 86 | - Not be subject to U.S. sanctions or reside in a U.S.-embargoed country. 87 | - Be at least 18 years old or, if younger, submit the vulnerability with the consent of a parent or guardian. 88 | 89 | ## Other Terms 90 | 91 | By submitting a report, you grant Soulwallet all rights necessary, including intellectual property rights, to validate, mitigate, and disclose the vulnerability. All reward decisions, including eligibility and amounts of rewards and the method of payment, are at our sole discretion. 92 | 93 | The terms and conditions of this Program may be changed at any time. 94 | -------------------------------------------------------------------------------- /contracts/SoulWallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {IAccount, PackedUserOperation} from "@soulwallet-core/contracts/interface/IAccount.sol"; 5 | import {EntryPointManager} from "@soulwallet-core/contracts/base/EntryPointManager.sol"; 6 | import {FallbackManager} from "@soulwallet-core/contracts/base/FallbackManager.sol"; 7 | import {StandardExecutor} from "@soulwallet-core/contracts/base/StandardExecutor.sol"; 8 | import {ValidatorManager} from "@soulwallet-core/contracts/base/ValidatorManager.sol"; 9 | import {SignatureDecoder} from "@soulwallet-core/contracts/utils/SignatureDecoder.sol"; 10 | import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; 11 | import {Errors} from "./libraries/Errors.sol"; 12 | import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 13 | import "./abstract/ERC1271Handler.sol"; 14 | import {SoulWalletOwnerManager} from "./abstract/SoulWalletOwnerManager.sol"; 15 | import {SoulWalletModuleManager} from "./abstract/SoulWalletModuleManager.sol"; 16 | import {SoulWalletHookManager} from "./abstract/SoulWalletHookManager.sol"; 17 | import {SoulWalletUpgradeManager} from "./abstract/SoulWalletUpgradeManager.sol"; 18 | 19 | /** 20 | * @title SoulWallet 21 | * @dev This contract is the main entry point for the SoulWallet. It implements the IAccount and IERC1271 interfaces, 22 | * and is compatible with the ERC-4337 standard. 23 | * It inherits from multiple base contracts and managers to provide the core functionality of the wallet. 24 | * This includes managing entry points, owners, modules, hooks, and upgrades, as well as handling ERC1271 signatures and providing a fallback function. 25 | */ 26 | contract SoulWallet is 27 | Initializable, 28 | IAccount, 29 | IERC1271, 30 | EntryPointManager, 31 | SoulWalletOwnerManager, 32 | SoulWalletModuleManager, 33 | SoulWalletHookManager, 34 | StandardExecutor, 35 | ValidatorManager, 36 | FallbackManager, 37 | SoulWalletUpgradeManager, 38 | ERC1271Handler 39 | { 40 | string public constant VERSION = "1.1.0"; 41 | address internal immutable _DEFAULT_VALIDATOR; 42 | 43 | constructor(address _entryPoint, address defaultValidator) EntryPointManager(_entryPoint) { 44 | _DEFAULT_VALIDATOR = defaultValidator; 45 | _disableInitializers(); 46 | } 47 | 48 | /** 49 | * @notice Initializes the SoulWallet contract 50 | * @dev This function can only be called once. It sets the initial owners, default callback handler, modules, and hooks. 51 | */ 52 | function initialize( 53 | bytes32[] calldata owners, 54 | address defalutCallbackHandler, 55 | bytes[] calldata modules, 56 | bytes[] calldata hooks 57 | ) external initializer { 58 | _addOwners(owners); 59 | _setFallbackHandler(defalutCallbackHandler); 60 | _installValidator(_DEFAULT_VALIDATOR, hex""); 61 | for (uint256 i = 0; i < modules.length;) { 62 | _addModule(modules[i]); 63 | unchecked { 64 | i++; 65 | } 66 | } 67 | for (uint256 i = 0; i < hooks.length;) { 68 | _installHook(hooks[i]); 69 | unchecked { 70 | i++; 71 | } 72 | } 73 | } 74 | 75 | function _uninstallValidator(address validator) internal override { 76 | require(validator != _DEFAULT_VALIDATOR, "can't uninstall default validator"); 77 | super._uninstallValidator(validator); 78 | } 79 | 80 | function isValidSignature(bytes32 _hash, bytes calldata signature) 81 | public 82 | view 83 | override 84 | returns (bytes4 magicValue) 85 | { 86 | bytes32 datahash = _encodeRawHash(_hash); 87 | 88 | (address validator, bytes calldata validatorSignature, bytes calldata hookSignature) = 89 | SignatureDecoder.signatureSplit(signature); 90 | _preIsValidSignatureHook(datahash, hookSignature); 91 | return _isValidSignature(datahash, validator, validatorSignature); 92 | } 93 | 94 | function _decodeSignature(bytes calldata signature) 95 | internal 96 | pure 97 | virtual 98 | returns (address validator, bytes calldata validatorSignature, bytes calldata hookSignature) 99 | { 100 | return SignatureDecoder.signatureSplit(signature); 101 | } 102 | 103 | function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) 104 | public 105 | payable 106 | virtual 107 | override 108 | returns (uint256 validationData) 109 | { 110 | _onlyEntryPoint(); 111 | 112 | assembly ("memory-safe") { 113 | if missingAccountFunds { 114 | // ignore failure (its EntryPoint's job to verify, not account.) 115 | pop(call(gas(), caller(), missingAccountFunds, 0x00, 0x00, 0x00, 0x00)) 116 | } 117 | } 118 | (address validator, bytes calldata validatorSignature, bytes calldata hookSignature) = 119 | _decodeSignature(userOp.signature); 120 | 121 | /* 122 | Warning!!! 123 | This function uses `return` to terminate the execution of the entire contract. 124 | If any `Hook` fails, this function will stop the contract's execution and 125 | return `SIG_VALIDATION_FAILED`, skipping all the subsequent unexecuted code. 126 | */ 127 | _preUserOpValidationHook(userOp, userOpHash, missingAccountFunds, hookSignature); 128 | 129 | /* 130 | When any hook execution fails, this line will not be executed. 131 | */ 132 | return _validateUserOp(userOp, userOpHash, validator, validatorSignature); 133 | } 134 | 135 | /* 136 | The permission to upgrade the logic contract is exclusively granted to modules (UpgradeModule), 137 | meaning that even the wallet owner cannot directly invoke `upgradeTo` for upgrades. 138 | This design is implemented for security reasons, ensuring that even if the signer's credentials 139 | are compromised, attackers cannot upgrade the logic contract, potentially rendering the wallet unusable. 140 | Users can regain control over their wallet through social recovery mechanisms. 141 | This approach safeguards the wallet's integrity, maintaining its availability and security. 142 | */ 143 | function upgradeTo(address newImplementation) external override { 144 | _onlyModule(); 145 | _upgradeTo(newImplementation); 146 | } 147 | 148 | /// @notice Handles the upgrade from an old implementation 149 | /// @param oldImplementation Address of the old implementation 150 | function upgradeFrom(address oldImplementation) external pure override { 151 | (oldImplementation); 152 | revert Errors.NOT_IMPLEMENTED(); //Initial version no need data migration 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /contracts/abstract/DefaultCallbackHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 5 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; 7 | 8 | contract DefaultCallbackHandler is IERC721Receiver, IERC1155Receiver { 9 | bytes4 private constant _ERC721_RECEIVED = 10 | IERC721Receiver.onERC721Received.selector; 11 | bytes4 private constant _ERC1155_RECEIVED = 12 | IERC1155Receiver.onERC1155Received.selector; 13 | bytes4 private constant _ERC1155_BATCH_RECEIVED = 14 | IERC1155Receiver.onERC1155BatchReceived.selector; 15 | 16 | bytes4 private constant _INTERFACE_ID_ERC721_RECEIVER = 17 | type(IERC721Receiver).interfaceId; 18 | bytes4 private constant _INTERFACE_ID_ERC1155_RECEIVER = 19 | type(IERC1155Receiver).interfaceId; 20 | bytes4 private constant _INTERFACE_ID_ERC165 = type(IERC165).interfaceId; 21 | 22 | function onERC721Received( 23 | address, 24 | address, 25 | uint256, 26 | bytes calldata 27 | ) external pure override returns (bytes4) { 28 | return _ERC721_RECEIVED; 29 | } 30 | 31 | function onERC1155Received( 32 | address, 33 | address, 34 | uint256, 35 | uint256, 36 | bytes calldata 37 | ) external pure override returns (bytes4) { 38 | return _ERC1155_RECEIVED; 39 | } 40 | 41 | function onERC1155BatchReceived( 42 | address, 43 | address, 44 | uint256[] calldata, 45 | uint256[] calldata, 46 | bytes calldata 47 | ) external pure override returns (bytes4) { 48 | return _ERC1155_BATCH_RECEIVED; 49 | } 50 | 51 | function supportsInterface( 52 | bytes4 interfaceId 53 | ) external view virtual override returns (bool) { 54 | return 55 | interfaceId == _INTERFACE_ID_ERC721_RECEIVER || 56 | interfaceId == _INTERFACE_ID_ERC1155_RECEIVER || 57 | interfaceId == _INTERFACE_ID_ERC165; 58 | } 59 | 60 | receive() external payable {} 61 | 62 | fallback() external payable {} 63 | } 64 | -------------------------------------------------------------------------------- /contracts/abstract/ERC1271Handler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Authority} from "@soulwallet-core/contracts/base/Authority.sol"; 5 | 6 | abstract contract ERC1271Handler is Authority { 7 | // Magic value indicating a valid signature for ERC-1271 contracts 8 | // bytes4(keccak256("isValidSignature(bytes32,bytes)") 9 | bytes4 internal constant MAGICVALUE = 0x1626ba7e; 10 | // Constants indicating different invalid states 11 | bytes4 internal constant INVALID_ID = 0xffffffff; 12 | 13 | bytes32 private constant SOUL_WALLET_MSG_TYPEHASH = keccak256("SoulWalletMessage(bytes32 message)"); 14 | 15 | bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = 16 | keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"); 17 | 18 | function _encodeRawHash(bytes32 rawHash) internal view returns (bytes32) { 19 | bytes32 encode1271MessageHash = keccak256(abi.encode(SOUL_WALLET_MSG_TYPEHASH, rawHash)); 20 | bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, getChainId(), address(this))); 21 | return keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator, encode1271MessageHash)); 22 | } 23 | 24 | function getChainId() public view returns (uint256) { 25 | uint256 id; 26 | // solhint-disable-next-line no-inline-assembly 27 | assembly { 28 | id := chainid() 29 | } 30 | return id; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /contracts/abstract/SoulWalletHookManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {HookManager} from "@soulwallet-core/contracts/base/HookManager.sol"; 5 | import {ISoulWalletHookManager} from "../interfaces/ISoulWalletHookManager.sol"; 6 | 7 | abstract contract SoulWalletHookManager is ISoulWalletHookManager, HookManager { 8 | function _installHook(bytes calldata hookAndDataWithFlag) internal virtual { 9 | _installHook( 10 | address(bytes20(hookAndDataWithFlag[:20])), 11 | hookAndDataWithFlag[20:hookAndDataWithFlag.length - 1], 12 | uint8(bytes1((hookAndDataWithFlag[hookAndDataWithFlag.length - 1:hookAndDataWithFlag.length]))) 13 | ); 14 | } 15 | 16 | function installHook(bytes calldata hookAndData, uint8 capabilityFlags) external virtual override { 17 | pluginManagementAccess(); 18 | _installHook(address(bytes20(hookAndData[:20])), hookAndData[20:], capabilityFlags); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/abstract/SoulWalletModuleManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {ModuleManager} from "@soulwallet-core/contracts/base/ModuleManager.sol"; 5 | import {ISoulWalletModuleManager} from "../interfaces/ISoulWalletModuleManager.sol"; 6 | import {ISoulWalletModule} from "../modules/interfaces/ISoulWalletModule.sol"; 7 | import {Errors} from "../libraries/Errors.sol"; 8 | import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 9 | 10 | abstract contract SoulWalletModuleManager is ISoulWalletModuleManager, ModuleManager { 11 | function installModule(bytes calldata moduleAndData) external override { 12 | pluginManagementAccess(); 13 | _addModule(moduleAndData); 14 | } 15 | 16 | /** 17 | * The current function is inside the 18 | * `function _installModule(address moduleAddress, bytes memory initData, bytes4[] memory selectors)` 19 | */ 20 | function _isSupportsModuleInterface(address moduleAddress) internal view override returns (bool supported) { 21 | bytes memory callData = 22 | abi.encodeWithSelector(IERC165.supportsInterface.selector, type(ISoulWalletModule).interfaceId); 23 | assembly ("memory-safe") { 24 | // memorySafe: The scratch space between memory offset 0 and 64. 25 | let result := staticcall(gas(), moduleAddress, add(callData, 0x20), mload(callData), 0x00, 0x20) 26 | if gt(result, 0) { supported := mload(0x00) } 27 | } 28 | } 29 | 30 | function _addModule(bytes calldata moduleAndData) internal { 31 | address moduleAddress = address(bytes20(moduleAndData[:20])); 32 | ISoulWalletModule aModule = ISoulWalletModule(moduleAddress); 33 | bytes4[] memory requiredFunctions = aModule.requiredFunctions(); 34 | if (requiredFunctions.length == 0) { 35 | revert Errors.MODULE_SELECTORS_EMPTY(); 36 | } 37 | _installModule(address(bytes20(moduleAndData[:20])), moduleAndData[20:], requiredFunctions); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/abstract/SoulWalletOwnerManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {OwnerManager} from "@soulwallet-core/contracts/base/OwnerManager.sol"; 5 | import {ISoulWalletOwnerManager} from "../interfaces/ISoulWalletOwnerManager.sol"; 6 | 7 | abstract contract SoulWalletOwnerManager is ISoulWalletOwnerManager, OwnerManager { 8 | function _addOwners(bytes32[] calldata owners) internal { 9 | for (uint256 i = 0; i < owners.length;) { 10 | _addOwner(owners[i]); 11 | unchecked { 12 | i++; 13 | } 14 | } 15 | } 16 | 17 | function addOwners(bytes32[] calldata owners) external override { 18 | ownerManagementAccess(); 19 | _addOwners(owners); 20 | } 21 | 22 | function resetOwners(bytes32[] calldata newOwners) external override { 23 | ownerManagementAccess(); 24 | _clearOwner(); 25 | _addOwners(newOwners); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/abstract/SoulWalletUpgradeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "../interfaces/IUpgradable.sol"; 5 | import "../libraries/Errors.sol"; 6 | /** 7 | * @title SoulWalletUpgradeManager 8 | * @dev This contract allows for the logic of a proxy to be upgraded 9 | */ 10 | 11 | abstract contract SoulWalletUpgradeManager is IUpgradable { 12 | /** 13 | * @dev Storage slot with the address of the current implementation 14 | * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1 15 | */ 16 | bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 17 | /** 18 | * @dev Upgrades the logic to a new implementation 19 | * @param newImplementation Address of the new implementation 20 | */ 21 | 22 | function _upgradeTo(address newImplementation) internal { 23 | bool isContract; 24 | assembly ("memory-safe") { 25 | isContract := gt(extcodesize(newImplementation), 0) 26 | } 27 | if (!isContract) { 28 | revert Errors.INVALID_LOGIC_ADDRESS(); 29 | } 30 | address oldImplementation; 31 | assembly ("memory-safe") { 32 | oldImplementation := and(sload(_IMPLEMENTATION_SLOT), 0xffffffffffffffffffffffffffffffffffffffff) 33 | } 34 | if (oldImplementation == newImplementation) { 35 | revert Errors.SAME_LOGIC_ADDRESS(); 36 | } 37 | assembly ("memory-safe") { 38 | sstore(_IMPLEMENTATION_SLOT, newImplementation) 39 | } 40 | 41 | // delegatecall to new implementation 42 | (bool success,) = 43 | newImplementation.delegatecall(abi.encodeWithSelector(IUpgradable.upgradeFrom.selector, oldImplementation)); 44 | if (!success) { 45 | revert Errors.UPGRADE_FAILED(); 46 | } 47 | emit Upgraded(oldImplementation, newImplementation); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /contracts/abstract/SoulWalletValidatorManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {ValidatorManager} from "@soulwallet-core/contracts/base/ValidatorManager.sol"; 5 | import {ISoulWalletValidatorManager} from "../interfaces/ISoulWalletValidatorManager.sol"; 6 | 7 | abstract contract SoulWalletValidatorManager is ISoulWalletValidatorManager, ValidatorManager { 8 | function installValidator(bytes calldata validatorAndData) external virtual override { 9 | validatorManagementAccess(); 10 | _installValidator(address(bytes20(validatorAndData[:20])), validatorAndData[20:]); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/automation/AaveUsdcSaveAutomation.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | interface IAaveV3 { 7 | function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external; 8 | } 9 | 10 | /** 11 | * @title AaveUsdcSaveAutomation 12 | * @dev This contract allows a bot to deposit USDC to Aave on behalf of a user. 13 | */ 14 | contract AaveUsdcSaveAutomation is Ownable { 15 | using SafeERC20 for IERC20; 16 | 17 | event BotAdded(address bot); 18 | event BotRemoved(address bot); 19 | event UsdcDepositedToAave(address user, uint256 amount); 20 | 21 | IERC20 immutable usdcToken; 22 | IAaveV3 immutable aave; 23 | mapping(address => bool) public bots; 24 | 25 | /** 26 | * @dev Modifier to make a function callable only by a bot. 27 | */ 28 | modifier onlyBot() { 29 | require(bots[msg.sender], "no permission"); 30 | _; 31 | } 32 | 33 | constructor(address _owner, address _usdcAddr, address _aaveUsdcPoolAddr) Ownable(_owner) { 34 | usdcToken = IERC20(_usdcAddr); 35 | aave = IAaveV3(_aaveUsdcPoolAddr); 36 | usdcToken.approve(address(aave), 2 ** 256 - 1); 37 | } 38 | 39 | /** 40 | * @notice Deposits USDC to Aave on behalf of a user 41 | * @dev This function can only be called by a bot 42 | * @param _user The address of the user for whom to deposit USDC 43 | * @param amount The amount of USDC to deposit 44 | */ 45 | function depositUsdcToAave(address _user, uint256 amount) public onlyBot { 46 | usdcToken.safeTransferFrom(_user, address(this), amount); 47 | aave.supply(address(usdcToken), amount, _user, 0); 48 | emit UsdcDepositedToAave(_user, amount); 49 | } 50 | 51 | /** 52 | * @notice Deposits USDC to Aave on behalf of multiple users 53 | * @dev This function can only be called by a bot 54 | * @param _users An array of addresses of the users for whom to deposit USDC 55 | * @param amounts An array of amounts of USDC to deposit for each user 56 | */ 57 | function depositUsdcToAaveBatch(address[] calldata _users, uint256[] calldata amounts) public onlyBot { 58 | require(_users.length == amounts.length, "invalid input"); 59 | for (uint256 i = 0; i < _users.length; i++) { 60 | depositUsdcToAave(_users[i], amounts[i]); 61 | } 62 | } 63 | 64 | function addBot(address bot) public onlyOwner { 65 | bots[bot] = true; 66 | emit BotAdded(bot); 67 | } 68 | 69 | function removeBot(address bot) public onlyOwner { 70 | bots[bot] = false; 71 | emit BotRemoved(bot); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /contracts/automation/ClaimInterest.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 4 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 5 | import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | /** 9 | * @title ClaimInterest 10 | * @dev This contract allows users to claim their interest. 11 | * The interest claim is authenticated by a signature from a trusted signer. 12 | * The owner of the contract can change the signer. 13 | */ 14 | contract ClaimInterest is Ownable { 15 | using SafeERC20 for IERC20; 16 | using ECDSA for bytes32; 17 | 18 | event SignerChanged(address indexed signer, bool isSigner); 19 | event Withdrawn(address indexed to, uint256 amount); 20 | event Deposited(address indexed addr, uint256 amount); 21 | 22 | mapping(address => bool) public signers; 23 | IERC20 public token; 24 | mapping(address => uint256) public nonces; 25 | 26 | constructor(address _owner, address _signer, address _token) Ownable(_owner) { 27 | signers[_signer] = true; 28 | token = IERC20(_token); 29 | } 30 | 31 | /** 32 | * @notice Claim the interest amount. 33 | * @dev The claim is authenticated by a signature from the trusted signer. 34 | * @param interestAmount The amount of interest to claim. 35 | * @param signature The signature from the signer. 36 | */ 37 | function claimInterest(uint256 interestAmount, uint256 nonce, uint256 expiryTime, bytes memory signature) public { 38 | require(nonce == nonces[msg.sender], "Invalid nonce"); 39 | require(block.timestamp <= expiryTime, "Signature expired"); 40 | require(token.balanceOf(address(this)) >= interestAmount, "Insufficient balance"); 41 | bytes32 message = 42 | keccak256(abi.encodePacked(msg.sender, interestAmount, nonce, expiryTime, address(this), getChainId())); 43 | bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(message); 44 | require(signers[ethSignedMessageHash.recover(signature)], "Invalid signature"); 45 | nonces[msg.sender] += 1; // Increment nonce for the user 46 | token.safeTransfer(msg.sender, interestAmount); 47 | } 48 | 49 | function getChainId() public view returns (uint256) { 50 | uint256 id; 51 | // solhint-disable-next-line no-inline-assembly 52 | assembly { 53 | id := chainid() 54 | } 55 | return id; 56 | } 57 | 58 | function changeSigner(address newSigner, bool isSigner) public onlyOwner { 59 | require(newSigner != address(0), "Invalid address"); 60 | signers[newSigner] = isSigner; 61 | emit SignerChanged(newSigner, isSigner); 62 | } 63 | 64 | function withdraw(address to, uint256 amount) public onlyOwner { 65 | require(token.balanceOf(address(this)) >= amount, "Insufficient balance"); 66 | token.safeTransfer(to, amount); 67 | emit Withdrawn(to, amount); 68 | } 69 | 70 | function deposit(uint256 amount) public { 71 | token.safeTransferFrom(msg.sender, address(this), amount); 72 | emit Deposited(msg.sender, amount); 73 | } 74 | 75 | /** 76 | * @notice Admin function to force increment a user's nonce 77 | * @dev signer can increment a user's nonce to invalidate previous signatures 78 | * @param user The address of the user nonce to change. 79 | */ 80 | function incrementNonce(address user) public { 81 | require(signers[msg.sender], "Only signer can change user nonce"); 82 | nonces[user] += 1; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /contracts/dev/EIP1271Wallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts/interfaces/IERC1271.sol"; 5 | 6 | contract EIP1271Wallet is IERC1271 { 7 | bytes4 internal constant MAGICVALUE = 0x1626ba7e; 8 | bytes4 internal constant INVALID_ID = 0xffffffff; 9 | 10 | function isValidSignature(bytes32 hash, bytes memory signature) 11 | external 12 | pure 13 | override 14 | returns (bytes4 magicValue) 15 | { 16 | (bytes32 _hash, bool _valid) = abi.decode(signature, (bytes32, bool)); 17 | if (_hash != hash || !_valid) { 18 | return INVALID_ID; 19 | } 20 | return MAGICVALUE; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/dev/HelloWorld.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.8.17; 4 | 5 | contract HelloWorld { 6 | function output() external pure returns (string memory) { 7 | return "Hello World"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/dev/NewImplementation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.8.20; 4 | 5 | import "../abstract/SoulWalletUpgradeManager.sol"; 6 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 7 | 8 | contract NewImplementation is Initializable, SoulWalletUpgradeManager { 9 | address public immutable WALLETIMPL; 10 | bytes32 public constant CURRENT_UPGRADE_SLOT = keccak256("soul.wallet.upgradeTo_NewImplementation"); 11 | 12 | constructor() { 13 | WALLETIMPL = address(this); 14 | _disableInitializers(); 15 | } 16 | 17 | function initialize( 18 | bytes32[] calldata owners, 19 | address defalutCallbackHandler, 20 | bytes[] calldata modules, 21 | bytes[] calldata hooks 22 | ) external initializer {} 23 | 24 | function hello() external pure returns (string memory) { 25 | return "hello world"; 26 | } 27 | 28 | function upgradeTo(address newImplementation) external override { 29 | _upgradeTo(newImplementation); 30 | } 31 | 32 | function upgradeFrom(address oldImplementation) external override { 33 | (oldImplementation); 34 | require(oldImplementation != WALLETIMPL); 35 | bool hasUpgraded = false; 36 | 37 | bytes32 _CURRENT_UPGRADE_SLOT = CURRENT_UPGRADE_SLOT; 38 | assembly { 39 | hasUpgraded := sload(_CURRENT_UPGRADE_SLOT) 40 | } 41 | require(!hasUpgraded, "already upgraded"); 42 | assembly { 43 | sstore(_CURRENT_UPGRADE_SLOT, 1) 44 | } 45 | 46 | // data migration during upgrade 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /contracts/dev/ReceivePayment.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.8.20; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | /** 8 | * @title ReceivePayment 9 | * @dev This contract isto collect social recovery fees from users. 10 | * Users pay ETH to this contract, and we will performs the social recovery operation for them. 11 | * so that user only need to collect guardian signatures, after that, our backend will perform the recovery operation. 12 | */ 13 | contract ReceivePayment is Ownable { 14 | event PaymentReceived(bytes32 indexed paymentId, address indexed sender, uint256 amount); 15 | 16 | constructor(address _owner) Ownable(_owner) {} 17 | 18 | /** 19 | * @notice Makes a payment to the contract 20 | * @dev This function is payable, meaning it can receive Ether. It emits a PaymentReceived event. 21 | * @param _paymentId The ID of the payment, generated by the backend 22 | */ 23 | function pay(bytes32 _paymentId) external payable { 24 | emit PaymentReceived(_paymentId, msg.sender, msg.value); 25 | } 26 | 27 | /** 28 | * @notice Withdraws the contract balance to a specified address 29 | * @dev This function can only be called by the owner of the contract. It emits a Withdraw event. 30 | * @param _to The address to which the funds will be withdrawn 31 | */ 32 | function withdraw(address _to) external onlyOwner { 33 | (bool success,) = payable(_to).call{value: address(this).balance}(""); 34 | require(success, "Withdraw failed"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/dev/TestOracle.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.20; 2 | 3 | contract TestOracle { 4 | int256 public price; 5 | 6 | constructor(int256 _price) { 7 | price = _price; 8 | } 9 | 10 | function decimals() external pure returns (uint8) { 11 | return 8; 12 | } 13 | 14 | function latestRoundData() 15 | external 16 | view 17 | returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) 18 | { 19 | return (110680464442257311610, price, 1685339747, block.timestamp, 110680464442257311610); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/dev/tokens/TokenERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract TokenERC20 is ERC20 { 7 | uint8 private immutable __decimals; 8 | 9 | constructor(uint8 _decimals) ERC20("ERC20", "ERC20") { 10 | _mint(msg.sender, 1000000000000000000000000); 11 | __decimals = _decimals; 12 | } 13 | 14 | function decimals() public view override returns (uint8) { 15 | return __decimals; 16 | } 17 | 18 | function sudoMint(address _to, uint256 _amount) external { 19 | _mint(_to, _amount); 20 | } 21 | 22 | function sudoTransfer(address _from, address _to) external { 23 | _transfer(_from, _to, balanceOf(_from)); 24 | } 25 | 26 | function sudoApprove(address _from, address _to, uint256 _amount) external { 27 | _approve(_from, _to, _amount); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/factory/SoulWalletFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | /* solhint-disable avoid-low-level-calls */ 5 | /* solhint-disable no-inline-assembly */ 6 | /* solhint-disable reason-string */ 7 | 8 | import "../SoulWallet.sol"; 9 | import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; 10 | import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 11 | import "@openzeppelin/contracts/access/Ownable.sol"; 12 | 13 | /** 14 | * @title SoulWalletFactory 15 | * @author soulwallet team 16 | * @notice A factory contract to create soul wallets 17 | * @dev This contract is called by the entrypoint which uses the "initCode" to create and return the sender's wallet address 18 | */ 19 | contract SoulWalletFactory is Ownable { 20 | address public immutable _WALLETIMPL; 21 | IEntryPoint public immutable entryPoint; 22 | string public constant VERSION = "0.0.1"; 23 | 24 | event SoulWalletCreation(address indexed proxy); 25 | 26 | /** 27 | * @dev Initializes the factory with the wallet implementation and entry point addresses 28 | * @param _walletImpl Address of the SoulWallet implementation 29 | * @param _entryPoint Address of the EntryPoint contract 30 | * @param _owner Address of the contract owner 31 | */ 32 | constructor(address _walletImpl, address _entryPoint, address _owner) Ownable(_owner) { 33 | require(_walletImpl != address(0), "Invalid wallet implementation address"); 34 | _WALLETIMPL = _walletImpl; 35 | require(_entryPoint != address(0), "Invalid entry point address"); 36 | entryPoint = IEntryPoint(_entryPoint); 37 | } 38 | 39 | function _calcSalt(bytes memory _initializer, bytes32 _salt) private pure returns (bytes32 salt) { 40 | return keccak256(abi.encodePacked(keccak256(_initializer), _salt)); 41 | } 42 | 43 | /** 44 | * @dev Deploys the SoulWallet using a proxy and returns the proxy's address 45 | * @param _initializer Initialization data 46 | * @param _salt Salt for the create2 deployment 47 | * @return proxy Address of the deployed proxy 48 | */ 49 | function createWallet(bytes memory _initializer, bytes32 _salt) external returns (address proxy) { 50 | // factory expected to return the wallet address even if the wallet has already been created. 51 | address addr = getWalletAddress(_initializer, _salt); 52 | uint256 codeSize = addr.code.length; 53 | if (codeSize > 0) { 54 | return addr; 55 | } 56 | bytes memory deploymentData = _proxyCode(_WALLETIMPL); 57 | bytes32 salt = _calcSalt(_initializer, _salt); 58 | assembly ("memory-safe") { 59 | proxy := create2(0x0, add(deploymentData, 0x20), mload(deploymentData), salt) 60 | } 61 | if (proxy == address(0)) { 62 | revert(); 63 | } 64 | assembly ("memory-safe") { 65 | let succ := call(gas(), proxy, 0, add(_initializer, 0x20), mload(_initializer), 0, 0) 66 | if eq(succ, 0) { revert(0, 0) } 67 | } 68 | emit SoulWalletCreation(proxy); 69 | } 70 | 71 | /** 72 | * @notice Returns the proxy's creation code 73 | * @dev Used by soulwalletlib to calculate the SoulWallet address 74 | * @return Byte array representing the proxy's creation code 75 | */ 76 | function proxyCode() external view returns (bytes memory) { 77 | return _proxyCode(_WALLETIMPL); 78 | } 79 | /** 80 | * @notice using solay ERC1967 https://github.com/Vectorized/solady/blob/5eff720c27746987dc95e5e2b720615d3d96f7ee/src/utils/LibClone.sol#L774C18-L774C18 81 | */ 82 | 83 | function _proxyCode(address implementation) private pure returns (bytes memory deploymentData) { 84 | deploymentData = abi.encodePacked( 85 | hex"603d3d8160223d3973", 86 | implementation, 87 | hex"60095155f3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3" 88 | ); 89 | } 90 | 91 | /** 92 | * @notice Calculates the counterfactual address of the SoulWallet as it would be returned by `createWallet` 93 | * @param _initializer Initialization data 94 | * @param _salt Salt for the create2 deployment 95 | * @return proxy Counterfactual address of the SoulWallet 96 | */ 97 | function getWalletAddress(bytes memory _initializer, bytes32 _salt) public view returns (address proxy) { 98 | bytes memory deploymentData = _proxyCode(_WALLETIMPL); 99 | bytes32 salt = _calcSalt(_initializer, _salt); 100 | proxy = Create2.computeAddress(salt, keccak256(deploymentData)); 101 | } 102 | 103 | /** 104 | * @notice Deposits ETH to the entry point on behalf of the contract 105 | */ 106 | function deposit() public payable { 107 | entryPoint.depositTo{value: msg.value}(address(this)); 108 | } 109 | 110 | /** 111 | * @notice Allows the owner to withdraw ETH from entrypoint contract 112 | * @param withdrawAddress Address to receive the withdrawn ETH 113 | * @param amount Amount of ETH to withdraw 114 | */ 115 | function withdrawTo(address payable withdrawAddress, uint256 amount) public onlyOwner { 116 | entryPoint.withdrawTo(withdrawAddress, amount); 117 | } 118 | 119 | /** 120 | * @notice Allows the owner to add stake to the entry point 121 | * @param unstakeDelaySec Duration (in seconds) after which the stake can be unlocked 122 | */ 123 | function addStake(uint32 unstakeDelaySec) external payable onlyOwner { 124 | entryPoint.addStake{value: msg.value}(unstakeDelaySec); 125 | } 126 | 127 | /** 128 | * @notice Allows the owner to unlock their stake from the entry point 129 | */ 130 | function unlockStake() external onlyOwner { 131 | entryPoint.unlockStake(); 132 | } 133 | 134 | /** 135 | * @notice Allows the owner to withdraw their stake from the entry point 136 | * @param withdrawAddress Address to receive the withdrawn stake 137 | */ 138 | function withdrawStake(address payable withdrawAddress) external onlyOwner { 139 | entryPoint.withdrawStake(withdrawAddress); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /contracts/hooks/2fa/Crypto2FAHook.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {IHook, PackedUserOperation} from "@soulwallet-core/contracts/interface/IHook.sol"; 5 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 6 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 7 | 8 | contract Crypto2FAHook is IHook { 9 | using ECDSA for bytes32; 10 | using MessageHashUtils for bytes32; 11 | 12 | uint256 public constant TIME_LOCK_DURATION = 1 days; 13 | 14 | struct User2FA { 15 | bool initialized; 16 | address wallet2FAAddr; 17 | address pending2FAAddr; 18 | uint256 effectiveTime; 19 | } 20 | 21 | mapping(address => User2FA) public user2FA; 22 | 23 | function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { 24 | return interfaceId == type(IHook).interfaceId; 25 | } 26 | 27 | function Init(bytes calldata data) external override { 28 | User2FA storage _user2fa = user2FA[msg.sender]; 29 | require(_user2fa.initialized == false, "already initialized"); 30 | address wallet2FAAddr = address(bytes20(data[:20])); 31 | _user2fa.initialized = true; 32 | _user2fa.wallet2FAAddr = wallet2FAAddr; 33 | } 34 | 35 | function DeInit() external override { 36 | User2FA storage _user2fa = user2FA[msg.sender]; 37 | require(_user2fa.initialized == true, "cannot deinit"); 38 | delete user2FA[msg.sender]; 39 | } 40 | 41 | function preIsValidSignatureHook(bytes32 hash, bytes calldata hookSignature) external view override { 42 | address recoveredAddress = hash.toEthSignedMessageHash().recover(hookSignature); 43 | require(recoveredAddress == user2FA[msg.sender].wallet2FAAddr, "Crypto2FAHook: invalid signature"); 44 | } 45 | 46 | function preUserOpValidationHook( 47 | PackedUserOperation calldata userOp, 48 | bytes32 userOpHash, 49 | uint256 missingAccountFunds, 50 | bytes calldata hookSignature 51 | ) external view override { 52 | (userOp, userOpHash, missingAccountFunds, hookSignature); 53 | address recoveredAddress = userOpHash.toEthSignedMessageHash().recover(hookSignature); 54 | require(recoveredAddress == user2FA[msg.sender].wallet2FAAddr, "Crypto2FAHook: invalid signature"); 55 | } 56 | 57 | function initiateChange2FA(address new2FA) external { 58 | User2FA storage _user2fa = user2FA[msg.sender]; 59 | require(_user2fa.initialized, "User not initialized"); 60 | _user2fa.pending2FAAddr = new2FA; 61 | _user2fa.effectiveTime = block.timestamp + TIME_LOCK_DURATION; 62 | } 63 | 64 | function applyChange2FA() external { 65 | User2FA storage _user2fa = user2FA[msg.sender]; 66 | require(_user2fa.pending2FAAddr != address(0), "No pending change"); 67 | require(_user2fa.effectiveTime > 0 && block.timestamp >= _user2fa.effectiveTime, "Time lock not expired"); 68 | _user2fa.wallet2FAAddr = _user2fa.pending2FAAddr; 69 | _user2fa.pending2FAAddr = address(0); 70 | _user2fa.effectiveTime = 0; 71 | } 72 | 73 | function cancelChange2FA() external { 74 | User2FA storage _user2fa = user2FA[msg.sender]; 75 | require(block.timestamp < _user2fa.effectiveTime, "Change already effective"); 76 | _user2fa.pending2FAAddr = address(0); 77 | _user2fa.effectiveTime = 0; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /contracts/interfaces/ISoulWallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {ISoulWalletHookManager} from "../interfaces/ISoulWalletHookManager.sol"; 5 | import {ISoulWalletModuleManager} from "../interfaces/ISoulWalletModuleManager.sol"; 6 | import {ISoulWalletOwnerManager} from "../interfaces/ISoulWalletOwnerManager.sol"; 7 | import {ISoulWalletOwnerManager} from "../interfaces/ISoulWalletOwnerManager.sol"; 8 | import {IUpgradable} from "../interfaces/IUpgradable.sol"; 9 | import {IStandardExecutor} from "@soulwallet-core/contracts/interface/IStandardExecutor.sol"; 10 | 11 | interface ISoulWallet is 12 | ISoulWalletHookManager, 13 | ISoulWalletModuleManager, 14 | ISoulWalletOwnerManager, 15 | IStandardExecutor, 16 | IUpgradable 17 | { 18 | function initialize( 19 | bytes32[] calldata owners, 20 | address defalutCallbackHandler, 21 | bytes[] calldata modules, 22 | bytes[] calldata hooks 23 | ) external; 24 | } 25 | -------------------------------------------------------------------------------- /contracts/interfaces/ISoulWalletHookManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {IHookManager} from "@soulwallet-core/contracts/interface/IHookManager.sol"; 5 | 6 | interface ISoulWalletHookManager is IHookManager { 7 | function installHook(bytes calldata hookAndData, uint8 capabilityFlags) external; 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interfaces/ISoulWalletModuleManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {IModuleManager} from "@soulwallet-core/contracts/interface/IModuleManager.sol"; 5 | 6 | interface ISoulWalletModuleManager is IModuleManager { 7 | function installModule(bytes calldata moduleAndData) external; 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interfaces/ISoulWalletOwnerManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | import {IOwnerManager} from "@soulwallet-core/contracts/interface/IOwnerManager.sol"; 4 | interface ISoulWalletOwnerManager is IOwnerManager { 5 | function addOwners(bytes32[] calldata owners) external; 6 | function resetOwners(bytes32[] calldata newOwners) external; 7 | } 8 | -------------------------------------------------------------------------------- /contracts/interfaces/ISoulWalletValidatorManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {IValidatorManager} from "@soulwallet-core/contracts/interface/IValidatorManager.sol"; 5 | 6 | interface ISoulWalletValidatorManager is IValidatorManager { 7 | function installValidator(bytes calldata validatorAndData) external; 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interfaces/IUpgradable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | /** 5 | * @title Upgradable Interface 6 | * @dev This interface provides functionalities to upgrade the implementation of a contract 7 | * It emits an event when the implementation is changed, either to a new version or from an old version 8 | */ 9 | interface IUpgradable { 10 | event Upgraded(address indexed oldImplementation, address indexed newImplementation); 11 | 12 | /** 13 | * @dev Upgrade the current implementation to the provided new implementation address 14 | * @param newImplementation The address of the new contract implementation 15 | */ 16 | function upgradeTo(address newImplementation) external; 17 | 18 | /** 19 | * @dev Upgrade from the current implementation, given the old implementation address 20 | * @param oldImplementation The address of the old contract implementation that is being replaced 21 | */ 22 | function upgradeFrom(address oldImplementation) external; 23 | } 24 | -------------------------------------------------------------------------------- /contracts/libraries/Base64Url.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // fork from: OpenZeppelin Contracts (last updated v4.7.0) (utils/Base64.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Provides a set of functions to operate with Base64Url strings (with all trailing '=' characters omitted). 8 | * see: 9 | * 1. https://www.w3.org/TR/webauthn-2/#sctn-dependencies 10 | * 2. https://datatracker.ietf.org/doc/html/rfc4648 11 | * 12 | * 13 | * _Available since v4.5._ 14 | */ 15 | library Base64Url { 16 | /** 17 | * @dev Base64Url Encoding/Decoding Table 18 | */ 19 | string internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; 20 | 21 | /** 22 | * @dev Converts a `bytes` to its Bytes64Url `string` representation. 23 | */ 24 | function encode(bytes memory data) internal pure returns (string memory) { 25 | /** 26 | * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence 27 | * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol 28 | */ 29 | 30 | uint256 dataLen; 31 | assembly { 32 | dataLen := mload(data) 33 | } 34 | if (dataLen == 0) return ""; 35 | 36 | // Loads the table into memory 37 | string memory table = _TABLE; 38 | 39 | uint256 encodedLen; 40 | assembly { 41 | encodedLen := mul(4, div(dataLen, 3)) //4 * (dataLen / 3); 42 | let padding := mod(dataLen, 3) 43 | if gt(padding, 0) { encodedLen := add(add(encodedLen, padding), 1) } 44 | } 45 | 46 | // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter 47 | // and split into 4 numbers of 6 bits. 48 | string memory result = new string(encodedLen); 49 | 50 | /// @solidity memory-safe-assembly 51 | assembly { 52 | // Prepare the lookup table (skip the first "length" byte) 53 | let tablePtr := add(table, 1) 54 | 55 | // Prepare result pointer, jump over length 56 | let resultPtr := add(result, 32) 57 | 58 | // Run over the input, 3 bytes at a time 59 | for { 60 | let dataPtr := data 61 | let endPtr := add(data, dataLen) 62 | } lt(dataPtr, endPtr) {} { 63 | // Advance 3 bytes 64 | dataPtr := add(dataPtr, 3) 65 | let input := mload(dataPtr) 66 | 67 | // To write each character, shift the 3 bytes (18 bits) chunk 68 | // 4 times in blocks of 6 bits for each character (18, 12, 6, 0) 69 | // and apply logical AND with 0x3F which is the number of 70 | // the previous character in the ASCII table prior to the Base64 Table 71 | // The result is then added to the table to get the character to write, 72 | // and finally write it in the result pointer but with a left shift 73 | // of 256 (1 byte) - 8 (1 ASCII char) = 248 bits 74 | 75 | mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) 76 | resultPtr := add(resultPtr, 1) // Advance 77 | 78 | mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) 79 | resultPtr := add(resultPtr, 1) // Advance 80 | 81 | mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F)))) 82 | resultPtr := add(resultPtr, 1) // Advance 83 | 84 | mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) 85 | resultPtr := add(resultPtr, 1) // Advance 86 | } 87 | } 88 | 89 | return result; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /contracts/libraries/DecodeCalldata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | library DecodeCalldata { 5 | function decodeMethodId(bytes memory data) internal pure returns (bytes4 methodId) { 6 | assembly ("memory-safe") { 7 | let dataLength := mload(data) 8 | if lt(dataLength, 0x04) { revert(0, 0) } 9 | methodId := mload(add(data, 0x20)) 10 | } 11 | } 12 | 13 | function decodeMethodCalldata(bytes memory data) internal pure returns (bytes memory MethodCalldata) { 14 | assembly ("memory-safe") { 15 | let dataLength := mload(data) 16 | if lt(dataLength, 0x04) { revert(0, 0) } 17 | let methodDataLength := sub(dataLength, 0x04) 18 | MethodCalldata := mload(0x40) 19 | mstore(0x40, add(MethodCalldata, and(add(methodDataLength, 0x3f), not(0x1f)))) 20 | mstore(MethodCalldata, methodDataLength) 21 | let MethodCalldataStart := add(MethodCalldata, 0x20) 22 | let dataStart := add(data, 0x24) 23 | for { let i := 0x00 } lt(i, methodDataLength) { i := add(i, 0x20) } { 24 | mstore(add(MethodCalldataStart, i), mload(add(dataStart, i))) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/libraries/Errors.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | library Errors { 5 | error ADDRESS_ALREADY_EXISTS(); 6 | error ADDRESS_NOT_EXISTS(); 7 | error DATA_ALREADY_EXISTS(); 8 | error DATA_NOT_EXISTS(); 9 | error CALLER_MUST_BE_SELF_OR_MODULE(); 10 | error CALLER_MUST_BE_MODULE(); 11 | error HASH_ALREADY_APPROVED(); 12 | error HASH_ALREADY_REJECTED(); 13 | error INVALID_ADDRESS(); 14 | error INVALID_SELECTOR(); 15 | error INVALID_SIGNTYPE(); 16 | error MODULE_SELECTORS_EMPTY(); 17 | error MODULE_EXECUTE_FROM_MODULE_RECURSIVE(); 18 | error SELECTOR_ALREADY_EXISTS(); 19 | error SELECTOR_NOT_EXISTS(); 20 | error INVALID_LOGIC_ADDRESS(); 21 | error SAME_LOGIC_ADDRESS(); 22 | error UPGRADE_FAILED(); 23 | error NOT_IMPLEMENTED(); 24 | error INVALID_SIGNATURE(); 25 | error INVALID_TIME_RANGE(); 26 | error UNAUTHORIZED(); 27 | error INVALID_DATA(); 28 | error GUARDIAN_SIGNATURE_INVALID(); 29 | } 30 | -------------------------------------------------------------------------------- /contracts/libraries/TypeConversion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | /** 4 | * @title TypeConversion 5 | * @notice A library to facilitate address to bytes32 conversions 6 | */ 7 | 8 | library TypeConversion { 9 | /** 10 | * @notice Converts an address to bytes32 11 | * @param addr The address to be converted 12 | * @return Resulting bytes32 representation of the input address 13 | */ 14 | function toBytes32(address addr) internal pure returns (bytes32) { 15 | return bytes32(uint256(uint160(addr))); 16 | } 17 | /** 18 | * @notice Converts an array of addresses to an array of bytes32 19 | * @param addresses Array of addresses to be converted 20 | * @return Array of bytes32 representations of the input addresses 21 | */ 22 | 23 | function addressesToBytes32Array(address[] memory addresses) internal pure returns (bytes32[] memory) { 24 | bytes32[] memory result = new bytes32[](addresses.length); 25 | for (uint256 i = 0; i < addresses.length; i++) { 26 | result[i] = toBytes32(addresses[i]); 27 | } 28 | return result; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/modules/BaseModule.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "./interfaces/ISoulWalletModule.sol"; 5 | import "./../interfaces/ISoulWallet.sol"; 6 | 7 | /** 8 | * @title BaseModule 9 | * @notice An abstract base contract that provides a foundation for other modules. 10 | * It ensures the initialization, de-initialization, and proper authorization of modules. 11 | */ 12 | abstract contract BaseModule is ISoulWalletModule { 13 | event ModuleInit(address indexed wallet); 14 | event ModuleDeInit(address indexed wallet); 15 | /** 16 | * @notice Checks if the module is initialized for a particular wallet. 17 | * @param wallet Address of the wallet. 18 | * @return True if the module is initialized, false otherwise. 19 | */ 20 | 21 | function inited(address wallet) internal view virtual returns (bool); 22 | /** 23 | * @notice Initialization logic for the module. 24 | * @param data Initialization data for the module. 25 | */ 26 | function _init(bytes calldata data) internal virtual; 27 | /** 28 | * @notice De-initialization logic for the module. 29 | */ 30 | function _deInit() internal virtual; 31 | /** 32 | * @notice Helper function to get the sender of the transaction. 33 | * @return Address of the transaction sender. 34 | */ 35 | 36 | function sender() internal view returns (address) { 37 | return msg.sender; 38 | } 39 | /** 40 | * @notice Initializes the module for a wallet. 41 | * @param data Initialization data for the module. 42 | */ 43 | 44 | function Init(bytes calldata data) external { 45 | address _sender = sender(); 46 | if (!inited(_sender)) { 47 | if (!ISoulWallet(_sender).isInstalledModule(address(this))) { 48 | revert("not authorized module"); 49 | } 50 | _init(data); 51 | emit ModuleInit(_sender); 52 | } 53 | } 54 | /** 55 | * @notice De-initializes the module for a wallet. 56 | */ 57 | 58 | function DeInit() external { 59 | address _sender = sender(); 60 | if (inited(_sender)) { 61 | if (ISoulWallet(_sender).isInstalledModule(address(this))) { 62 | revert("authorized module"); 63 | } 64 | _deInit(); 65 | emit ModuleDeInit(_sender); 66 | } 67 | } 68 | /** 69 | * @notice Verifies if the module supports a specific interface. 70 | * @param interfaceId ID of the interface to be checked. 71 | * @return True if the module supports the given interface, false otherwise. 72 | */ 73 | 74 | function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { 75 | return interfaceId == type(ISoulWalletModule).interfaceId || interfaceId == type(IModule).interfaceId; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/modules/README.md: -------------------------------------------------------------------------------- 1 | # Module Introduction 2 | 3 | SoulWallet modules introduce customizable features to SoulWallet contracts. They are smart contracts designed to extend the functionality of SoulWallet, while keeping module logic distinct from the core contracts of SoulWallet. A basic SoulWallet operates independently of any modules, which are optional and intended to enhance functionality based on user needs. 4 | 5 | ## Implement new Module 6 | 7 | To add a new module, the contract should inherit from BaseModule. BaseModule is an abstract base contract that provides a foundation for other modules. It ensures the initialization, de-initialization, and proper authorization of modules. 8 | 9 | ```solidity 10 | import "./BaseModule.sol"; 11 | 12 | contract NewModule is BaseModule { 13 | function requiredFunctions() external pure override returns (bytes4[] memory) 14 | { 15 | // return wallet functions that modules need access to 16 | } 17 | 18 | function inited(address wallet) internal view virtual override returns (bool) { 19 | // Implement your checking logic 20 | } 21 | 22 | function _init(bytes calldata data) internal virtual override { 23 | // Implement initialization logic 24 | } 25 | 26 | function _deInit() internal virtual override { 27 | // Implement de-initialization logic 28 | } 29 | } 30 | 31 | ``` 32 | 33 | - **requiredFunctions** 34 | The requiredFunctions feature mandates that modules explicitly declare which SoulWallet core functions they need to access, akin to how Android apps request specific permissions. This mechanism ensures security and transparency by allowing modules to only interact with the necessary parts of the SoulWallet, minimizing potential security risks. For example, a module managing daily spending cannot access owner management functions, embodying the principle of least privilege and risk isolation. This design requires developers to clearly outline their module's intended interactions with SoulWallet upon integration, fostering a secure modular environment. 35 | - **\_init** 36 | The \_init function is only triggered by SoulWallet during a module's addition, it facilitates the initial setup and configuration of the module. 37 | - **\_deInit** 38 | The \_deInit function is called upon module removal from SoulWallet, enabling the cleanup of related data. 39 | 40 | ![module pic](readmepic/module.png) 41 | -------------------------------------------------------------------------------- /contracts/modules/interfaces/ISoulWalletModule.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | import {IModule} from "@soulwallet-core/contracts/interface/IModule.sol"; 4 | 5 | interface ISoulWalletModule is IModule { 6 | function requiredFunctions() external pure returns (bytes4[] memory); 7 | } 8 | -------------------------------------------------------------------------------- /contracts/modules/readmepic/module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elytro-eth/soul-wallet-contract/fc7cc084563ad1bda870df841b77caa9ee3a3661/contracts/modules/readmepic/module.png -------------------------------------------------------------------------------- /contracts/modules/socialRecovery/README.md: -------------------------------------------------------------------------------- 1 | # SocialRecoveryModule 2 | 3 | The `SocialRecoveryModule` is a Solidity contract that can be installed in wallets to enable a social recovery mechanism. This module allows a user to designate a list of guardians for their wallet and establish a recovery threshold. If a wallet is lost or compromised, the guardians can initiate a recovery process by signing a special EIP712 signature. However, this recovery process is subject to a user-defined time lock period, and the guardians can only execute the recovery after this period has passed. This mechanism ensures that the user's assets remain secure and recoverable, even in unforeseen circumstances. 4 | 5 | ## Recovery flow 6 | 7 | ![social recovery flow](socialReoceryFlow.png) 8 | 9 | - Step 1: Users install the Social Recovery Module in their SoulWallet. They need to configure the guardian hash and the execution delay period when installing the module. The guardian hash refers to the keccak256 hash of the GuardianData, ensuring the privacy of guardian identities. Others cannot check your guardians' settings on-chain and they are only revealed when the user initiates the social recovery process. 10 | 11 | ```solidity 12 | struct GuardianData { 13 | address[] guardians; 14 | uint256 threshold; 15 | uint256 salt; 16 | } 17 | ``` 18 | 19 | - Step 2: When users want to recover their wallet using the guardians, they have to contact the guardians to sign an EIP-712 based signature and use the following scheme: 20 | 21 | - EIP712Domain 22 | 23 | ```json 24 | { 25 | "EIP712Domain": [ 26 | { "type": "uint256", "name": "chainId" }, 27 | { "type": "address", "name": "SocialRecovery" } 28 | ] 29 | } 30 | ``` 31 | 32 | - SocialRecovery 33 | 34 | ```json 35 | { 36 | "SocialRecovery": [ 37 | { "type": "address", "name": "wallet" }, 38 | { "type": "uint256", "name": "nonce" }, 39 | { "type": "bytes32[]", "name": "newOwners" } 40 | ] 41 | } 42 | ``` 43 | 44 | Once the signatures are collected and the threshold is met, it can call scheduleRecovery to enter the waiting queue for recovery. 45 | 46 | - Step 3: If the timelock period has passed, one can call `executeRecovery` to perform the recovery. The social recovery module will then reset the owners based on the previous setting. 47 | 48 | ## Considerations 49 | 50 | - Users can call `cancelAllRecovery` to invalidate transactions in the pending queue. 51 | - Users can call `setGuardian` to change guardians' settings without a timelock. 52 | -------------------------------------------------------------------------------- /contracts/modules/socialRecovery/SocialRecoveryModule.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.20; 2 | 3 | import "../BaseModule.sol"; 4 | import "./base/BaseSocialRecovery.sol"; 5 | 6 | /** 7 | * @title SocialRecoveryModule 8 | * @dev This contract extends BaseModule and BaseSocialRecovery to provide social recovery functionality for a wallet. 9 | * It allows a wallet owner to set a list of guardians and a recovery delay period. If the wallet is lost or compromised, 10 | * the guardians can recover the wallet after the delay period has passed. 11 | */ 12 | contract SocialRecoveryModule is BaseModule, BaseSocialRecovery { 13 | bytes4 private constant _FUNC_RESET_OWNER = bytes4(keccak256("resetOwner(bytes32)")); 14 | bytes4 private constant _FUNC_RESET_OWNERS = bytes4(keccak256("resetOwners(bytes32[])")); 15 | mapping(address => bool) walletInited; 16 | 17 | event SocialRecoveryInitialized(address indexed wallet, bytes32 guardianHash, uint256 delayPeriod); 18 | event SocialRecoveryDeInitialized(address indexed wallet); 19 | 20 | constructor() EIP712("SocialRecovery", "1") {} 21 | 22 | /** 23 | * @dev De-initializes the social recovery settings for the sender's wallet. 24 | */ 25 | function _deInit() internal override { 26 | address _sender = sender(); 27 | _clearWalletSocialRecoveryInfo(_sender); 28 | walletInited[_sender] = false; 29 | emit SocialRecoveryDeInitialized(_sender); 30 | } 31 | 32 | /** 33 | * @dev Initializes the social recovery settings for the sender's wallet. 34 | * @param _data The encoded guardian hash and delay period. 35 | */ 36 | function _init(bytes calldata _data) internal override { 37 | address _sender = sender(); 38 | (bytes32 guardianHash, uint256 delayPeriod) = abi.decode(_data, (bytes32, uint256)); 39 | _setGuardianHash(_sender, guardianHash); 40 | _setDelayPeriod(_sender, delayPeriod); 41 | walletInited[_sender] = true; 42 | emit SocialRecoveryInitialized(_sender, guardianHash, delayPeriod); 43 | } 44 | 45 | /** 46 | * @dev Checks if the social recovery settings for a wallet have been initialized. 47 | * @param wallet The address of the wallet. 48 | * @return A boolean indicating whether the social recovery settings for the wallet have been initialized. 49 | */ 50 | function inited(address wallet) internal view override returns (bool) { 51 | return walletInited[wallet]; 52 | } 53 | 54 | /** 55 | * @dev Returns the list of functions required by this module. 56 | * @return An array of function selectors. 57 | */ 58 | function requiredFunctions() external pure override returns (bytes4[] memory) { 59 | bytes4[] memory functions = new bytes4[](2); 60 | functions[0] = _FUNC_RESET_OWNER; 61 | functions[1] = _FUNC_RESET_OWNERS; 62 | return functions; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /contracts/modules/socialRecovery/interfaces/ISocialRecovery.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | interface ISocialRecovery { 5 | struct SocialRecoveryInfo { 6 | bytes32 guardianHash; 7 | uint256 nonce; 8 | // id to operation valid time 9 | mapping(bytes32 id => uint256) operationValidAt; 10 | uint256 delayPeriod; 11 | } 12 | 13 | function walletNonce(address wallet) external view returns (uint256 _nonce); 14 | 15 | /** 16 | * @notice . 17 | * @dev . 18 | * @param wallet to recovery 19 | * @param newOwners bytes32[] owners 20 | * @param rawGuardian abi.encode(GuardianData) 21 | * struct GuardianData { 22 | * address[] guardians; 23 | * uint256 threshold; 24 | * uint256 salt; 25 | * } 26 | * @param guardianSignature . 27 | * @return recoveryId . 28 | */ 29 | function scheduleRecovery( 30 | address wallet, 31 | bytes32[] calldata newOwners, 32 | bytes calldata rawGuardian, 33 | bytes calldata guardianSignature 34 | ) external returns (bytes32 recoveryId); 35 | 36 | function executeRecovery(address wallet, bytes32[] calldata newOwners) external; 37 | 38 | function setGuardian(bytes32 newGuardianHash) external; 39 | function setDelayPeriod(uint256 newDelay) external; 40 | 41 | enum OperationState { 42 | Unset, 43 | Waiting, 44 | Ready, 45 | Done 46 | } 47 | 48 | struct GuardianData { 49 | address[] guardians; 50 | uint256 threshold; 51 | uint256 salt; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /contracts/modules/socialRecovery/socialReoceryFlow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elytro-eth/soul-wallet-contract/fc7cc084563ad1bda870df841b77caa9ee3a3661/contracts/modules/socialRecovery/socialReoceryFlow.png -------------------------------------------------------------------------------- /contracts/modules/upgrade/IUpgrade.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | interface IUpgrade { 5 | event Upgrade(address indexed newLogic, address indexed oldLogic); 6 | 7 | function upgrade(address wallet) external; 8 | } 9 | -------------------------------------------------------------------------------- /contracts/modules/upgrade/UpgradeModule.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "../BaseModule.sol"; 5 | import "./IUpgrade.sol"; 6 | import "../../interfaces/IUpgradable.sol"; 7 | 8 | /* 9 | The UpgradeModule is responsible for upgrading the logic contract of SoulWallet. 10 | */ 11 | contract UpgradeModule is BaseModule, IUpgrade { 12 | address public newImplementation; 13 | mapping(address => uint256) private _inited; 14 | mapping(address => bool) private _upgraded; 15 | 16 | constructor(address _newImplementation) { 17 | newImplementation = _newImplementation; 18 | } 19 | 20 | function inited(address wallet) internal view override returns (bool) { 21 | return _inited[wallet] != 0; 22 | } 23 | 24 | function _init(bytes calldata data) internal override { 25 | (data); 26 | _inited[sender()] = 1; 27 | } 28 | 29 | function _deInit() internal override { 30 | _inited[sender()] = 0; 31 | } 32 | 33 | function upgrade(address wallet) external override { 34 | require(_inited[wallet] != 0, "not inited"); 35 | require(_upgraded[wallet] == false, "already upgraded"); 36 | IUpgradable(wallet).upgradeTo(newImplementation); 37 | _upgraded[wallet] = true; 38 | } 39 | 40 | function requiredFunctions() external pure override returns (bytes4[] memory) { 41 | bytes4[] memory _funcs = new bytes4[](1); 42 | _funcs[0] = IUpgradable.upgradeTo.selector; 43 | return _funcs; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contracts/paymaster/interfaces/IOracle.sol: -------------------------------------------------------------------------------- 1 | 2 | // SPDX-License-Identifier: MIT 3 | pragma solidity ^0.8.0; 4 | 5 | interface IOracle { 6 | function decimals() external view returns (uint8); 7 | function latestRoundData() 8 | external 9 | view 10 | returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); 11 | } 12 | -------------------------------------------------------------------------------- /contracts/validator/README.md: -------------------------------------------------------------------------------- 1 | # Validator Introduction 2 | 3 | The `Validator` contract is responsible for validating signatures. It implements the `IValidator` interface, which defines the necessary methods for signature validation. 4 | 5 | By default, SoulWallet implements the `Validator` contract with two signature verification schemas: ECDSA and passkey. 6 | 7 | Users can generate their passkeys, which can then be used for signature verification. This feature provides an additional layer of security and flexibility, allowing users to authenticate their operations in a way that best suits their needs. 8 | 9 | ## Signature Validation Methods 10 | 11 | The `Validator` contract supports two main validation flows: user operation validation and ERC1271 standard signature validation for contracts. 12 | 13 | ### User operation validation flow 14 | 15 | In the standard ERC4337 user operation, the entry point calls `account.validateUserOp` in the `handleOps` operation. First, the user operation signature is decoded in the `validateUserOp` function. The first 32 bytes define the validator address. If the validator address is installed in the SoulWallet contract, it will trigger a view call to the validation contract. The `validator` will then validate the correctness of the signature. 16 | 17 | ### ERC1271 Standard Signature Validation 18 | 19 | The `Validator` contract also supports the ERC1271 standard for signature validation. This standard defines a `isValidSignature` function that contracts can implement to validate signatures. Similar to the user operation signature decoding, when `isValidSignature` is invoked, the initial 32 bytes define the validator address. 20 | 21 | The SoulWallet user operation signature format is as follows: 22 | 23 | - `[0:20]`: `validator address` - This is the address of the validator contract that will verify the signature. 24 | - `[20:24]`: `validator signature length` - This is the length of the validator signature. It is a 4-byte value, allowing for a maximum signature length of 2^32. 25 | - `[24:24+n]`: `validator signature` - This is the actual signature produced by the validator. Its length is determined by the `validator signature length` field. 26 | - `[24+n:]`: `hook signature` - This is an optional signature for a hook function that can be executed after the main operation. 27 | 28 | ![validation flow](validation_flow.png) 29 | -------------------------------------------------------------------------------- /contracts/validator/SoulWalletDefaultValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {IValidator} from "@soulwallet-core/contracts/interface/IValidator.sol"; 5 | import {IOwnable} from "@soulwallet-core/contracts/interface/IOwnable.sol"; 6 | import {PackedUserOperation} from "@soulwallet-core/contracts/interface/IHook.sol"; 7 | import "@account-abstraction/contracts/core/Helpers.sol"; 8 | import "./libraries/ValidatorSigDecoder.sol"; 9 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 10 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 11 | import {Errors} from "../libraries/Errors.sol"; 12 | import {TypeConversion} from "../libraries/TypeConversion.sol"; 13 | import {WebAuthn} from "../libraries/WebAuthn.sol"; 14 | 15 | /** 16 | * @title SoulWalletDefaultValidator 17 | * @dev A contract that implements the IValidator interface for validating user operations and signatures. 18 | */ 19 | contract SoulWalletDefaultValidator is IValidator { 20 | // Magic value indicating a valid signature for ERC-1271 contracts 21 | // bytes4(keccak256("isValidSignature(bytes32,bytes)") 22 | bytes4 internal constant MAGICVALUE = 0x1626ba7e; 23 | // Constants indicating different invalid states 24 | bytes4 internal constant INVALID_ID = 0xffffffff; 25 | bytes4 internal constant INVALID_TIME_RANGE = 0xfffffffe; 26 | // Utility for Ethereum typed structured data hashing 27 | 28 | using MessageHashUtils for bytes32; 29 | using TypeConversion for address; 30 | 31 | function validateUserOp(PackedUserOperation calldata, bytes32 userOpHash, bytes calldata validatorSignature) 32 | external 33 | view 34 | override 35 | returns (uint256 validationData) 36 | { 37 | uint8 signatureType; 38 | bytes calldata signature; 39 | (signatureType, validationData, signature) = ValidatorSigDecoder.decodeValidatorSignature(validatorSignature); 40 | 41 | bytes32 hash = _packSignatureHash(userOpHash, signatureType, validationData); 42 | bytes32 recovered; 43 | bool success; 44 | (recovered, success) = recover(signatureType, hash, signature); 45 | if (!success) { 46 | return SIG_VALIDATION_FAILED; 47 | } 48 | bool ownerCheck = _isOwner(recovered); 49 | if (!ownerCheck) { 50 | return SIG_VALIDATION_FAILED; 51 | } 52 | return validationData; 53 | } 54 | 55 | function validateSignature(address, /*unused sender*/ bytes32 rawHash, bytes calldata validatorSignature) 56 | external 57 | view 58 | override 59 | returns (bytes4 magicValue) 60 | { 61 | uint8 signatureType; 62 | bytes calldata signature; 63 | uint256 validationData; 64 | (signatureType, validationData, signature) = ValidatorSigDecoder.decodeValidatorSignature(validatorSignature); 65 | 66 | bytes32 hash = _pack1271SignatureHash(rawHash, signatureType, validationData); 67 | bytes32 recovered; 68 | bool success; 69 | (recovered, success) = recover(signatureType, hash, signature); 70 | if (!success) { 71 | return INVALID_ID; 72 | } 73 | bool ownerCheck = _isOwner(recovered); 74 | if (!ownerCheck) { 75 | return INVALID_ID; 76 | } 77 | 78 | if (validationData > 0) { 79 | ValidationData memory _validationData = _parseValidationData(validationData); 80 | bool outOfTimeRange = 81 | (block.timestamp > _validationData.validUntil) || (block.timestamp < _validationData.validAfter); 82 | if (outOfTimeRange) { 83 | return INVALID_TIME_RANGE; 84 | } 85 | } 86 | return MAGICVALUE; 87 | } 88 | 89 | function _packSignatureHash(bytes32 hash, uint8 signatureType, uint256 validationData) 90 | internal 91 | pure 92 | returns (bytes32 packedHash) 93 | { 94 | if (signatureType == 0x0) { 95 | packedHash = hash.toEthSignedMessageHash(); 96 | } else if (signatureType == 0x1) { 97 | packedHash = keccak256(abi.encodePacked(hash, validationData)).toEthSignedMessageHash(); 98 | } else if (signatureType == 0x2) { 99 | // passkey sign doesn't need toEthSignedMessageHash 100 | packedHash = hash; 101 | } else if (signatureType == 0x3) { 102 | // passkey sign doesn't need toEthSignedMessageHash 103 | packedHash = keccak256(abi.encodePacked(hash, validationData)); 104 | } else { 105 | revert Errors.INVALID_SIGNTYPE(); 106 | } 107 | } 108 | 109 | function _pack1271SignatureHash(bytes32 hash, uint8 signatureType, uint256 validationData) 110 | internal 111 | pure 112 | returns (bytes32 packedHash) 113 | { 114 | if (signatureType == 0x0) { 115 | packedHash = hash; 116 | } else if (signatureType == 0x1) { 117 | packedHash = keccak256(abi.encodePacked(hash, validationData)); 118 | } else if (signatureType == 0x2) { 119 | packedHash = hash; 120 | } else if (signatureType == 0x3) { 121 | packedHash = keccak256(abi.encodePacked(hash, validationData)); 122 | } else { 123 | revert Errors.INVALID_SIGNTYPE(); 124 | } 125 | } 126 | 127 | function _isOwner(bytes32 recovered) private view returns (bool isOwner) { 128 | return IOwnable(address(msg.sender)).isOwner(recovered); 129 | } 130 | 131 | function recover(uint8 signatureType, bytes32 rawHash, bytes calldata rawSignature) 132 | internal 133 | view 134 | returns (bytes32 recovered, bool success) 135 | { 136 | if (signatureType == 0x0 || signatureType == 0x1) { 137 | //ecdas recover 138 | (address recoveredAddr, ECDSA.RecoverError error,) = ECDSA.tryRecover(rawHash, rawSignature); 139 | if (error != ECDSA.RecoverError.NoError) { 140 | success = false; 141 | } else { 142 | success = true; 143 | } 144 | recovered = recoveredAddr.toBytes32(); 145 | } else if (signatureType == 0x2 || signatureType == 0x3) { 146 | bytes32 publicKey = WebAuthn.recover(rawHash, rawSignature); 147 | if (publicKey == 0) { 148 | recovered = publicKey; 149 | success = false; 150 | } else { 151 | recovered = publicKey; 152 | success = true; 153 | } 154 | } else { 155 | revert Errors.INVALID_SIGNTYPE(); 156 | } 157 | } 158 | 159 | function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { 160 | return interfaceId == type(IValidator).interfaceId; 161 | } 162 | 163 | function Init(bytes calldata) external override {} 164 | 165 | function DeInit() external override {} 166 | } 167 | -------------------------------------------------------------------------------- /contracts/validator/validation_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elytro-eth/soul-wallet-contract/fc7cc084563ad1bda870df841b77caa9ee3a3661/contracts/validator/validation_flow.png -------------------------------------------------------------------------------- /doc/anonymous_guardian.md: -------------------------------------------------------------------------------- 1 | # Anonymous guardian with create2 2 | 3 | ## Why anonymous guardian 4 | 5 | If the user has a lot of money and doesn't want their friends to know how much they have unless there is an emergency, they may not want to store the list of guardians on the blockchain because others can easily see the list, which would result in poor privacy. 6 | 7 | ## Related background 8 | 9 | [Create deterministic contract address using create2](https://eips.ethereum.org/EIPS/eip-1014) 10 | 11 | [Erc1271 Standard Signature Validation Method for Contracts](https://eips.ethereum.org/EIPS/eip-1271) 12 | 13 | [Gnosis safe multi sig wallet](https://github.com/safe-global/safe-contracts/blob/c36bcab46578a442862d043e12a83fec41143dec/contracts/GnosisSafe.sol#L240) 14 | 15 | ## Goal achievement 16 | 17 | - The identities of the smart contract guardians are not publicly known. 18 | - Each guardian is unaware of the identities of the other guardians. 19 | - The guardians are only revealed when recovery is needed. 20 | 21 | 22 | ## Implementation 23 | 24 | ### Add guardian 25 | 26 | 1. The Guardian module is a separate smart contract. The main functionality is a multi-sig wallet which stores the list of guardians for social recovery of the soul wallet. 27 | 2. This contract exposes the ```function isValidSignature(bytes calldata _data, bytes calldata _signature)``` **(ERC1271 interface)** to the public for signature verification. 28 | 3. Inside the **isValidSignature** function, it accepts signatures which come from guardians and performs verification with a threshold, for example, a 3/5 or 4/7 multi-sig verification. [Gnosis code reference](https://github.com/safe-global/safe-contracts/blob/c36bcab46578a442862d043e12a83fec41143dec/contracts/GnosisSafe.sol#L240) 29 | 30 | 31 | 32 |

33 | 34 |

35 | 36 | ### Using guardian for social recovery 37 | 38 | 39 | 1. When users want to replace the signing key using social recovery, the user needs to provide the guardian's list to the security center. The security center will generate the init code with the user-provided guardian list and compute the guardian multi-sig address by using singleton factory. The security center will give back the calculated address for the user to verify if the address is equal to the guardian address setting on the soul wallet contract. 40 | 2. Users will compose a ```transferOwner``` operation and ask guardians to sign it. 41 | 3. Once the user collects enough signatures from guardians, the user will send this ```transferOwner``` operation on the chain. 42 | 4. The soul wallet contract has a module called **GuardianControl** which includes a function called ```_validateGuardiansSignatureCallData```. This function deploys the Guardian Multi-Sig contract on the fly and then verifies the signature of the guardians using the function ```isValidSignature(bytes calldata _data, bytes calldata _signature)```. 43 | 5. Inside the ```isValidSignature``` function in the Guardian Multi-Sig contract, the multi-sig check for the guardians is performed. 44 | 6. If all the checks are passed, the soulwallet smart contract will call the ```transferOwner``` function and replace the signing key with the new one. 45 | 46 |

47 | 48 |

49 | -------------------------------------------------------------------------------- /doc/contract_architecture.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Contract architecture 4 | 5 |

6 | 7 |

-------------------------------------------------------------------------------- /doc/images/add_guardian_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elytro-eth/soul-wallet-contract/fc7cc084563ad1bda870df841b77caa9ee3a3661/doc/images/add_guardian_diagram.png -------------------------------------------------------------------------------- /doc/images/contract_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elytro-eth/soul-wallet-contract/fc7cc084563ad1bda870df841b77caa9ee3a3661/doc/images/contract_architecture.png -------------------------------------------------------------------------------- /doc/images/recovery_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elytro-eth/soul-wallet-contract/fc7cc084563ad1bda870df841b77caa9ee3a3661/doc/images/recovery_diagram.png -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "contracts" 3 | test = 'test' 4 | out = "out" 5 | libs = ["lib"] 6 | 7 | solc = "0.8.24" 8 | optimizer = true 9 | optimizer_runs = 100000 10 | via_ir = true 11 | evm_version = "cancun" 12 | bytecode_hash = "none" 13 | cbor_metadata = false 14 | 15 | # RPC config in .env file 16 | [rpc_endpoints] 17 | goerli = "${GOERLI_RPC_URL}" 18 | arbitrumgoerli = "${ARBITRUM_GOERLI_RPC_URL}" 19 | optimismgoerli = "${OPTIMISM_GOERLI_RPC_URL}" 20 | etherum = "${ETHERUM_RPC_URL}" 21 | arbitrum = "${ARBITRUM_RPC_URL}" 22 | optimism = "${OPTIMISM_RPC_URL}" 23 | scrollsepolia = "${SCROLL_SEPOLIA_RPC_URL}" 24 | basesepolia = "${BASE_SEPOLIA_RPC_URL}" 25 | 26 | [etherscan] 27 | goerli = { key = "${GOERLI_SCAN_API_KEY}", chain = "goerli" } 28 | optimismgoerli = { key = "${OPTIMISM_GOERLI_SCAN_API_KEY}", chain = "optimism-goerli" } 29 | scrollsepolia = { key = "#{SCROLL_SEPOLIA_SCAN_API_KEY}", url = "https://api-sepolia.scrollscan.com/api/", chain = 534351 } 30 | basesepolia = { key = "#{BASE_SEPOLIA_SCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api/", chain = 84532 } 31 | 32 | 33 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 34 | -------------------------------------------------------------------------------- /lib/solenv/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | env: 10 | FOUNDRY_PROFILE: ci 11 | 12 | jobs: 13 | check: 14 | strategy: 15 | fail-fast: true 16 | 17 | name: Foundry project 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | submodules: recursive 23 | 24 | - name: Install Foundry 25 | uses: foundry-rs/foundry-toolchain@v1 26 | with: 27 | version: nightly 28 | 29 | - name: Run Forge build 30 | run: | 31 | forge --version 32 | forge build --sizes 33 | id: build 34 | 35 | - name: Run Forge tests 36 | run: | 37 | forge test -vvv 38 | id: test 39 | -------------------------------------------------------------------------------- /lib/solenv/.gitignore: -------------------------------------------------------------------------------- 1 | cache/ 2 | out/ 3 | -------------------------------------------------------------------------------- /lib/solenv/.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/solidity-stringutils"] 5 | path = lib/solidity-stringutils 6 | url = https://github.com/proximacapital/solidity-stringutils 7 | -------------------------------------------------------------------------------- /lib/solenv/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } -------------------------------------------------------------------------------- /lib/solenv/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Memester 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/solenv/README.md: -------------------------------------------------------------------------------- 1 | #

solenv

2 | 3 | **Load .env files in Solidity scripts/tests** 4 | 5 | ![Github Actions](https://github.com/memester-xyz/solenv/workflows/test/badge.svg) 6 | 7 | # ⚠️ Note ⚠️ 8 | [Foundry recently shipped default dotenv parsing](https://github.com/foundry-rs/foundry/pull/2587). It's in active development but soon the solenv library will no longer be needed. Yay for upstreaming! 9 | 10 | ## Installation 11 | 12 | ``` 13 | forge install memester-xyz/solenv 14 | ``` 15 | 16 | ## Usage 17 | 18 | Firstly, it's very important that you do not commit your `.env` file. It should go without saying but make sure to add it to your `.gitignore` file! This repo has committed the `.env` and `.env.test` files only for examples and tests. 19 | 20 | 1. Add this import to your script or test: 21 | ```solidity 22 | import {Solenv} from "solenv/Solenv.sol"; 23 | ``` 24 | 25 | 2. Call `.config()` somewhere. It defaults to using `.env` in your project root, but you can pass another string as a parameter to load another file in instead. 26 | ```solidity 27 | // Inside a test 28 | function setUp() public { 29 | Solenv.config(); 30 | } 31 | 32 | // Inside a script, load a file with a different name 33 | function run() public { 34 | Solenv.config(".env.prod"); 35 | 36 | // Continue with your script... 37 | } 38 | ``` 39 | 40 | 3. You can then use the [standard "env" cheatcodes](https://book.getfoundry.sh/cheatcodes/external.html) in order to read your variables. e.g. `envString`, `envUint`, `envBool`, etc. 41 | ```solidity 42 | string memory apiKey = vm.envString("API_KEY"); 43 | uint256 retries = vm.envUint("RETRIES"); 44 | bool ouputLogs = vm.envBool("OUTPUT_LOGS"); 45 | ``` 46 | 47 | 4. You must enable [ffi](https://book.getfoundry.sh/cheatcodes/ffi.html) in order to use the library. You can either pass the `--ffi` flag to any forge commands you run (e.g. `forge script Script --ffi`), or you can add `ffi = true` to your `foundry.toml` file. 48 | 49 | ### Notes 50 | 51 | - Comments start with `#` and must be on a newline 52 | - If you set a key twice, the last value in the file is used 53 | - It assumes you are running on a UNIX based machine with `sh`, `cast` and `xxd` installed. 54 | 55 | ## Example 56 | 57 | We have example usage for both [tests](./test/Solenv.t.sol) and [scripts](./script/Solenv.s.sol). 58 | 59 | To see the script in action, you can run: 60 | ``` 61 | forge script SolenvScript 62 | ``` 63 | 64 | ## Contributing 65 | 66 | Clone this repo and run: 67 | 68 | ``` 69 | forge install 70 | ``` 71 | 72 | Make sure all tests pass, add new ones if needed: 73 | 74 | ``` 75 | forge test 76 | ``` 77 | 78 | ## Why? 79 | 80 | [Forge scripting](https://book.getfoundry.sh/tutorials/solidity-scripting.html) is becoming more popular. With solenv your scripts are even more powerful and natural to work with. 81 | 82 | ## Development 83 | 84 | This project uses [Foundry](https://getfoundry.sh). See the [book](https://book.getfoundry.sh/getting-started/installation.html) for instructions on how to install and use Foundry. 85 | -------------------------------------------------------------------------------- /lib/solenv/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | ffi = true 6 | verbosity = 3 7 | 8 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 9 | -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | check: 6 | name: Foundry project 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | submodules: recursive 12 | 13 | - name: Install Foundry 14 | uses: onbjerg/foundry-toolchain@v1 15 | with: 16 | version: nightly 17 | 18 | - name: Install dependencies 19 | run: forge install 20 | - name: Run tests 21 | run: forge test -vvv 22 | - name: Build Test with older solc versions 23 | run: | 24 | forge build --contracts src/Test.sol --use solc:0.8.0 25 | forge build --contracts src/Test.sol --use solc:0.7.0 26 | forge build --contracts src/Test.sol --use solc:0.6.0 27 | -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/.gitignore: -------------------------------------------------------------------------------- 1 | cache/ 2 | out/ 3 | .vscode 4 | .idea -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/ds-test"] 2 | path = lib/ds-test 3 | url = https://github.com/dapphub/ds-test 4 | -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Brock Elmore 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE O THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE.R 26 | -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/lib/ds-test/.gitignore: -------------------------------------------------------------------------------- 1 | /.dapple 2 | /build 3 | /out 4 | -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/lib/ds-test/Makefile: -------------------------------------------------------------------------------- 1 | all:; dapp build 2 | 3 | test: 4 | -dapp --use solc:0.4.23 build 5 | -dapp --use solc:0.4.26 build 6 | -dapp --use solc:0.5.17 build 7 | -dapp --use solc:0.6.12 build 8 | -dapp --use solc:0.7.5 build 9 | 10 | demo: 11 | DAPP_SRC=demo dapp --use solc:0.7.5 build 12 | -hevm dapp-test --verbose 3 13 | 14 | .PHONY: test demo 15 | -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/lib/ds-test/default.nix: -------------------------------------------------------------------------------- 1 | { solidityPackage, dappsys }: solidityPackage { 2 | name = "ds-test"; 3 | src = ./src; 4 | } 5 | -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/lib/ds-test/demo/demo.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | import "../src/test.sol"; 5 | 6 | contract DemoTest is DSTest { 7 | function test_this() public pure { 8 | require(true); 9 | } 10 | function test_logs() public { 11 | emit log("-- log(string)"); 12 | emit log("a string"); 13 | 14 | emit log("-- log_named_uint(string, uint)"); 15 | emit log_named_uint("uint", 512); 16 | 17 | emit log("-- log_named_int(string, int)"); 18 | emit log_named_int("int", -512); 19 | 20 | emit log("-- log_named_address(string, address)"); 21 | emit log_named_address("address", address(this)); 22 | 23 | emit log("-- log_named_bytes32(string, bytes32)"); 24 | emit log_named_bytes32("bytes32", "a string"); 25 | 26 | emit log("-- log_named_bytes(string, bytes)"); 27 | emit log_named_bytes("bytes", hex"cafefe"); 28 | 29 | emit log("-- log_named_string(string, string)"); 30 | emit log_named_string("string", "a string"); 31 | 32 | emit log("-- log_named_decimal_uint(string, uint, uint)"); 33 | emit log_named_decimal_uint("decimal uint", 1.0e18, 18); 34 | 35 | emit log("-- log_named_decimal_int(string, int, uint)"); 36 | emit log_named_decimal_int("decimal int", -1.0e18, 18); 37 | } 38 | event log_old_named_uint(bytes32,uint); 39 | function test_old_logs() public { 40 | emit log_old_named_uint("key", 500); 41 | emit log_named_bytes32("bkey", "val"); 42 | } 43 | function test_trace() public view { 44 | this.echo("string 1", "string 2"); 45 | } 46 | function test_multiline() public { 47 | emit log("a multiline\\nstring"); 48 | emit log("a multiline string"); 49 | emit log_bytes("a string"); 50 | emit log_bytes("a multiline\nstring"); 51 | emit log_bytes("a multiline\\nstring"); 52 | emit logs(hex"0000"); 53 | emit log_named_bytes("0x0000", hex"0000"); 54 | emit logs(hex"ff"); 55 | } 56 | function echo(string memory s1, string memory s2) public pure 57 | returns (string memory, string memory) 58 | { 59 | return (s1, s2); 60 | } 61 | 62 | function prove_this(uint x) public { 63 | emit log_named_uint("sym x", x); 64 | assertGt(x + 1, 0); 65 | } 66 | 67 | function test_logn() public { 68 | assembly { 69 | log0(0x01, 0x02) 70 | log1(0x01, 0x02, 0x03) 71 | log2(0x01, 0x02, 0x03, 0x04) 72 | log3(0x01, 0x02, 0x03, 0x04, 0x05) 73 | } 74 | } 75 | 76 | event MyEvent(uint, uint indexed, uint, uint indexed); 77 | function test_events() public { 78 | emit MyEvent(1, 2, 3, 4); 79 | } 80 | 81 | function test_asserts() public { 82 | string memory err = "this test has failed!"; 83 | emit log("## assertTrue(bool)\n"); 84 | assertTrue(false); 85 | emit log("\n"); 86 | assertTrue(false, err); 87 | 88 | emit log("\n## assertEq(address,address)\n"); 89 | assertEq(address(this), msg.sender); 90 | emit log("\n"); 91 | assertEq(address(this), msg.sender, err); 92 | 93 | emit log("\n## assertEq32(bytes32,bytes32)\n"); 94 | assertEq32("bytes 1", "bytes 2"); 95 | emit log("\n"); 96 | assertEq32("bytes 1", "bytes 2", err); 97 | 98 | emit log("\n## assertEq(bytes32,bytes32)\n"); 99 | assertEq32("bytes 1", "bytes 2"); 100 | emit log("\n"); 101 | assertEq32("bytes 1", "bytes 2", err); 102 | 103 | emit log("\n## assertEq(uint,uint)\n"); 104 | assertEq(uint(0), 1); 105 | emit log("\n"); 106 | assertEq(uint(0), 1, err); 107 | 108 | emit log("\n## assertEq(int,int)\n"); 109 | assertEq(-1, -2); 110 | emit log("\n"); 111 | assertEq(-1, -2, err); 112 | 113 | emit log("\n## assertEqDecimal(int,int,uint)\n"); 114 | assertEqDecimal(-1.0e18, -1.1e18, 18); 115 | emit log("\n"); 116 | assertEqDecimal(-1.0e18, -1.1e18, 18, err); 117 | 118 | emit log("\n## assertEqDecimal(uint,uint,uint)\n"); 119 | assertEqDecimal(uint(1.0e18), 1.1e18, 18); 120 | emit log("\n"); 121 | assertEqDecimal(uint(1.0e18), 1.1e18, 18, err); 122 | 123 | emit log("\n## assertGt(uint,uint)\n"); 124 | assertGt(uint(0), 0); 125 | emit log("\n"); 126 | assertGt(uint(0), 0, err); 127 | 128 | emit log("\n## assertGt(int,int)\n"); 129 | assertGt(-1, -1); 130 | emit log("\n"); 131 | assertGt(-1, -1, err); 132 | 133 | emit log("\n## assertGtDecimal(int,int,uint)\n"); 134 | assertGtDecimal(-2.0e18, -1.1e18, 18); 135 | emit log("\n"); 136 | assertGtDecimal(-2.0e18, -1.1e18, 18, err); 137 | 138 | emit log("\n## assertGtDecimal(uint,uint,uint)\n"); 139 | assertGtDecimal(uint(1.0e18), 1.1e18, 18); 140 | emit log("\n"); 141 | assertGtDecimal(uint(1.0e18), 1.1e18, 18, err); 142 | 143 | emit log("\n## assertGe(uint,uint)\n"); 144 | assertGe(uint(0), 1); 145 | emit log("\n"); 146 | assertGe(uint(0), 1, err); 147 | 148 | emit log("\n## assertGe(int,int)\n"); 149 | assertGe(-1, 0); 150 | emit log("\n"); 151 | assertGe(-1, 0, err); 152 | 153 | emit log("\n## assertGeDecimal(int,int,uint)\n"); 154 | assertGeDecimal(-2.0e18, -1.1e18, 18); 155 | emit log("\n"); 156 | assertGeDecimal(-2.0e18, -1.1e18, 18, err); 157 | 158 | emit log("\n## assertGeDecimal(uint,uint,uint)\n"); 159 | assertGeDecimal(uint(1.0e18), 1.1e18, 18); 160 | emit log("\n"); 161 | assertGeDecimal(uint(1.0e18), 1.1e18, 18, err); 162 | 163 | emit log("\n## assertLt(uint,uint)\n"); 164 | assertLt(uint(0), 0); 165 | emit log("\n"); 166 | assertLt(uint(0), 0, err); 167 | 168 | emit log("\n## assertLt(int,int)\n"); 169 | assertLt(-1, -1); 170 | emit log("\n"); 171 | assertLt(-1, -1, err); 172 | 173 | emit log("\n## assertLtDecimal(int,int,uint)\n"); 174 | assertLtDecimal(-1.0e18, -1.1e18, 18); 175 | emit log("\n"); 176 | assertLtDecimal(-1.0e18, -1.1e18, 18, err); 177 | 178 | emit log("\n## assertLtDecimal(uint,uint,uint)\n"); 179 | assertLtDecimal(uint(2.0e18), 1.1e18, 18); 180 | emit log("\n"); 181 | assertLtDecimal(uint(2.0e18), 1.1e18, 18, err); 182 | 183 | emit log("\n## assertLe(uint,uint)\n"); 184 | assertLe(uint(1), 0); 185 | emit log("\n"); 186 | assertLe(uint(1), 0, err); 187 | 188 | emit log("\n## assertLe(int,int)\n"); 189 | assertLe(0, -1); 190 | emit log("\n"); 191 | assertLe(0, -1, err); 192 | 193 | emit log("\n## assertLeDecimal(int,int,uint)\n"); 194 | assertLeDecimal(-1.0e18, -1.1e18, 18); 195 | emit log("\n"); 196 | assertLeDecimal(-1.0e18, -1.1e18, 18, err); 197 | 198 | emit log("\n## assertLeDecimal(uint,uint,uint)\n"); 199 | assertLeDecimal(uint(2.0e18), 1.1e18, 18); 200 | emit log("\n"); 201 | assertLeDecimal(uint(2.0e18), 1.1e18, 18, err); 202 | 203 | emit log("\n## assertEq(string,string)\n"); 204 | string memory s1 = "string 1"; 205 | string memory s2 = "string 2"; 206 | assertEq(s1, s2); 207 | emit log("\n"); 208 | assertEq(s1, s2, err); 209 | 210 | emit log("\n## assertEq0(bytes,bytes)\n"); 211 | assertEq0(hex"abcdef01", hex"abcdef02"); 212 | emit log("\n"); 213 | assertEq0(hex"abcdef01", hex"abcdef02", err); 214 | } 215 | } 216 | 217 | contract DemoTestWithSetUp { 218 | function setUp() public { 219 | } 220 | function test_pass() public pure { 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/src/Script.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.6.0 <0.9.0; 3 | 4 | import "./Vm.sol"; 5 | import "./console.sol"; 6 | import "./console2.sol"; 7 | 8 | abstract contract Script { 9 | bool public IS_SCRIPT = true; 10 | address constant private VM_ADDRESS = 11 | address(bytes20(uint160(uint256(keccak256('hevm cheat code'))))); 12 | 13 | Vm public constant vm = Vm(VM_ADDRESS); 14 | } 15 | -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/src/Vm.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.6.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | interface Vm { 6 | // Sets block.timestamp (newTimestamp) 7 | function warp(uint256) external; 8 | // Sets block.height (newHeight) 9 | function roll(uint256) external; 10 | // Sets block.basefee (newBasefee) 11 | function fee(uint256) external; 12 | // Sets block.chainid 13 | function chainId(uint256) external; 14 | // Loads a storage slot from an address (who, slot) 15 | function load(address,bytes32) external returns (bytes32); 16 | // Stores a value to an address' storage slot, (who, slot, value) 17 | function store(address,bytes32,bytes32) external; 18 | // Signs data, (privateKey, digest) => (v, r, s) 19 | function sign(uint256,bytes32) external returns (uint8,bytes32,bytes32); 20 | // Gets the address for a given private key, (privateKey) => (address) 21 | function addr(uint256) external returns (address); 22 | // Gets the nonce of an account 23 | function getNonce(address) external returns (uint64); 24 | // Sets the nonce of an account; must be higher than the current nonce of the account 25 | function setNonce(address, uint64) external; 26 | // Performs a foreign function call via the terminal, (stringInputs) => (result) 27 | function ffi(string[] calldata) external returns (bytes memory); 28 | // Sets environment variables, (name, value) 29 | function setEnv(string calldata, string calldata) external; 30 | // Reads environment variables, (name) => (value) 31 | function envBool(string calldata) external returns (bool); 32 | function envUint(string calldata) external returns (uint256); 33 | function envInt(string calldata) external returns (int256); 34 | function envAddress(string calldata) external returns (address); 35 | function envBytes32(string calldata) external returns (bytes32); 36 | function envString(string calldata) external returns (string memory); 37 | function envBytes(string calldata) external returns (bytes memory); 38 | // Reads environment variables as arrays, (name, delim) => (value[]) 39 | function envBool(string calldata, string calldata) external returns (bool[] memory); 40 | function envUint(string calldata, string calldata) external returns (uint256[] memory); 41 | function envInt(string calldata, string calldata) external returns (int256[] memory); 42 | function envAddress(string calldata, string calldata) external returns (address[] memory); 43 | function envBytes32(string calldata, string calldata) external returns (bytes32[] memory); 44 | function envString(string calldata, string calldata) external returns (string[] memory); 45 | function envBytes(string calldata, string calldata) external returns (bytes[] memory); 46 | // Sets the *next* call's msg.sender to be the input address 47 | function prank(address) external; 48 | // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called 49 | function startPrank(address) external; 50 | // Sets the *next* call's msg.sender to be the input address, and the tx.origin to be the second input 51 | function prank(address,address) external; 52 | // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called, and the tx.origin to be the second input 53 | function startPrank(address,address) external; 54 | // Resets subsequent calls' msg.sender to be `address(this)` 55 | function stopPrank() external; 56 | // Sets an address' balance, (who, newBalance) 57 | function deal(address, uint256) external; 58 | // Sets an address' code, (who, newCode) 59 | function etch(address, bytes calldata) external; 60 | // Expects an error on next call 61 | function expectRevert(bytes calldata) external; 62 | function expectRevert(bytes4) external; 63 | function expectRevert() external; 64 | // Records all storage reads and writes 65 | function record() external; 66 | // Gets all accessed reads and write slot from a recording session, for a given address 67 | function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes); 68 | // Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData). 69 | // Call this function, then emit an event, then call a function. Internally after the call, we check if 70 | // logs were emitted in the expected order with the expected topics and data (as specified by the booleans) 71 | function expectEmit(bool,bool,bool,bool) external; 72 | function expectEmit(bool,bool,bool,bool,address) external; 73 | // Mocks a call to an address, returning specified data. 74 | // Calldata can either be strict or a partial match, e.g. if you only 75 | // pass a Solidity selector to the expected calldata, then the entire Solidity 76 | // function will be mocked. 77 | function mockCall(address,bytes calldata,bytes calldata) external; 78 | // Mocks a call to an address with a specific msg.value, returning specified data. 79 | // Calldata match takes precedence over msg.value in case of ambiguity. 80 | function mockCall(address,uint256,bytes calldata,bytes calldata) external; 81 | // Clears all mocked calls 82 | function clearMockedCalls() external; 83 | // Expects a call to an address with the specified calldata. 84 | // Calldata can either be a strict or a partial match 85 | function expectCall(address,bytes calldata) external; 86 | // Expects a call to an address with the specified msg.value and calldata 87 | function expectCall(address,uint256,bytes calldata) external; 88 | // Gets the code from an artifact file. Takes in the relative path to the json file 89 | function getCode(string calldata) external returns (bytes memory); 90 | // Labels an address in call traces 91 | function label(address, string calldata) external; 92 | // If the condition is false, discard this run's fuzz inputs and generate new ones 93 | function assume(bool) external; 94 | // Sets block.coinbase (who) 95 | function coinbase(address) external; 96 | // Using the address that calls the test contract, has the next call (at this call depth only) create a transaction that can later be signed and sent onchain 97 | function broadcast() external; 98 | // Has the next call (at this call depth only) create a transaction with the address provided as the sender that can later be signed and sent onchain 99 | function broadcast(address) external; 100 | // Using the address that calls the test contract, has all subsequent calls (at this call depth only) create transactions that can later be signed and sent onchain 101 | function startBroadcast() external; 102 | // Has all subsequent calls (at this call depth only) create transactions that can later be signed and sent onchain 103 | function startBroadcast(address) external; 104 | // Stops collecting onchain transactions 105 | function stopBroadcast() external; 106 | // Reads the entire content of file to string, (path) => (data) 107 | function readFile(string calldata) external returns (string memory); 108 | // Reads next line of file to string, (path) => (line) 109 | function readLine(string calldata) external returns (string memory); 110 | // Writes data to file, creating a file if it does not exist, and entirely replacing its contents if it does. 111 | // (path, data) => () 112 | function writeFile(string calldata, string calldata) external; 113 | // Writes line to file, creating a file if it does not exist. 114 | // (path, data) => () 115 | function writeLine(string calldata, string calldata) external; 116 | // Closes file for reading, resetting the offset and allowing to read it from beginning with readLine. 117 | // (path) => () 118 | function closeFile(string calldata) external; 119 | // Removes file. This cheatcode will revert in the following situations, but is not limited to just these cases: 120 | // - Path points to a directory. 121 | // - The file doesn't exist. 122 | // - The user lacks permissions to remove the file. 123 | // (path) => () 124 | function removeFile(string calldata) external; 125 | } 126 | -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/src/test/StdCheats.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.7.0 <0.9.0; 3 | 4 | import "../Test.sol"; 5 | 6 | contract StdCheatsTest is Test { 7 | Bar test; 8 | 9 | function setUp() public { 10 | test = new Bar(); 11 | } 12 | 13 | function testSkip() public { 14 | vm.warp(100); 15 | skip(25); 16 | assertEq(block.timestamp, 125); 17 | } 18 | 19 | function testRewind() public { 20 | vm.warp(100); 21 | rewind(25); 22 | assertEq(block.timestamp, 75); 23 | } 24 | 25 | function testHoax() public { 26 | hoax(address(1337)); 27 | test.bar{value: 100}(address(1337)); 28 | } 29 | 30 | function testHoaxOrigin() public { 31 | hoax(address(1337), address(1337)); 32 | test.origin{value: 100}(address(1337)); 33 | } 34 | 35 | function testHoaxDifferentAddresses() public { 36 | hoax(address(1337), address(7331)); 37 | test.origin{value: 100}(address(1337), address(7331)); 38 | } 39 | 40 | function testStartHoax() public { 41 | startHoax(address(1337)); 42 | test.bar{value: 100}(address(1337)); 43 | test.bar{value: 100}(address(1337)); 44 | vm.stopPrank(); 45 | test.bar(address(this)); 46 | } 47 | 48 | function testStartHoaxOrigin() public { 49 | startHoax(address(1337), address(1337)); 50 | test.origin{value: 100}(address(1337)); 51 | test.origin{value: 100}(address(1337)); 52 | vm.stopPrank(); 53 | test.bar(address(this)); 54 | } 55 | 56 | function testChangePrank() public { 57 | vm.startPrank(address(1337)); 58 | test.bar(address(1337)); 59 | changePrank(address(0xdead)); 60 | test.bar(address(0xdead)); 61 | changePrank(address(1337)); 62 | test.bar(address(1337)); 63 | vm.stopPrank(); 64 | } 65 | 66 | function testDeal() public { 67 | deal(address(this), 1 ether); 68 | assertEq(address(this).balance, 1 ether); 69 | } 70 | 71 | function testDealToken() public { 72 | Bar barToken = new Bar(); 73 | address bar = address(barToken); 74 | deal(bar, address(this), 10000e18); 75 | assertEq(barToken.balanceOf(address(this)), 10000e18); 76 | } 77 | 78 | function testDealTokenAdjustTS() public { 79 | Bar barToken = new Bar(); 80 | address bar = address(barToken); 81 | deal(bar, address(this), 10000e18, true); 82 | assertEq(barToken.balanceOf(address(this)), 10000e18); 83 | assertEq(barToken.totalSupply(), 20000e18); 84 | deal(bar, address(this), 0, true); 85 | assertEq(barToken.balanceOf(address(this)), 0); 86 | assertEq(barToken.totalSupply(), 10000e18); 87 | } 88 | 89 | function testBound() public { 90 | assertEq(bound(5, 0, 4), 0); 91 | assertEq(bound(0, 69, 69), 69); 92 | assertEq(bound(0, 68, 69), 68); 93 | assertEq(bound(10, 150, 190), 160); 94 | assertEq(bound(300, 2800, 3200), 3100); 95 | assertEq(bound(9999, 1337, 6666), 6006); 96 | } 97 | 98 | function testCannotBoundMaxLessThanMin() public { 99 | vm.expectRevert(bytes("Test bound(uint256,uint256,uint256): Max is less than min.")); 100 | bound(5, 100, 10); 101 | } 102 | 103 | function testBound( 104 | uint256 num, 105 | uint256 min, 106 | uint256 max 107 | ) public { 108 | if (min > max) (min, max) = (max, min); 109 | 110 | uint256 bounded = bound(num, min, max); 111 | 112 | assertGe(bounded, min); 113 | assertLe(bounded, max); 114 | } 115 | 116 | function testBoundUint256Max() public { 117 | assertEq(bound(0, type(uint256).max - 1, type(uint256).max), type(uint256).max - 1); 118 | assertEq(bound(1, type(uint256).max - 1, type(uint256).max), type(uint256).max); 119 | } 120 | 121 | function testCannotBoundMaxLessThanMin( 122 | uint256 num, 123 | uint256 min, 124 | uint256 max 125 | ) public { 126 | vm.assume(min > max); 127 | vm.expectRevert(bytes("Test bound(uint256,uint256,uint256): Max is less than min.")); 128 | bound(num, min, max); 129 | } 130 | 131 | function testDeployCode() public { 132 | address deployed = deployCode("StdCheats.t.sol:StdCheatsTest", bytes("")); 133 | assertEq(string(getCode(deployed)), string(getCode(address(this)))); 134 | } 135 | 136 | function testDeployCodeNoArgs() public { 137 | address deployed = deployCode("StdCheats.t.sol:StdCheatsTest"); 138 | assertEq(string(getCode(deployed)), string(getCode(address(this)))); 139 | } 140 | 141 | function testDeployCodeFail() public { 142 | vm.expectRevert(bytes("Test deployCode(string): Deployment failed.")); 143 | this.deployCode("StdCheats.t.sol:RevertingContract"); 144 | } 145 | 146 | function getCode(address who) internal view returns (bytes memory o_code) { 147 | /// @solidity memory-safe-assembly 148 | assembly { 149 | // retrieve the size of the code, this needs assembly 150 | let size := extcodesize(who) 151 | // allocate output byte array - this could also be done without assembly 152 | // by using o_code = new bytes(size) 153 | o_code := mload(0x40) 154 | // new "memory end" including padding 155 | mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) 156 | // store length in memory 157 | mstore(o_code, size) 158 | // actually retrieve the code, this needs assembly 159 | extcodecopy(who, add(o_code, 0x20), 0, size) 160 | } 161 | } 162 | } 163 | 164 | contract Bar { 165 | constructor() { 166 | /// `DEAL` STDCHEAT 167 | totalSupply = 10000e18; 168 | balanceOf[address(this)] = totalSupply; 169 | } 170 | 171 | /// `HOAX` STDCHEATS 172 | function bar(address expectedSender) public payable { 173 | require(msg.sender == expectedSender, "!prank"); 174 | } 175 | function origin(address expectedSender) public payable { 176 | require(msg.sender == expectedSender, "!prank"); 177 | require(tx.origin == expectedSender, "!prank"); 178 | } 179 | function origin(address expectedSender, address expectedOrigin) public payable { 180 | require(msg.sender == expectedSender, "!prank"); 181 | require(tx.origin == expectedOrigin, "!prank"); 182 | } 183 | 184 | /// `DEAL` STDCHEAT 185 | mapping (address => uint256) public balanceOf; 186 | uint256 public totalSupply; 187 | } 188 | 189 | contract RevertingContract { 190 | constructor() { 191 | revert(); 192 | } 193 | } -------------------------------------------------------------------------------- /lib/solenv/lib/forge-std/src/test/StdError.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.10 <0.9.0; 3 | 4 | import "../Test.sol"; 5 | 6 | contract StdErrorsTest is Test { 7 | ErrorsTest test; 8 | 9 | function setUp() public { 10 | test = new ErrorsTest(); 11 | } 12 | 13 | function testExpectAssertion() public { 14 | vm.expectRevert(stdError.assertionError); 15 | test.assertionError(); 16 | } 17 | 18 | function testExpectArithmetic() public { 19 | vm.expectRevert(stdError.arithmeticError); 20 | test.arithmeticError(10); 21 | } 22 | 23 | function testExpectDiv() public { 24 | vm.expectRevert(stdError.divisionError); 25 | test.divError(0); 26 | } 27 | 28 | function testExpectMod() public { 29 | vm.expectRevert(stdError.divisionError); 30 | test.modError(0); 31 | } 32 | 33 | function testExpectEnum() public { 34 | vm.expectRevert(stdError.enumConversionError); 35 | test.enumConversion(1); 36 | } 37 | 38 | function testExpectEncodeStg() public { 39 | vm.expectRevert(stdError.encodeStorageError); 40 | test.encodeStgError(); 41 | } 42 | 43 | function testExpectPop() public { 44 | vm.expectRevert(stdError.popError); 45 | test.pop(); 46 | } 47 | 48 | function testExpectOOB() public { 49 | vm.expectRevert(stdError.indexOOBError); 50 | test.indexOOBError(1); 51 | } 52 | 53 | function testExpectMem() public { 54 | vm.expectRevert(stdError.memOverflowError); 55 | test.mem(); 56 | } 57 | 58 | function testExpectIntern() public { 59 | vm.expectRevert(stdError.zeroVarError); 60 | test.intern(); 61 | } 62 | 63 | function testExpectLowLvl() public { 64 | vm.expectRevert(stdError.lowLevelError); 65 | test.someArr(0); 66 | } 67 | } 68 | 69 | contract ErrorsTest { 70 | enum T { 71 | T1 72 | } 73 | 74 | uint256[] public someArr; 75 | bytes someBytes; 76 | 77 | function assertionError() public pure { 78 | assert(false); 79 | } 80 | 81 | function arithmeticError(uint256 a) public pure { 82 | a -= 100; 83 | } 84 | 85 | function divError(uint256 a) public pure { 86 | 100 / a; 87 | } 88 | 89 | function modError(uint256 a) public pure { 90 | 100 % a; 91 | } 92 | 93 | function enumConversion(uint256 a) public pure { 94 | T(a); 95 | } 96 | 97 | function encodeStgError() public { 98 | /// @solidity memory-safe-assembly 99 | assembly { 100 | sstore(someBytes.slot, 1) 101 | } 102 | keccak256(someBytes); 103 | } 104 | 105 | function pop() public { 106 | someArr.pop(); 107 | } 108 | 109 | function indexOOBError(uint256 a) public pure { 110 | uint256[] memory t = new uint256[](0); 111 | t[a]; 112 | } 113 | 114 | function mem() public pure { 115 | uint256 l = 2**256 / 32; 116 | new uint256[](l); 117 | } 118 | 119 | function intern() public returns (uint256) { 120 | function(uint256) internal returns (uint256) x; 121 | x(2); 122 | return 7; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | on: "push" 3 | jobs: 4 | tests: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2.3.4 8 | - uses: cachix/install-nix-action@v13 9 | - name: Install dapp 10 | run: nix-env -iA dapp -f $(curl -sS https://api.github.com/repos/dapphub/dapptools/releases/latest | jq -r .tarball_url) 11 | - name: Fetch submodules 12 | run: git submodule update --init 13 | - name: Run tests 14 | run: make test 15 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/.gitignore: -------------------------------------------------------------------------------- 1 | **/chain_db//out 2 | build 3 | out 4 | 5 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/ds-test"] 2 | path = lib/ds-test 3 | url = https://github.com/dapphub/ds-test 4 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/Makefile: -------------------------------------------------------------------------------- 1 | all :; dapp build 2 | clean :; dapp clean 3 | test :; dapp test 4 | deploy :; dapp create SolidityStringutils 5 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/README: -------------------------------------------------------------------------------- 1 | Basic string utilities for Solidity, optimized for low gas usage. 2 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/dappfile: -------------------------------------------------------------------------------- 1 | version: 2.0.0 2 | tags: [] 3 | layout: 4 | sol_sources: . 5 | build_dir: build 6 | dependencies: {} 7 | ignore: [] 8 | name: ethereum-stringutils 9 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/lib/ds-test/.gitignore: -------------------------------------------------------------------------------- 1 | /.dapple 2 | /build 3 | /out 4 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/lib/ds-test/Makefile: -------------------------------------------------------------------------------- 1 | all:; dapp build 2 | 3 | test: 4 | -dapp --use solc:0.4.23 build 5 | -dapp --use solc:0.4.26 build 6 | -dapp --use solc:0.5.17 build 7 | -dapp --use solc:0.6.12 build 8 | -dapp --use solc:0.7.5 build 9 | 10 | demo: 11 | DAPP_SRC=demo dapp --use solc:0.7.5 build 12 | -hevm dapp-test --verbose 3 13 | 14 | .PHONY: test demo 15 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/lib/ds-test/default.nix: -------------------------------------------------------------------------------- 1 | { solidityPackage, dappsys }: solidityPackage { 2 | name = "ds-test"; 3 | src = ./src; 4 | } 5 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/lib/ds-test/demo/demo.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.4.23; 3 | 4 | import "../src/test.sol"; 5 | 6 | contract DemoTest is DSTest { 7 | function test_this() public pure { 8 | require(true); 9 | } 10 | function test_logs() public { 11 | emit log("-- log(string)"); 12 | emit log("a string"); 13 | 14 | emit log("-- log_named_uint(string, uint)"); 15 | log_named_uint("uint", 512); 16 | 17 | emit log("-- log_named_int(string, int)"); 18 | log_named_int("int", -512); 19 | 20 | emit log("-- log_named_address(string, address)"); 21 | log_named_address("address", address(this)); 22 | 23 | emit log("-- log_named_bytes32(string, bytes32)"); 24 | log_named_bytes32("bytes32", "a string"); 25 | 26 | emit log("-- log_named_bytes(string, bytes)"); 27 | log_named_bytes("bytes", hex"cafefe"); 28 | 29 | emit log("-- log_named_string(string, string)"); 30 | log_named_string("string", "a string"); 31 | 32 | emit log("-- log_named_decimal_uint(string, uint, uint)"); 33 | log_named_decimal_uint("decimal uint", 1.0e18, 18); 34 | 35 | emit log("-- log_named_decimal_int(string, int, uint)"); 36 | log_named_decimal_int("decimal int", -1.0e18, 18); 37 | } 38 | event log_old_named_uint(bytes32,uint); 39 | function test_old_logs() public { 40 | log_old_named_uint("key", 500); 41 | log_named_bytes32("bkey", "val"); 42 | } 43 | function test_trace() public view { 44 | this.echo("string 1", "string 2"); 45 | } 46 | function test_multiline() public { 47 | emit log("a multiline\\n" "string"); 48 | emit log("a multiline " "string"); 49 | log_bytes("a string"); 50 | log_bytes("a multiline\n" "string"); 51 | log_bytes("a multiline\\n" "string"); 52 | emit log(unicode"Ώ"); 53 | logs(hex"0000"); 54 | log_named_bytes("0x0000", hex"0000"); 55 | logs(hex"ff"); 56 | } 57 | function echo(string memory s1, string memory s2) public pure 58 | returns (string memory, string memory) 59 | { 60 | return (s1, s2); 61 | } 62 | 63 | function prove_this(uint x) public { 64 | log_named_uint("sym x", x); 65 | assertGt(x + 1, 0); 66 | } 67 | 68 | function test_logn() public { 69 | assembly { 70 | log0(0x01, 0x02) 71 | log1(0x01, 0x02, 0x03) 72 | log2(0x01, 0x02, 0x03, 0x04) 73 | log3(0x01, 0x02, 0x03, 0x04, 0x05) 74 | } 75 | } 76 | 77 | event MyEvent(uint, uint indexed, uint, uint indexed); 78 | function test_events() public { 79 | emit MyEvent(1, 2, 3, 4); 80 | } 81 | 82 | function test_asserts() public { 83 | string memory err = "this test has failed!"; 84 | emit log("## assertTrue(bool)\n"); 85 | assertTrue(false); 86 | emit log("\n"); 87 | assertTrue(false, err); 88 | 89 | emit log("\n## assertEq(address,address)\n"); 90 | assertEq(address(this), msg.sender); 91 | emit log("\n"); 92 | assertEq(address(this), msg.sender, err); 93 | 94 | emit log("\n## assertEq32(bytes32,bytes32)\n"); 95 | assertEq32("bytes 1", "bytes 2"); 96 | emit log("\n"); 97 | assertEq32("bytes 1", "bytes 2", err); 98 | 99 | emit log("\n## assertEq(bytes32,bytes32)\n"); 100 | assertEq32("bytes 1", "bytes 2"); 101 | emit log("\n"); 102 | assertEq32("bytes 1", "bytes 2", err); 103 | 104 | emit log("\n## assertEq(uint,uint)\n"); 105 | assertEq(uint(0), 1); 106 | emit log("\n"); 107 | assertEq(uint(0), 1, err); 108 | 109 | emit log("\n## assertEq(int,int)\n"); 110 | assertEq(-1, -2); 111 | emit log("\n"); 112 | assertEq(-1, -2, err); 113 | 114 | emit log("\n## assertEqDecimal(int,int,uint)\n"); 115 | assertEqDecimal(-1.0e18, -1.1e18, 18); 116 | emit log("\n"); 117 | assertEqDecimal(-1.0e18, -1.1e18, 18, err); 118 | 119 | emit log("\n## assertEqDecimal(uint,uint,uint)\n"); 120 | assertEqDecimal(uint(1.0e18), 1.1e18, 18); 121 | emit log("\n"); 122 | assertEqDecimal(uint(1.0e18), 1.1e18, 18, err); 123 | 124 | emit log("\n## assertGt(uint,uint)\n"); 125 | assertGt(uint(0), 0); 126 | emit log("\n"); 127 | assertGt(uint(0), 0, err); 128 | 129 | emit log("\n## assertGt(int,int)\n"); 130 | assertGt(-1, -1); 131 | emit log("\n"); 132 | assertGt(-1, -1, err); 133 | 134 | emit log("\n## assertGtDecimal(int,int,uint)\n"); 135 | assertGtDecimal(-2.0e18, -1.1e18, 18); 136 | emit log("\n"); 137 | assertGtDecimal(-2.0e18, -1.1e18, 18, err); 138 | 139 | emit log("\n## assertGtDecimal(uint,uint,uint)\n"); 140 | assertGtDecimal(uint(1.0e18), 1.1e18, 18); 141 | emit log("\n"); 142 | assertGtDecimal(uint(1.0e18), 1.1e18, 18, err); 143 | 144 | emit log("\n## assertGe(uint,uint)\n"); 145 | assertGe(uint(0), 1); 146 | emit log("\n"); 147 | assertGe(uint(0), 1, err); 148 | 149 | emit log("\n## assertGe(int,int)\n"); 150 | assertGe(-1, 0); 151 | emit log("\n"); 152 | assertGe(-1, 0, err); 153 | 154 | emit log("\n## assertGeDecimal(int,int,uint)\n"); 155 | assertGeDecimal(-2.0e18, -1.1e18, 18); 156 | emit log("\n"); 157 | assertGeDecimal(-2.0e18, -1.1e18, 18, err); 158 | 159 | emit log("\n## assertGeDecimal(uint,uint,uint)\n"); 160 | assertGeDecimal(uint(1.0e18), 1.1e18, 18); 161 | emit log("\n"); 162 | assertGeDecimal(uint(1.0e18), 1.1e18, 18, err); 163 | 164 | emit log("\n## assertLt(uint,uint)\n"); 165 | assertLt(uint(0), 0); 166 | emit log("\n"); 167 | assertLt(uint(0), 0, err); 168 | 169 | emit log("\n## assertLt(int,int)\n"); 170 | assertLt(-1, -1); 171 | emit log("\n"); 172 | assertLt(-1, -1, err); 173 | 174 | emit log("\n## assertLtDecimal(int,int,uint)\n"); 175 | assertLtDecimal(-1.0e18, -1.1e18, 18); 176 | emit log("\n"); 177 | assertLtDecimal(-1.0e18, -1.1e18, 18, err); 178 | 179 | emit log("\n## assertLtDecimal(uint,uint,uint)\n"); 180 | assertLtDecimal(uint(2.0e18), 1.1e18, 18); 181 | emit log("\n"); 182 | assertLtDecimal(uint(2.0e18), 1.1e18, 18, err); 183 | 184 | emit log("\n## assertLe(uint,uint)\n"); 185 | assertLe(uint(1), 0); 186 | emit log("\n"); 187 | assertLe(uint(1), 0, err); 188 | 189 | emit log("\n## assertLe(int,int)\n"); 190 | assertLe(0, -1); 191 | emit log("\n"); 192 | assertLe(0, -1, err); 193 | 194 | emit log("\n## assertLeDecimal(int,int,uint)\n"); 195 | assertLeDecimal(-1.0e18, -1.1e18, 18); 196 | emit log("\n"); 197 | assertLeDecimal(-1.0e18, -1.1e18, 18, err); 198 | 199 | emit log("\n## assertLeDecimal(uint,uint,uint)\n"); 200 | assertLeDecimal(uint(2.0e18), 1.1e18, 18); 201 | emit log("\n"); 202 | assertLeDecimal(uint(2.0e18), 1.1e18, 18, err); 203 | 204 | emit log("\n## assertEq(string,string)\n"); 205 | string memory s1 = "string 1"; 206 | string memory s2 = "string 2"; 207 | assertEq(s1, s2); 208 | emit log("\n"); 209 | assertEq(s1, s2, err); 210 | 211 | emit log("\n## assertEq0(bytes,bytes)\n"); 212 | assertEq0(hex"abcdef01", hex"abcdef02"); 213 | log("\n"); 214 | assertEq0(hex"abcdef01", hex"abcdef02", err); 215 | } 216 | } 217 | 218 | contract DemoTestWithSetUp { 219 | function setUp() public { 220 | } 221 | function test_pass() public pure { 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /lib/solenv/lib/solidity-stringutils/strings.sol: -------------------------------------------------------------------------------- 1 | ./src/strings.sol -------------------------------------------------------------------------------- /lib/solenv/remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/forge-std/lib/ds-test/src/ 2 | forge-std/=lib/forge-std/src/ 3 | solidity-stringutils/=lib/solidity-stringutils/src/ -------------------------------------------------------------------------------- /lib/solenv/script/Solenv.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import {console} from "forge-std/console.sol"; 6 | import {Solenv} from "src/Solenv.sol"; 7 | 8 | contract SolenvScript is Script { 9 | function setUp() public { 10 | Solenv.config(); 11 | } 12 | 13 | function run() public { 14 | console.log('reading environment variable "WHY_USE_THIS_KEY"'); 15 | console.log(vm.envString("WHY_USE_THIS_KEY")); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/solenv/src/Solenv.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | import {strings} from "solidity-stringutils/strings.sol"; 6 | 7 | string constant DEFAULT_ENV_LOCATION = ".env"; 8 | 9 | library Solenv { 10 | using strings for *; 11 | 12 | Vm constant vm = Vm(address(bytes20(uint160(uint256(keccak256("hevm cheat code")))))); 13 | 14 | function _envExists(string memory key) private returns (bool) { 15 | try vm.envString(key) returns (string memory rEnv) { 16 | if (keccak256(abi.encodePacked(rEnv)) == keccak256("")) { 17 | return false; 18 | } else { 19 | return true; 20 | } 21 | } catch { 22 | return false; 23 | } 24 | } 25 | 26 | // todo: check if we can support setting delimiters 27 | function _config(string memory filename, bool overwrite) private { 28 | string[] memory inputs = new string[](3); 29 | inputs[0] = "sh"; 30 | inputs[1] = "-c"; 31 | inputs[2] = string( 32 | bytes.concat( 33 | 'cast abi-encode "response(bool)" $(test -f ', bytes(filename), ' && echo "true" || echo "false")' 34 | ) 35 | ); 36 | bytes memory res = vm.ffi(inputs); 37 | 38 | bool exists = abi.decode(res, (bool)); 39 | 40 | if (exists) { 41 | inputs[0] = "sh"; 42 | inputs[1] = "-c"; 43 | inputs[2] = 44 | string(bytes.concat('cast abi-encode "response(bytes)" $(xxd -p -c 999999999 ', bytes(filename), ")")); 45 | 46 | res = vm.ffi(inputs); 47 | 48 | strings.slice memory data = abi.decode(res, (string)).toSlice(); 49 | 50 | strings.slice memory lineDelim = "\n".toSlice(); 51 | strings.slice memory keyDelim = "=".toSlice(); 52 | strings.slice memory commentDelim = "#".toSlice(); 53 | 54 | uint256 length = data.count(lineDelim) + 1; 55 | for (uint256 i = 0; i < length; i++) { 56 | strings.slice memory line = data.split(lineDelim); 57 | if (!line.startsWith(commentDelim)) { 58 | string memory key = line.split(keyDelim).toString(); 59 | // Ignore empty lines 60 | if (bytes(key).length != 0) { 61 | if (overwrite == true) { 62 | vm.setEnv(key, line.toString()); 63 | } else { 64 | if (_envExists(key)) { 65 | // pre-existing found, do not overwrite 66 | } else { 67 | // pre-existing not found, insert 68 | vm.setEnv(key, line.toString()); 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | function config(string memory filename, bool overwrite) internal { 78 | _config(filename, overwrite); 79 | } 80 | 81 | function config(string memory filename) internal { 82 | config(filename, true); 83 | } 84 | 85 | function config(bool overwrite) internal { 86 | config(DEFAULT_ENV_LOCATION, overwrite); 87 | } 88 | 89 | function config() internal { 90 | config(DEFAULT_ENV_LOCATION); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/solenv/test/Solenv.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import {Solenv} from "src/Solenv.sol"; 6 | 7 | contract SolenvTest is Test { 8 | function setUp() external { 9 | Solenv.config(); 10 | } 11 | 12 | function _assertDefault() private { 13 | assertEq(vm.envString("WHY_USE_THIS_KEY"), "because we can can can"); 14 | assertEq(vm.envString("SOME_VERY_IMPORTANT_API_KEY"), "omgnoway"); 15 | assertEq(vm.envString("A_COMPLEX_ENV_VARIABLE"), "y&2U9xiEINv!vM8Gez"); 16 | assertEq(vm.envUint("A_NUMBER"), 100); 17 | assertEq(vm.envBool("A_TRUE_BOOL"), true); 18 | assertEq(vm.envBool("A_FALSE_BOOL"), false); 19 | assertEq(vm.envBool("A_FALSE_BOOL"), false); 20 | assertEq(vm.envAddress("AN_ADDRESS"), 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 21 | assertEq(vm.envBytes32("A_BYTES_32"), 0x0000000000000000000000000000000000000000000000000000000000000010); 22 | } 23 | 24 | function _resetEnv() private { 25 | vm.setEnv("WHY_USE_THIS_KEY", ""); 26 | vm.setEnv("SOME_VERY_IMPORTANT_API_KEY", ""); 27 | vm.setEnv("A_COMPLEX_ENV_VARIABLE", ""); 28 | vm.setEnv("A_NUMBER", ""); 29 | vm.setEnv("A_TRUE_BOOL", ""); 30 | vm.setEnv("A_FALSE_BOOL", ""); 31 | vm.setEnv("A_FALSE_BOOL", ""); 32 | vm.setEnv("AN_ADDRESS", ""); 33 | vm.setEnv("A_BYTES_32", ""); 34 | 35 | assertEq(vm.envString("WHY_USE_THIS_KEY"), "", "failed to reset"); 36 | assertEq(vm.envString("SOME_VERY_IMPORTANT_API_KEY"), "", "failed to reset"); 37 | assertEq(vm.envString("A_COMPLEX_ENV_VARIABLE"), "", "failed to reset"); 38 | assertEq(vm.envString("A_NUMBER"), "", "failed to reset"); 39 | assertEq(vm.envString("A_TRUE_BOOL"), "", "failed to reset"); 40 | assertEq(vm.envString("A_FALSE_BOOL"), "", "failed to reset"); 41 | assertEq(vm.envString("A_FALSE_BOOL"), "", "failed to reset"); 42 | assertEq(vm.envString("AN_ADDRESS"), "", "failed to reset"); 43 | assertEq(vm.envString("A_BYTES_32"), "", "failed to reset"); 44 | } 45 | 46 | function testAll() public { 47 | // LOAD CONFIG IN SETUP 48 | _assertDefault(); 49 | _resetEnv(); 50 | 51 | // LOAD DEFAULT CONFIG 52 | Solenv.config(); 53 | _assertDefault(); 54 | _resetEnv(); 55 | 56 | // TEST ANOTHER FILENAME 57 | Solenv.config(".env.test"); 58 | assertEq(vm.envString("SOME_VERY_IMPORTANT_API_KEY"), "adifferentone"); 59 | _resetEnv(); 60 | 61 | // TEST MERGE INSTEAD OF OVERWRITE 62 | // arrange - set some pre-existing env 63 | vm.setEnv("WHY_USE_THIS_KEY", "different value"); 64 | vm.setEnv("A_NUMBER", "1337"); 65 | vm.setEnv("A_TRUE_BOOL", "false"); 66 | vm.setEnv("A_FALSE_BOOL", "true"); 67 | 68 | // act 69 | Solenv.config(".env", false); 70 | 71 | // assert 72 | // from file 73 | assertEq(vm.envString("SOME_VERY_IMPORTANT_API_KEY"), "omgnoway"); 74 | assertEq(vm.envString("A_COMPLEX_ENV_VARIABLE"), "y&2U9xiEINv!vM8Gez"); 75 | assertEq(vm.envAddress("AN_ADDRESS"), 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 76 | assertEq(vm.envBytes32("A_BYTES_32"), 0x0000000000000000000000000000000000000000000000000000000000000010); 77 | 78 | // set manually 79 | assertEq(vm.envString("WHY_USE_THIS_KEY"), "different value"); 80 | assertEq(vm.envUint("A_NUMBER"), 1337); 81 | assertEq(vm.envBool("A_TRUE_BOOL"), false); 82 | assertEq(vm.envBool("A_FALSE_BOOL"), true); 83 | 84 | // cleanup 85 | _resetEnv(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soulwallet-contract", 3 | "scripts": { 4 | "build": "forge build --sizes", 5 | "test": "forge test -vvv", 6 | "deploy:soulwallet": "source .env && forge script script/WalletDeployer.s.sol:WalletDeployer --ffi --rpc-url $RPC_URL --broadcast", 7 | "deploy:singletonFactory": "source .env && forge script script/SingletonFactory.s.sol:SingletonFactory --ffi --rpc-url $RPC_URL --broadcast", 8 | "deploy:paymaster": "source .env && forge script script/PaymasterDeployer.s.sol:PaymasterDeployer --ffi --rpc-url $RPC_URL --broadcast", 9 | "deploy:op:soulwallet": "source .env && forge script script/WalletDeployer.s.sol:WalletDeployer --ffi --rpc-url $OPTIMISM_RPC_URL --broadcast --verify --etherscan-api-key $OPTIMISM_SCAN_API_KEY", 10 | "deploy:op:automation": "source .env && forge script script/AutomationDeployer.s.sol:AutomationDeployer --ffi --slow --rpc-url $OPTIMISM_RPC_URL --broadcast --verify --etherscan-api-key $OPTIMISM_SCAN_API_KEY", 11 | "deploy:sepolia:socialRecovery": "source .env && forge script script/SocialRecoveryDeployer.s.sol:SocialRecoveryDeployer --ffi --slow --rpc-url $SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $SEPOLIA_SCAN_API_KEY", 12 | "deploy:sepolia:soulwallet": "source .env && forge script script/WalletDeployer.s.sol:WalletDeployer --ffi --slow --rpc-url $SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $SEPOLIA_SCAN_API_KEY", 13 | "deploy:sepolia:automation": "source .env && forge script script/AutomationDeployer.s.sol:AutomationDeployer --ffi --slow --rpc-url $SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $SEPOLIA_SCAN_API_KEY", 14 | "deploy:sepolia:paymaster": "source .env && forge script script/PaymasterDeployer.s.sol:PaymasterDeployer --ffi --slow --rpc-url $SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $SEPOLIA_SCAN_API_KEY", 15 | "deploy:arb-sepolia:socialRecovery": "source .env && forge script script/SocialRecoveryDeployer.s.sol:SocialRecoveryDeployer --ffi --slow --rpc-url $ARB_SEPOLIA_RPC_URL --gas-estimate-multiplier 1900 --legacy --with-gas-price 3000000000 --broadcast --verify --etherscan-api-key $ARB_SEPOLIA_SCAN_API_KEY --verifier-url https://api-sepolia.arbiscan.io/api", 16 | "deploy:arb-sepolia:soulwallet": "source .env && forge script script/WalletDeployer.s.sol:WalletDeployer --ffi --slow --rpc-url $ARB_SEPOLIA_RPC_URL --gas-estimate-multiplier 1900 --legacy --with-gas-price 3000000000 --broadcast --verify --etherscan-api-key $ARB_SEPOLIA_SCAN_API_KEY --verifier-url https://api-sepolia.arbiscan.io/api", 17 | "deploy:arb-sepolia:paymaster": "source .env && forge script script/PaymasterDeployer.s.sol:PaymasterDeployer --ffi --slow --rpc-url $ARB_SEPOLIA_RPC_URL --gas-estimate-multiplier 1900 --legacy --with-gas-price 3000000000 --broadcast --verify --etherscan-api-key $ARB_SEPOLIA_SCAN_API_KEY --verifier-url https://api-sepolia.arbiscan.io/api", 18 | "deploy:op-sepolia:socialRecovery": "source .env && forge script script/SocialRecoveryDeployer.s.sol:SocialRecoveryDeployer --ffi --slow --rpc-url $OPTIMISM_SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $OPTIMISM_SEPOLIA_SCAN_API_KEY --verifier-url https://api-sepolia-optimistic.etherscan.io/api", 19 | "deploy:op-sepolia:soulwallet": "source .env && forge script script/WalletDeployer.s.sol:WalletDeployer --ffi --slow --rpc-url $OPTIMISM_SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $OPTIMISM_SEPOLIA_SCAN_API_KEY --verifier-url https://api-sepolia-optimistic.etherscan.io/api", 20 | "deploy:op-sepolia:paymaster": "source .env && forge script script/PaymasterDeployer.s.sol:PaymasterDeployer --ffi --slow --rpc-url $OPTIMISM_SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $OPTIMISM_SEPOLIA_SCAN_API_KEY --verifier-url https://api-sepolia-optimistic.etherscan.io/api", 21 | "deploy:sepolia:all": "npm run deploy:sepolia:socialRecovery && npm run deploy:sepolia:soulwallet && npm run deploy:sepolia:paymaster", 22 | "deploy:arb-sepolia:all": "npm run deploy:arb-sepolia:socialRecovery && npm run deploy:arb-sepolia:soulwallet && npm run deploy:arb-sepolia:paymaster", 23 | "deploy:op-sepolia:all": "npm run deploy:op-sepolia:socialRecovery && npm run deploy:op-sepolia:soulwallet && npm run deploy:op-sepolia:paymaster", 24 | "deploy:base-sepolia:soulwallet": "source .env && forge script script/WalletDeployer.s.sol:WalletDeployer --ffi --slow --rpc-url $BASE_SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $BASE_SEPOLIA_SCAN_API_KEY", 25 | "deploy:base-sepolia:automation": "source .env && forge script script/AutomationDeployer.s.sol:AutomationDeployer --ffi --slow --rpc-url $BASE_SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $BASE_SEPOLIA_SCAN_API_KEY", 26 | "deploy:all-sepolia": "npm run deploy:sepolia:all && npm run deploy:scroll-sepolia:all && npm run deploy:arb-sepolia:all && npm run deploy:op-sepolia:all", 27 | "deploy:sepolia:receivePayment": "source .env && forge script script/ReceivePayment.s.sol:ReceivePaymentDeployer --ffi --slow --rpc-url $SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $SEPOLIA_SCAN_API_KEY", 28 | "deploy:op-sepolia:receivePayment": "source .env && forge script script/ReceivePayment.s.sol:ReceivePaymentDeployer --ffi --slow --rpc-url $OPTIMISM_SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $OPTIMISM_SEPOLIA_SCAN_API_KEY --verifier-url https://api-sepolia-optimistic.etherscan.io/api", 29 | "deploy:arb-sepolia:receivePayment": "source .env && forge script script/ReceivePayment.s.sol:ReceivePaymentDeployer --ffi --slow --rpc-url $ARB_SEPOLIA_RPC_URL --gas-estimate-multiplier 1900 --legacy --with-gas-price 3000000000 --broadcast --verify --etherscan-api-key $ARB_SEPOLIA_SCAN_API_KEY --verifier-url https://api-sepolia.arbiscan.io/api", 30 | "deploy:all-sepolia:receivePayment": "npm run deploy:sepolia:receivePayment && npm run deploy:op-sepolia:receivePayment && npm run deploy:arb-sepolia:receivePayment", 31 | "gas-report": "forge test --gas-report" 32 | }, 33 | "devDependencies": { 34 | "@ethersproject/abi": "^5.6.4", 35 | "@ethersproject/providers": "^5.6.8", 36 | "dotenv": "^16.0.3", 37 | "ethereumjs-util": "^7.1.5", 38 | "ethers": "^5.6.9", 39 | "npm-run-all": "^4.1.5", 40 | "ts-node": "^10.9.1", 41 | "typescript": "^4.9.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @soulwallet-core/=lib/soulwallet-core/ 2 | @source/=contracts/ 3 | @arbitrum/nitro-contracts=lib/nitro-contracts/ 4 | @solady=lib/solady/ 5 | @solenv=lib/solenv/src/ 6 | @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ 7 | @account-abstraction/=lib/account-abstraction/ 8 | @crypto-lib/=lib/crypto-lib/src/ -------------------------------------------------------------------------------- /script/AutomationDeployer.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "./DeployHelper.sol"; 6 | import "@source/automation/AaveUsdcSaveAutomation.sol"; 7 | import "@source/automation/ClaimInterest.sol"; 8 | 9 | contract AutomationDeployer is Script, DeployHelper { 10 | address automationOwner; 11 | uint256 automationOwnerPrivateKey; 12 | address soulwalletFactory; 13 | address automationBot; 14 | uint256 automationBotPrivateKey; 15 | 16 | function run() public { 17 | automationOwnerPrivateKey = vm.envUint("AUTOMATION_OWNER_PRIVATE_KEY"); 18 | require(automationOwnerPrivateKey != 0, "AUTOMATION_OWNER_PRIVATE_KEY not provided"); 19 | automationOwner = vm.addr(automationOwnerPrivateKey); 20 | automationBotPrivateKey = vm.envUint("AUTOMATION_BOT_PRIVATE_KEY"); 21 | automationBot = vm.addr(automationBotPrivateKey); 22 | require(automationOwner != address(0), "AUTOMATION_OWNER_ADDRESS not provided"); 23 | soulwalletFactory = vm.envAddress("SOULWALLET_FACTORY_ADDRESS"); 24 | require(soulwalletFactory != address(0), "SOULWALLET_FACTORY_ADDRESS not provided"); 25 | require(address(soulwalletFactory).code.length > 0, "soulwalletFactory needs be deployed"); 26 | vm.startBroadcast(privateKey); 27 | 28 | Network network = getNetwork(); 29 | if (network == Network.Sepolia) { 30 | console.log("deploy automation contract on Sepolia"); 31 | // same logic as localtestnet 32 | delpoySepolia(); 33 | } else if (network == Network.OptimismSepolia) { 34 | console.log("deploy automation contract on OptimismSepolia"); 35 | delpoyOpSepolia(); 36 | } else if (network == Network.ArbitrumSepolia) { 37 | console.log("deploy automation contract on ArbitrumSepolia"); 38 | delpoyArbSepolia(); 39 | } else if (network == Network.BaseSepolia) { 40 | console.log("deploy automation contract on BaseSepolia"); 41 | delpoyBaseSepolia(); 42 | } else if (network == Network.Optimism) { 43 | console.log("deploy automation contract on OP mainnet"); 44 | delpoyOpClaimInterest(); 45 | } else { 46 | console.log("deploy automation contract on testnet"); 47 | deploy(); 48 | } 49 | } 50 | 51 | function deploy() private pure { 52 | revert("not implemented"); 53 | } 54 | 55 | function delpoySepolia() private { 56 | address usdc = 0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8; 57 | address aaveUscPool = 0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951; 58 | 59 | address aaveUsdcAutomation = deploy( 60 | "AaveUsdcSaveAutomationSepolia", 61 | bytes.concat(type(AaveUsdcSaveAutomation).creationCode, abi.encode(automationOwner, usdc, aaveUscPool)) 62 | ); 63 | writeAddressToEnv("SOUL_WALLET_AAVE_USDC_AUTOMATION_SEPOLIA", aaveUsdcAutomation); 64 | } 65 | 66 | function delpoyArbSepolia() private { 67 | address usdc = 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d; 68 | address aaveUscPool = 0xBfC91D59fdAA134A4ED45f7B584cAf96D7792Eff; 69 | 70 | address aaveUsdcAutomation = deploy( 71 | "AaveUsdcSaveAutomationArbSepolia", 72 | bytes.concat(type(AaveUsdcSaveAutomation).creationCode, abi.encode(automationOwner, usdc, aaveUscPool)) 73 | ); 74 | writeAddressToEnv("SOUL_WALLET_AAVE_USDC_AUTOMATION_ARB_SEPOLIA", aaveUsdcAutomation); 75 | } 76 | 77 | function delpoyOpSepolia() private { 78 | address usdc = 0x5fd84259d66Cd46123540766Be93DFE6D43130D7; 79 | address aaveUscPool = 0xb50201558B00496A145fE76f7424749556E326D8; 80 | 81 | address aaveUsdcAutomation = deploy( 82 | "AaveUsdcSaveAutomationOpSepolia", 83 | bytes.concat(type(AaveUsdcSaveAutomation).creationCode, abi.encode(automationOwner, usdc, aaveUscPool)) 84 | ); 85 | writeAddressToEnv("SOUL_WALLET_AAVE_USDC_AUTOMATION_OP_SEPOLIA", aaveUsdcAutomation); 86 | } 87 | 88 | function delpoyOp() private { 89 | address usdc = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; 90 | address aaveUscPool = 0x794a61358D6845594F94dc1DB02A252b5b4814aD; 91 | 92 | address aaveUsdcAutomation = deploy( 93 | "AaveUsdcSaveAutomationOpMainnet", 94 | bytes.concat(type(AaveUsdcSaveAutomation).creationCode, abi.encode(automationOwner, usdc, aaveUscPool)) 95 | ); 96 | writeAddressToEnv("SOUL_WALLET_AAVE_USDC_AUTOMATION_OP_MAINNET", aaveUsdcAutomation); 97 | } 98 | 99 | function delpoyOpClaimInterest() private { 100 | address usdc = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; 101 | address claimInterest = deploy( 102 | "ClaimInterestOpMainnet", 103 | bytes.concat( 104 | type(ClaimInterest).creationCode, 105 | abi.encode(automationOwner, "0xf4bF967767Cc55dd73EF19E1DA7b58A1B39f0782", usdc) 106 | ) 107 | ); 108 | writeAddressToEnv("CLAIM_INTEREST_OP_MAINNET", claimInterest); 109 | } 110 | 111 | function delpoyBaseSepolia() private { 112 | address usdc = 0x036CbD53842c5426634e7929541eC2318f3dCF7e; 113 | address aaveUscPool = 0x07eA79F68B2B3df564D0A34F8e19D9B1e339814b; 114 | 115 | address aaveUsdcAutomation = deploy( 116 | "AaveUsdcSaveAutomationBaseSepolia", 117 | bytes.concat(type(AaveUsdcSaveAutomation).creationCode, abi.encode(automationOwner, usdc, aaveUscPool)) 118 | ); 119 | writeAddressToEnv("SOUL_WALLET_AAVE_USDC_AUTOMATION_BASE_SEPOLIA", aaveUsdcAutomation); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /script/CreateWalletDirect.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "@source/factory/SoulWalletFactory.sol"; 6 | import "./DeployHelper.sol"; 7 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 8 | import {NetWorkLib} from "./DeployHelper.sol"; 9 | import "@source/libraries/TypeConversion.sol"; 10 | import {Solenv} from "@solenv/Solenv.sol"; 11 | 12 | contract CreateWalletDirect is Script { 13 | using MessageHashUtils for bytes32; 14 | using TypeConversion for address; 15 | 16 | uint256 guardianThreshold = 1; 17 | uint64 initialGuardianSafePeriod = 2 days; 18 | 19 | address walletSigner; 20 | uint256 walletSingerPrivateKey; 21 | 22 | address guardianAddress; 23 | uint256 guardianPrivateKey; 24 | 25 | address defaultCallbackHandler; 26 | 27 | SoulWalletFactory soulwalletFactory; 28 | 29 | address payable soulwalletAddress; 30 | 31 | bytes32 private constant _TYPEHASH = 32 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); 33 | 34 | bytes32 private DOMAIN_SEPARATOR; 35 | 36 | function run() public { 37 | Solenv.config(".env_backend"); 38 | // wallet signer info 39 | walletSingerPrivateKey = vm.envUint("WALLET_SIGNGER_PRIVATE_KEY"); 40 | walletSigner = vm.addr(walletSingerPrivateKey); 41 | // guardian info 42 | guardianPrivateKey = vm.envUint("GUARDIAN_PRIVATE_KEY"); 43 | guardianAddress = vm.addr(guardianPrivateKey); 44 | 45 | vm.startBroadcast(walletSingerPrivateKey); 46 | string memory networkName = NetWorkLib.getNetworkName(); 47 | console.log("create wallet on ", networkName); 48 | createWallet(); 49 | } 50 | 51 | function createWallet() private { 52 | bytes32 salt = bytes32(0); 53 | bytes[] memory modules = new bytes[](0); 54 | bytes32[] memory owners = new bytes32[](1); 55 | owners[0] = walletSigner.toBytes32(); 56 | 57 | bytes[] memory hooks = new bytes[](0); 58 | 59 | defaultCallbackHandler = loadEnvContract("DefaultCallbackHandler"); 60 | bytes memory initializer = abi.encodeWithSignature( 61 | "initialize(bytes32[],address,bytes[],bytes[])", owners, defaultCallbackHandler, modules, hooks 62 | ); 63 | soulwalletFactory = SoulWalletFactory(loadEnvContract("SoulwalletFactory")); 64 | address cacluatedAddress = soulwalletFactory.getWalletAddress(initializer, salt); 65 | 66 | soulwalletAddress = payable(soulwalletFactory.createWallet(initializer, salt)); 67 | require(cacluatedAddress == soulwalletAddress, "calculated address not match"); 68 | console.log("wallet address: ", soulwalletAddress); 69 | } 70 | 71 | function loadEnvContract(string memory label) private view returns (address) { 72 | address contractAddress = vm.envAddress(label); 73 | require(contractAddress != address(0), string(abi.encodePacked(label, " not provided"))); 74 | require(contractAddress.code.length > 0, string(abi.encodePacked(label, " needs be deployed"))); 75 | return contractAddress; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /script/CreateWalletEntryPoint.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "@source/factory/SoulWalletFactory.sol"; 6 | import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; 7 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 8 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 9 | import "@source/libraries/TypeConversion.sol"; 10 | import {Solenv} from "@solenv/Solenv.sol"; 11 | import {UserOperationHelper} from "@soulwallet-core/test/dev/userOperationHelper.sol"; 12 | import {IStandardExecutor} from "@soulwallet-core/contracts/interface/IStandardExecutor.sol"; 13 | import {Execution} from "@soulwallet-core/contracts/interface/IStandardExecutor.sol"; 14 | 15 | contract CreateWalletEntryPoint is Script { 16 | using ECDSA for bytes32; 17 | using MessageHashUtils for bytes32; 18 | using TypeConversion for address; 19 | 20 | uint256 guardianThreshold = 1; 21 | uint64 initialGuardianSafePeriod = 2 days; 22 | 23 | address walletSigner; 24 | uint256 walletSingerPrivateKey; 25 | 26 | address guardianAddress; 27 | uint256 guardianPrivateKey; 28 | 29 | address defaultCallbackHandler; 30 | address soulWalletDefaultValidator; 31 | 32 | SoulWalletFactory soulwalletFactory; 33 | 34 | bytes emptyBytes; 35 | EntryPoint public entryPoint = EntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); 36 | 37 | function run() public { 38 | Solenv.config(".env_backend"); 39 | // wallet signer info 40 | walletSingerPrivateKey = vm.envUint("WALLET_SIGNGER_NEW_PRIVATE_KEY"); 41 | soulWalletDefaultValidator = loadEnvContract("SoulWalletDefaultValidator"); 42 | walletSigner = vm.addr(walletSingerPrivateKey); 43 | 44 | // guardian info 45 | guardianPrivateKey = vm.envUint("GUARDIAN_PRIVATE_KEY"); 46 | guardianAddress = vm.addr(guardianPrivateKey); 47 | 48 | createWallet(); 49 | } 50 | 51 | function createWallet() private { 52 | vm.startBroadcast(walletSingerPrivateKey); 53 | bytes32 salt = bytes32(uint256(3)); 54 | bytes[] memory modules = new bytes[](0); 55 | 56 | bytes32[] memory owners = new bytes32[](1); 57 | owners[0] = walletSigner.toBytes32(); 58 | 59 | bytes[] memory hooks = new bytes[](0); 60 | 61 | defaultCallbackHandler = loadEnvContract("DefaultCallbackHandler"); 62 | bytes memory initializer = abi.encodeWithSignature( 63 | "initialize(bytes32[],address,bytes[],bytes[])", owners, defaultCallbackHandler, modules, hooks 64 | ); 65 | soulwalletFactory = SoulWalletFactory(loadEnvContract("SoulwalletFactory")); 66 | address cacluatedAddress = soulwalletFactory.getWalletAddress(initializer, salt); 67 | 68 | bytes memory soulWalletFactoryCall = abi.encodeWithSignature("createWallet(bytes,bytes32)", initializer, salt); 69 | bytes memory initCode = abi.encodePacked(address(soulwalletFactory), soulWalletFactoryCall); 70 | console.log("cacluatedAddress", cacluatedAddress); 71 | 72 | entryPoint.depositTo{value: 0.005 ether}(cacluatedAddress); 73 | PackedUserOperation[] memory ops = new PackedUserOperation[](1); 74 | 75 | address aaveUsdcAutomationAddress = loadEnvContract("SOUL_WALLET_AAVE_USDC_AUTOMATION_BASE_SEPOLIA"); 76 | bytes memory approveData = 77 | abi.encodeWithSignature("approve(address,uint256)", aaveUsdcAutomationAddress, 10000 ether); 78 | address usdcAddress = loadEnvContract("USDC_BASE"); 79 | 80 | PackedUserOperation memory userOperation = UserOperationHelper.newUserOp({ 81 | sender: cacluatedAddress, 82 | nonce: 0, 83 | initCode: initCode, 84 | callData: abi.encodeWithSelector(IStandardExecutor.execute.selector, usdcAddress, 0, approveData), 85 | callGasLimit: 900000, 86 | verificationGasLimit: 1000000, 87 | preVerificationGas: 300000, 88 | maxFeePerGas: 10000, 89 | maxPriorityFeePerGas: 10000, 90 | paymasterAndData: hex"" 91 | }); 92 | userOperation.signature = signUserOp(userOperation, walletSingerPrivateKey, soulWalletDefaultValidator); 93 | logUserOp(userOperation); 94 | 95 | ops[0] = userOperation; 96 | 97 | entryPoint.handleOps(ops, payable(walletSigner)); 98 | } 99 | 100 | function withDrawAndTransfer() private { 101 | vm.startBroadcast(walletSingerPrivateKey); 102 | Execution[] memory executions = new Execution[](2); 103 | executions[0].target = address(0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951); 104 | executions[0].value = 0; 105 | executions[0].data = abi.encodeWithSignature( 106 | "withdraw(address,uint256,address)", 107 | address(0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8), 108 | 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 109 | address(0x7aD6443c55A1eD75b63b9Cce601E1591F20B42f3) 110 | ); 111 | executions[1].target = address(0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8); 112 | executions[1].value = 0; 113 | executions[1].data = abi.encodeWithSignature( 114 | "transfer(address,uint256)", address(0xf4bF967767Cc55dd73EF19E1DA7b58A1B39f0782), 1000e6 115 | ); 116 | 117 | bytes memory callData = abi.encodeWithSignature("executeBatch((address,uint256,bytes)[])", executions); 118 | 119 | PackedUserOperation memory userOperation = UserOperationHelper.newUserOp({ 120 | sender: 0x7aD6443c55A1eD75b63b9Cce601E1591F20B42f3, 121 | nonce: 4, 122 | initCode: hex"", 123 | callData: callData, 124 | callGasLimit: 1900000, 125 | verificationGasLimit: 1000000, 126 | preVerificationGas: 300000, 127 | maxFeePerGas: 10000, 128 | maxPriorityFeePerGas: 10000, 129 | paymasterAndData: hex"" 130 | }); 131 | userOperation.signature = signUserOp(userOperation, walletSingerPrivateKey, soulWalletDefaultValidator); 132 | logUserOp(userOperation); 133 | PackedUserOperation[] memory ops = new PackedUserOperation[](1); 134 | 135 | ops[0] = userOperation; 136 | 137 | entryPoint.handleOps(ops, payable(walletSigner)); 138 | } 139 | 140 | function logUserOp(PackedUserOperation memory op) private pure { 141 | console.log("sender: ", op.sender); 142 | console.log("nonce: ", op.nonce); 143 | console.log("initCode: "); 144 | console.logBytes(op.initCode); 145 | console.log("callData: "); 146 | console.logBytes(op.callData); 147 | console.log("paymasterAndData: "); 148 | console.logBytes(op.paymasterAndData); 149 | console.log("signature: "); 150 | console.logBytes(op.signature); 151 | } 152 | 153 | function signUserOp(PackedUserOperation memory op, uint256 _key, address _validator) 154 | public 155 | view 156 | returns (bytes memory signature) 157 | { 158 | bytes32 hash = entryPoint.getUserOpHash(op); 159 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_key, hash.toEthSignedMessageHash()); 160 | bytes memory opSig; 161 | bytes memory signatureData = abi.encodePacked(r, s, v); 162 | uint8 signType = 0; 163 | bytes4 signatureLength = bytes4(uint32(1 + signatureData.length)); 164 | opSig = abi.encodePacked(_validator, signatureLength, signType, signatureData); 165 | signature = opSig; 166 | } 167 | 168 | function loadEnvContract(string memory label) private view returns (address) { 169 | address contractAddress = vm.envAddress(label); 170 | require(contractAddress != address(0), string(abi.encodePacked(label, " not provided"))); 171 | require(contractAddress.code.length > 0, string(abi.encodePacked(label, " needs be deployed"))); 172 | return contractAddress; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /script/CreateWalletEntryPointPaymaster.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "@source/factory/SoulWalletFactory.sol"; 6 | import "@source/SoulWallet.sol"; 7 | import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; 8 | import {UserOperationLib} from "@account-abstraction/contracts/core/UserOperationLib.sol"; 9 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 10 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 11 | import "@source/libraries/TypeConversion.sol"; 12 | import {Solenv} from "@solenv/Solenv.sol"; 13 | import {Execution} from "@soulwallet-core/contracts/interface/IStandardExecutor.sol"; 14 | import {UserOperationHelper} from "@soulwallet-core/test/dev/userOperationHelper.sol"; 15 | 16 | contract CreateWalletEntryPointPaymaster is Script { 17 | using ECDSA for bytes32; 18 | using MessageHashUtils for bytes32; 19 | using TypeConversion for address; 20 | using UserOperationLib for PackedUserOperation; 21 | 22 | uint256 guardianThreshold = 1; 23 | uint64 initialGuardianSafePeriod = 2 days; 24 | 25 | address walletSigner; 26 | uint256 walletSingerPrivateKey; 27 | 28 | address guardianAddress; 29 | uint256 guardianPrivateKey; 30 | 31 | address defaultCallbackHandler; 32 | address soulWalletDefaultValidator; 33 | 34 | SoulWalletFactory soulwalletFactory; 35 | 36 | address payable soulwalletAddress; 37 | 38 | bytes emptyBytes; 39 | EntryPoint public entryPoint = EntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); 40 | 41 | function run() public { 42 | Solenv.config(".env_backend"); 43 | // wallet signer info 44 | walletSingerPrivateKey = vm.envUint("WALLET_SIGNGER_NEW_PRIVATE_KEY"); 45 | walletSigner = vm.addr(walletSingerPrivateKey); 46 | 47 | // guardian info 48 | guardianPrivateKey = vm.envUint("GUARDIAN_PRIVATE_KEY"); 49 | guardianAddress = vm.addr(guardianPrivateKey); 50 | 51 | createWallet(); 52 | } 53 | 54 | function createWallet() private { 55 | bytes32 salt = bytes32(uint256(12)); 56 | bytes[] memory modules = new bytes[](0); 57 | bytes32[] memory owners = new bytes32[](1); 58 | owners[0] = walletSigner.toBytes32(); 59 | 60 | bytes[] memory hooks = new bytes[](0); 61 | 62 | defaultCallbackHandler = loadEnvContract("DefaultCallbackHandler"); 63 | bytes memory initializer = abi.encodeWithSignature( 64 | "initialize(bytes32[],address,bytes[],bytes[])", owners, defaultCallbackHandler, modules, hooks 65 | ); 66 | soulwalletFactory = SoulWalletFactory(loadEnvContract("SoulwalletFactory")); 67 | address cacluatedAddress = soulwalletFactory.getWalletAddress(initializer, salt); 68 | 69 | bytes memory soulWalletFactoryCall = abi.encodeWithSignature("createWallet(bytes,bytes32)", initializer, salt); 70 | bytes memory initCode = abi.encodePacked(address(soulwalletFactory), soulWalletFactoryCall); 71 | console.log("cacluatedAddress", cacluatedAddress); 72 | 73 | address payToken = loadEnvContract("PAYTOKEN_ADDRESS"); 74 | address paymaster = loadEnvContract("Paymaster"); 75 | 76 | Execution[] memory executions = new Execution[](1); 77 | executions[0].target = address(payToken); 78 | executions[0].value = 0; 79 | executions[0].data = abi.encodeWithSignature("approve(address,uint256)", address(paymaster), 10000e6); 80 | bytes memory callData = abi.encodeWithSignature("executeBatch((address,uint256,bytes)[])", executions); 81 | bytes memory paymasterAndData = abi.encodePacked( 82 | abi.encodePacked(address(paymaster), uint128(400000), uint128(400000)), 83 | abi.encode(address(payToken), uint256(10000e6)) 84 | ); 85 | 86 | PackedUserOperation[] memory ops = new PackedUserOperation[](1); 87 | 88 | PackedUserOperation memory userOperation = UserOperationHelper.newUserOp({ 89 | sender: cacluatedAddress, 90 | nonce: 0, 91 | initCode: initCode, 92 | callData: callData, 93 | callGasLimit: 5000000, 94 | verificationGasLimit: 1000000, 95 | preVerificationGas: 500000, 96 | maxFeePerGas: 10000, 97 | maxPriorityFeePerGas: 10000, 98 | paymasterAndData: paymasterAndData 99 | }); 100 | userOperation.signature = signUserOp(userOperation, walletSingerPrivateKey, soulWalletDefaultValidator); 101 | 102 | ops[0] = userOperation; 103 | 104 | uint256 bundlerSinger = vm.envUint("WALLET_SIGNGER_PRIVATE_KEY"); 105 | console.log("bundelr address ", vm.addr(bundlerSinger)); 106 | vm.startBroadcast(bundlerSinger); 107 | console.log("entryPoint", address(entryPoint)); 108 | console.log("beneficiary ", vm.addr(bundlerSinger)); 109 | entryPoint.handleOps(ops, payable(vm.addr(bundlerSinger))); 110 | } 111 | 112 | function signUserOp(PackedUserOperation memory op, uint256 _key, address _validator) 113 | public 114 | view 115 | returns (bytes memory signature) 116 | { 117 | bytes32 hash = entryPoint.getUserOpHash(op); 118 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_key, hash.toEthSignedMessageHash()); 119 | bytes memory opSig; 120 | bytes memory signatureData = abi.encodePacked(r, s, v); 121 | uint8 signType = 0; 122 | bytes4 signatureLength = bytes4(uint32(1 + signatureData.length)); 123 | opSig = abi.encodePacked(_validator, signatureLength, signType, signatureData); 124 | signature = opSig; 125 | } 126 | 127 | function loadEnvContract(string memory label) private view returns (address) { 128 | address contractAddress = vm.envAddress(label); 129 | require(contractAddress != address(0), string(abi.encodePacked(label, " not provided"))); 130 | require(contractAddress.code.length > 0, string(abi.encodePacked(label, " needs be deployed"))); 131 | return contractAddress; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /script/ReceivePayment.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "./DeployHelper.sol"; 6 | import "@source/dev/ReceivePayment.sol"; 7 | 8 | contract ReceivePaymentDeployer is Script, DeployHelper { 9 | address paymasterOwner; 10 | uint256 paymasterOwnerPrivateKey; 11 | address soulwalletFactory; 12 | 13 | function run() public { 14 | vm.startBroadcast(privateKey); 15 | deploy(); 16 | } 17 | 18 | function deploy() private { 19 | address receivePaymentOwner = vm.envAddress("RECEIVE_PAYMENT_OWNER_ADDRESS"); 20 | deploy("ReceivePayment", bytes.concat(type(ReceivePayment).creationCode, abi.encode(receivePaymentOwner))); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /script/SingletonFactory.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "./DeployHelper.sol"; 6 | 7 | contract SingletonFactory is Script, DeployHelper { 8 | function run() public { 9 | deploySingletonFactory(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /script/SocialRecoveryDeployer.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "@source/modules/socialRecovery/SocialRecoveryModule.sol"; 6 | import "./DeployHelper.sol"; 7 | 8 | contract SocialRecoveryDeployer is Script, DeployHelper { 9 | function run() public { 10 | vm.startBroadcast(privateKey); 11 | deploy("SocialRecoveryModule", type(SocialRecoveryModule).creationCode); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /script/WalletDeployer.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "@source/factory/SoulWalletFactory.sol"; 6 | import "@source/SoulWallet.sol"; 7 | import "@source/abstract/DefaultCallbackHandler.sol"; 8 | import {SoulWalletDefaultValidator} from "@source/validator/SoulWalletDefaultValidator.sol"; 9 | import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; 10 | import "./DeployHelper.sol"; 11 | 12 | contract WalletDeployer is Script, DeployHelper { 13 | function run() public { 14 | vm.startBroadcast(privateKey); 15 | Network network = getNetwork(); 16 | string memory networkName = NetWorkLib.getNetworkName(); 17 | console.log("deploy soul wallet contract on ", networkName); 18 | if (network == Network.Anvil) { 19 | deploySingletonFactory(); 20 | deployLocalEntryPoint(); 21 | } 22 | deploy(); 23 | } 24 | 25 | function deploy() private { 26 | address soulWalletDefaultValidator = 27 | deploy("SoulWalletDefaultValidator", type(SoulWalletDefaultValidator).creationCode); 28 | writeAddressToEnv("SOUL_WALLET_DEFAULT_VALIDATOR", soulWalletDefaultValidator); 29 | address soulwalletInstance = deploy( 30 | "SoulwalletInstance", 31 | bytes.concat(type(SoulWallet).creationCode, abi.encode(ENTRYPOINT_ADDRESS, soulWalletDefaultValidator)) 32 | ); 33 | address soulwalletFactoryOwner = vm.envAddress("SOULWALLET_FACTORY_OWNER"); 34 | address soulwalletFactoryAddress = deploy( 35 | "SoulwalletFactory", 36 | bytes.concat( 37 | type(SoulWalletFactory).creationCode, 38 | abi.encode(soulwalletInstance, ENTRYPOINT_ADDRESS, soulwalletFactoryOwner) 39 | ) 40 | ); 41 | writeAddressToEnv("SOULWALLET_FACTORY_ADDRESS", soulwalletFactoryAddress); 42 | 43 | deploy("DefaultCallbackHandler", type(DefaultCallbackHandler).creationCode); 44 | } 45 | 46 | function deployLocalEntryPoint() private { 47 | ENTRYPOINT_ADDRESS = deploy("EntryPoint", type(EntryPoint).creationCode); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /script/ffi/save_to_env.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require("fs"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | const envFilePath = path.resolve(__dirname, "..", "..", ".env"); 7 | 8 | const readEnvVars = () => fs.readFileSync(envFilePath, "utf-8").split(os.EOL); 9 | 10 | const writeToEnv = (key, value) => { 11 | const envVars = readEnvVars(); 12 | const targetLine = envVars.find((line) => line.split("=")[0] === key); 13 | if (targetLine !== undefined) { 14 | const targetLineIndex = envVars.indexOf(targetLine); 15 | if (value.startsWith("0x")) { 16 | envVars.splice(targetLineIndex, 1, `${key}=${value}`); 17 | } else { 18 | envVars.splice(targetLineIndex, 1, `${key}=${value}`); 19 | } 20 | } else { 21 | if (value.startsWith("0x")) { 22 | envVars.push(`${key}=${value}`); 23 | } else { 24 | envVars.push(`${key}="${value}"`); 25 | } 26 | } 27 | fs.writeFileSync(envFilePath, envVars.join(os.EOL)); 28 | }; 29 | var arguments = process.argv.slice(2); 30 | if (arguments.length != 2) { 31 | console.log("wrong number of arguments"); 32 | process.exit(1); 33 | } 34 | writeToEnv(arguments[0], arguments[1]); 35 | -------------------------------------------------------------------------------- /script/ffi/save_to_env_backend.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const os = require("os"); 3 | const path = require("path"); 4 | 5 | const envFilePath = path.resolve(__dirname, "..", "..", ".env_backend"); 6 | 7 | const readEnvVars = () => fs.readFileSync(envFilePath, "utf-8").split(os.EOL); 8 | 9 | const writeToEnv = (key, value) => { 10 | const envVars = readEnvVars(); 11 | const targetLine = envVars.find((line) => line.split("=")[0] === key); 12 | if (targetLine !== undefined) { 13 | const targetLineIndex = envVars.indexOf(targetLine); 14 | if (value.startsWith("0x")) { 15 | envVars.splice(targetLineIndex, 1, `${key}=${value}`); 16 | } else { 17 | envVars.splice(targetLineIndex, 1, `${key}=${value}`); 18 | } 19 | } else { 20 | if (value.startsWith("0x")) { 21 | envVars.push(`${key}=${value}`); 22 | } else { 23 | envVars.push(`${key}="${value}"`); 24 | } 25 | } 26 | fs.writeFileSync(envFilePath, envVars.join(os.EOL)); 27 | }; 28 | var arguments = process.argv.slice(2); 29 | if (arguments.length != 2) { 30 | console.log("wrong number of arguments"); 31 | process.exit(1); 32 | } 33 | writeToEnv(arguments[0], arguments[1]); 34 | -------------------------------------------------------------------------------- /test/automation/ClaimInterest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/Test.sol"; 5 | import "@source/automation/ClaimInterest.sol"; 6 | import "@source/dev/tokens/TokenERC20.sol"; 7 | 8 | contract ClaimInterestTest is Test { 9 | ClaimInterest claimInterest; 10 | address owner; 11 | uint256 ownerKey; 12 | address signer; 13 | uint256 signerKey; 14 | TokenERC20 token; 15 | 16 | function setUp() public { 17 | (owner, ownerKey) = makeAddrAndKey("owner"); 18 | (signer, signerKey) = makeAddrAndKey("signer"); 19 | vm.startBroadcast(ownerKey); 20 | token = new TokenERC20(6); 21 | claimInterest = new ClaimInterest(owner, signer, address(token)); 22 | // deposit interest to contract 23 | token.transfer(address(claimInterest), uint256(10000e6)); 24 | vm.stopBroadcast(); 25 | } 26 | 27 | function test_claim_interest() public { 28 | (address user, uint256 userKey) = makeAddrAndKey("user"); 29 | // test user can claim 100 usdc interest 30 | uint256 userNonce = claimInterest.nonces(user); 31 | uint256 expiredTime = block.timestamp + 1 days; 32 | bytes32 message = keccak256( 33 | abi.encodePacked( 34 | user, uint256(100e6), userNonce, expiredTime, address(claimInterest), claimInterest.getChainId() 35 | ) 36 | ); 37 | bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(message); 38 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, ethSignedMessageHash); 39 | bytes memory signature = abi.encodePacked(r, s, v); 40 | vm.startBroadcast(userKey); 41 | assertEq(token.balanceOf(user), 0); 42 | claimInterest.claimInterest(100e6, userNonce, expiredTime, signature); 43 | assertEq(token.balanceOf(user), 100e6); 44 | // should not be able to claim again with the same nonce 45 | vm.expectRevert(); 46 | claimInterest.claimInterest(100e6, userNonce, expiredTime, signature); 47 | vm.stopBroadcast(); 48 | } 49 | 50 | function test_invalidate_nonce() public { 51 | (address user, uint256 userKey) = makeAddrAndKey("user"); 52 | uint256 userNonce = claimInterest.nonces(user); 53 | vm.prank(signer); 54 | // force increment nonce 55 | claimInterest.incrementNonce(user); 56 | uint256 expiredTime = block.timestamp + 1 days; 57 | bytes32 message = keccak256( 58 | abi.encodePacked( 59 | user, uint256(100e6), userNonce, expiredTime, address(claimInterest), claimInterest.getChainId() 60 | ) 61 | ); 62 | bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(message); 63 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, ethSignedMessageHash); 64 | bytes memory signature = abi.encodePacked(r, s, v); 65 | vm.startBroadcast(userKey); 66 | assertEq(token.balanceOf(user), 0); 67 | vm.expectRevert(); 68 | claimInterest.claimInterest(100e6, userNonce, expiredTime, signature); 69 | vm.stopBroadcast(); 70 | } 71 | 72 | function test_expired_signature() public { 73 | (address user, uint256 userKey) = makeAddrAndKey("user"); 74 | uint256 userNonce = claimInterest.nonces(user); 75 | uint256 expiredTime = block.timestamp - 1; 76 | bytes32 message = keccak256( 77 | abi.encodePacked( 78 | user, uint256(100e6), userNonce, expiredTime, address(claimInterest), claimInterest.getChainId() 79 | ) 80 | ); 81 | bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(message); 82 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, ethSignedMessageHash); 83 | bytes memory signature = abi.encodePacked(r, s, v); 84 | vm.startBroadcast(userKey); 85 | assertEq(token.balanceOf(user), 0); 86 | vm.expectRevert("Signature expired"); 87 | claimInterest.claimInterest(100e6, userNonce, expiredTime, signature); 88 | vm.stopBroadcast(); 89 | } 90 | 91 | function test_change_signer() public { 92 | vm.startBroadcast(ownerKey); 93 | claimInterest.signers(signer); 94 | assertEq(claimInterest.signers(signer), true); 95 | address newSigner = makeAddr("newSigner"); 96 | claimInterest.changeSigner(newSigner, true); 97 | claimInterest.changeSigner(signer, false); 98 | assertEq(claimInterest.signers(newSigner), true); 99 | assertEq(claimInterest.signers(signer), false); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/helper/Bundler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/Test.sol"; 5 | import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; 6 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 7 | import "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; 8 | import "@source/libraries/DecodeCalldata.sol"; 9 | 10 | contract Bundler is Test { 11 | /* 12 | address sender; 13 | uint256 nonce; 14 | bytes initCode; 15 | bytes callData; 16 | uint256 callGasLimit; 17 | uint256 verificationGasLimit; 18 | uint256 preVerificationGas; 19 | uint256 maxFeePerGas; 20 | uint256 maxPriorityFeePerGas; 21 | bytes paymasterAndData; 22 | bytes signature; 23 | */ 24 | function post(IEntryPoint entryPoint, PackedUserOperation memory userOp) external { 25 | PackedUserOperation[] memory userOperations = new PackedUserOperation[](1); 26 | userOperations[0] = userOp; 27 | address payable beneficiary = payable(address(0x111)); 28 | uint256 gas_before = gasleft(); 29 | entryPoint.handleOps(userOperations, beneficiary); 30 | uint256 gas_after = gasleft(); 31 | console.log("entryPoint.handleOps => gas:", gas_before - gas_after); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/helper/UserOpHelper.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; 5 | import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; 6 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 7 | 8 | import "forge-std/Test.sol"; 9 | 10 | abstract contract UserOpHelper is Test { 11 | EntryPoint public entryPoint; 12 | 13 | using MessageHashUtils for bytes32; 14 | 15 | constructor() { 16 | entryPoint = new EntryPoint(); 17 | } 18 | 19 | function signUserOp(PackedUserOperation memory op, uint256 _key, address _validator) 20 | public 21 | view 22 | returns (bytes memory signature) 23 | { 24 | bytes32 hash = entryPoint.getUserOpHash(op); 25 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_key, hash.toEthSignedMessageHash()); 26 | bytes memory opSig; 27 | bytes memory signatureData = abi.encodePacked(r, s, v); 28 | uint8 signType = 0; 29 | bytes4 signatureLength = bytes4(uint32(1 + signatureData.length)); 30 | opSig = abi.encodePacked(_validator, signatureLength, signType, signatureData); 31 | signature = opSig; 32 | } 33 | 34 | function signUserOp(PackedUserOperation memory op, uint256 _key, address _validator, bytes memory hookData) 35 | public 36 | view 37 | returns (bytes memory signature) 38 | { 39 | bytes32 hash = entryPoint.getUserOpHash(op); 40 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_key, hash.toEthSignedMessageHash()); 41 | bytes memory opSig; 42 | bytes memory signatureData = abi.encodePacked(r, s, v); 43 | uint8 signType = 0; 44 | bytes4 signatureLength = bytes4(uint32(1 + signatureData.length)); 45 | opSig = abi.encodePacked(_validator, signatureLength, signType, signatureData, hookData); 46 | signature = opSig; 47 | } 48 | 49 | function signUserOp(EntryPoint _entryPoint, PackedUserOperation memory op, uint256 _key, address _validator) 50 | public 51 | view 52 | returns (bytes memory signature) 53 | { 54 | bytes32 hash = _entryPoint.getUserOpHash(op); 55 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_key, hash.toEthSignedMessageHash()); 56 | bytes memory opSig; 57 | bytes memory signatureData = abi.encodePacked(r, s, v); 58 | uint8 signType = 0; 59 | bytes4 signatureLength = bytes4(uint32(1 + signatureData.length)); 60 | opSig = abi.encodePacked(_validator, signatureLength, signType, signatureData); 61 | signature = opSig; 62 | } 63 | 64 | function signUserOp( 65 | EntryPoint _entryPoint, 66 | PackedUserOperation memory op, 67 | uint256 _key, 68 | address _validator, 69 | bytes memory hookData 70 | ) public view returns (bytes memory signature) { 71 | bytes32 hash = _entryPoint.getUserOpHash(op); 72 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_key, hash.toEthSignedMessageHash()); 73 | bytes memory opSig; 74 | bytes memory signatureData = abi.encodePacked(r, s, v); 75 | uint8 signType = 0; 76 | bytes4 signatureLength = bytes4(uint32(1 + signatureData.length)); 77 | opSig = abi.encodePacked(_validator, signatureLength, signType, signatureData, hookData); 78 | signature = opSig; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/hooks/2fa/Crypto2FAHook.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../../soulwallet/base/SoulWalletInstence.sol"; 6 | import {SoulWalletDefaultValidator} from "@source/validator/SoulWalletDefaultValidator.sol"; 7 | import {Crypto2FAHook} from "@source/hooks/2fa/Crypto2FAHook.sol"; 8 | import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; 9 | import {UserOpHelper} from "../../helper/UserOpHelper.t.sol"; 10 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 11 | import {Bundler} from "../../helper/Bundler.t.sol"; 12 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 13 | import {UserOperationHelper} from "@soulwallet-core/test/dev/userOperationHelper.sol"; 14 | 15 | contract Crypto2FAHookTest is Test, UserOpHelper { 16 | using TypeConversion for address; 17 | using ECDSA for bytes32; 18 | using MessageHashUtils for bytes32; 19 | 20 | SoulWalletInstence public soulWalletInstence; 21 | SoulWalletDefaultValidator public soulWalletDefaultValidator; 22 | ISoulWallet public soulWallet; 23 | address public walletOwner; 24 | uint256 public walletOwnerPrivateKey; 25 | 26 | address public wallet2faOwner; 27 | uint256 public wallet2faOwnerPrivateKey; 28 | 29 | Crypto2FAHook public crypto2FAHook; 30 | 31 | Bundler public bundler; 32 | 33 | function setUp() public { 34 | (walletOwner, walletOwnerPrivateKey) = makeAddrAndKey("owner"); 35 | (wallet2faOwner, wallet2faOwnerPrivateKey) = makeAddrAndKey("2fa"); 36 | crypto2FAHook = new Crypto2FAHook(); 37 | 38 | bytes[] memory modules = new bytes[](0); 39 | bytes[] memory hooks = new bytes[](1); 40 | uint8 capabilityFlags = 3; 41 | // 3 means preUserOpValidationHook and preIsValidSignatureHook 42 | hooks[0] = abi.encodePacked(address(crypto2FAHook), address(wallet2faOwner), capabilityFlags); 43 | 44 | bytes32[] memory owners = new bytes32[](1); 45 | owners[0] = walletOwner.toBytes32(); 46 | soulWalletDefaultValidator = new SoulWalletDefaultValidator(); 47 | bytes32 salt = bytes32(0); 48 | soulWalletInstence = 49 | new SoulWalletInstence(address(0), address(soulWalletDefaultValidator), owners, modules, hooks, salt); 50 | soulWallet = soulWalletInstence.soulWallet(); 51 | (address[] memory preIsValidSignatureHooks, address[] memory preUserOpValidationHooks) = soulWallet.listHook(); 52 | assertEq(preIsValidSignatureHooks.length, 1, "preIsValidSignatureHooks length error"); 53 | assertEq(preUserOpValidationHooks.length, 1, "preUserOpValidationHooks length error"); 54 | assertEq(preIsValidSignatureHooks[0], address(crypto2FAHook), "preIsValidSignatureHooks address error"); 55 | assertEq(preUserOpValidationHooks[0], address(crypto2FAHook), "preUserOpValidationHooks address error"); 56 | } 57 | 58 | function test_hook() public { 59 | vm.deal(address(soulWallet), 1000 ether); 60 | uint256 nonce = 0; 61 | bytes memory initCode; 62 | bytes memory callData; 63 | uint256 callGasLimit; 64 | uint256 verificationGasLimit; 65 | uint256 preVerificationGas; 66 | uint256 maxFeePerGas; 67 | uint256 maxPriorityFeePerGas; 68 | bytes memory paymasterAndData; 69 | { 70 | callGasLimit = 900000; 71 | callData = abi.encodeWithSelector(IStandardExecutor.execute.selector, address(10), 1 ether, ""); 72 | verificationGasLimit = 1000000; 73 | preVerificationGas = 300000; 74 | maxFeePerGas = 100 gwei; 75 | maxPriorityFeePerGas = 100 gwei; 76 | } 77 | PackedUserOperation memory userOperation = UserOperationHelper.newUserOp({ 78 | sender: address(soulWallet), 79 | nonce: nonce, 80 | initCode: initCode, 81 | callData: callData, 82 | callGasLimit: callGasLimit, 83 | verificationGasLimit: verificationGasLimit, 84 | preVerificationGas: preVerificationGas, 85 | maxFeePerGas: maxFeePerGas, 86 | maxPriorityFeePerGas: maxPriorityFeePerGas, 87 | paymasterAndData: paymasterAndData 88 | }); 89 | bytes32 hookSignHash = soulWalletInstence.entryPoint().getUserOpHash(userOperation); 90 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet2faOwnerPrivateKey, hookSignHash.toEthSignedMessageHash()); 91 | bytes memory hookSignatureData = abi.encodePacked(r, s, v); 92 | bytes4 hookSignatureLength = bytes4(uint32(hookSignatureData.length)); 93 | 94 | bytes memory hookAndData = abi.encodePacked(address(crypto2FAHook), hookSignatureLength, hookSignatureData); 95 | vm.startBroadcast(wallet2faOwnerPrivateKey); 96 | PackedUserOperation[] memory ops = new PackedUserOperation[](1); 97 | userOperation.signature = signUserOp( 98 | soulWalletInstence.entryPoint(), 99 | userOperation, 100 | walletOwnerPrivateKey, 101 | address(soulWalletDefaultValidator), 102 | hookAndData 103 | ); 104 | ops[0] = userOperation; 105 | soulWalletInstence.entryPoint().handleOps(ops, payable(walletOwner)); 106 | } 107 | 108 | function test_applyChange2faWithoutInitiateChange2FA() public { 109 | vm.startPrank(address(soulWallet)); 110 | vm.expectRevert("No pending change"); 111 | crypto2FAHook.applyChange2FA(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/modules/upgrade/UpgradeModule.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../../soulwallet/base/SoulWalletInstence.sol"; 6 | import {SoulWalletDefaultValidator} from "@source/validator/SoulWalletDefaultValidator.sol"; 7 | import "@source/modules/upgrade/UpgradeModule.sol"; 8 | import "@source/dev/NewImplementation.sol"; 9 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 10 | 11 | contract UpgradeTest is Test { 12 | using TypeConversion for address; 13 | using ECDSA for bytes32; 14 | 15 | SoulWalletInstence public soulWalletInstence; 16 | ISoulWallet public soulWallet; 17 | address public walletOwner; 18 | uint256 public walletOwnerPrivateKey; 19 | UpgradeModule public upgradeModule; 20 | address public newImplementation; 21 | 22 | function setUp() public { 23 | (walletOwner, walletOwnerPrivateKey) = makeAddrAndKey("owner"); 24 | newImplementation = address(new NewImplementation()); 25 | upgradeModule = new UpgradeModule(newImplementation); 26 | 27 | bytes[] memory modules = new bytes[](1); 28 | bytes memory upgradeModule_initData; 29 | modules[0] = abi.encodePacked(address(upgradeModule), upgradeModule_initData); 30 | bytes[] memory hooks = new bytes[](0); 31 | 32 | bytes32[] memory owners = new bytes32[](1); 33 | owners[0] = walletOwner.toBytes32(); 34 | 35 | bytes32 salt = bytes32(0); 36 | soulWalletInstence = 37 | new SoulWalletInstence(address(0), address(new SoulWalletDefaultValidator()), owners, modules, hooks, salt); 38 | soulWallet = soulWalletInstence.soulWallet(); 39 | 40 | (address[] memory _modules, bytes4[][] memory _selectors) = soulWallet.listModule(); 41 | assertEq(_modules.length, 1, "module length error"); 42 | assertEq(_selectors.length, 1, "selector length error"); 43 | assertEq(_modules[0], address(upgradeModule), "module address error"); 44 | assertEq(_selectors[0].length, 1); 45 | assertEq(_selectors[0][0], bytes4(keccak256("upgradeTo(address)")), "upgradeTo selector error"); 46 | } 47 | 48 | function test_upgrade() public { 49 | bytes32 _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 50 | 51 | bytes32 _oldImplementation = vm.load(address(soulWallet), _IMPLEMENTATION_SLOT); 52 | address oldImplementation; 53 | assembly { 54 | oldImplementation := _oldImplementation 55 | } 56 | upgradeModule.upgrade(address(soulWallet)); 57 | 58 | // test new implementation 59 | (bool success, bytes memory result) = 60 | address(soulWallet).staticcall(abi.encodeWithSelector(NewImplementation.hello.selector)); 61 | require(success, "call failed"); 62 | assertEq(abi.decode(result, (string)), "hello world"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/soulwallet/1271/SoulWallet1271.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../base/SoulWalletInstence.sol"; 6 | import {SoulWalletDefaultValidator} from "@source/validator/SoulWalletDefaultValidator.sol"; 7 | import "@source/libraries/TypeConversion.sol"; 8 | import "@source/abstract/DefaultCallbackHandler.sol"; 9 | import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; 10 | 11 | contract DeployDirectTest is Test { 12 | using TypeConversion for address; 13 | 14 | bytes4 internal constant MAGICVALUE = 0x1626ba7e; 15 | bytes4 internal constant INVALID_ID = 0xffffffff; 16 | 17 | ISoulWallet soulWallet; 18 | SoulWalletInstence soulWalletInstence; 19 | SoulWalletDefaultValidator soulWalletDefaultValidator; 20 | address public walletOwner; 21 | uint256 public walletOwnerPrivateKey; 22 | bytes32 private constant SOUL_WALLET_MSG_TYPEHASH = keccak256("SoulWalletMessage(bytes32 message)"); 23 | 24 | bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = 25 | keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"); 26 | 27 | function encodeRawHash(bytes32 rawHash, address account) private view returns (bytes32) { 28 | bytes32 encode1271MessageHash = keccak256(abi.encode(SOUL_WALLET_MSG_TYPEHASH, rawHash)); 29 | bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, getChainId(), address(account))); 30 | return keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator, encode1271MessageHash)); 31 | } 32 | 33 | function getChainId() private view returns (uint256) { 34 | uint256 id; 35 | // solhint-disable-next-line no-inline-assembly 36 | assembly { 37 | id := chainid() 38 | } 39 | return id; 40 | } 41 | 42 | function setUp() public { 43 | (walletOwner, walletOwnerPrivateKey) = makeAddrAndKey("owner"); 44 | bytes[] memory modules = new bytes[](0); 45 | bytes[] memory hooks = new bytes[](0); 46 | bytes32 salt = bytes32(0); 47 | DefaultCallbackHandler defaultCallbackHandler = new DefaultCallbackHandler(); 48 | bytes32[] memory owners = new bytes32[](1); 49 | owners[0] = walletOwner.toBytes32(); 50 | soulWalletDefaultValidator = new SoulWalletDefaultValidator(); 51 | soulWalletInstence = new SoulWalletInstence( 52 | address(defaultCallbackHandler), address(soulWalletDefaultValidator), owners, modules, hooks, salt 53 | ); 54 | soulWallet = soulWalletInstence.soulWallet(); 55 | } 56 | 57 | function signMsg(uint256 privateKey, bytes32 _hash, address validatorAddress) 58 | private 59 | pure 60 | returns (bytes memory signature) 61 | { 62 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, _hash); 63 | bytes memory signatureData = abi.encodePacked(r, s, v); 64 | uint8 signType = 0; 65 | bytes4 signatureLength = bytes4(uint32(1 + signatureData.length)); 66 | return abi.encodePacked(address(validatorAddress), signatureLength, signType, signatureData); 67 | } 68 | 69 | function testVerify1271Signature() public view { 70 | bytes32 hash = keccak256("hello world"); 71 | bytes32 rawHash = encodeRawHash(hash, address(soulWallet)); 72 | bytes memory signature = signMsg(walletOwnerPrivateKey, rawHash, address(soulWalletDefaultValidator)); 73 | console.log("soulwallet", address(soulWallet)); 74 | bytes4 result = IERC1271(address(soulWallet)).isValidSignature(hash, signature); 75 | assertEq(result, MAGICVALUE); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/soulwallet/base/SoulWalletInstence.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "./SoulWalletLogicInstence.sol"; 5 | import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; 6 | import "@source/factory/SoulWalletFactory.sol"; 7 | import "@source/libraries/TypeConversion.sol"; 8 | import "@source/interfaces/ISoulWallet.sol"; 9 | 10 | contract SoulWalletInstence { 11 | using TypeConversion for address; 12 | 13 | SoulWalletLogicInstence public soulWalletLogicInstence; 14 | SoulWalletFactory public soulWalletFactory; 15 | EntryPoint public entryPoint; 16 | ISoulWallet public soulWallet; 17 | 18 | constructor( 19 | address defaultCallbackHandler, 20 | address defaultValidator, 21 | bytes32[] memory owners, 22 | bytes[] memory modules, 23 | bytes[] memory hooks, 24 | bytes32 salt 25 | ) { 26 | entryPoint = new EntryPoint(); 27 | soulWalletLogicInstence = new SoulWalletLogicInstence(address(entryPoint), address(defaultValidator)); 28 | 29 | soulWalletFactory = new SoulWalletFactory( 30 | address(soulWalletLogicInstence.soulWalletLogic()), address(entryPoint), address(this) 31 | ); 32 | 33 | // soulWalletLogicInstence.initialize(owners, defaultCallbackHandler, modules, hooks); 34 | bytes memory initializer = abi.encodeWithSignature( 35 | "initialize(bytes32[],address,bytes[],bytes[])", owners, defaultCallbackHandler, modules, hooks 36 | ); 37 | address walletAddress1 = soulWalletFactory.getWalletAddress(initializer, salt); 38 | address walletAddress2 = soulWalletFactory.createWallet(initializer, salt); 39 | require(walletAddress1 == walletAddress2, "walletAddress1 != walletAddress2"); 40 | require(walletAddress2.code.length > 0, "wallet code is empty"); 41 | // walletAddress1 as SoulWallet 42 | soulWallet = ISoulWallet(walletAddress1); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/soulwallet/base/SoulWalletLogicInstence.sol: -------------------------------------------------------------------------------- 1 | 2 | 3 | // SPDX-License-Identifier: GPL-3.0 4 | pragma solidity ^0.8.20; 5 | 6 | import "@source/SoulWallet.sol"; 7 | 8 | contract SoulWalletLogicInstence { 9 | SoulWallet public soulWalletLogic; 10 | 11 | constructor(address _entryPoint, address defaultValidator) { 12 | soulWalletLogic = new SoulWallet(_entryPoint, defaultValidator); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /test/soulwallet/deploy/direct.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../base/SoulWalletInstence.sol"; 6 | import {SoulWalletDefaultValidator} from "@source/validator/SoulWalletDefaultValidator.sol"; 7 | 8 | import "@source/libraries/TypeConversion.sol"; 9 | import "@source/dev/tokens/TokenERC20.sol"; 10 | import "@source/abstract/DefaultCallbackHandler.sol"; 11 | 12 | contract DeployDirectTest is Test { 13 | using TypeConversion for address; 14 | 15 | function setUp() public {} 16 | 17 | error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); 18 | 19 | function test_Deploy() public { 20 | bytes[] memory modules = new bytes[](0); 21 | bytes[] memory hooks = new bytes[](0); 22 | bytes32 salt = bytes32(0); 23 | DefaultCallbackHandler defaultCallbackHandler = new DefaultCallbackHandler(); 24 | bytes32[] memory owners = new bytes32[](1); 25 | owners[0] = address(this).toBytes32(); 26 | SoulWalletInstence soulWalletInstence = new SoulWalletInstence( 27 | address(defaultCallbackHandler), address(new SoulWalletDefaultValidator()), owners, modules, hooks, salt 28 | ); 29 | ISoulWallet soulWallet = soulWalletInstence.soulWallet(); 30 | assertEq(soulWallet.isOwner(address(this).toBytes32()), true); 31 | assertEq(soulWallet.isOwner(address(0x1111).toBytes32()), false); 32 | 33 | TokenERC20 token = new TokenERC20(18); 34 | 35 | vm.startPrank(address(soulWalletInstence.entryPoint())); 36 | // execute(address dest, uint256 value, bytes calldata func) 37 | vm.expectRevert( 38 | abi.encodeWithSelector(ERC20InsufficientBalance.selector, address(soulWalletInstence.soulWallet()), 0, 1) 39 | ); 40 | soulWallet.execute(address(token), 0, abi.encodeWithSignature("transfer(address,uint256)", address(0x1), 1)); 41 | vm.stopPrank(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/soulwallet/deploy/protocol.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/Test.sol"; 5 | import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; 6 | import {ISoulWallet} from "@source/interfaces/ISoulWallet.sol"; 7 | import "@source/validator/SoulWalletDefaultValidator.sol"; 8 | import {SoulWalletFactory} from "@source/factory/SoulWalletFactory.sol"; 9 | import "@source/abstract/DefaultCallbackHandler.sol"; 10 | import "@source/libraries/TypeConversion.sol"; 11 | import {SoulWalletLogicInstence} from "../base/SoulWalletLogicInstence.sol"; 12 | import {UserOpHelper} from "../../helper/UserOpHelper.t.sol"; 13 | import {Bundler} from "../../helper/Bundler.t.sol"; 14 | import {UserOperationHelper} from "@soulwallet-core/test/dev/userOperationHelper.sol"; 15 | 16 | contract DeployProtocolTest is Test, UserOpHelper { 17 | using TypeConversion for address; 18 | 19 | SoulWalletDefaultValidator public soulWalletDefaultValidator; 20 | SoulWalletLogicInstence public soulWalletLogicInstence; 21 | SoulWalletFactory public soulWalletFactory; 22 | Bundler public bundler; 23 | 24 | function setUp() public { 25 | entryPoint = new EntryPoint(); 26 | soulWalletDefaultValidator = new SoulWalletDefaultValidator(); 27 | soulWalletLogicInstence = new SoulWalletLogicInstence(address(entryPoint), address(soulWalletDefaultValidator)); 28 | address logic = address(soulWalletLogicInstence.soulWalletLogic()); 29 | 30 | soulWalletFactory = new SoulWalletFactory(logic, address(entryPoint), address(this)); 31 | require(soulWalletFactory._WALLETIMPL() == logic, "logic address not match"); 32 | 33 | bundler = new Bundler(); 34 | } 35 | 36 | function test_Deploy() public { 37 | address sender; 38 | uint256 nonce; 39 | bytes memory initCode; 40 | bytes memory callData; 41 | uint256 callGasLimit = 10000000; 42 | uint256 verificationGasLimit; 43 | uint256 preVerificationGas; 44 | uint256 maxFeePerGas; 45 | uint256 maxPriorityFeePerGas; 46 | bytes memory paymasterAndData; 47 | 48 | (address walletOwner, uint256 walletOwnerPrivateKey) = makeAddrAndKey("walletOwner"); 49 | { 50 | nonce = 0; 51 | 52 | bytes[] memory modules = new bytes[](0); 53 | bytes[] memory hooks = new bytes[](0); 54 | 55 | bytes32 salt = bytes32(0); 56 | 57 | DefaultCallbackHandler defaultCallbackHandler = new DefaultCallbackHandler(); 58 | bytes32[] memory owners = new bytes32[](1); 59 | owners[0] = walletOwner.toBytes32(); 60 | bytes memory initializer = abi.encodeWithSignature( 61 | "initialize(bytes32[],address,bytes[],bytes[])", owners, defaultCallbackHandler, modules, hooks 62 | ); 63 | sender = soulWalletFactory.getWalletAddress(initializer, salt); 64 | 65 | /* 66 | function createWallet(bytes memory _initializer, bytes32 _salt) 67 | */ 68 | bytes memory soulWalletFactoryCall = 69 | abi.encodeWithSignature("createWallet(bytes,bytes32)", initializer, salt); 70 | initCode = abi.encodePacked(address(soulWalletFactory), soulWalletFactoryCall); 71 | 72 | verificationGasLimit = 2000000; 73 | preVerificationGas = 200000; 74 | maxFeePerGas = 10 gwei; 75 | maxPriorityFeePerGas = 10 gwei; 76 | } 77 | 78 | PackedUserOperation memory userOperation = UserOperationHelper.newUserOp( 79 | sender, 80 | nonce, 81 | initCode, 82 | callData, 83 | callGasLimit, 84 | verificationGasLimit, 85 | preVerificationGas, 86 | maxFeePerGas, 87 | maxPriorityFeePerGas, 88 | paymasterAndData 89 | ); 90 | 91 | bytes32 userOpHash = entryPoint.getUserOpHash(userOperation); 92 | (userOpHash); 93 | userOperation.signature = signUserOp(userOperation, walletOwnerPrivateKey, address(soulWalletDefaultValidator)); 94 | vm.expectRevert(); 95 | bundler.post(entryPoint, userOperation); 96 | assertEq(sender.code.length, 0, "A1:sender.code.length != 0"); 97 | 98 | vm.deal(userOperation.sender, 10 ether); 99 | bundler.post(entryPoint, userOperation); 100 | assertEq(sender.code.length > 0, true, "A2:sender.code.length == 0"); 101 | ISoulWallet soulWallet = ISoulWallet(sender); 102 | assertEq(soulWallet.isOwner(walletOwner.toBytes32()), true); 103 | assertEq(soulWallet.isOwner(address(0x1111).toBytes32()), false); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/soulwallet/factory/SoulWalletFactory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/Test.sol"; 5 | import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; 6 | import "@source/validator/SoulWalletDefaultValidator.sol"; 7 | import {SoulWalletFactory} from "@source/factory/SoulWalletFactory.sol"; 8 | import "@source/libraries/TypeConversion.sol"; 9 | import {SoulWalletLogicInstence} from "../base/SoulWalletLogicInstence.sol"; 10 | import {UserOpHelper} from "../../helper/UserOpHelper.t.sol"; 11 | import {UserOperationHelper} from "@soulwallet-core/test/dev/userOperationHelper.sol"; 12 | import "@source/abstract/DefaultCallbackHandler.sol"; 13 | 14 | contract SoulWalletFactoryTest is Test, UserOpHelper { 15 | using TypeConversion for address; 16 | 17 | SoulWalletDefaultValidator public soulWalletDefaultValidator; 18 | SoulWalletLogicInstence public soulWalletLogicInstence; 19 | SoulWalletFactory public soulWalletFactory; 20 | DefaultCallbackHandler public defaultCallbackHandler; 21 | 22 | function setUp() public { 23 | defaultCallbackHandler = new DefaultCallbackHandler(); 24 | entryPoint = new EntryPoint(); 25 | soulWalletDefaultValidator = new SoulWalletDefaultValidator(); 26 | soulWalletLogicInstence = new SoulWalletLogicInstence(address(entryPoint), address(soulWalletDefaultValidator)); 27 | address logic = address(soulWalletLogicInstence.soulWalletLogic()); 28 | 29 | soulWalletFactory = new SoulWalletFactory(logic, address(entryPoint), address(this)); 30 | require(soulWalletFactory._WALLETIMPL() == logic, "logic address not match"); 31 | } 32 | 33 | function test_deployWallet() public { 34 | bytes[] memory modules; 35 | bytes[] memory hooks; 36 | bytes32[] memory owners = new bytes32[](1); 37 | owners[0] = address(this).toBytes32(); 38 | bytes32 salt = bytes32(0); 39 | bytes memory initializer = abi.encodeWithSignature( 40 | "initialize(bytes32[],address,bytes[],bytes[])", owners, defaultCallbackHandler, modules, hooks 41 | ); 42 | address walletAddress1 = soulWalletFactory.getWalletAddress(initializer, salt); 43 | address walletAddress2 = soulWalletFactory.createWallet(initializer, salt); 44 | require(walletAddress1 == walletAddress2, "walletAddress1 != walletAddress2"); 45 | } 46 | // test return the wallet account address even if it has already been created 47 | 48 | function test_alreadyDeployedWallet() public { 49 | bytes[] memory modules; 50 | bytes[] memory hooks; 51 | bytes32[] memory owners = new bytes32[](1); 52 | owners[0] = address(this).toBytes32(); 53 | bytes32 salt = bytes32(0); 54 | bytes memory initializer = abi.encodeWithSignature( 55 | "initialize(bytes32[],address,bytes[],bytes[])", owners, defaultCallbackHandler, modules, hooks 56 | ); 57 | address walletAddress1 = soulWalletFactory.getWalletAddress(initializer, salt); 58 | address walletAddress2 = soulWalletFactory.createWallet(initializer, salt); 59 | require(walletAddress1 == walletAddress2, "walletAddress1 != walletAddress2"); 60 | address walletAddress3 = soulWalletFactory.createWallet(initializer, salt); 61 | require(walletAddress3 == walletAddress2, "walletAddress3 != walletAddress2"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true 9 | } 10 | } --------------------------------------------------------------------------------