From 3787306f9e96f9f6a74f81f765a8a0fcdaa6b398 Mon Sep 17 00:00:00 2001 From: filmakarov Date: Thu, 29 Feb 2024 17:06:21 +0300 Subject: [PATCH] [WIP] Refactor and Start implementing methods (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐Ÿ™ˆ Add cache_forge to .gitignore * Add .husky to .gitignore * ๐ŸŽจ Update tab width to 4 spaces in .prettierrc * ๐Ÿšง Disable no-inline-assembly rule in .solhint.json * โšก๏ธ Add account-abstraction dependency and update husky hooks * ๐Ÿ”ฅ Remove Git hooks for branch name validation * ๐Ÿ”ง Update tsconfig.json to disable strict mode * ๐ŸŽจ Update Solidity version and remappings, delete unused contracts and tests * ๐Ÿ™ˆ Add .vscode/settings.json to .gitignore * ๐Ÿ”ฅ Delete unused contracts and tests * โœจ Add utility functions for conversion and formatting * โœจ Add buildUserOp function to utils.ts * โœจ Add git hook to check branch names * โœจ Add SmartAccount contract * โœจ Add AccountConfig contract implementation * โœจ Add Execution contract for account execution * โœจ Add ModuleConfig contract for managing modules * โœจ Add Storage contract for isolated storage access * โœจ Add Validator contract for user operation validation * โšก๏ธ Add ERC-7579 interfaces for smart account configuration, execution, module, and module configuration * โœจ Add IStorage interface for ERC20 account storage * ๐Ÿš€ Add deployment of SmartAccount contract * ๐Ÿšง Update import statement and variable name in Deploy script * โšก๏ธ Add Entrypoint 0.7.0 * โšก๏ธ Add Imports.sol for consolidated imports * โœ… Add SmartAccount test file * ๐Ÿ‘ท Remove unnecessary branch from PR Automation Workflow * โœ๏ธ Update storage location for SmartAccount in contracts * Feat/slither (#14) * ๐Ÿ‘ท Add Slither analysis workflow * โ™ป๏ธ Update Slither workflow to include Foundry installation*** * Add permissions and update Slither configuration * Update Slither workflow to ensure tool availability * Update Slither workflow * Add Slither workflow to run static analysis * ๐Ÿ’š add yarn.lock * โœจ Update Slither workflow to include Node.js setup and SARIF report generation * Remove SARIF file upload step in slither.yml * Update Slither configuration in workflow * ๐Ÿšง Update Slither workflow to include Foundry installation and contract building * Remove target directory for analysis in slither.yml workflow * ๐Ÿš‘ Update Slither workflow to include SARIF file upload * Add token to SARIF file upload * Add comment.js and update slither.yml workflow * Update node version in slither.yml * Update Slither workflow to fail on medium severity issues * Add target directory for Slither analysis * Update slither.yml with filter paths for mock contracts * Update slither-args in slither.yml * Fix slither-args path in GitHub workflow * Update slither configuration to exclude node_modules directory * Update slither-args in slither.yml workflow * Update Slither version to 0.10.0 * Update slither.yml to fail on no severity issues * Add check for pull request event in comment.js * Update GitHub Actions workflow to trigger on pull requests (#15) * Update GitHub Actions workflow to trigger on pull requests * Update Slither workflow permissions and arguments * Refactor CI workflow and remove redundant coverage and slither workflows * Update Node.js and Foundry versions * Add Foundry to PATH * Update CI workflow to install lcov and make other improvements * Add cache for Foundry Toolchain * Update CI workflow configuration * Refactor GitHub Actions workflow*** * Update CI workflow to cache node_modules and Foundry toolchain * Update CI workflow and add linting, unit tests, coverage, and static analysis * Update cache keys and add Foundry toolchain * Add Foundry cache key generation and ensure Foundry directory exists * Update CI workflow and cache actions*** * Update CI workflow and dependencies * Update CI workflow to install Foundry and generate coverage report * Add lcov installation step and update Codecov upload for Foundry and Hardhat coverage reports * Refactor CI workflow and add Slither analysis * Update CI Workflow to include linting, unit tests, coverage, and slither analysis * lint comment.js module * prettier on branch name check and add comments * Refactor CI workflow and update Slither analysis * Refactor comment.js and ci.yml to improve Slither analysis report generation * Add Module contract implementation * Fix returnData initialization bug in Execution.sol and import IModule in Module.sol * Refactor getEmoji function to use text instead of impact level * Update TYPE_ID constant to uint256 * Refactor comment.js to add URL shortening and emoji processing * Update constant declaration in Module.sol * Refactor comment.js to improve readability and add emojis * Refactor comment.js to improve URL shortening and emoji handling * refactor, change acc id * :art: further refactor. add module types * :gear: validateUserOp implementation * :white_check_mark: add execute via EP test * Add remappings and solady module * ๐Ÿ“ฆ Add new dependencies and update existing ones * ๐Ÿ”จ Update package.json with new check-branch-name script * Add 'deployments' to .gitignore * Add hardhat-deploy package * Refactor comment.js * Refactor buildUserOp function and add new utility functions * Add UserOperation and PackedUserOperation interfaces * ๐Ÿ“ฆ Update dependencies in yarn.lock * Update husky hooks in package.json * ๐Ÿ”ฅ Remove unused contract files * ๐Ÿ› revert remappings in remappings.txt due to issue with hh * Remove duplicated account-abstraction dep * Update import paths for PackedUserOperation * Remove unused contracts and imports * โ™ป๏ธ Update AccountConfig implementation ID * ๐Ÿ™ˆ Add .solcover.js configuration file * ๐Ÿ™ˆ add solcover * ๐Ÿ”ฅ remove utils * ๐Ÿ”ฅ remove account.test.ts * ๐ŸŽจ improve code with interface * โœจ add encoding utils function * โœจ add helpers for operation * ๐Ÿ”ฅ remove unused module * ๐Ÿš€ utils for deployment * โœจ add Counter test contract * โœ… add mockvalidator for test purpose * ๐ŸŽจ simplify Execution funcs for quick test * โœจ add onInstall hook on moduleManager * ๐Ÿ”ฅ remove empty useless contract * ๐Ÿš€ add basic AccountFactory * ๐ŸŽจ add interface for AccountFactory * โœ… add deployment tests * โœ… add configuration tests * โœ… add module management tests * โœ… add acc execution tests * ๐Ÿšจ lint fix * chore: forge init * forge install: forge-std v1.7.6 * chore: forge init * Delete CounterTest contract and related tests * Remove submodule lib/forge-std * ๐Ÿ”ง Update remappings in remappings.txt * ๐Ÿ“ฆ Add ds-test dependency * ๐Ÿ“ฆ Add ds-test dependency * โœจ Add computeAccountAddress function to AccountFactory * Add test function to ignore coverage of ModuleTypeLib.sol * Add test function to MockValidator to ignore coverage * Remove forge-std subproject * Update import path for Script.sol * โœจ Add Structs.sol with ModuleType enum * ๐Ÿšš Update import statement in Storage.sol * ๐Ÿšš Update module interface in AccountFactory * Remove deprecated interfaces * Add IStorage interface definition * Update imports in Imports.sol * โœจ Add Helpers.sol with utility functions * Refactor code to improve performance and readability * Refactor BicoTestBase and import Helpers and console2.sol * Add Forge-std Test import and refactor newWallet function * ๐Ÿšจ Lint fix * Delete unnecessary files * ๐Ÿ”ฅ Remove unused function isInitialized() * โšก๏ธ Refactor comment posting logic to delete existing Slither comments * lint fix (Remove empty line in IModule.sol) * ๐Ÿ› fix missing var on comment.js * Add uniqueSlitherHeader to markdownComment * Remove test workflow * refactor * proposed naming convention changes * rename to supportsExecutionMode * lint refactor * refactor as per discussion / PR * fix linter with unused import * :art: lint --------- Co-authored-by: aboudjem Co-authored-by: Filipp Makarov Co-authored-by: livingrockrises <90545960+livingrockrises@users.noreply.github.com> --- .github/scripts/comment.js | 96 ++++-- .github/workflows/ci.yml | 2 +- .gitignore | 1 + .solcover.js | 3 + README.md | 200 ++--------- contracts/Account/AccountConfig.sol | 34 -- contracts/Account/Execution.sol | 50 --- contracts/SmartAccount.sol | 40 ++- contracts/base/AccountConfig.sol | 25 ++ contracts/base/AccountExecution.sol | 39 +++ contracts/base/BaseAccount.sol | 57 ++++ .../ModuleManager.sol} | 36 +- contracts/{Account => base}/Storage.sol | 4 +- contracts/factory/AccountFactory.sol | 42 +++ .../Validator.sol => interfaces/IAccount.sol} | 13 +- contracts/interfaces/IModule.sol | 33 -- .../interfaces/{ => base}/IAccountConfig.sol | 6 +- .../IAccountExecution.sol} | 15 +- contracts/interfaces/base/IHookManager.sol | 11 + .../IModuleManager.sol} | 21 +- contracts/interfaces/{ => base}/IStorage.sol | 2 +- .../interfaces/factory/IAccountFactory.sol | 7 + .../interfaces/modules/IERC7579Modules.sol | 20 ++ contracts/interfaces/modules/IExecutor.sol | 8 + contracts/interfaces/modules/IFallback.sol | 8 + contracts/interfaces/modules/IHook.sol | 9 + contracts/interfaces/modules/IModule.sol | 46 +++ contracts/interfaces/modules/IValidator.sol | 33 ++ contracts/lib/ModuleTypeLib.sol | 33 ++ contracts/test/mocks/Counter.sol | 22 ++ contracts/{Mock => test/mocks}/Entrypoint.sol | 4 - contracts/test/mocks/MockValidator.sol | 70 ++++ hardhat.config.ts | 1 + package.json | 13 +- remappings.txt | 4 +- script/Counter.s.sol | 12 + scripts/git-hooks/checkBranchNames.js | 12 +- src/Counter.sol | 14 + test/foundry/Account.t.sol | 177 ++++++---- test/foundry/Imports.sol | 26 -- test/foundry/utils/BicoTestBase.t.sol | 38 +++ test/foundry/utils/CheatCodes.sol | 77 +++++ test/foundry/utils/Helpers.sol | 177 ++++++++++ test/foundry/utils/Imports.sol | 46 +++ test/foundry/utils/Structs.sol | 9 + .../hardhat/01_SmartAccountDeployment.test.ts | 195 +++++++++++ test/hardhat/02_Configuration.test.ts | 47 +++ test/hardhat/03_ModuleManagement.test.ts | 75 +++++ test/hardhat/04_Execution.test.ts | 158 +++++++++ test/hardhat/Account.test.ts | 177 ---------- test/hardhat/utils/deployment.ts | 164 +++++++++ test/hardhat/utils/encoding.ts | 60 ++++ test/hardhat/utils/operationHelpers.ts | 312 ++++++++++++++++++ test/hardhat/utils/types.ts | 63 ++++ test/hardhat/utils/utils.ts | 84 ----- yarn.lock | 20 +- 56 files changed, 2228 insertions(+), 723 deletions(-) create mode 100644 .solcover.js delete mode 100644 contracts/Account/AccountConfig.sol delete mode 100644 contracts/Account/Execution.sol create mode 100644 contracts/base/AccountConfig.sol create mode 100644 contracts/base/AccountExecution.sol create mode 100644 contracts/base/BaseAccount.sol rename contracts/{Account/ModuleConfig.sol => base/ModuleManager.sol} (59%) rename contracts/{Account => base}/Storage.sol (92%) create mode 100644 contracts/factory/AccountFactory.sol rename contracts/{Account/Validator.sol => interfaces/IAccount.sol} (91%) delete mode 100644 contracts/interfaces/IModule.sol rename contracts/interfaces/{ => base}/IAccountConfig.sol (80%) rename contracts/interfaces/{IExecution.sol => base/IAccountExecution.sol} (76%) create mode 100644 contracts/interfaces/base/IHookManager.sol rename contracts/interfaces/{IModuleConfig.sol => base/IModuleManager.sol} (59%) rename contracts/interfaces/{ => base}/IStorage.sol (91%) create mode 100644 contracts/interfaces/factory/IAccountFactory.sol create mode 100644 contracts/interfaces/modules/IERC7579Modules.sol create mode 100644 contracts/interfaces/modules/IExecutor.sol create mode 100644 contracts/interfaces/modules/IFallback.sol create mode 100644 contracts/interfaces/modules/IHook.sol create mode 100644 contracts/interfaces/modules/IModule.sol create mode 100644 contracts/interfaces/modules/IValidator.sol create mode 100644 contracts/lib/ModuleTypeLib.sol create mode 100644 contracts/test/mocks/Counter.sol rename contracts/{Mock => test/mocks}/Entrypoint.sol (61%) create mode 100644 contracts/test/mocks/MockValidator.sol create mode 100644 script/Counter.s.sol create mode 100644 src/Counter.sol delete mode 100644 test/foundry/Imports.sol create mode 100644 test/foundry/utils/BicoTestBase.t.sol create mode 100644 test/foundry/utils/CheatCodes.sol create mode 100644 test/foundry/utils/Helpers.sol create mode 100644 test/foundry/utils/Imports.sol create mode 100644 test/foundry/utils/Structs.sol create mode 100644 test/hardhat/01_SmartAccountDeployment.test.ts create mode 100644 test/hardhat/02_Configuration.test.ts create mode 100644 test/hardhat/03_ModuleManagement.test.ts create mode 100644 test/hardhat/04_Execution.test.ts delete mode 100644 test/hardhat/Account.test.ts create mode 100644 test/hardhat/utils/deployment.ts create mode 100644 test/hardhat/utils/encoding.ts create mode 100644 test/hardhat/utils/operationHelpers.ts create mode 100644 test/hardhat/utils/types.ts delete mode 100644 test/hardhat/utils/utils.ts diff --git a/.github/scripts/comment.js b/.github/scripts/comment.js index 3b9b5c3f..bfd172fc 100644 --- a/.github/scripts/comment.js +++ b/.github/scripts/comment.js @@ -1,28 +1,82 @@ module.exports = async ({ github, context, header, body }) => { - const comment = [header, body].join("\n"); - - // Check if the workflow is triggered by a pull request event - if (!context.payload.pull_request) { - console.log('This workflow is not triggered by a pull request. Skipping comment creation/update.'); - return; - } - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, + const uniqueSlitherHeader = "# Slither report"; + + // Function to select emoji based on the impact level found in the text + const getEmoji = (text) => { + if (text.includes("High")) return ":red_circle:"; + if (text.includes("Medium")) return ":yellow_circle:"; + if (text.includes("Low")) return ":large_blue_circle:"; + if (text.includes("Informational")) return ":information_source:"; + return ""; + }; + + // Function to shorten GitHub URLs to Markdown link format + const shortenUrls = (text) => { + const urlRegex = + /https:\/\/github\.com\/([\w-]+\/[\w-]+)\/blob\/([a-z0-9]+)\/(.+?)(#L\d+(-L\d+)?)/g; + return text.replace(urlRegex, (_, repo, commit, path, hash) => { + const shortPath = path.replace(/^contracts\/contracts\//, ""); + return `[${shortPath}${hash}](https://github.com/${repo}/blob/${commit}/${path}${hash})`; }); - - const botComment = comments.find( - comment => comment.user.type === 'Bot' && comment.body.startsWith(header) + }; + + // Process the body to add emojis and shorten URLs + const processedBody = body + .split("\n") + .map((line) => { + let processedLine = shortenUrls(line); // Apply URL shortening + const emoji = getEmoji(processedLine); + return emoji ? `${emoji} ${processedLine}` : processedLine; + }) + .join("\n"); + + const markdownComment = ` +## :robot: Slither Analysis Report :mag_right: + +${uniqueSlitherHeader} + +${header} + +${processedBody} + +_This comment was automatically generated by the GitHub Actions workflow._ +`; + + // Check if the workflow is triggered by a pull request event + if (!context.payload.pull_request) { + console.log( + "This workflow is not triggered by a pull request. Skipping comment creation/update.", ); - - const commentFn = botComment ? 'updateComment' : 'createComment'; - - await github.rest.issues[commentFn]({ + return; + } + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + }); + + // Delete all Slither comments before posting a new one + for (const comment of comments.filter(comment => comment.user.type === "Bot" && comment.body.includes(uniqueSlitherHeader))) { + await github.rest.issues.deleteComment({ owner: context.repo.owner, repo: context.repo.repo, - body: comment, - ...(botComment ? { comment_id: botComment.id } : { issue_number: context.payload.pull_request.number }), + comment_id: comment.id, }); + } + + + // After deleting, post a new comment + const response = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: markdownComment, + }); + + console.log( + response.status === 200 + ? "Slither analysis comment created or updated successfully." + : "Failed to create or update the comment.", + ); }; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 539637f8..9c435ab6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,7 +136,7 @@ jobs: slither-version: "0.10.0" node-version: "18" fail-on: "none" - slither-args: '--filter-paths "contracts/mock|node_modules" --checklist --markdown-root ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/contracts/' + slither-args: '--exclude assembly --exclude solc-version --filter-paths "contracts/mock|node_modules" --checklist --markdown-root ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/contracts/' - name: Create/update checklist as PR comment uses: actions/github-script@v7 diff --git a/.gitignore b/.gitignore index 99392764..c7a3c407 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ out docs storageLayout .husky +deployments # files *.env diff --git a/.solcover.js b/.solcover.js new file mode 100644 index 00000000..eef0a7ea --- /dev/null +++ b/.solcover.js @@ -0,0 +1,3 @@ +module.exports = { + skipFiles: ["test", "lib/ModuleTypeLib"], +}; diff --git a/README.md b/README.md index 3dd95f8e..8817d6ab 100644 --- a/README.md +++ b/README.md @@ -1,194 +1,66 @@ -[![Biconomy](https://img.shields.io/badge/Made_with_%F0%9F%8D%8A_by-Biconomy-ff4e17?style=flat)](https://biconomy.io) [![License MIT](https://img.shields.io/badge/License-MIT-blue?&style=flat)](./LICENSE) [![Hardhat](https://img.shields.io/badge/Built%20with-Hardhat-FFDB1C.svg)](https://hardhat.org/) [![Foundry](https://img.shields.io/badge/Built%20with-Foundry-FFBD10.svg)](https://getfoundry.sh/) +## Foundry -![Codecov Hardhat Coverage](https://img.shields.io/codecov/c/github/bcnmy/erc7579-modular-smart-account?token=oyX38XKbO9&flag=hardhat&label=Hardhat%20Coverage&logo=codecov) ![Codecov Foundry Coverage](https://img.shields.io/codecov/c/github/bcnmy/erc7579-modular-smart-account?token=oyX38XKbO9&flag=foundry&label=Foundry%20Coverage&logo=codecov) +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** -# ERC-7579 Modular Smart Account Base ๐Ÿš€ +Foundry consists of: -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/bcnmy/erc7579-modular-smart-account) +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. -This repository serves as a comprehensive foundation for smart contract projects, streamlining the development process with a focus on best practices, security, and efficiency. +## Documentation -## ๐Ÿ“š Table of Contents +https://book.getfoundry.sh/ -- [ERC-7579 Modular Smart Account Base ๐Ÿš€](#erc-7579-modular-smart-account-base-) - - [๐Ÿ“š Table of Contents](#-table-of-contents) - - [Getting Started](#getting-started) - - [Prerequisites](#prerequisites) - - [Installation](#installation) - - [๐Ÿ› ๏ธ Essential Scripts](#๏ธ-essential-scripts) - - [๐Ÿ—๏ธ Build Contracts](#๏ธ-build-contracts) - - [๐Ÿงช Run Tests](#-run-tests) - - [โ›ฝ Gas Report](#-gas-report) - - [๐Ÿ“Š Coverage Report](#-coverage-report) - - [๐Ÿ“„ Documentation](#-documentation) - - [๐Ÿš€ Deploy Contracts](#-deploy-contracts) - - [๐ŸŽจ Lint Code](#-lint-code) - - [๐Ÿ–Œ๏ธ Auto-fix Linting Issues](#๏ธ-auto-fix-linting-issues) - - [๐Ÿš€ Generating Storage Layout](#-generating-storage-layout) - - [๐Ÿ”’ Security Audits](#-security-audits) - - [๐Ÿ† Biconomy Champions League ๐Ÿ†](#-biconomy-champions-league-) - - [Champions Roster](#champions-roster) - - [Entering the League](#entering-the-league) - - [Documentation and Resources](#documentation-and-resources) - - [License](#license) - - [Connect with Biconomy ๐ŸŠ](#connect-with-biconomy-) +## Usage -## Getting Started +### Build -To kickstart, follow these steps: - -### Prerequisites - -- Node.js (v18.x or later) -- Yarn (or npm) -- Foundry (Refer to [Foundry installation instructions](https://getfoundry.sh/docs/installation)) - -### Installation - -1. **Clone the repository:** - -```bash -git clone https://github.com/bcnmy/erc7579-modular-smart-account.git -cd erc7579-modular-smart-account -``` - -2. **Install dependencies:** - -```bash -yarn install -``` - -3. **Setup environment variables:** - -Copy `.env.example` to `.env` and fill in your details. - -## ๐Ÿ› ๏ธ Essential Scripts - -Execute key operations for Foundry and Hardhat with these scripts. Append `:forge` or `:hardhat` to run them in the respective environment. - -### ๐Ÿ—๏ธ Build Contracts - -```bash -yarn build +```shell +$ forge build ``` -Compiles contracts for both Foundry and Hardhat. +### Test -### ๐Ÿงช Run Tests - -```bash -yarn test +```shell +$ forge test ``` -Carries out tests to verify contract functionality. - -### โ›ฝ Gas Report +### Format -```bash -yarn test:gas +```shell +$ forge fmt ``` -Creates detailed reports for test coverage. - -### ๐Ÿ“Š Coverage Report +### Gas Snapshots -```bash -yarn coverage +```shell +$ forge snapshot ``` -Creates detailed reports for test coverage. +### Anvil -### ๐Ÿ“„ Documentation - -```bash -yarn docs +```shell +$ anvil ``` -Generate documentation from NatSpec comments. - -### ๐Ÿš€ Deploy Contracts +### Deploy -```bash -yarn deploy +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key ``` -Deploys contracts onto the blockchain network. - -### ๐ŸŽจ Lint Code +### Cast -```bash -yarn lint +```shell +$ cast ``` -Checks code for style and potential errors. +### Help -### ๐Ÿ–Œ๏ธ Auto-fix Linting Issues - -```bash -yarn lint:fix +```shell +$ forge --help +$ anvil --help +$ cast --help ``` - -Automatically fixes linting problems found. - -### ๐Ÿš€ Generating Storage Layout - -```bash -yarn check -``` - -To generate reports of the storage layout for potential upgrades safety using `hardhat-storage-layout`. - -๐Ÿ”„ Add `:forge` or `:hardhat` to any script above to target only Foundry or Hardhat environment, respectively. - -## ๐Ÿ”’ Security Audits - -| Auditor | Date | Final Report Link | -| --------- | ---------- | ----------------------- | -| Firm Name | DD-MM-YYYY | [View Report](./audits) | -| Firm Name | DD-MM-YYYY | [View Report](./audits) | -| Firm Name | DD-MM-YYYY | [View Report](./audits) | - -## ๐Ÿ† Biconomy Champions League ๐Ÿ† - -Welcome to the Champions League, a place where your contributions to Biconomy are celebrated and immortalized in our Hall of Fame. This elite group showcases individuals who have significantly advanced our mission, from enhancing code efficiency to strengthening security, and enriching our documentation. - -### Champions Roster - -| ๐ŸŠ Contributor | ๐Ÿ›ก๏ธ Domain | -| -------------- | ----------------- | -| @user1 | Code Optimization | -| @user2 | Security | -| @user3 | Documentation | -| ... | ... | - -### Entering the League - -Your journey to becoming a champion can start in any domain: - -- **Code Wizards**: Dive into our [Gas Optimization](./GAS_OPTIMIZATION.md) efforts. -- **Security Guardians**: Enhance our safety following the [Security Guidelines](./SECURITY.md). -- **Documentation Scribes**: Elevate our knowledge base with your contributions. - -The **Champions League** is not just a recognition, it's a testament to the impactful work done by our community. Whether you're optimizing gas usage or securing our contracts, your contributions help shape the future of Biconomy. - -> **To Join**: Leave a lasting impact in your chosen area. Our Hall of Fame is regularly updated to honor our most dedicated contributors. - -Let's build a legacy together, championing innovation and excellence in the blockchain space. - -## Documentation and Resources - -For a comprehensive understanding of our project and to contribute effectively, please refer to the following resources: - -- [**Contributing Guidelines**](./CONTRIBUTING.md): Learn how to contribute to our project, from code contributions to documentation improvements. -- [**Code of Conduct**](./CODE_OF_CONDUCT.md): Our commitment to fostering an open and welcoming environment. -- [**Security Policy**](./SECURITY.md): Guidelines for reporting security vulnerabilities. -- [**Gas Optimization Program**](./GAS_OPTIMIZATION.md): Contribute towards optimizing gas efficiency of our smart contracts. -- [**Changelog**](./CHANGELOG.md): Stay updated with the changes and versions. - -## License - -This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. - -## Connect with Biconomy ๐ŸŠ - -[![Website](https://img.shields.io/badge/๐ŸŠ-Website-ff4e17?style=for-the-badge&logoColor=white)](https://biconomy.io) [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/biconomy) [![Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/biconomy) [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/company/biconomy) [![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/biconomy) [![YouTube](https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/channel/UC0CtA-Dw9yg-ENgav_VYjRw) [![GitHub](https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/bcnmy/) diff --git a/contracts/Account/AccountConfig.sol b/contracts/Account/AccountConfig.sol deleted file mode 100644 index d9a6eb9f..00000000 --- a/contracts/Account/AccountConfig.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import { IAccountConfig } from "../interfaces/IAccountConfig.sol"; - -contract AccountConfig is IAccountConfig { - /** - * @notice Returns the account id of the smart account. - * @return accountImplementationId The account id of the smart account. - */ - function accountId() external view returns (string memory accountImplementationId) { - return "ModularSmartAccount"; - } - - /** - * @notice Checks if the account supports a certain execution mode. - * @param encodedMode The encoded mode. - * @return True if the account supports the mode, false otherwise. - */ - function supportsAccountMode(bytes32 encodedMode) external view returns (bool) { - encodedMode; - return true; - } - - /** - * @notice Checks if the account supports a certain module typeId. - * @param moduleTypeId The module type ID. - * @return True if the account supports the module type, false otherwise. - */ - function supportsModule(uint256 moduleTypeId) external view returns (bool) { - moduleTypeId; - return true; - } -} diff --git a/contracts/Account/Execution.sol b/contracts/Account/Execution.sol deleted file mode 100644 index 191dd551..00000000 --- a/contracts/Account/Execution.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; -import { IExecution } from "../interfaces/IExecution.sol"; - -contract Execution is IExecution { - /** - * @notice Executes a transaction on behalf of the account. - * @dev Must ensure adequate authorization control. - * @param mode The encoded execution mode of the transaction. - * @param executionCalldata The encoded execution call data. - */ - function execute(bytes32 mode, bytes calldata executionCalldata) external payable { - mode; - executionCalldata; - } - - /** - * @notice Executes a transaction on behalf of the account via an Executor Module. - * @dev Must ensure adequate authorization control. - * @param mode The encoded execution mode of the transaction. - * @param executionCalldata The encoded execution call data. - * @return returnData The return data from the executed call. - */ - function executeFromExecutor( - bytes32 mode, - bytes calldata executionCalldata - ) - external - payable - returns (bytes[] memory returnData) - { - mode; - executionCalldata; - bytes[] memory returnData = new bytes[](0); - return returnData; - } - - /** - * @notice Executes a user operation as per ERC-4337. - * @dev This function is intended to be called by the ERC-4337 EntryPoint contract. - * @param userOp The packed user operation data. - * @param userOpHash The hash of the packed user operation. - */ - function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external payable { - userOp; - userOpHash; - } -} diff --git a/contracts/SmartAccount.sol b/contracts/SmartAccount.sol index 421a6c16..15be58fa 100644 --- a/contracts/SmartAccount.sol +++ b/contracts/SmartAccount.sol @@ -1,13 +1,41 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity ^0.8.24; -import { AccountConfig } from "./Account/AccountConfig.sol"; -import { Execution } from "./Account/Execution.sol"; -import { ModuleConfig } from "./Account/ModuleConfig.sol"; -import { Validator } from "./Account/Validator.sol"; +import { AccountConfig } from "./base/AccountConfig.sol"; +import { AccountExecution } from "./base/AccountExecution.sol"; +import { ModuleManager } from "./base/ModuleManager.sol"; +import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import { BaseAccount } from "./base/BaseAccount.sol"; +import { IValidator } from "./interfaces/modules/IValidator.sol"; -contract SmartAccount is AccountConfig, Execution, ModuleConfig, Validator { +contract SmartAccount is AccountConfig, AccountExecution, ModuleManager, BaseAccount { constructor() { // solhint-disable-previous-line no-empty-blocks } + + /// @inheritdoc BaseAccount + /// @dev expects IValidator module address to be encoded in the nonce + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) + external + virtual + override + payPrefund(missingAccountFunds) + returns (uint256) + { + address validator; + uint256 nonce = userOp.nonce; + assembly { + validator := shr(96, nonce) + } + // check if validator is enabled. If terminate the validation phase. + //if (!_isValidatorInstalled(validator)) return VALIDATION_FAILED; + + // bubble up the return value of the validator module + uint256 validationData = IValidator(validator).validateUserOp(userOp, userOpHash); + return validationData; + } } diff --git a/contracts/base/AccountConfig.sol b/contracts/base/AccountConfig.sol new file mode 100644 index 00000000..3f14890c --- /dev/null +++ b/contracts/base/AccountConfig.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { IAccountConfig } from "../interfaces/base/IAccountConfig.sol"; + +contract AccountConfig is IAccountConfig { + string internal constant _ACCOUNT_IMPLEMENTATION_ID = "biconomy.modular-smart-account.1.0.0-alpha"; + + /// @inheritdoc IAccountConfig + function supportsExecutionMode(bytes32 encodedMode) external view returns (bool) { + encodedMode; + return true; + } + + /// @inheritdoc IAccountConfig + function supportsModule(uint256 moduleTypeId) external view returns (bool) { + moduleTypeId; + return true; + } + + /// @inheritdoc IAccountConfig + function accountId() external pure returns (string memory) { + return _ACCOUNT_IMPLEMENTATION_ID; + } +} diff --git a/contracts/base/AccountExecution.sol b/contracts/base/AccountExecution.sol new file mode 100644 index 00000000..379f17e2 --- /dev/null +++ b/contracts/base/AccountExecution.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { IAccountExecution } from "../interfaces/base/IAccountExecution.sol"; +import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; + +// TODO +// Review this could be an abtract contract +contract AccountExecution is IAccountExecution { + /// @inheritdoc IAccountExecution + function execute(bytes32 mode, bytes calldata executionCalldata) external payable virtual { + mode; + (address target, uint256 value, bytes memory callData) = + abi.decode(executionCalldata, (address, uint256, bytes)); + target.call{ value: value }(callData); + } + + /// @inheritdoc IAccountExecution + function executeFromExecutor( + bytes32 mode, + bytes calldata executionCalldata + ) + external + payable + virtual + returns (bytes[] memory returnData) + { + mode; + (address target, uint256 value, bytes memory callData) = + abi.decode(executionCalldata, (address, uint256, bytes)); + target.call{ value: value }(callData); + } + + /// @inheritdoc IAccountExecution + function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external payable virtual { + userOp; + userOpHash; + } +} diff --git a/contracts/base/BaseAccount.sol b/contracts/base/BaseAccount.sol new file mode 100644 index 00000000..c2e5ac0c --- /dev/null +++ b/contracts/base/BaseAccount.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { IAccount, PackedUserOperation } from "../interfaces/IAccount.sol"; + +abstract contract BaseAccount is IAccount { + error AccountAccessUnauthorized(); + ///////////////////////////////////////////////////// + // Access Control + //////////////////////////////////////////////////// + + modifier onlyEntryPointOrSelf() virtual { + if (!(msg.sender == entryPoint() || msg.sender == address(this))) { + revert AccountAccessUnauthorized(); + } + _; + } + + /// @dev Sends to the EntryPoint (i.e. `msg.sender`) the missing funds for this transaction. + /// Subclass MAY override this modifier for better funds management. + /// (e.g. send to the EntryPoint more than the minimum required, so that in future transactions + /// it will not be required to send again) + /// + /// `missingAccountFunds` is the minimum value this modifier should send the EntryPoint, + /// which MAY be zero, in case there is enough deposit, or the userOp has a paymaster. + modifier payPrefund(uint256 missingAccountFunds) virtual { + _; + /// @solidity memory-safe-assembly + assembly { + if missingAccountFunds { + // Ignore failure (it's EntryPoint's job to verify, not the account's). + pop(call(gas(), caller(), missingAccountFunds, codesize(), 0x00, codesize(), 0x00)) + } + } + } + + /// @inheritdoc IAccount + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) + external + virtual + returns (uint256); + + // Todo + /*function nonce( + uint192 key + ) public view virtual override returns (uint256) { + return entryPoint().getNonce(address(this), key); + }*/ + + function entryPoint() public view virtual returns (address) { + return 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + } +} diff --git a/contracts/Account/ModuleConfig.sol b/contracts/base/ModuleManager.sol similarity index 59% rename from contracts/Account/ModuleConfig.sol rename to contracts/base/ModuleManager.sol index 780c7b79..19f755bc 100644 --- a/contracts/Account/ModuleConfig.sol +++ b/contracts/base/ModuleManager.sol @@ -1,45 +1,57 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity ^0.8.24; -import { IModuleConfig } from "../interfaces/IModuleConfig.sol"; +import { IModuleManager } from "../interfaces/base/IModuleManager.sol"; import { Storage } from "./Storage.sol"; +import { IModule } from "../interfaces/modules/IModule.sol"; -contract ModuleConfig is Storage, IModuleConfig { +// Todo: Implement methods for installing specific module types +contract ModuleManager is Storage, IModuleManager { /** * @notice Installs a Module of a certain type on the smart account. - * @param moduleType The module type ID. + * @param moduleTypeId The module type ID. * @param module The module address. * @param initData Initialization data for the module. */ - function installModule(uint256 moduleType, address module, bytes calldata initData) external payable { + function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external payable virtual { AccountStorage storage $ = _getAccountStorage(); $.modules[module] = module; - moduleType; + + IModule(module).onInstall(initData); + moduleTypeId; initData; } /** * @notice Uninstalls a Module of a certain type from the smart account. - * @param moduleType The module type ID. + * @param moduleTypeId The module type ID. * @param module The module address. * @param deInitData De-initialization data for the module. */ - function uninstallModule(uint256 moduleType, address module, bytes calldata deInitData) external payable { + function uninstallModule( + uint256 moduleTypeId, + address module, + bytes calldata deInitData + ) + external + payable + virtual + { AccountStorage storage $ = _getAccountStorage(); - moduleType; + moduleTypeId; deInitData; delete $.modules[module]; } /** * @notice Checks if a module is installed on the smart account. - * @param moduleType The module type ID. + * @param moduleTypeId The module type ID. * @param module The module address. * @param additionalContext Additional context for checking installation. * @return True if the module is installed, false otherwise. */ function isModuleInstalled( - uint256 moduleType, + uint256 moduleTypeId, address module, bytes calldata additionalContext ) @@ -49,7 +61,7 @@ contract ModuleConfig is Storage, IModuleConfig { { AccountStorage storage $ = _getAccountStorage(); additionalContext; - moduleType; + moduleTypeId; return $.modules[module] != address(0); } } diff --git a/contracts/Account/Storage.sol b/contracts/base/Storage.sol similarity index 92% rename from contracts/Account/Storage.sol rename to contracts/base/Storage.sol index dc9571cc..41ba18ea 100644 --- a/contracts/Account/Storage.sol +++ b/contracts/base/Storage.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity ^0.8.24; -import { IStorage } from "../interfaces/IStorage.sol"; +import { IStorage } from "../interfaces/base/IStorage.sol"; contract Storage is IStorage { /// @custom:storage-location erc7201:biconomy.storage.SmartAccount diff --git a/contracts/factory/AccountFactory.sol b/contracts/factory/AccountFactory.sol new file mode 100644 index 00000000..36a6d389 --- /dev/null +++ b/contracts/factory/AccountFactory.sol @@ -0,0 +1,42 @@ +pragma solidity ^0.8.24; + +import { SmartAccount } from "../SmartAccount.sol"; +import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; +import { IAccountFactory } from "../interfaces/factory/IAccountFactory.sol"; +import { IModuleManager } from "../interfaces/base/IModuleManager.sol"; +import { StakeManager } from "account-abstraction/contracts/core/StakeManager.sol"; + +contract AccountFactory is IAccountFactory, StakeManager { + function createAccount(address module, uint256 index, bytes calldata data) external returns (address account) { + bytes32 salt = keccak256(abi.encodePacked(module, index, data)); + + bytes memory bytecode = abi.encodePacked(type(SmartAccount).creationCode); + account = Create2.computeAddress(salt, keccak256(bytecode)); + if (account.code.length > 0) { + return account; + } + account = Create2.deploy(0, salt, bytecode); + IModuleManager(account).installModule(index, module, data); + } + + /** + * @dev Computes the expected address of a SmartAccount contract created via the factory. + * @param module The address of the module to be used in the SmartAccount. + * @param index The index or type of the module, for differentiation if needed. + * @param data The initialization data for the module. + * @return expectedAddress The address at which the new SmartAccount contract will be deployed. + */ + function computeAccountAddress( + address module, + uint256 index, + bytes calldata data + ) + external + view + returns (address expectedAddress) + { + bytes32 salt = keccak256(abi.encodePacked(module, index, data)); + bytes memory bytecode = abi.encodePacked(type(SmartAccount).creationCode); + expectedAddress = Create2.computeAddress(salt, keccak256(bytecode)); + } +} diff --git a/contracts/Account/Validator.sol b/contracts/interfaces/IAccount.sol similarity index 91% rename from contracts/Account/Validator.sol rename to contracts/interfaces/IAccount.sol index c1099065..a153da88 100644 --- a/contracts/Account/Validator.sol +++ b/contracts/interfaces/IAccount.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity ^0.8.24; import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; -contract Validator { +interface IAccount { /** * Validate user's signature and nonce * the entryPoint will make the call to the recipient only if this validation call returns successfully. @@ -13,7 +13,7 @@ contract Validator { * * @dev Must validate caller is the entryPoint. * Must validate the signature and nonce - * @param userOp - The operation that is about to be executed. + * @param userOp - The user operation that is about to be executed. * @param userOpHash - Hash of the user's request data. can be used as the basis for signature. * @param missingAccountFunds - Missing funds on the account's deposit in the entrypoint. * This is the minimum amount to transfer to the sender(entryPoint) to be @@ -37,10 +37,5 @@ contract Validator { uint256 missingAccountFunds ) external - returns (uint256 validationData) - { - userOp; - userOpHash; - missingAccountFunds; - } + returns (uint256 validationData); } diff --git a/contracts/interfaces/IModule.sol b/contracts/interfaces/IModule.sol deleted file mode 100644 index e62e31c2..00000000 --- a/contracts/interfaces/IModule.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -/** - * @title ERC-7579 Module Interface - * @dev Basic interface for all types of modules. - */ -interface IModule { - /** - * @notice Called by the smart account during installation of the module. - * @param data Initialization data for the module. - */ - function onInstall(bytes calldata data) external; - - /** - * @notice Called by the smart account during uninstallation of the module. - * @param data De-initialization data for the module. - */ - function onUninstall(bytes calldata data) external; - - /** - * @notice Checks if the module is of a certain type. - * @param typeID The module type ID. - * @return True if the module is of the given type, false otherwise. - */ - function isModuleType(uint256 typeID) external view returns (bool); - - /** - * @notice Returns bit-encoded integer of the module types. - * @return The bit-encoded type IDs of the module. - */ - function getModuleTypes() external view returns (uint256); -} diff --git a/contracts/interfaces/IAccountConfig.sol b/contracts/interfaces/base/IAccountConfig.sol similarity index 80% rename from contracts/interfaces/IAccountConfig.sol rename to contracts/interfaces/base/IAccountConfig.sol index 03211e77..fdf6b0b0 100644 --- a/contracts/interfaces/IAccountConfig.sol +++ b/contracts/interfaces/base/IAccountConfig.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity ^0.8.24; /** * @title ERC-7579 Account Configuration Interface @@ -10,14 +10,14 @@ interface IAccountConfig { * @notice Returns the account id of the smart account. * @return accountImplementationId The account id of the smart account. */ - function accountId() external view returns (string memory accountImplementationId); + function accountId() external view returns (string memory); /** * @notice Checks if the account supports a certain execution mode. * @param encodedMode The encoded mode. * @return True if the account supports the mode, false otherwise. */ - function supportsAccountMode(bytes32 encodedMode) external view returns (bool); + function supportsExecutionMode(bytes32 encodedMode) external view returns (bool); /** * @notice Checks if the account supports a certain module typeId. diff --git a/contracts/interfaces/IExecution.sol b/contracts/interfaces/base/IAccountExecution.sol similarity index 76% rename from contracts/interfaces/IExecution.sol rename to contracts/interfaces/base/IAccountExecution.sol index edb90f76..c7d12a1a 100644 --- a/contracts/interfaces/IExecution.sol +++ b/contracts/interfaces/base/IAccountExecution.sol @@ -1,15 +1,17 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity ^0.8.24; import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; /** - * @title ERC-7579 Execution Interface for Smart Accounts - * @dev Interface for executing transactions on behalf of the smart account, including ERC-4337 user operations. + * @title Execution Interface for Biconomy Smart Accounts + * @dev Interface for executing transactions on behalf of the smart account, + * including ERC7579 executions and ERC-4337 user operations as per ERC-4337-v-0.7 */ -interface IExecution { +interface IAccountExecution { /** - * @notice Executes a transaction on behalf of the account. + * @notice ERC7579 Main Execution flow. + * Executes a transaction on behalf of the account. * @dev Must ensure adequate authorization control. * @param mode The encoded execution mode of the transaction. * @param executionCalldata The encoded execution call data. @@ -17,7 +19,8 @@ interface IExecution { function execute(bytes32 mode, bytes calldata executionCalldata) external payable; /** - * @notice Executes a transaction on behalf of the account via an Executor Module. + * @notice ERC7579 Execution from Executor flow. + * Executes a transaction from an Executor Module. * @dev Must ensure adequate authorization control. * @param mode The encoded execution mode of the transaction. * @param executionCalldata The encoded execution call data. diff --git a/contracts/interfaces/base/IHookManager.sol b/contracts/interfaces/base/IHookManager.sol new file mode 100644 index 00000000..c1af90a5 --- /dev/null +++ b/contracts/interfaces/base/IHookManager.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/** + * @title ERC-7579 Hook Manager Interface + * @dev Interface for configuring hooks in a smart account. + */ +interface IModuleManager { + // Placeholder + function installHook(address hook, bytes calldata data) external payable; +} diff --git a/contracts/interfaces/IModuleConfig.sol b/contracts/interfaces/base/IModuleManager.sol similarity index 59% rename from contracts/interfaces/IModuleConfig.sol rename to contracts/interfaces/base/IModuleManager.sol index 6a51518e..7d73a810 100644 --- a/contracts/interfaces/IModuleConfig.sol +++ b/contracts/interfaces/base/IModuleManager.sol @@ -1,36 +1,39 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity ^0.8.24; /** - * @title ERC-7579 Module Configuration Interface + * @title ERC-7579 Module Manager Interface * @dev Interface for configuring modules in a smart account. */ -interface IModuleConfig { +interface IModuleManager { + event ModuleInstalled(uint256 moduleTypeId, address module); + event ModuleUninstalled(uint256 moduleTypeId, address module); + /** * @notice Installs a Module of a certain type on the smart account. - * @param moduleType The module type ID. + * @param moduleTypeId The module type ID. * @param module The module address. * @param initData Initialization data for the module. */ - function installModule(uint256 moduleType, address module, bytes calldata initData) external payable; + function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external payable; /** * @notice Uninstalls a Module of a certain type from the smart account. - * @param moduleType The module type ID. + * @param moduleTypeId The module type ID. * @param module The module address. * @param deInitData De-initialization data for the module. */ - function uninstallModule(uint256 moduleType, address module, bytes calldata deInitData) external payable; + function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external payable; /** * @notice Checks if a module is installed on the smart account. - * @param moduleType The module type ID. + * @param moduleTypeId The module type ID. * @param module The module address. * @param additionalContext Additional context for checking installation. * @return True if the module is installed, false otherwise. */ function isModuleInstalled( - uint256 moduleType, + uint256 moduleTypeId, address module, bytes calldata additionalContext ) diff --git a/contracts/interfaces/IStorage.sol b/contracts/interfaces/base/IStorage.sol similarity index 91% rename from contracts/interfaces/IStorage.sol rename to contracts/interfaces/base/IStorage.sol index b8877193..debf9ca7 100644 --- a/contracts/interfaces/IStorage.sol +++ b/contracts/interfaces/base/IStorage.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/ERC20.sol) -pragma solidity 0.8.24; +pragma solidity ^0.8.24; interface IStorage { /// @custom:storage-location erc7201:biconomy.storage.SmartAccount diff --git a/contracts/interfaces/factory/IAccountFactory.sol b/contracts/interfaces/factory/IAccountFactory.sol new file mode 100644 index 00000000..994a5682 --- /dev/null +++ b/contracts/interfaces/factory/IAccountFactory.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.24; + +interface IAccountFactory { + event AccountCreated(address account, address owner); + + function createAccount(address module, uint256 index, bytes calldata data) external returns (address account); +} diff --git a/contracts/interfaces/modules/IERC7579Modules.sol b/contracts/interfaces/modules/IERC7579Modules.sol new file mode 100644 index 00000000..f62b4017 --- /dev/null +++ b/contracts/interfaces/modules/IERC7579Modules.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { IValidator } from "./IValidator.sol"; +import { IExecutor } from "./IExecutor.sol"; +import { IFallback } from "./IFallback.sol"; +import { IHook } from "./IHook.sol"; + +uint256 constant VALIDATION_SUCCESS = 0; +uint256 constant VALIDATION_FAILED = 1; + +uint256 constant MODULE_TYPE_VALIDATOR = 1; +uint256 constant MODULE_TYPE_EXECUTOR = 2; +uint256 constant MODULE_TYPE_FALLBACK = 3; +uint256 constant MODULE_TYPE_HOOK = 4; + +// TODO // Review +interface IERC7579Modules is IValidator, IExecutor, IFallback, IHook { +// solhint-disable-previous-line no-empty-blocks +} diff --git a/contracts/interfaces/modules/IExecutor.sol b/contracts/interfaces/modules/IExecutor.sol new file mode 100644 index 00000000..10b3c4ab --- /dev/null +++ b/contracts/interfaces/modules/IExecutor.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { IModule } from "./IModule.sol"; + +interface IExecutor is IModule { +// solhint-disable-previous-line no-empty-blocks +} diff --git a/contracts/interfaces/modules/IFallback.sol b/contracts/interfaces/modules/IFallback.sol new file mode 100644 index 00000000..594850cb --- /dev/null +++ b/contracts/interfaces/modules/IFallback.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { IModule } from "./IModule.sol"; + +interface IFallback is IModule { +// solhint-disable-previous-line no-empty-blocks +} diff --git a/contracts/interfaces/modules/IHook.sol b/contracts/interfaces/modules/IHook.sol new file mode 100644 index 00000000..56300cb5 --- /dev/null +++ b/contracts/interfaces/modules/IHook.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { IModule } from "./IModule.sol"; + +interface IHook is IModule { + function preCheck(address msgSender, bytes calldata msgData) external returns (bytes memory hookData); + function postCheck(bytes calldata hookData) external returns (bool success); +} diff --git a/contracts/interfaces/modules/IModule.sol b/contracts/interfaces/modules/IModule.sol new file mode 100644 index 00000000..d8d3c6a5 --- /dev/null +++ b/contracts/interfaces/modules/IModule.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { EncodedModuleTypes } from "../../lib/ModuleTypeLib.sol"; + +/** + * @title ERC-7579 Module Interface + * @dev Basic interface for all types of modules. + */ +interface IModule { + error AlreadyInitialized(address smartAccount); + error NotInitialized(address smartAccount); + + /** + * @dev This function is called by the smart account during installation of the module + * @param data arbitrary data that may be required on the module during `onInstall` + * initialization + * + * MUST revert on error (i.e. if module is already enabled) + */ + function onInstall(bytes calldata data) external; + + /** + * @dev This function is called by the smart account during uninstallation of the module + * @param data arbitrary data that may be required on the module during `onUninstall` + * de-initialization + * + * MUST revert on error + */ + function onUninstall(bytes calldata data) external; + + /** + * @dev Returns boolean value if module is a certain type + * @param moduleTypeId the module type ID according the ERC-7579 spec + * + * MUST return true if the module is of the given type and false otherwise + */ + function isModuleType(uint256 moduleTypeId) external view returns (bool); + + /** + * @dev Returns bit-encoded integer of the different typeIds of the module + * + * MUST return all the bit-encoded typeIds of the module + */ + function getModuleTypes() external view returns (EncodedModuleTypes); +} diff --git a/contracts/interfaces/modules/IValidator.sol b/contracts/interfaces/modules/IValidator.sol new file mode 100644 index 00000000..4e68fc28 --- /dev/null +++ b/contracts/interfaces/modules/IValidator.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import { IModule } from "./IModule.sol"; + +interface IValidator is IModule { + error InvalidTargetAddress(address target); + + /** + * @dev Validates a transaction on behalf of the account. + * This function is intended to be called by the MSA during the ERC-4337 validaton phase + * Note: solely relying on bytes32 hash and signature is not suffcient for some + * validation implementations (i.e. SessionKeys often need access to userOp.calldata) + * @param userOp The user operation to be validated. The userOp MUST NOT contain any metadata. + * The MSA MUST clean up the userOp before sending it to the validator. + * @param userOpHash The hash of the user operation to be validated + * @return return value according to ERC-4337 + */ + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); + + /** + * Validator can be used for ERC-1271 validation + */ + function isValidSignatureWithSender( + address sender, + bytes32 hash, + bytes calldata data + ) + external + view + returns (bytes4); +} diff --git a/contracts/lib/ModuleTypeLib.sol b/contracts/lib/ModuleTypeLib.sol new file mode 100644 index 00000000..9c76d318 --- /dev/null +++ b/contracts/lib/ModuleTypeLib.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +type EncodedModuleTypes is uint256; + +type ModuleType is uint256; + +library ModuleTypeLib { + function isType(EncodedModuleTypes self, ModuleType moduleTypeId) internal pure returns (bool) { + return (EncodedModuleTypes.unwrap(self) & (2 ** ModuleType.unwrap(moduleTypeId))) != 0; + } + + function bitEncode(ModuleType[] memory moduleTypes) internal pure returns (EncodedModuleTypes) { + uint256 result; + for (uint256 i; i < moduleTypes.length; i++) { + result = result + uint256(2 ** ModuleType.unwrap(moduleTypes[i])); + } + return EncodedModuleTypes.wrap(result); + } + + function bitEncodeCalldata(ModuleType[] calldata moduleTypes) internal pure returns (EncodedModuleTypes) { + uint256 result; + for (uint256 i; i < moduleTypes.length; i++) { + result = result + uint256(2 ** ModuleType.unwrap(moduleTypes[i])); + } + return EncodedModuleTypes.wrap(result); + } + + // TODO: marked for deletion + function test(uint256 a) private { + a; + } +} diff --git a/contracts/test/mocks/Counter.sol b/contracts/test/mocks/Counter.sol new file mode 100644 index 00000000..c10b330e --- /dev/null +++ b/contracts/test/mocks/Counter.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +contract Counter { + uint256 private _number; + + function incrementNumber() public { + _number++; + } + + function decrementNumber() public { + _number--; + } + + /** + * @dev Return value + * @return value of 'number' + */ + function getNumber() public view returns (uint256) { + return _number; + } +} diff --git a/contracts/Mock/Entrypoint.sol b/contracts/test/mocks/Entrypoint.sol similarity index 61% rename from contracts/Mock/Entrypoint.sol rename to contracts/test/mocks/Entrypoint.sol index c4fb3eee..cc82c0dd 100644 --- a/contracts/Mock/Entrypoint.sol +++ b/contracts/test/mocks/Entrypoint.sol @@ -2,7 +2,3 @@ pragma solidity >=0.8.24; import { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol"; - -contract Entrypoint is EntryPoint { -// solhint-disable-previous-line no-empty-blocks -} diff --git a/contracts/test/mocks/MockValidator.sol b/contracts/test/mocks/MockValidator.sol new file mode 100644 index 00000000..0b25dfb2 --- /dev/null +++ b/contracts/test/mocks/MockValidator.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { IModule } from "../../interfaces/modules/IModule.sol"; +import { IValidator, VALIDATION_SUCCESS, VALIDATION_FAILED } from "../../interfaces/modules/IERC7579Modules.sol"; +import { EncodedModuleTypes } from "../../lib/ModuleTypeLib.sol"; +import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import { ECDSA } from "solady/src/utils/ECDSA.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +contract MockValidator is IValidator { + mapping(address => address) public smartAccountOwners; + + /// @inheritdoc IValidator + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) + external + returns (uint256 validation) + { + return ECDSA.recover(MessageHashUtils.toEthSignedMessageHash(userOpHash), userOp.signature) + == smartAccountOwners[msg.sender] ? VALIDATION_SUCCESS : VALIDATION_FAILED; + } + + /// @inheritdoc IValidator + function isValidSignatureWithSender( + address sender, + bytes32 hash, + bytes calldata data + ) + external + view + returns (bytes4) + { + sender; + hash; + data; + return 0xffffffff; + } + + /// @inheritdoc IModule + function onInstall(bytes calldata data) external { + smartAccountOwners[msg.sender] = address(bytes20(data)); + } + + /// @inheritdoc IModule + function onUninstall(bytes calldata data) external { + delete smartAccountOwners[msg.sender]; + } + + /// @inheritdoc IModule + function isModuleType(uint256 moduleTypeId) external view returns (bool) { + moduleTypeId; + return true; + } + + function isOwner(address account, address owner) external view returns (bool) { + return smartAccountOwners[account] == owner; + } + + /// @inheritdoc IModule + function getModuleTypes() external view returns (EncodedModuleTypes) { + // solhint-disable-previous-line no-empty-blocks + } + + function test(uint256 a) public { + a; + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 9b68848b..26dba724 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -2,6 +2,7 @@ import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; import "hardhat-storage-layout"; import "@bonadocs/docgen"; +import "hardhat-deploy"; const config: HardhatUserConfig = { solidity: { diff --git a/package.json b/package.json index fd695b79..dabe70a8 100644 --- a/package.json +++ b/package.json @@ -18,23 +18,28 @@ "@nomicfoundation/hardhat-network-helpers": "^1.0.10", "@nomicfoundation/hardhat-toolbox": "^4.0.0", "@nomicfoundation/hardhat-verify": "^2.0.4", + "@nomiclabs/hardhat-ethers": "^2.2.3", "@prb/test": "^0.6.4", "@typechain/ethers-v6": "^0.5.1", "@typechain/hardhat": "^9.1.0", "@types/chai": "^4.3.11", "@types/mocha": ">=10.0.6", "@types/node": ">=20.11.19", + "account-abstraction": "github:eth-infinitism/account-abstraction#develop", "chai": "^4.3.7", "codecov": "^3.8.3", "ethers": "^6.11.1", + "ds-test": "github:dapphub/ds-test", "forge-std": "github:foundry-rs/forge-std#v1.7.6", + "hardhat-deploy": "^0.11.45", + "hardhat-deploy-ethers": "^0.4.1", "hardhat-gas-reporter": "^1.0.10", "hardhat-storage-layout": "^0.1.7", "husky": "^9.0.11", "modulekit": "github:rhinestonewtf/modulekit", - "account-abstraction": "github:eth-infinitism/account-abstraction#develop", "prettier": "^3.2.5", "prettier-plugin-solidity": "^1.3.1", + "solady": "github:Vectorized/solady", "solhint": "^4.1.1", "solhint-plugin-prettier": "^0.1.0", "solidity-coverage": "^0.8.7", @@ -86,13 +91,13 @@ "lint:ts-fix": "yarn prettier --write 'test/**/*.ts' 'scripts/**/*.ts'", "lint": "yarn run lint:sol && yarn run lint:ts", "lint:fix": "yarn run lint:sol-fix && yarn run lint:ts-fix", - "check-branch-name": "node scripts/checkBranchName.js" + "check-branch-name": "node scripts/git-hooks/checkBranchNames.js" }, "husky": { "hooks": { - "pre-commit": "npm run check-branch-name && npm run lint-fix", + "pre-commit": "yarn run check-branch-name && npm run lint-fix", "pre-push": "npm run check-branch-name", - "post-checkout": "npm run check-branch-name" + "post-checkout": "npm run check-branch-name && yarn install" } } } diff --git a/remappings.txt b/remappings.txt index f85e71a3..f69ba169 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,4 +2,6 @@ @prb/test/=node_modules/@prb/test/ forge-std/=node_modules/forge-std/ modulekit/=node_modules/modulekit/src/ -account-abstraction/=node_modules/account-abstraction/ \ No newline at end of file +account-abstraction/=node_modules/account-abstraction/ +solady/=node_modules/solady +ds-test/=node_modules/ds-test/src/ \ No newline at end of file diff --git a/script/Counter.s.sol b/script/Counter.s.sol new file mode 100644 index 00000000..df9ee8b0 --- /dev/null +++ b/script/Counter.s.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; + +contract CounterScript is Script { + function setUp() public {} + + function run() public { + vm.broadcast(); + } +} diff --git a/scripts/git-hooks/checkBranchNames.js b/scripts/git-hooks/checkBranchNames.js index afc41e1e..b9da9474 100644 --- a/scripts/git-hooks/checkBranchNames.js +++ b/scripts/git-hooks/checkBranchNames.js @@ -1,13 +1,15 @@ // Use Node.js APIs to execute shell commands and handle logic -const execSync = require('child_process').execSync; -const branchName = execSync('git branch --show-current').toString().trim(); +const execSync = require("child_process").execSync; +const branchName = execSync("git branch --show-current").toString().trim(); const pattern = /^(feat\/|fix\/|release\/|chore\/)/; const ignoreBranches = /^(main|dev)$/; if (!ignoreBranches.test(branchName) && !pattern.test(branchName)) { - console.error('๐Ÿ›‘ ERROR: Your branch name does not meet the required pattern (feat/, fix/, release/, chore/).'); + console.error( + "๐Ÿ›‘ ERROR: Your branch name does not meet the required pattern (feat/, fix/, release/, chore/).", + ); process.exit(1); } else { - console.log('โœ… SUCCESS: Your branch name meets the required pattern.'); - process.exit(0); + console.log("โœ… SUCCESS: Your branch name meets the required pattern."); + process.exit(0); } diff --git a/src/Counter.sol b/src/Counter.sol new file mode 100644 index 00000000..079350c1 --- /dev/null +++ b/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/test/foundry/Account.t.sol b/test/foundry/Account.t.sol index 0b5288a9..eda9367f 100644 --- a/test/foundry/Account.t.sol +++ b/test/foundry/Account.t.sol @@ -1,111 +1,148 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.24 <0.9.0; -import "./Imports.sol"; +import "./utils/BicoTestBase.t.sol"; +import "./utils/Imports.sol"; -contract SmartAccountTest is PRBTest, StdCheats { - SmartAccount public smartAccount; +contract SmartAccountTest is BicoTestBase { + SmartAccount public BOB_ACCOUNT; + SmartAccount public ALICE_ACCOUNT; + SmartAccount public CHARLIE_ACCOUNT; + Counter public COUNTER; + uint256 public snapshotId; function setUp() public { - smartAccount = new SmartAccount(); + init(); + BOB_ACCOUNT = SmartAccount(deploySmartAccount(BOB)); + ALICE_ACCOUNT = SmartAccount(deploySmartAccount(ALICE)); + CHARLIE_ACCOUNT = SmartAccount(deploySmartAccount(CHARLIE)); + COUNTER = new Counter(); + } + + function testAccountAddress() public { + address validatorModuleAddress = address(VALIDATOR_MODULE); + uint256 validationModuleType = uint256(ModuleType.Validation); + + // Compute and assert the account addresses for BOB, ALICE, and CHARLIE + assertEq( + address(BOB_ACCOUNT), + FACTORY.computeAccountAddress(validatorModuleAddress, validationModuleType, abi.encodePacked(BOB.addr)) + ); + assertEq( + address(ALICE_ACCOUNT), + FACTORY.computeAccountAddress(validatorModuleAddress, validationModuleType, abi.encodePacked(ALICE.addr)) + ); + assertEq( + address(CHARLIE_ACCOUNT), + FACTORY.computeAccountAddress(validatorModuleAddress, validationModuleType, abi.encodePacked(CHARLIE.addr)) + ); } function testAccountId() public { - string memory expectedAccountId = "ModularSmartAccount"; + string memory expectedAccountId = "biconomy.modular-smart-account.1.0.0-alpha"; // Assuming `accountId` is set in the `SmartAccount` constructor or through some initialization function - assertEq(smartAccount.accountId(), expectedAccountId); + assertEq(BOB_ACCOUNT.accountId(), expectedAccountId); + assertEq(ALICE_ACCOUNT.accountId(), expectedAccountId); + assertEq(CHARLIE_ACCOUNT.accountId(), expectedAccountId); } - function testSupportsAccountMode() public { + function testSupportsExecutionMode() public { // Example encodedMode, replace with actual data bytes32 encodedMode = keccak256("exampleMode"); // Assuming the SmartAccount contract has logic to support certain modes - assertTrue(smartAccount.supportsAccountMode(encodedMode)); + assertTrue(BOB_ACCOUNT.supportsExecutionMode(encodedMode)); + assertTrue(ALICE_ACCOUNT.supportsExecutionMode(encodedMode)); + assertTrue(CHARLIE_ACCOUNT.supportsExecutionMode(encodedMode)); } function testSupportsModule() public { uint256 moduleTypeId = 1; // Example module type ID // Assuming the SmartAccount contract has logic to support certain module types - assertTrue(smartAccount.supportsModule(moduleTypeId)); + assertTrue(BOB_ACCOUNT.supportsModule(moduleTypeId)); + assertTrue(ALICE_ACCOUNT.supportsModule(moduleTypeId)); + assertTrue(CHARLIE_ACCOUNT.supportsModule(moduleTypeId)); } - function testInstallAndCheckModule( - uint256 dummyModuleType, - address dummyModuleAddress, - bytes calldata dummyInitData - ) - public - { - vm.assume(dummyModuleAddress != address(0)); - vm.assume(dummyModuleType != 0); - smartAccount.installModule(dummyModuleType, dummyModuleAddress, dummyInitData); - assertTrue(smartAccount.isModuleInstalled(dummyModuleType, dummyModuleAddress, dummyInitData)); + function testInstallAndCheckModule(bytes calldata dummyInitData) public { + uint256 moduleTypeId = uint256(ModuleType.Validation); + BOB_ACCOUNT.installModule(moduleTypeId, address(VALIDATOR_MODULE), dummyInitData); + assertTrue(BOB_ACCOUNT.isModuleInstalled(moduleTypeId, address(VALIDATOR_MODULE), dummyInitData)); + snapshotId = createSnapshot(); } - function testUninstallAndCheckModule( - uint256 dummyModuleType, - address dummyModuleAddress, - bytes calldata dummyInitData - ) - public - { - smartAccount.uninstallModule(dummyModuleType, dummyModuleAddress, dummyInitData); - // assertFalse(smartAccount.isModuleInstalled(dummyModuleType, dummyModuleAddress, "0x")); + function testUninstallAndCheckModule(bytes calldata dummyInitData) public { + revertToSnapshot(snapshotId); + uint256 moduleTypeId = uint256(ModuleType.Validation); + vm.assume(BOB_ACCOUNT.isModuleInstalled(moduleTypeId, address(VALIDATOR_MODULE), dummyInitData)); + BOB_ACCOUNT.uninstallModule(moduleTypeId, address(VALIDATOR_MODULE), dummyInitData); + assertFalse(BOB_ACCOUNT.isModuleInstalled(moduleTypeId, address(VALIDATOR_MODULE), "0x")); } function testExecute() public { - // Prepare test data - bytes32 mode = keccak256("TEST_MODE"); - bytes memory executionCalldata = abi.encodeWithSignature("testFunction()"); + assertEq(COUNTER.getNumber(), 0); + bytes32 mode = keccak256("EXECUTE_MODE"); + + bytes memory counterCallData = abi.encodeWithSignature("incrementNumber()"); + + bytes memory executionCalldata = abi.encode(address(COUNTER), 0, counterCallData); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); - // Since the execute function doesn't have actual logic, can't directly test its effects. - smartAccount.execute(mode, executionCalldata); + userOps[0] = + buildPackedUserOp(address(ALICE_ACCOUNT), _getNonce(address(ALICE_ACCOUNT), address(VALIDATOR_MODULE))); + userOps[0].callData = abi.encodeWithSignature("execute(bytes32,bytes)", mode, executionCalldata); + + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); + userOps[0].signature = signMessageAndGetSignatureBytes(ALICE, userOpHash); + + ENTRYPOINT.handleOps(userOps, payable(ALICE.addr)); + assertEq(COUNTER.getNumber(), 1); } function testExecuteFromExecutor() public { // Similar setup to testExecute, adapted for executeFromExecutor specifics + assertEq(COUNTER.getNumber(), 0); + COUNTER.incrementNumber(); + assertEq(COUNTER.getNumber(), 1); + bytes32 mode = keccak256("EXECUTOR_MODE"); - bytes memory executionCalldata = abi.encodeWithSignature("executorFunction()"); - // Since the execute function doesn't have actual logic, can't directly test its effects. - bytes[] memory res = smartAccount.executeFromExecutor(mode, executionCalldata); - assertEq(res.length, 0); + bytes memory counterCallData = abi.encodeWithSignature("decrementNumber()"); + + bytes memory executionCalldata = abi.encode(address(COUNTER), 0, counterCallData); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + + userOps[0] = + buildPackedUserOp(address(ALICE_ACCOUNT), _getNonce(address(ALICE_ACCOUNT), address(VALIDATOR_MODULE))); + userOps[0].callData = abi.encodeWithSignature("executeFromExecutor(bytes32,bytes)", mode, executionCalldata); + + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); + userOps[0].signature = signMessageAndGetSignatureBytes(ALICE, userOpHash); + + ENTRYPOINT.handleOps(userOps, payable(ALICE.addr)); + assertEq(COUNTER.getNumber(), 0); } function testExecuteUserOp() public { - // Mock a PackedUserOperation struct - PackedUserOperation memory userOp = PackedUserOperation({ - sender: address(this), - nonce: 1, - initCode: "", - callData: abi.encodeWithSignature("test()"), - accountGasLimits: bytes32(0), - preVerificationGas: 0, - gasFees: bytes32(0), - paymasterAndData: "", - signature: "" - }); - bytes32 userOpHash = keccak256(abi.encode(userOp)); - - smartAccount.executeUserOp(userOp, userOpHash); + assertEq(COUNTER.getNumber(), 0); + bytes32 mode = keccak256("EXECUTOR_MODE"); + + bytes memory counterCallData = abi.encodeWithSignature("incrementNumber()"); + + bytes memory executionCalldata = abi.encode(address(COUNTER), 0, counterCallData); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + + userOps[0] = + buildPackedUserOp(address(ALICE_ACCOUNT), _getNonce(address(ALICE_ACCOUNT), address(VALIDATOR_MODULE))); + + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); + BOB_ACCOUNT.executeUserOp(userOps[0], userOpHash); } - function testValidateUserOp() public { - PackedUserOperation memory userOp = PackedUserOperation({ - sender: address(this), - nonce: 1, - initCode: "", - callData: abi.encodeWithSignature("test()"), - accountGasLimits: bytes32(0), - preVerificationGas: 0, - gasFees: bytes32(0), - paymasterAndData: "", - signature: "" - }); - bytes32 userOpHash = keccak256(abi.encode(userOp)); - - uint256 missingAccountFunds = 0; - uint256 res = smartAccount.validateUserOp(userOp, userOpHash, missingAccountFunds); - assertEq(res, 0); + function testIsValidSignatureWithSender() public { + bytes memory data = abi.encodeWithSignature("incrementNumber()"); + bytes4 result = VALIDATOR_MODULE.isValidSignatureWithSender(ALICE.addr, keccak256(data), "0x"); } } diff --git a/test/foundry/Imports.sol b/test/foundry/Imports.sol deleted file mode 100644 index ee7f679a..00000000 --- a/test/foundry/Imports.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -// Importing interfaces -import "../../contracts/interfaces/IAccountConfig.sol"; -import "../../contracts/interfaces/IExecution.sol"; -import "../../contracts/interfaces/IModule.sol"; -import "../../contracts/interfaces/IModuleConfig.sol"; -import "../../contracts/interfaces/IStorage.sol"; - -// Importing contract implementations -import "../../contracts/Account/AccountConfig.sol"; -import "../../contracts/Account/Execution.sol"; -import "../../contracts/Account/ModuleConfig.sol"; -import "../../contracts/Account/Validator.sol"; -import "../../contracts/SmartAccount.sol"; - -import "account-abstraction/contracts/core/EntryPoint.sol"; -import { PRBTest } from "@prb/test/src/PRBTest.sol"; -import { StdCheats } from "forge-std/src/StdCheats.sol"; - -contract Imports { -// This contract acts as a single point of import for Foundry tests. -// It does not require any logic, as its sole purpose is to consolidate imports. -// You can extend this contract in your test files to access all imported contracts. -} diff --git a/test/foundry/utils/BicoTestBase.t.sol b/test/foundry/utils/BicoTestBase.t.sol new file mode 100644 index 00000000..e13f7ce3 --- /dev/null +++ b/test/foundry/utils/BicoTestBase.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.24 <0.9.0; + +import "./Helpers.sol"; +import "forge-std/src/console2.sol"; + +contract BicoTestBase is Helpers { + SmartAccount public implementation; + SmartAccount public smartAccount; + + function init() public { + setAddress(); + implementation = new SmartAccount(); + } + + function deploySmartAccount(Vm.Wallet memory wallet) public returns (address) { + address account = getAccountAddress(wallet.addr); + address signer = wallet.addr; + + bytes memory initCode = createInitCode(wallet.addr, FACTORY.createAccount.selector); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = buildPackedUserOp(account, _getNonce(account, address(VALIDATOR_MODULE))); + + userOps[0].initCode = initCode; + + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); + + userOps[0].signature = signMessageAndGetSignatureBytes(wallet, userOpHash); + ENTRYPOINT.depositTo{ value: 100 ether }(account); + ENTRYPOINT.handleOps(userOps, payable(wallet.addr)); + return account; + } + + function testBico(uint256 a) public { + a; + } +} diff --git a/test/foundry/utils/CheatCodes.sol b/test/foundry/utils/CheatCodes.sol new file mode 100644 index 00000000..e2879192 --- /dev/null +++ b/test/foundry/utils/CheatCodes.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./Imports.sol"; +import "forge-std/src/Test.sol"; + +contract CheatCodes is Test { + // Assign a readable name to an address to improve test output readability + function labelAddress(address addr, string memory name) internal { + vm.label(addr, name); + } + + // Create a new wallet from a given name, generating a private key, and label the address + function newWallet(string memory name) internal returns (Vm.Wallet memory) { + Vm.Wallet memory wallet = vm.createWallet(name); + vm.label(wallet.addr, name); + return wallet; + } + + function warpTo(uint256 timestamp) internal { + vm.warp(timestamp); + } + + function setBalance(address addr, uint256 balance) internal { + vm.deal(addr, balance); + } + + function signMessage(address signer, bytes32 hash) internal returns (uint8 v, bytes32 r, bytes32 s) { + uint256 privateKey = uint256(keccak256(abi.encodePacked(signer))); + (v, r, s) = vm.sign(privateKey, hash); + } + + function assume(bool condition) internal { + vm.assume(condition); + } + + function startPrank(address addr) internal { + vm.startPrank(addr); + } + + function stopPrank() internal { + vm.stopPrank(); + } + + function createSnapshot() internal returns (uint256) { + return vm.snapshot(); + } + + function revertToSnapshot(uint256 snapshotId) internal { + vm.revertTo(snapshotId); + } + + function skipTest(bool condition) internal { + if (condition) { + vm.skip(true); + } + } + + // Set the block base fee + function setBaseFee(uint256 baseFee) internal { + vm.fee(baseFee); + } + + // Load storage slot directly from a contract + function loadStorageAtSlot(address contractAddress, bytes32 slot) internal returns (bytes32) { + return vm.load(contractAddress, slot); + } + + // Set contract code for an address + function setContractCode(address contractAddress, bytes memory code) internal { + vm.etch(contractAddress, code); + } + + function test(uint256 a) public { + a; + } +} diff --git a/test/foundry/utils/Helpers.sol b/test/foundry/utils/Helpers.sol new file mode 100644 index 00000000..0f9d0734 --- /dev/null +++ b/test/foundry/utils/Helpers.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./CheatCodes.sol"; +import "./Imports.sol"; +import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; + +contract Helpers is CheatCodes { + // Pre-defined roles + Vm.Wallet public DEPLOYER; + Vm.Wallet public ALICE; + Vm.Wallet public BOB; + Vm.Wallet public CHARLIE; + Vm.Wallet public BUNDLER; + + address public DEPLOYER_ADDRESS; + address public ALICE_ADDRESS; + address public BOB_ADDRESS; + address public CHARLIE_ADDRESS; + address public BUNDLER_ADDRESS; + IEntryPoint public ENTRYPOINT; + AccountFactory public FACTORY; + MockValidator public VALIDATOR_MODULE; + + function setAddress() public virtual { + DEPLOYER = newWallet("DEPLOYER"); + DEPLOYER_ADDRESS = DEPLOYER.addr; + vm.deal(DEPLOYER_ADDRESS, 1000 ether); + + ALICE = newWallet("ALICE"); + ALICE_ADDRESS = ALICE.addr; + vm.deal(ALICE_ADDRESS, 1000 ether); + + BOB = newWallet("BOB"); + BOB_ADDRESS = BOB.addr; + vm.deal(BOB_ADDRESS, 1000 ether); + + CHARLIE = newWallet("CHARLIE"); + CHARLIE_ADDRESS = CHARLIE.addr; + vm.deal(CHARLIE_ADDRESS, 1000 ether); + + BUNDLER = newWallet("BUNDLER"); + BUNDLER_ADDRESS = BUNDLER.addr; + vm.deal(BUNDLER_ADDRESS, 1000 ether); + + ENTRYPOINT = new EntryPoint(); + changeContractAddress(address(ENTRYPOINT), 0x0000000071727De22E5E9d8BAf0edAc6f37da032); + ENTRYPOINT = IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032); + + FACTORY = new AccountFactory(); + + VALIDATOR_MODULE = new MockValidator(); + } + + function sendEther(address to, uint256 amount) internal { + payable(to).transfer(amount); + } + + function setupContractAs( + address sender, + uint256 value, + bytes memory constructorArgs, + bytes memory bytecode + ) + internal + returns (address) + { + startPrank(sender); + address deployedAddress; // Deploy the contract + stopPrank(); + return deployedAddress; + } + + function assertBalance(address addr, uint256 expectedBalance, string memory message) internal { + require(addr.balance == expectedBalance, message); + } + + function simulateTimePassing(uint256 nbDays) internal { + warpTo(block.timestamp + nbDays * 1 days); + } + + // Helper to modify the address of a deployed contract in a test environment + function changeContractAddress(address originalAddress, address newAddress) internal { + setContractCode(originalAddress, address(originalAddress).code); + setContractCode(newAddress, originalAddress.code); + } + + // Helper to build a user operation struct for account abstraction tests + function buildPackedUserOp(address sender, uint256 nonce) internal pure returns (PackedUserOperation memory) { + return PackedUserOperation({ + sender: sender, + nonce: nonce, + initCode: "", + callData: "", + accountGasLimits: bytes32(abi.encodePacked(uint128(3e6), uint128(3e6))), + preVerificationGas: 3e6, + gasFees: bytes32(abi.encodePacked(uint128(3e6), uint128(3e6))), + paymasterAndData: "", + signature: "" + }); + } + + // Utility method to encode and sign a message, then pack r, s, v into bytes + function signMessageAndGetSignatureBytes( + Vm.Wallet memory wallet, + bytes32 messageHash + ) + internal + returns (bytes memory signature) + { + bytes32 userOpHash = ECDSA.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, userOpHash); + signature = abi.encodePacked(r, s, v); + } + + function getAccountAddress(address signer) internal view returns (address account) { + bytes memory initData = abi.encodePacked(signer); + + uint256 moduleTypeId = uint256(ModuleType.Validation); + + account = FACTORY.computeAccountAddress(address(VALIDATOR_MODULE), moduleTypeId, initData); + + return account; + } + // Method to create a UserOperation + + function createUserOperation( + Vm.Wallet memory wallet, + address module + ) + internal + returns (PackedUserOperation memory userOp) + { + address accountAddress = getAccountAddress(wallet.addr); + + // Constructing the UserOperation with the signed hash + userOp = PackedUserOperation({ + sender: accountAddress, + nonce: _getNonce(accountAddress, module), + initCode: "", + callData: "", + accountGasLimits: bytes32(abi.encodePacked(uint128(3e6), uint128(3e6))), + preVerificationGas: 3e6, + gasFees: bytes32(abi.encodePacked(uint128(3e6), uint128(3e6))), + paymasterAndData: "", + signature: "" + }); + } + + function createInitCode( + address ownerAddress, + bytes4 createAccountSelector + ) + internal + view + returns (bytes memory initCode) + { + address module = address(VALIDATOR_MODULE); + uint256 moduleTypeId = uint256(ModuleType.Validation); + bytes memory moduleInitData = abi.encodePacked(ownerAddress); + + // Prepend the factory address to the encoded function call to form the initCode + initCode = abi.encodePacked( + address(FACTORY), + abi.encodeWithSelector(FACTORY.createAccount.selector, module, moduleTypeId, moduleInitData) + ); + } + + function _getNonce(address account, address validator) internal returns (uint256 nonce) { + uint192 key = uint192(bytes24(bytes20(address(validator)))); + nonce = ENTRYPOINT.getNonce(address(account), key); + } + + function testHelpers(uint256 a) public { + a; + } +} diff --git a/test/foundry/utils/Imports.sol b/test/foundry/utils/Imports.sol new file mode 100644 index 00000000..be07665c --- /dev/null +++ b/test/foundry/utils/Imports.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +// Standard library imports +import "forge-std/src/console2.sol"; +import "forge-std/src/Test.sol"; +import "forge-std/src/Vm.sol"; + +// Utility libraries +import { ECDSA } from "solady/src/utils/ECDSA.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { PRBTest } from "@prb/test/src/PRBTest.sol"; + +// Account Abstraction imports +import { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol"; +import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; + +// Interface imports +import "contracts/interfaces/base/IAccountConfig.sol"; +import "contracts/interfaces/base/IAccountExecution.sol"; +import "contracts/interfaces/base/IModuleManager.sol"; +import "contracts/interfaces/modules/IModule.sol"; +import "contracts/interfaces/base/IStorage.sol"; +import "contracts/interfaces/factory/IAccountFactory.sol"; + +// Contract implementations +import "contracts/base/AccountConfig.sol"; +import "contracts/base/AccountExecution.sol"; +import "contracts/base/ModuleManager.sol"; +import "contracts/SmartAccount.sol"; +import "contracts/factory/AccountFactory.sol"; + +// Mock contracts for testing +import "contracts/test/mocks/MockValidator.sol"; +import "contracts/test/mocks/Counter.sol"; + +// Helper and Struct imports +import "./Structs.sol"; +import "./Helpers.sol"; + +contract Imports { +// This contract acts as a single point of import for Foundry tests. +// It does not require any logic, as its sole purpose is to consolidate imports. +// You can extend this contract in your test files to access all imported contracts and libraries. +} diff --git a/test/foundry/utils/Structs.sol b/test/foundry/utils/Structs.sol new file mode 100644 index 00000000..39687351 --- /dev/null +++ b/test/foundry/utils/Structs.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +enum ModuleType { + Validation, + Execution, + Fallback, + Hooks +} diff --git a/test/hardhat/01_SmartAccountDeployment.test.ts b/test/hardhat/01_SmartAccountDeployment.test.ts new file mode 100644 index 00000000..f67db153 --- /dev/null +++ b/test/hardhat/01_SmartAccountDeployment.test.ts @@ -0,0 +1,195 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { AddressLike, Signer } from "ethers"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { + AccountFactory, + EntryPoint, + MockValidator, + SmartAccount, +} from "../../typechain-types"; +import { ModuleType } from "./utils/types"; +import { deploySmartAccountFixture } from "./utils/deployment"; +import { to18 } from "./utils/encoding"; +import { + generateFullInitCode, + getAccountAddress, + buildPackedUserOp, +} from "./utils/operationHelpers"; + +describe("SmartAccount Contract Integration Tests", function () { + let factory: AccountFactory; + let smartAccount: SmartAccount; + let entryPoint: EntryPoint; + let module: MockValidator; + let accounts: Signer[]; + let addresses: string[] | AddressLike[]; + let factoryAddress: AddressLike; + let entryPointAddress: AddressLike; + let smartAccountAddress: AddressLike; + let moduleAddress: AddressLike; + let owner: Signer; + let ownerAddress: AddressLike; + let bundler: Signer; + let bundlerAddress: AddressLike; + + beforeEach(async function () { + const setup = await loadFixture(deploySmartAccountFixture); + entryPoint = setup.entryPoint; + smartAccount = setup.smartAccount; + module = setup.module; + factory = setup.factory; + accounts = setup.accounts; + addresses = setup.addresses; + + entryPointAddress = await entryPoint.getAddress(); + smartAccountAddress = await smartAccount.getAddress(); + moduleAddress = await module.getAddress(); + factoryAddress = await factory.getAddress(); + owner = ethers.Wallet.createRandom(); + ownerAddress = await owner.getAddress(); + bundler = ethers.Wallet.createRandom(); + bundlerAddress = await bundler.getAddress(); + }); + + describe("Contract Deployment", function () { + it("Should deploy the SmartAccount contract successfully", async function () { + // Checks if the smart account's address contains bytecode, indicating successful deployment + expect(ethers.provider.getCode(smartAccountAddress)).to.not.equal("0x"); + }); + + it("Should deploy the EntryPoint contract successfully", async function () { + expect(ethers.provider.getCode(entryPointAddress)).to.not.equal("0x"); + }); + + it("Should deploy the Module contract successfully", async function () { + expect(ethers.provider.getCode(moduleAddress)).to.not.equal("0x"); + }); + + it("Should handle account creation correctly, including when the account already exists", async function () { + const SmartAccount = await ethers.getContractFactory("SmartAccount"); + + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address"], + [ownerAddress], + ); // Example data, customize as needed + + // Calculate expected account address + const salt = ethers.keccak256( + ethers.solidityPacked( + ["address", "uint256", "bytes"], + [moduleAddress, ModuleType.Validation, data], + ), + ); + const bytecodeHash = ethers.keccak256(SmartAccount.bytecode); + + // First account creation attempt + const expectedAccountAddress = ethers.getCreate2Address( + factoryAddress.toString(), + salt, + bytecodeHash, + ); + + // First account creation attempt + await factory.createAccount(moduleAddress, ModuleType.Validation, data); + + // Verify that the account was created + const codeAfterFirstCreation = await ethers.provider.getCode( + expectedAccountAddress, + ); + expect(codeAfterFirstCreation).to.not.equal( + "0x", + "Account should have bytecode after the first creation attempt", + ); + + // Second account creation attempt with the same parameters + await factory.createAccount(moduleAddress, ModuleType.Validation, data); + + // Verify that the account address remains the same and no additional deployment occurred + const codeAfterSecondCreation = await ethers.provider.getCode( + expectedAccountAddress, + ); + expect(codeAfterSecondCreation).to.equal( + codeAfterFirstCreation, + "Account bytecode should remain unchanged after the second creation attempt", + ); + }); + }); + + describe("SmartAccount Deployment via EntryPoint", function () { + it("Should successfully deploy a SmartAccount via the EntryPoint", async function () { + // This involves preparing a user operation (userOp), signing it, and submitting it through the EntryPoint + const initCode = await generateFullInitCode( + ownerAddress, + factoryAddress, + moduleAddress, + ModuleType.Validation, + ); + + // Calculate the expected account address + const accountAddress = await getAccountAddress( + ownerAddress, + factoryAddress, + moduleAddress, + ModuleType.Validation, + ); + + const nonce = await entryPoint.getNonce( + accountAddress, + ethers.zeroPadBytes(moduleAddress.toString(), 24), + ); + + const packedUserOp = buildPackedUserOp({ + sender: accountAddress, + nonce, + initCode, + }); + + const userOpHash = await entryPoint.getUserOpHash(packedUserOp); + + const sig = await owner.signMessage(ethers.getBytes(userOpHash)); + + packedUserOp.signature = sig; + + await entryPoint.depositTo(accountAddress, { value: to18(1) }); + + await entryPoint.handleOps([packedUserOp], bundlerAddress); + }); + + it("Should fail SmartAccount deployment with an unauthorized signer", async function () { + const initCode = await generateFullInitCode( + ownerAddress, + factoryAddress, + moduleAddress, + ModuleType.Validation, + ); + const accountAddress = await getAccountAddress( + ownerAddress, + factoryAddress, + moduleAddress, + ModuleType.Validation, + ); + + const nonce = await entryPoint.getNonce( + accountAddress, + ethers.zeroPadBytes(moduleAddress.toString(), 24), + ); + + const packedUserOp = buildPackedUserOp({ + sender: accountAddress, + nonce: nonce, + initCode: initCode, + }); + + const userOpHash = await entryPoint.getUserOpHash(packedUserOp); + + const sig = await accounts[10].signMessage(ethers.getBytes(userOpHash)); + packedUserOp.signature = sig; + await entryPoint.depositTo(accountAddress, { value: to18(1) }); + + await expect(entryPoint.handleOps([packedUserOp], bundlerAddress)) + .to.be.revertedWithCustomError(entryPoint, "FailedOp") + .withArgs(0, "AA24 signature error"); + }); + }); +}); diff --git a/test/hardhat/02_Configuration.test.ts b/test/hardhat/02_Configuration.test.ts new file mode 100644 index 00000000..9af830db --- /dev/null +++ b/test/hardhat/02_Configuration.test.ts @@ -0,0 +1,47 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { AddressLike, Signer } from "ethers"; +import { MockValidator, SmartAccount } from "../../typechain-types"; +import { ModuleType } from "./utils/types"; +import { deploySmartAccountFixture } from "./utils/deployment"; +import { toBytes32 } from "./utils/encoding"; + +describe("SmartAccount Configuration Tests", function () { + let smartAccount: SmartAccount; + let module: MockValidator; + let owner: Signer; + let ownerAddress: AddressLike; + let moduleAddress: AddressLike; + + before(async function () { + ({ smartAccount, module } = await deploySmartAccountFixture()); + owner = ethers.Wallet.createRandom(); + ownerAddress = await owner.getAddress(); + moduleAddress = await module.getAddress(); + }); + + describe("Account ID and Supported Modes", function () { + it("Should correctly return the SmartAccount's ID", async function () { + expect(await smartAccount.accountId()).to.equal( + "biconomy.modular-smart-account.1.0.0-alpha", + ); + }); + + it("Should verify supported account modes", async function () { + expect(await smartAccount.supportsExecutionMode(toBytes32("0x01"))).to.be + .true; + expect(await smartAccount.supportsExecutionMode(toBytes32("0xFF"))).to.be + .true; + }); + + it("Should confirm support for specified module types", async function () { + // Checks support for predefined module types (e.g., Validation, Execution) + expect(await smartAccount.supportsModule(ModuleType.Validation)).to.be + .true; + expect(await smartAccount.supportsModule(ModuleType.Execution)).to.be + .true; + expect(await smartAccount.supportsModule(ModuleType.Hooks)).to.be.true; + expect(await smartAccount.supportsModule(ModuleType.Fallback)).to.be.true; + }); + }); +}); diff --git a/test/hardhat/03_ModuleManagement.test.ts b/test/hardhat/03_ModuleManagement.test.ts new file mode 100644 index 00000000..579ab572 --- /dev/null +++ b/test/hardhat/03_ModuleManagement.test.ts @@ -0,0 +1,75 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { AddressLike, Signer } from "ethers"; +import { MockValidator, SmartAccount } from "../../typechain-types"; +import { ModuleType } from "./utils/types"; +import { deploySmartAccountFixture } from "./utils/deployment"; + +describe("SmartAccount Module Management", () => { + let smartAccount: SmartAccount; + let module: MockValidator; + let owner: Signer; + let ownerAddress: AddressLike; + let moduleAddress: AddressLike; + + before(async function () { + ({ smartAccount, module } = await deploySmartAccountFixture()); + owner = ethers.Wallet.createRandom(); + ownerAddress = await owner.getAddress(); + moduleAddress = await module.getAddress(); + }); + + describe("Installation and Uninstallation", () => { + it("Should correctly install a module on the smart account", async () => { + // Verify the module is not installed initially + expect( + await smartAccount.isModuleInstalled( + ModuleType.Validation, + moduleAddress, + ethers.hexlify("0x"), + ), + ).to.be.false; + + await smartAccount.installModule( + ModuleType.Validation, + moduleAddress, + ethers.hexlify("0x"), + ); + + // Verify the module is installed after the installation + expect( + await smartAccount.isModuleInstalled( + ModuleType.Validation, + moduleAddress, + ethers.hexlify("0x"), + ), + ).to.be.true; + }); + + it("Should correctly uninstall a previously installed module", async () => { + // Precondition: The module is installed before the test + expect( + await smartAccount.isModuleInstalled( + ModuleType.Validation, + moduleAddress, + ethers.hexlify("0x"), + ), + ).to.be.true; + + await smartAccount.uninstallModule( + ModuleType.Validation, + moduleAddress, + ethers.hexlify("0x"), + ); + + // Verify the module is no longer installed + expect( + await smartAccount.isModuleInstalled( + ModuleType.Validation, + moduleAddress, + ethers.hexlify("0x"), + ), + ).to.be.false; + }); + }); +}); diff --git a/test/hardhat/04_Execution.test.ts b/test/hardhat/04_Execution.test.ts new file mode 100644 index 00000000..60cdd1fc --- /dev/null +++ b/test/hardhat/04_Execution.test.ts @@ -0,0 +1,158 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; + +import { AddressLike, Signer } from "ethers"; +import { + AccountFactory, + EntryPoint, + MockValidator, + SmartAccount, +} from "../../typechain-types"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { ExecutionMethod, ModuleType } from "./utils/types"; +import { deploySmartAccountWithEntrypointFixture } from "./utils/deployment"; +import { encodeData } from "./utils/encoding"; +import { + generateExecutionCallData, + buildSignedUserOp, + buildPackedUserOp, +} from "./utils/operationHelpers"; + +describe("SmartAccount Execution and Validation", () => { + let setup, bundler; + let factory, smartAccount, entryPoint, module, counter, owner; + let factoryAddress, + entryPointAddress, + smartAccountAddress, + moduleAddress, + counterAddress, + ownerAddress, + bundlerAddress; + + beforeEach(async () => { + setup = await loadFixture(deploySmartAccountWithEntrypointFixture); + ({ factory, smartAccount, entryPoint, module, counter, owner } = setup); + + factoryAddress = await factory.getAddress(); + entryPointAddress = await entryPoint.getAddress(); + smartAccountAddress = await smartAccount.getAddress(); + moduleAddress = await module.getAddress(); + counterAddress = await counter.getAddress(); + ownerAddress = await owner.getAddress(); + bundler = ethers.Wallet.createRandom(); + bundlerAddress = await bundler.getAddress(); + }); + + describe("SmartAccount Transaction Execution", () => { + it("Should execute a single transaction through the EntryPoint using execute", async () => { + // Generate calldata for executing the 'incrementNumber' function on the counter contract. + const callData = await generateExecutionCallData({ + executionMethod: ExecutionMethod.Execute, + targetContract: counter, + functionName: "incrementNumber", + mode: "TEST_MODE", + }); + + // Sign the operation with the owner's signature to authorize the transaction. + const signedPackedUserOps = await buildSignedUserOp( + { + sender: smartAccountAddress, + callData, + }, + owner, + setup, + ); + + // Assert the counter's state (testing contract) before execution to ensure it's at its initial state. + expect(await counter.getNumber()).to.equal(0); + + // Execute the signed userOp through the EntryPoint contract and verify the counter's state post-execution. + await entryPoint.handleOps([signedPackedUserOps], bundlerAddress); + + expect(await counter.getNumber()).to.equal(1); + }); + + it("Should handle transactions via the ExecuteFromExecutor method correctly", async () => { + // Generate calldata for 'executeFromExecutor' method, targeting the 'incrementNumber' function of the counter contract. + const callData = await generateExecutionCallData({ + executionMethod: ExecutionMethod.ExecuteFromExecutor, + targetContract: counter, + functionName: "incrementNumber", + mode: "TEST_MODE", + }); + + const signedPackedUserOps = await buildSignedUserOp( + { + sender: smartAccountAddress, + callData, + }, + owner, + setup, + ); + + expect(await counter.getNumber()).to.equal(0); + + // Execute the transaction using a different execution method but expecting the same outcome. + await entryPoint.handleOps([signedPackedUserOps], bundlerAddress); + expect(await counter.getNumber()).to.equal(1); + }); + + it("Should process executeUserOp method correctly", async () => { + // Prepare call data for the 'executeUserOp' method, involving direct interaction with userOps + const counterFuncData = + counter.interface.encodeFunctionData("incrementNumber"); + + // Note: encodeData is used to manually encode the transaction data for 'executeUserOp'. + const executionCalldata = encodeData( + ["address", "uint256", "bytes"], + [counterAddress, ModuleType.Validation, counterFuncData], + ); + + // Fetch the nonce for the userOp, to avoid replay attacks. + const nonce = await entryPoint.getNonce( + smartAccountAddress, + ethers.zeroPadBytes(moduleAddress as string, 24), + ); + + // Build the UserOp with the execution calldata, ready for signing and execution. + const packedUserOp = await buildPackedUserOp({ + sender: smartAccountAddress, + callData: executionCalldata as any, + nonce, + }); + + const userOpHash = await entryPoint.getUserOpHash(packedUserOp); + + // Sign the userOp hash with owner's signature + const signature = await owner.signMessage(ethers.getBytes(userOpHash)); + + packedUserOp.signature = signature; + + // Generate the call data specifically for the 'executeUserOp' method. + const callData = await generateExecutionCallData( + { + executionMethod: ExecutionMethod.ExecuteUserOp, + targetContract: counter, + functionName: "incrementNumber", + mode: "TEST_MODE", + }, + packedUserOp as any, + userOpHash, + ); + + // Assign the generated call data to the packedUserOp + + packedUserOp.callData = callData; + + // Re-sign the userOp with the updated hash due to calldata assignment + const executUserOpHash = await entryPoint.getUserOpHash(packedUserOp); + + packedUserOp.signature = await owner.signMessage( + ethers.getBytes(executUserOpHash), + ); + + await entryPoint.handleOps([packedUserOp], bundlerAddress); + expect(await counter.getNumber()).to.equal(0); + }); + }); +}); diff --git a/test/hardhat/Account.test.ts b/test/hardhat/Account.test.ts deleted file mode 100644 index 9c7f3717..00000000 --- a/test/hardhat/Account.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { Signer } from "ethers"; -import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; -import { buildUserOp, toBytes32 } from "./utils/utils"; - -async function deploySmartAccountFixture() { - const accounts: Signer[] = await ethers.getSigners(); - const addresses = await Promise.all( - accounts.map((account) => account.getAddress()), - ); - - const Entrypoint = await ethers.getContractFactory("EntryPoint"); - const entryPoint = await Entrypoint.deploy(); - await entryPoint.waitForDeployment(); - - const SmartAccount = await ethers.getContractFactory("SmartAccount"); - const smartAccount = await SmartAccount.deploy(); - await smartAccount.waitForDeployment(); - - return { entryPoint, smartAccount, accounts, addresses }; -} - -describe("SmartAccount Contract Tests", function () { - let smartAccount: any; - let entryPoint: any; - let accounts: Signer[]; - let addresses: string[]; - - before(async function () { - const setup = await loadFixture(deploySmartAccountFixture); - entryPoint = setup.entryPoint; - smartAccount = setup.smartAccount; - accounts = setup.accounts; - addresses = setup.addresses; - }); - - describe("Account Configuration", function () { - it("Should return the correct account ID", async function () { - expect(await smartAccount.accountId()).to.equal("ModularSmartAccount"); - }); - - it("Should support specific execution modes", async function () { - expect(await smartAccount.supportsAccountMode(toBytes32("0x01"))).to.be - .true; - expect(await smartAccount.supportsAccountMode(toBytes32("0xFF"))).to.be - .true; - }); - - it("Should support specific module types", async function () { - expect(await smartAccount.supportsModule(1)).to.be.true; - expect(await smartAccount.supportsModule(99)).to.be.true; - }); - }); - - describe("Module Configuration", function () { - let moduleAddress: string = ethers.hexlify(ethers.randomBytes(20)); - const moduleType = "1"; - - it("Should allow installing a module", async function () { - expect( - await smartAccount.isModuleInstalled( - moduleType, - moduleAddress, - ethers.hexlify("0x"), - ), - ).to.be.false; - - await smartAccount.installModule( - moduleType, - moduleAddress, - ethers.hexlify("0x"), - ); - - expect( - await smartAccount.isModuleInstalled( - moduleType, - moduleAddress, - ethers.hexlify("0x"), - ), - ).to.be.true; - }); - - it("Should allow uninstalling a module", async function () { - expect( - await smartAccount.isModuleInstalled( - moduleType, - moduleAddress, - ethers.hexlify("0x"), - ), - ).to.be.true; - - await smartAccount.uninstallModule( - moduleType, - moduleAddress, - ethers.hexlify("0x"), - ); - - expect( - await smartAccount.isModuleInstalled( - moduleType, - moduleAddress, - ethers.hexlify("0x"), - ), - ).to.be.false; - }); - }); - - describe("Execution", function () { - it("Should successfully call execute", async function () { - const mode = toBytes32("0x01"); // Example mode - const executionData = ethers.randomBytes(20); // Example execution data - - await expect(smartAccount.execute(mode, executionData)).to.not.be - .reverted; - }); - - it("Should successfully call executeFromExecutor", async function () { - const mode = toBytes32("0x01"); // Example mode - const executionData = ethers.randomBytes(20); // Example execution data - - await expect(smartAccount.executeFromExecutor(mode, executionData)).to.not - .be.reverted; - }); - - it("Should successfully call executeUserOp", async function () { - // Construct a dummy user operation - const userOp = { - sender: await smartAccount.getAddress(), - nonce: 0, - initCode: "0x", - callData: "0x", - callGasLimit: 0, - executionGasLimit: 0, - verificationGasLimit: 0, - preVerificationGas: 0, - maxFeePerGas: 0, - maxPriorityFeePerGas: 0, - paymaster: ethers.ZeroAddress, - paymasterData: "0x", - signature: "0x", - }; - - const packedUserOp = buildUserOp(userOp); - - const userOpHash = ethers.keccak256(ethers.toUtf8Bytes("dummy")); - - await expect(smartAccount.executeUserOp(packedUserOp, userOpHash)).to.not - .be.reverted; - }); - }); - - describe("Validation", function () { - it("Should validate user operations correctly", async function () { - const validUserOp = buildUserOp({ - sender: await smartAccount.getAddress(), - nonce: 1, - initCode: "0x", - callData: "0x", - callGasLimit: 400_000, - executionGasLimit: 100_000, - verificationGasLimit: 400_000, - preVerificationGas: 150_000, - maxFeePerGas: 100_000, - maxPriorityFeePerGas: 100_000, - paymaster: ethers.ZeroAddress, - paymasterData: "0x", - signature: "0x", - }); - - const userOpHash = await entryPoint.getUserOpHash(validUserOp); - const tx = await smartAccount.validateUserOp(validUserOp, userOpHash, 0); - const receipt = await tx.wait(); - expect(receipt.status).to.equal(1); - }); - }); -}); diff --git a/test/hardhat/utils/deployment.ts b/test/hardhat/utils/deployment.ts new file mode 100644 index 00000000..e0b54983 --- /dev/null +++ b/test/hardhat/utils/deployment.ts @@ -0,0 +1,164 @@ +import { Signer } from "ethers"; +import { deployments, ethers } from "hardhat"; +import { + AccountFactory, + Counter, + EntryPoint, + MockValidator, + SmartAccount, +} from "../../../typechain-types"; +import { DeploymentFixture, ModuleType } from "./types"; +import { + generateFullInitCode, + getAccountAddress, + signUserOperation, +} from "./operationHelpers"; +import { to18 } from "./encoding"; + +/** + * Generic function to deploy a contract using ethers.js. + * + * @param contractName The name of the contract to deploy. + * @param deployer The Signer object representing the deployer account. + * @returns A promise that resolves to the deployed contract instance. + */ +export async function deployContract( + contractName: string, + deployer: Signer, +): Promise { + const ContractFactory = await ethers.getContractFactory( + contractName, + deployer, + ); + const contract = await ContractFactory.deploy(); + await contract.waitForDeployment(); + return contract as T; +} + +/** + * Deploys the EntryPoint contract with a deterministic deployment. + * @returns A promise that resolves to the deployed EntryPoint contract instance. + */ +export async function deployEntrypoint(): Promise { + const accounts: Signer[] = await ethers.getSigners(); + const addresses = await Promise.all( + accounts.map((account) => account.getAddress()), + ); + + const Entrypoint = await ethers.getContractFactory("EntryPoint"); + const deterministicEntryPoint = await deployments.deploy("EntryPoint", { + from: addresses[0], + deterministicDeployment: true, + }); + + return Entrypoint.attach(deterministicEntryPoint.address) as EntryPoint; +} + +/** + * Deploys the smart contract infrastructure required for testing. + * This includes the EntryPoint, SmartAccount, AccountFactory, MockValidator, and Counter contracts. + * + * @returns A promise that resolves to a DeploymentFixture object containing deployed contracts and account information. + */ +export async function deploySmartAccountFixture(): Promise { + const [deployer, ...accounts] = await ethers.getSigners(); + const addresses = await Promise.all( + accounts.map((account) => account.getAddress()), + ); + + const entryPoint = await deployEntrypoint(); + const smartAccount = await deployContract( + "SmartAccount", + deployer, + ); + const factory = await deployContract( + "AccountFactory", + deployer, + ); + const module = await deployContract("MockValidator", deployer); + const counter = await deployContract("Counter", deployer); + + return { + entryPoint, + smartAccount, + factory, + module, + counter, + accounts, + addresses, + }; +} + +/** + * Deploys the smart contract infrastructure with a smart account through the entry point. + * This setup is designed to prepare a testing environment for smart account operations. + * + * @returns The deployment fixture including deployed contracts and the smart account owner. + */ +export async function deploySmartAccountWithEntrypointFixture(): Promise { + const owner = ethers.Wallet.createRandom(); + const [deployer, ...accounts] = await ethers.getSigners(); + + const entryPoint = await deployEntrypoint(); + const smartAccountFactory = await ethers.getContractFactory("SmartAccount"); + const module = await deployContract("MockValidator", deployer); + const factory = await deployContract( + "AccountFactory", + deployer, + ); + const counter = await deployContract("Counter", deployer); + + // Get the addresses of the deployed contracts + const factoryAddress = await factory.getAddress(); + const moduleAddress = await module.getAddress(); + const ownerAddress = await owner.getAddress(); + + // Generate the initialization code for the smart account + const initCode = await generateFullInitCode( + ownerAddress, + factoryAddress, + moduleAddress, + ModuleType.Validation, + ); + + // Get the counterfactual address of the smart account before deployment + const accountAddress = await getAccountAddress( + ownerAddress, + factoryAddress, + moduleAddress, + ModuleType.Validation, + ); + + // Sign the user operation for deploying the smart account + const packedUserOp = await signUserOperation( + accountAddress, + initCode, + entryPoint, + moduleAddress, + owner, + ); + + // Deposit ETH to the smart account + await entryPoint.depositTo(accountAddress, { value: to18(1) }); + + // Handle the user operation to deploy the smart account + await entryPoint.handleOps([packedUserOp], ownerAddress); + + // Attach the SmartAccount contract to the deployed address + const smartAccount = smartAccountFactory.attach(accountAddress); + + // Get the addresses of the other accounts + const addresses = await Promise.all( + accounts.map(async (acc) => await acc.getAddress()), + ); + + return { + entryPoint, + smartAccount, + factory, + module, + counter, + owner, + addresses, + }; +} diff --git a/test/hardhat/utils/encoding.ts b/test/hardhat/utils/encoding.ts new file mode 100644 index 00000000..7e9e596f --- /dev/null +++ b/test/hardhat/utils/encoding.ts @@ -0,0 +1,60 @@ +import { BigNumberish } from "ethers"; +import { ethers } from "hardhat"; + +/** + * Encodes data using the defaultAbiCoder from ethers.AbiCoder. + * @param types The types of the values being encoded. + * @param values The values to encode. + * @returns The encoded data. + */ +export function encodeData(types: string[], values: any[]): string { + return ethers.AbiCoder.defaultAbiCoder().encode(types, values); +} + +/** + * Converts a regular string to a bytes32 string. + * + * @param text The regular string to convert. + * @returns The converted bytes32 string. + */ +export const toBytes32 = (text: string): string => { + return ethers.encodeBytes32String(text); +}; + +/** + * Converts a bytes32 string to a regular string. + * + * @param bytes32 The bytes32 string to convert. + * @returns The converted regular string. + */ +export const fromBytes32 = (bytes32: string): string => { + return ethers.decodeBytes32String(bytes32); +}; + +/** + * Converts a numeric value to its equivalent in 18 decimal places. + * @param value The numeric value to convert. + * @returns The equivalent value in 18 decimal places as a bigint. + */ +export const to18 = (value: BigNumberish): bigint => { + return ethers.parseUnits(value.toString(), 18); +}; + +/** + * Converts a value from 18 decimal places to a string representation. + * + * @param value The value to convert. + * @returns The string representation of the converted value. + */ +export const from18 = (value: bigint): string => { + return ethers.formatUnits(value, 18); +}; + +/** + * Converts the given amount to Gwei. + * @param amount - The amount to convert. + * @returns The converted amount in Gwei. + */ +export function toGwei(amount: BigNumberish): BigNumberish { + return ethers.parseUnits(amount.toString(), "gwei"); +} diff --git a/test/hardhat/utils/operationHelpers.ts b/test/hardhat/utils/operationHelpers.ts new file mode 100644 index 00000000..bcd33335 --- /dev/null +++ b/test/hardhat/utils/operationHelpers.ts @@ -0,0 +1,312 @@ +import { ethers } from "hardhat"; +import { encodeData, toGwei } from "./encoding"; +import { + ExecutionMethod, + ModuleType, + PackedUserOperation, + UserOperation, +} from "./types"; +import { Signer, AddressLike, BytesLike, BigNumberish } from "ethers"; +import { EntryPoint } from "../../../typechain-types"; +import { Hexable } from "@ethersproject/bytes"; + +export const DefaultsForUserOp: UserOperation = { + sender: ethers.ZeroAddress, + nonce: 0, + initCode: "0x", + callData: "0x", + callGasLimit: 0, + verificationGasLimit: 150000, // default verification gas. Should add create2 cost (3200+200*length) if initCode exists + preVerificationGas: 21000, // should also cover calldata cost. + maxFeePerGas: 0, + maxPriorityFeePerGas: 1e9, + paymaster: ethers.ZeroAddress, + paymasterData: "0x", + paymasterVerificationGasLimit: 3e5, + paymasterPostOpGasLimit: 0, + signature: "0x", +}; + +/** + * Simplifies the creation of a PackedUserOperation object by abstracting repetitive logic and enhancing readability. + * @param userOp The user operation details. + * @returns The packed user operation object. + */ +export function buildPackedUserOp(userOp: UserOperation): PackedUserOperation { + const { + sender, + nonce, + initCode = "0x", + callData = "0x", + callGasLimit = 1_500_000, + verificationGasLimit = 1_500_000, + preVerificationGas = 2_000_000, + maxFeePerGas = toGwei("20"), + maxPriorityFeePerGas = toGwei("10"), + paymaster = ethers.ZeroAddress, + paymasterData = "0x", + paymasterVerificationGasLimit = 3_00_000, + paymasterPostOpGasLimit = 0, + signature = "0x", + } = userOp; + + // Construct the gasFees and accountGasLimits in a single step to reduce repetition + const packedValues = packGasValues( + callGasLimit, + verificationGasLimit, + maxFeePerGas, + maxPriorityFeePerGas, + ); + + // Construct paymasterAndData only if a paymaster is specified + const paymasterAndData = + paymaster !== ethers.ZeroAddress + ? encodeData(["address", "bytes"], [paymaster, paymasterData]) + : "0x"; + + // Return the PackedUserOperation, leveraging the simplicity of the refactored logic + return { + sender, + nonce, + initCode, + callData, + accountGasLimits: packedValues.accountGasLimits, + preVerificationGas, + gasFees: packedValues.gasFees, + paymasterAndData, + signature, + }; +} + +/** + * Generates a signed PackedUserOperation for testing purposes. + * @param {UserOperation} userOp - The user operation to be signed. + * @param {Signer} signer - The signer object to sign the operation. + * @param {Object} setup - The setup object containing deployed contracts and addresses. + * @param {string} [deposit] - Optional deposit amount in ETH. + * @returns {Promise} A Promise that resolves to a PackedUserOperation. + */ +export async function buildSignedUserOp( + userOp: UserOperation, + signer: Signer, + setup: { entryPoint: any; module: any }, + deposit?: string, +): Promise { + if (!setup.entryPoint || !setup.module) { + throw new Error("Setup object is missing required properties."); + } + if (!signer) { + throw new Error("Signer must be provided."); + } + + const moduleAddress = await setup.module.getAddress(); + const nonce = await setup.entryPoint.getNonce( + userOp.sender, + ethers.zeroPadBytes(moduleAddress, 24), + ); + + userOp.nonce = nonce; + const packedUserOp = buildPackedUserOp({ + ...userOp, + nonce: nonce.toString(), + }); + + const userOpHash = await setup.entryPoint.getUserOpHash(packedUserOp); + const signature = await signer.signMessage(ethers.getBytes(userOpHash)); + packedUserOp.signature = signature; + + if (deposit) { + const depositAmount = ethers.parseEther(deposit); + await setup.entryPoint.depositTo(userOp.sender, { value: depositAmount }); + } + + return packedUserOp; +} + +// TODO +// export function packPaymasterData(paymaster: string, paymasterVerificationGasLimit: BytesLike | number | bigint, postOpGasLimit: BytesLike | number | bigint, paymasterData: string): string { +// return hexConcat([ +// paymaster, +// hexZeroPad(BigNumber.from(paymasterVerificationGasLimit).toHexString(), 16), +// hexZeroPad(BigNumber.from(postOpGasLimit).toHexString(), 16), +// paymasterData +// ]); +// } + +export async function signUserOperation( + accountAddress: AddressLike, + initCode: BytesLike, + entryPoint: EntryPoint, + moduleAddress: AddressLike, + owner: Signer, +): Promise { + const nonce = await entryPoint.getNonce( + accountAddress, + ethers.zeroPadBytes(moduleAddress.toString(), 24), + ); + const userOp = buildPackedUserOp({ sender: accountAddress, nonce, initCode }); + const userOpHash = await entryPoint.getUserOpHash(userOp); + userOp.signature = await owner.signMessage(ethers.getBytes(userOpHash)); + return userOp; +} + +/** + * Generates the full initialization code for deploying a smart account. + * @param factoryAddress - The address of the AccountFactory contract. + * @param moduleAddress - The address of the module to be installed in the smart account. + * @param ownerAddress - The address of the owner of the new smart account. + * @param moduleType - The type of module to install, defaulting to "1". + * @returns The full initialization code as a hex string. + */ +export async function generateFullInitCode( + ownerAddress: AddressLike, + factoryAddress: AddressLike, + moduleAddress: AddressLike, + moduleType: ModuleType = ModuleType.Validation, +): Promise { + const AccountFactory = await ethers.getContractFactory("AccountFactory"); + const moduleInitData = ethers.solidityPacked(["address"], [ownerAddress]); + + // Encode the createAccount function call with the provided parameters + const initCode = AccountFactory.interface + .encodeFunctionData("createAccount", [ + moduleAddress, + moduleType, + moduleInitData, + ]) + .slice(2); + + return factoryAddress + initCode; +} + +/** + * Calculates the CREATE2 address for a smart account deployment. + * @param {AddressLike} signerAddress - The address of the signer (owner of the new smart account). + * @param {AddressLike} factoryAddress - The address of the AccountFactory contract. + * @param {AddressLike} moduleAddress - The address of the module to be installed in the smart account. + * @param {number | string} moduleType - The type of module to install. + * @returns {Promise} The calculated CREATE2 address. + */ +export async function getAccountAddress( + signerAddress: AddressLike, + factoryAddress: AddressLike, + moduleAddress: AddressLike, + moduleType: ModuleType = ModuleType.Validation, +): Promise { + // Ensure SmartAccount bytecode is fetched dynamically in case of contract upgrades + const SmartAccount = await ethers.getContractFactory("SmartAccount"); + const smartAccountBytecode = SmartAccount.bytecode; + + // Module initialization data, encoded + const moduleInitData = ethers.solidityPacked(["address"], [signerAddress]); + + // Salt for CREATE2, based on module address, type, and initialization data + const salt = ethers.solidityPackedKeccak256( + ["address", "uint256", "bytes"], + [moduleAddress, moduleType, moduleInitData], + ); + + // Calculate CREATE2 address using ethers utility function + const create2Address = ethers.getCreate2Address( + factoryAddress.toString(), + salt, + ethers.keccak256(smartAccountBytecode), + ); + + return create2Address; +} + +/** + * Packs gas values into the format required by PackedUserOperation. + * @param callGasLimit Call gas limit. + * @param verificationGasLimit Verification gas limit. + * @param maxFeePerGas Maximum fee per gas. + * @param maxPriorityFeePerGas Maximum priority fee per gas. + * @returns An object containing packed gasFees and accountGasLimits. + */ +export function packGasValues( + callGasLimit: BigNumberish, + verificationGasLimit: BigNumberish, + maxFeePerGas: BigNumberish, + maxPriorityFeePerGas: BigNumberish, +) { + const gasFees = ethers.solidityPacked( + ["uint128", "uint128"], + [maxPriorityFeePerGas, maxFeePerGas], + ); + const accountGasLimits = ethers.solidityPacked( + ["uint128", "uint128"], + [callGasLimit, verificationGasLimit], + ); + + return { gasFees, accountGasLimits }; +} + +/** + * Generates the execution call data for a given execution method. + * @param executionOptions - The options for the execution. + * @param packedUserOp - The packed user operation (optional). + * @param userOpHash - The hash of the user operation (optional). + * @returns The execution call data as a string. + */ +export async function generateExecutionCallData( + { executionMethod, targetContract, functionName, args = [], mode, value = 0 }, + packedUserOp = "0x", + userOpHash = "0x", +): Promise { + // Fetch the signer from the contract object + const AccountExecution = await ethers.getContractFactory("AccountExecution"); + + const targetAddress = await targetContract.getAddress(); + // Encode the target function call data + const functionCallData = targetContract.interface.encodeFunctionData( + functionName, + args, + ); + const modeHash = ethers.keccak256(ethers.toUtf8Bytes(mode)); + + // Encode the execution calldata + let executionCalldata; + switch (executionMethod) { + case ExecutionMethod.Execute: + case ExecutionMethod.ExecuteFromExecutor: + executionCalldata = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "bytes"], + [targetAddress, value, functionCallData], + ); + break; + case ExecutionMethod.ExecuteUserOp: + executionCalldata = ethers.AbiCoder.defaultAbiCoder().encode( + ["bytes32", "address", "uint256", "bytes"], + [modeHash, targetAddress, value, functionCallData], + ); + break; + default: + throw new Error("Invalid execution method type"); + } + + // Determine the method name based on the execution method + let methodName; + let executeCallData; + if (executionMethod === ExecutionMethod.Execute) { + methodName = "execute"; + + executeCallData = AccountExecution.interface.encodeFunctionData( + methodName, + [modeHash, executionCalldata], + ); + } else if (executionMethod === ExecutionMethod.ExecuteFromExecutor) { + methodName = "executeFromExecutor"; + executeCallData = AccountExecution.interface.encodeFunctionData( + methodName, + [modeHash, executionCalldata], + ); + } else if (executionMethod === ExecutionMethod.ExecuteUserOp) { + methodName = "executeUserOp"; + executeCallData = AccountExecution.interface.encodeFunctionData( + methodName, + [packedUserOp, userOpHash], + ); + } + return executeCallData; +} diff --git a/test/hardhat/utils/types.ts b/test/hardhat/utils/types.ts new file mode 100644 index 00000000..77320428 --- /dev/null +++ b/test/hardhat/utils/types.ts @@ -0,0 +1,63 @@ +import { NumberLike } from "@nomicfoundation/hardhat-network-helpers/dist/src/types"; +import { AddressLike, BigNumberish, BytesLike, Signer } from "ethers"; +import { + AccountFactory, + Counter, + EntryPoint, + MockValidator, + SmartAccount, +} from "../../../typechain-types"; + +export interface DeploymentFixture { + entryPoint: EntryPoint; + smartAccount: SmartAccount; + factory: AccountFactory; + module: MockValidator; + counter: Counter; + accounts: Signer[]; + addresses: string[]; +} + +// Todo +// Review: check for need of making these optional +export interface UserOperation { + sender: AddressLike; // Or string + nonce?: BigNumberish; + initCode?: BytesLike; + callData?: BytesLike; + callGasLimit?: BigNumberish; + verificationGasLimit?: BigNumberish; + preVerificationGas?: BigNumberish; + maxFeePerGas?: BigNumberish; + maxPriorityFeePerGas?: BigNumberish; + paymaster?: AddressLike; // Or string + paymasterVerificationGasLimit?: BigNumberish; + paymasterPostOpGasLimit?: BigNumberish; + paymasterData?: BytesLike; + signature?: BytesLike; +} + +export interface PackedUserOperation { + sender: AddressLike; // Or string + nonce: BigNumberish; + initCode: BytesLike; + callData: BytesLike; + accountGasLimits: BytesLike; + preVerificationGas: BigNumberish; + gasFees: BytesLike; + paymasterAndData: BytesLike; + signature: BytesLike; +} + +export enum ExecutionMethod { + Execute, + ExecuteFromExecutor, + ExecuteUserOp, +} + +export enum ModuleType { + Validation = 1, + Execution = 2, + Fallback = 3, + Hooks = 4, +} diff --git a/test/hardhat/utils/utils.ts b/test/hardhat/utils/utils.ts deleted file mode 100644 index 74480db8..00000000 --- a/test/hardhat/utils/utils.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { artifacts, ethers } from "hardhat"; - -// Conversion to Bytes32 -export const toBytes32 = (text: string): string => { - return ethers.encodeBytes32String(text); -}; - -// Conversion from Bytes32 -export const fromBytes32 = (bytes32: string): string => { - return ethers.decodeBytes32String(bytes32); -}; - -// Convert to 18 decimals -export const to18Decimals = (value: number | string): bigint => { - return ethers.parseUnits(value.toString(), 18); -}; - -// Convert from 18 decimals -export const from18Decimals = (value: bigint): string => { - return ethers.formatUnits(value, 18); -}; - -export function buildUserOp({ - sender, - nonce, - initCode = "", - callData = "", - callGasLimit, - executionGasLimit, // For the execution of callData - verificationGasLimit, // For the validateUserOp - preVerificationGas, - maxFeePerGas, - maxPriorityFeePerGas, - paymaster = ethers.ZeroAddress, - paymasterData = "0x", - signature = "0x", -}: { - sender: string; - nonce: number; - initCode?: string; - callData?: string; - callGasLimit: number; - executionGasLimit: number; // For the execution of callData - verificationGasLimit: number; // For the validateUserOp - preVerificationGas: number; - maxFeePerGas: number; - maxPriorityFeePerGas: number; - paymaster?: string; - paymasterData?: string; - signature?: string; -}) { - // Ensure maxFeePerGas and maxPriorityFeePerGas are provided in wei - const gasFees = ethers.solidityPacked( - ["uint128", "uint128"], - [maxFeePerGas, maxPriorityFeePerGas], - ); - - // Pack accountGasLimits as bytes32 combining callGasLimit and verificationGasLimit - const accountGasLimits = ethers.solidityPacked( - ["uint128", "uint128"], - [executionGasLimit, verificationGasLimit], - ); - - // Combine paymaster address and additional data into paymasterAndData - const paymasterAndData = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "bytes"], - [paymaster, paymasterData], - ); - - // Construct the PackedUserOperation object - const packedUserOp = { - sender, - nonce, - initCode, - callData, - accountGasLimits: ethers.hexlify(accountGasLimits), - preVerificationGas, - gasFees: ethers.hexlify(gasFees), - paymasterAndData, - signature, - }; - - return packedUserOp; -} diff --git a/yarn.lock b/yarn.lock index 9cb4d61a..f5acae4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -753,6 +753,11 @@ "@nomicfoundation/solidity-analyzer-win32-ia32-msvc" "0.1.1" "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.1" +"@nomiclabs/hardhat-ethers@^2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.2.3.tgz#b41053e360c31a32c2640c9a45ee981a7e603fe0" + integrity sha512-YhzPdzb612X591FOe68q+qXVXGG2ANZRvDo0RRUtimev85rCrAlv/TLMEZw5c+kq9AbzocLTVX/h2jVIFPL9Xg== + "@nomiclabs/hardhat-etherscan@^2.1.6": version "2.1.8" resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-2.1.8.tgz#e206275e96962cd15e5ba9148b44388bc922d8c2" @@ -2025,6 +2030,10 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +"ds-test@github:dapphub/ds-test": + version "1.0.0" + resolved "https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0" + elliptic@6.5.4, elliptic@^6.5.2, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -2811,7 +2820,12 @@ hardhat-deploy-ethers@^0.3.0-beta.11: resolved "https://registry.yarnpkg.com/hardhat-deploy-ethers/-/hardhat-deploy-ethers-0.3.0-beta.13.tgz#b96086ff768ddf69928984d5eb0a8d78cfca9366" integrity sha512-PdWVcKB9coqWV1L7JTpfXRCI91Cgwsm7KLmBcwZ8f0COSm1xtABHZTyz3fvF6p42cTnz1VM0QnfDvMFlIRkSNw== -hardhat-deploy@^0.11.23: +hardhat-deploy-ethers@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/hardhat-deploy-ethers/-/hardhat-deploy-ethers-0.4.1.tgz#dd70b0cc413ed99e98994047b383a004cf1c14f8" + integrity sha512-RM6JUcD0dOCjemxnKLtK7XQQI7NWn+LxF5qicGYax0PtWayEUXAewOb4WIHZ/yearhj+s2t6dL0MnHyLTENwJg== + +hardhat-deploy@^0.11.23, hardhat-deploy@^0.11.45: version "0.11.45" resolved "https://registry.yarnpkg.com/hardhat-deploy/-/hardhat-deploy-0.11.45.tgz#bed86118175a38a03bb58aba2ce1ed5e80a20bc8" integrity sha512-aC8UNaq3JcORnEUIwV945iJuvBwi65tjHVDU3v6mOcqik7WAzHVCJ7cwmkkipsHrWysrB5YvGF1q9S1vIph83w== @@ -4422,6 +4436,10 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +"solady@github:Vectorized/solady": + version "0.0.168" + resolved "https://codeload.github.com/Vectorized/solady/tar.gz/1372606383445c0a247e6c58eb255a529734258a" + "solady@github:vectorized/solady": version "0.0.168" resolved "https://codeload.github.com/vectorized/solady/tar.gz/9deb9ed36a27261a8745db5b7cd7f4cdc3b1cd4e"