rhinestone

Tutorials

Building modules

This tutorial will walk you through building a simple module. You can choose what kind of module you want to build. For more information about the different kinds of modules, check out the modules section of the docs.

This tutorial uses Foundry, a toolchain to simplify and speed up smart contract development. If you are not familiar with Foundry, feel free to check out their docs.

If you do not have foundry installed, run the following command and follow the onscreen instructions: bash curl -L https://foundry.paradigm.xyz | bash

Installation

  1. Get started with ModuleKit by using our template or adding it into an existing foundry project:

Installation

git clone https://github.com/rhinestonewtf/module-template.git
cd module-template
forge install

Building a module

ValidatorExecutor

In this section, we will walk you through building a simple validator module. This validator module allows the owner of an account to sign transactions using an EOA.

  1. Duplicate the file src/validators/ValidatorTemplate.sol and rename it to SimpleValidator.sol and the contract to SimpleValidator.

If you added the ModuleKit to an existing project, you will need to create the SimpleValidator.sol file yourself and copy the code from here.

  1. Add the following import statement to the top of the file:
import {ECDSA} from "solady/utils/ECDSA.sol";

and add the following code to the top of the contract:

contract SimpleValidator is BaseValidator {
    using ECDSA for bytes32;

    mapping(address => address) public owners;

    function setOwner(address owner) external {
        owners[msg.sender] = owner;
    }

    ...
}
  1. Edit the contract functions to match the following code:

validateUserOp

function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash)
    external
    view
    override
    returns (uint256)
{
    bytes32 hash = userOpHash.toEthSignedMessageHash();
    (bytes memory sig, ) = abi.decode(userOp.signature, (bytes, address));

    if (owners[msg.sender] != hash.recover(sig)) {
        return VALIDATION_FAILED;
    }
    return VALIDATION_SUCCESS;
}

This function validates the userOperation by checking that the signature is valid and made by the owner of the account.

isValidSignature

function isValidSignature(bytes32 signedDataHash, bytes memory moduleSignature)
    public
    view
    override
    returns (bytes4)
{
    bytes32 hash = signedDataHash.toEthSignedMessageHash();
    if (owners[msg.sender] != hash.recover(moduleSignature)) {
        return 0xffffffff;
    }
    return ERC1271_MAGICVALUE;
}

This function checks that the signature is valid and made by the owner of the account. This allows the validator to be used as an EIP-1271 validator.

Congratulations, you just built your first validator!