diff --git a/docs/code/OwnableValidator.sol b/docs/code/OwnableValidator.sol new file mode 100644 index 0000000..88cce1b --- /dev/null +++ b/docs/code/OwnableValidator.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +import { ERC7579ValidatorBase } from "../module-bases/ERC7579ValidatorBase.sol"; +import { PackedUserOperation } from + "@account-abstraction/contracts/core/UserOperationLib.sol"; + +import { SignatureCheckerLib } from "solady/utils/SignatureCheckerLib.sol"; +import { ECDSA } from "solady/utils/ECDSA.sol"; + +contract OwnableValidator is ERC7579ValidatorBase { + using SignatureCheckerLib for address; + + mapping(address subAccout => address owner) public owners; // [!code focus] + + function onInstall(bytes calldata data) external override { // [!code focus] + if (data.length == 0) return; // [!code focus] + address owner = abi.decode(data, (address)); // [!code focus] + owners[msg.sender] = owner; // [!code focus] + } + + function onUninstall(bytes calldata) external override { // [!code focus] + delete owners[msg.sender]; // [!code focus] + } // [!code focus] + + function validateUserOp( // [!code focus] + PackedUserOperation calldata userOp, // [!code focus] + bytes32 userOpHash // [!code focus] + ) + external + view + override + returns (ValidationData) + { + bool validSig = owners[userOp.sender].isValidSignatureNow( // [!code focus] + ECDSA.toEthSignedMessageHash(userOpHash), userOp.signature // [!code focus] + ); // [!code focus] + return _packValidationData(!validSig, type(uint48).max, 0); // [!code focus] + } + + function isValidSignatureWithSender( + address, + bytes32 hash, + bytes calldata data + ) + external + view + override + returns (bytes4) + { + address owner = owners[msg.sender]; + return SignatureCheckerLib.isValidSignatureNowCalldata(owner, hash, data) + ? EIP1271_SUCCESS + : EIP1271_FAILED; + } + + function name() external pure returns (string memory) { + return "OwnableValidator"; + } + + function version() external pure returns (string memory) { + return "0.0.1"; + } + + function isModuleType(uint256 typeID) external pure override returns (bool) { + return typeID == TYPE_VALIDATOR; + } + + function isInitialized(address smartAccount) external view returns (bool) { } +} diff --git a/docs/code/OwnableValidator.t.ts b/docs/code/OwnableValidator.t.ts new file mode 100644 index 0000000..30afa47 --- /dev/null +++ b/docs/code/OwnableValidator.t.ts @@ -0,0 +1,29 @@ +it('should add a ownable validator and execute ops with signatures', async () => { + const { user1, safe, ownableValidator, safe7579, entryPoint, relayer } = await setupTests() + + await entryPoint.depositTo(await safe.getAddress(), { value: ethers.parseEther('1.0') }) + + await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther('1') }) + + const call = {target: user1.address as Hex, value: ethers.parseEther('1'), callData: '0x' as Hex} // Added the callData property + + await execSafeTransaction(safe, await safe7579.initializeAccount.populateTransaction([], [], [], [], {registry: ZeroAddress, attesters: [], threshold: 0})); + + await execSafeTransaction(safe, {to: await safe.getAddress(), data: ((await safe7579.installModule.populateTransaction(1, await ownableValidator.getAddress(), utils.defaultAbiCoder.encode(['address'], [user1.address]))).data as string), value: 0}) + + const key = BigInt(pad(await ownableValidator.getAddress() as Hex, { + dir: "right", + size: 24, + }) || 0 + ) + const currentNonce = await entryPoint.getNonce(await safe.getAddress(), key); + + + let userOp = buildUnsignedUserOpTransaction(await safe.getAddress(), currentNonce, call) + + const typedDataHash = ethers.getBytes(await entryPoint.getUserOpHash(userOp)) + userOp.signature = await user1.signMessage(typedDataHash) + + await logGas('Execute UserOp without a prefund payment', entryPoint.handleOps([userOp], relayer)) + expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('0')) + }) \ No newline at end of file diff --git a/docs/pages/getting-started/building-7579-validator.md b/docs/pages/getting-started/building-7579-validator.md new file mode 100644 index 0000000..dfeb7f9 --- /dev/null +++ b/docs/pages/getting-started/building-7579-validator.md @@ -0,0 +1,264 @@ +--- +sidebar_position: 4 + +--- + +# Tutorial to build a validator and enable on Safe Account + +In this tutorial, we will create a basic 7579 validator module. This module can be installed into a Safe Account using the Safe 7579 adapter, enhancing the validation flow of the Safe Account. + + + +## Getting started: + +Before we install and enable any modules for Safe Accounts, they need to be developed and thoroughly tested. Once developed, these modules can be installed on either an existing or a new Safe Account. + +In this tutorial, we will build a basic external validator. This validator will have the capability to add a new owner and verify transactions for an existing Safe Account. + +::::steps + +### Clone the module template repository: + +```bash [Terminal] +git clone https://github.com/koshikraj/module-template-7579.git +``` + +To simplify development, let's start with a module template that comes with all the necessary dependencies pre-installed. This module package combines both Hardhat and Foundry projects for a streamlined setup. + +### Build the validator module + +We will create an ownable validator using the 7579 standard that accomplishes the following tasks: + +1. Adds a new owner address to the Safe by mapping it to the account address when installing the module. +2. Implements validation logic in the validateUserOp method to ensure the transaction is signed by the added owner. +3. Removes the new owner address mapped to the Safe account address when uninstalling the module. + +Here is the module code that achieves these requirements: + +```ts [ OwnableValidator.sol ] + +// [!include ~/code/OwnableValidator.sol] + +``` + +You can now build and test this code against the Safe Account by placing it under the module/contracts directory. + +```txt [ Project structure] +module-template-7579/ +├── module/ // [!code focus] +| └── contracts/ // [!code focus] +├── web/ +└── packages/ +``` + +Make sure the module can build without any errors inside the project. + +```bash [Terminal] +npm run build +``` + +### Test the validator module + + +The validator module can be tested against Safe using the 7579 adapter. The test setup automatically handles adding the 7579 adapter as the fallback handler and module to the Safe. + +To test our validator module against Safe, follow these steps + +1. **Install the Validator Module:** Use the Safe 7579 adapter to install the validator module by passing the owner address and module code, which is 1 for the validator. +2. **Construct the Nonce:** Add the validator address as the key to construct the nonce. +3. **Build the User Operation:** Create the User Operation and send it, appending the valid signature that the validator will verify. + + +```ts [ OwnableValidator.t.sol ] + + await execSafeTransaction(safe, + await safe7579.initializeAccount.populateTransaction([], [], [], [], {registry: ZeroAddress, attesters: [], threshold: 0})); // [!code focus] + + await execSafeTransaction(safe, + {to: await safe.getAddress(), // [!code focus] + data: ((await safe7579.installModule.populateTransaction(1, await ownableValidator.getAddress(), utils.defaultAbiCoder.encode(['address'], [user1.address]))).data as string), // [!code focus] + value: 0}) // [!code focus] + + const key = BigInt(pad(await ownableValidator.getAddress() as Hex, { // [!code focus] + dir: "right", + size: 24, + }) || 0 + ) + const currentNonce = await entryPoint.getNonce(await safe.getAddress(), key); // [!code focus] + + + let userOp = buildUnsignedUserOpTransaction(await safe.getAddress(), currentNonce, call) // [!code focus] + + const typedDataHash = ethers.getBytes(await entryPoint.getUserOpHash(userOp)) + userOp.signature = await user1.signMessage(typedDataHash) +``` + + +You can now test the validator module against the Safe Account by placing the test script under the module/test directory. + +```txt [ Project structure] +module-template-7579/ +├── module/ // [!code focus] +| └── test/ // [!code focus] +├── web/ +└── packages/ +``` + +After setting up the validation test flow, you can run the tests to ensure everything is working correctly. + +```bash [Terminal] +npm run test + + +Safe7579 - Basic tests + +3 passing (200ms) +``` + + + +### Install the validator module to Safe Account using a Safe App + +Now that you have thoroughly tested the validator code, you can proceed to deploy the validator module and integrate it with a Safe Account. Follow these steps to deploy the validator module and use it on a Safe Account: + +```bash [Terminal] +npm run deploy sepolia + +depolyed "Safe7579" at 0x94952C0Ea317E9b8Bca613490AF25f6185623284 +depolyed "OwnableValidator" at 0xe90044FE8855B307Fe8F9848fd9558D5D3479191 + +``` + + +To add the validator to the Safe Account using a Safe App via Safe {Wallet}, you can build a custom Safe App using the provided template from your code base. Follow these steps to create and deploy the Safe App: + +```txt [ Safe App structure] +web/src/ +├── logic/ // [!code hl] +| └── module.ts +├── pages/ // [!code hl] +└── utils/ +``` + +Replace the deployed Safe7579 and OwnableValidator contract addresses to add and interact with the module. + +The Safe App can be added to the Safe {Wallet} as a custom Safe App. + +![Validator App Add](/img/validator-app-add.png) + +Here is the snippet of code that +1. Enables Safe7579 adapter as a module and fallback handler. +2. Initializes the Safe7579 account. +3. Installs the validator. + +```ts [ module.ts ] + + +export const addValidatorModule = async (ownerAddress: string ) => { + + + if (!await isConnectedToSafe()) throw Error("Not connected to a Safe") + + const info = await getSafeInfo() + + const txs: BaseTransaction[] = [] + + + if (!await isModuleEnabled(info.safeAddress, safe7579Module)) { + txs.push(await buildEnableModule(info.safeAddress, safe7579Module)) + txs.push(await buildUpdateFallbackHandler(info.safeAddress, safe7579Module)) + } + + txs.push(await buildInitSafe7579()) + + txs.push(await buildInstallOwnable(ownerAddress)) + + const provider = await getProvider() + // Updating the provider RPC if it's from the Safe App. + const chainId = (await provider.getNetwork()).chainId.toString() + + if (txs.length > 0) + await submitTxs(txs) +} + +``` + +Before the OwnableValidator is installed, we just need to provide the owner address that needs to be added. + +![Validator App Home](/img/validator-app-home.png) + +The Safe App will then enable the Safe 7579 adapter as module and fallback handler along with installing the validator. + +![Validator App Install](/img/validator-install.png) + + + +### Use the Safe Account via new validator + +Now that the validator module has been installed via Safe 7579 adapter, it can be validated via the new owner. +As the Safe Account is fullt compliant with the ERC-4337, thanks to the adapter, we can create and execute transactions via User Operations via bundlers. + +Here is how we can contruct the user operation for the Ownable validator and execute it. + + +```ts [ module.ts ] + +export const sendTransaction = async (chainId: string, recipient: string, amount: bigint, walletProvider: any, safeAccount: string): Promise => { + + const call = { target: recipient as Hex, value: amount, callData: '0x' as Hex } + + const key = BigInt(pad(ownableModule as Hex, { + dir: "right", + size: 24, + }) || 0 + ) + + const nonce = await getAccountNonce(publicClient(parseInt(chainId)), { + sender: safeAccount as Hex, + entryPoint: ENTRYPOINT_ADDRESS_V07, + key: key + }) + + let unsignedUserOp = buildUnsignedUserOpTransaction( + safeAccount as Hex, + call, + nonce, + ) + + const signUserOperation = async function signUserOperation(userOperation: UserOperation<"v0.7">) { + + const provider = await getJsonRpcProvider(chainId) + + const entryPoint = new Contract( + ENTRYPOINT_ADDRESS_V07, + EntryPoint.abi, + provider + ) + let typedDataHash = getBytes(await entryPoint.getUserOpHash(getPackedUserOperation(userOperation))) + return await walletProvider.signMessage(typedDataHash) as `0x${string}` + + } + + const userOperationHash = await sendUserOperation(chainId, unsignedUserOp, signUserOperation ) + + return userOperationHash; +} +``` + +![Validator UserOp](/img/validator-userop-tx.png) + +As soon as the transaction is executed, it can be verified via the Safe {Wallet} transactions. +![Validator TX](/img/validator-tx.png) +:::: + + + +## Essential Links + +- [Rhinestone Module Kit](https://github.com/rhinestonewtf/modulekit/tree/main) - *Repo* +- [Module template](https://github.com/rhinestonewtf/module-template) - *Repo* +- [How to build a module](https://docs.rhinestone.wtf/modulekit/build/module-basics) - *Docs* +- [7579 Module template](https://github.com/koshikraj/module-template-7579) +- [ERC 7579](https://erc7579.com) + + diff --git a/docs/pages/getting-started/erc-7579.mdx b/docs/pages/getting-started/erc-7579.mdx index 8d179cd..a8a0d25 100644 --- a/docs/pages/getting-started/erc-7579.mdx +++ b/docs/pages/getting-started/erc-7579.mdx @@ -25,28 +25,48 @@ There are various implementations for ERC-6900 accounts including: ERC-7579 based modules are natively not supported on Safe smart accounts. They can be easily supported via adapters. Safe along with Rhinestone team has developed a [7579 adapter](https://github.com/rhinestonewtf/safe7579) that will make Safe compatible with 7579 based modules. +This also makes the Safe Accounts full ERC4337 and ERC7579 compliant. There is also a launchpad that allows the new Safe Account to be created with 7579 enabled. +Launchpad is a factory that creates Safe accounts with Safe7579 and initializes them with the required modules. -### How to make a Safe comaptible with 7579 standard +### How to make an existing Safe comaptible with 7579 standard - An existing Safe can be 7579 compatible just by adding the [7579 adapter](https://github.com/rhinestonewtf/safe7579) as a Safe Module and Safe Fallback handler. ### Getting started: -The best way to get started with the module development is with the help of [module-template](https://github.com/rhinestonewtf/module-template) by Rhinestone. +Before we install and enable any modules for Safe Accounts, they need to be developed and test. They can then be installed on an existing or a new Safe Account. -Here are a few links to get started with building and testing. +#### Steps to Build and Test Your Module +The best way to get started with the module development is with the help of Module Template by Rhinestone. -- [With Rhinestone Module Kit](https://github.com/rhinestonewtf/modulekit/tree/main) - *Repo* +1. Clone the Module Template: Start by cloning the [module-template](https://github.com/rhinestonewtf/module-template) repository to use as a base for your development. + +2. Follow the Docs: Refer to the "[How to build a module](https://docs.rhinestone.wtf/modulekit/build/module-basics)" documentation for detailed steps and best practices. + +3. Develop Your Module: Customize and build your module using the template as a guide. + + +#### Essential Links + +- [Rhinestone Module Kit](https://github.com/rhinestonewtf/modulekit/tree/main) - *Repo* - [Module template](https://github.com/rhinestonewtf/module-template) - *Repo* - [How to build a module](https://docs.rhinestone.wtf/modulekit/build/module-basics) - *Docs* +#### Testing and Enabling Your Module on Safe + Once the module has been independantly developed. It can be tested and enabled on Safe Accounts. If you are looking to enable the module on the existing Safe, it can be achieved by building a Safe App along with the [7579 adapter](https://github.com/rhinestonewtf/safe7579). Here is a [module template](https://github.com/koshikraj/module-template-7579) to get started building module and Safe App that enables and interacts with the module developed. +Using this template, + +- Testing module with Safe 7579 adapter: The module can be tested against Safe Accounts using the hardhat tests. + +- Enable on Safe Accounts: To enable the module on existing Safe accounts, you can create a Safe App using the 7579 adapter. + ## Usage ::::steps diff --git a/docs/public/img/validator-app-add.png b/docs/public/img/validator-app-add.png new file mode 100644 index 0000000..33313e3 Binary files /dev/null and b/docs/public/img/validator-app-add.png differ diff --git a/docs/public/img/validator-app-home.png b/docs/public/img/validator-app-home.png new file mode 100644 index 0000000..ea20a76 Binary files /dev/null and b/docs/public/img/validator-app-home.png differ diff --git a/docs/public/img/validator-auth.png b/docs/public/img/validator-auth.png new file mode 100644 index 0000000..46163bf Binary files /dev/null and b/docs/public/img/validator-auth.png differ diff --git a/docs/public/img/validator-install.png b/docs/public/img/validator-install.png new file mode 100644 index 0000000..fa2cd60 Binary files /dev/null and b/docs/public/img/validator-install.png differ diff --git a/docs/public/img/validator-tx.png b/docs/public/img/validator-tx.png new file mode 100644 index 0000000..f0900b3 Binary files /dev/null and b/docs/public/img/validator-tx.png differ diff --git a/docs/public/img/validator-userop-tx.png b/docs/public/img/validator-userop-tx.png new file mode 100644 index 0000000..d8f7d5d Binary files /dev/null and b/docs/public/img/validator-userop-tx.png differ diff --git a/vocs.config.ts b/vocs.config.ts index 5856d14..5fe3d4d 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -29,11 +29,16 @@ export default defineConfig({ text: 'Build using ERC-7579', link: '/getting-started/erc-7579', }, + { + text: 'Build ERC-7579 validator', + link: '/getting-started/building-7579-validator', + }, { text: 'Build using ERC-6900', link: '/getting-started/erc-6900', } + ], }, {