Skip to content

Commit

Permalink
feat: adds guide to build a 7579 validator
Browse files Browse the repository at this point in the history
  • Loading branch information
koshikraj committed Jun 11, 2024
1 parent ce4f6c0 commit 4d5aa1a
Show file tree
Hide file tree
Showing 11 changed files with 392 additions and 4 deletions.
70 changes: 70 additions & 0 deletions docs/code/OwnableValidator.sol
Original file line number Diff line number Diff line change
@@ -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) { }
}
29 changes: 29 additions & 0 deletions docs/code/OwnableValidator.t.ts
Original file line number Diff line number Diff line change
@@ -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'))
})
264 changes: 264 additions & 0 deletions docs/pages/getting-started/building-7579-validator.md
Original file line number Diff line number Diff line change
@@ -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<any> => {

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)


Loading

0 comments on commit 4d5aa1a

Please sign in to comment.