Skip to content

Commit

Permalink
Added Sample 4337 Module
Browse files Browse the repository at this point in the history
  • Loading branch information
nlordell committed Nov 8, 2023
1 parent fbc5c97 commit e7e4eb5
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 0 deletions.
144 changes: 144 additions & 0 deletions contracts/modules/erc4337/SafeProtocol4337Module.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.18;

import {SafeProtocolAction, SafeTransaction} from "../../DataTypes.sol";
import {PLUGIN_PERMISSION_EXECUTE_CALL} from "../../common/Constants.sol";
import {IAccount} from "../../interfaces/Accounts.sol";
import {ISafeProtocolManager} from "../../interfaces/Manager.sol";
import {IERC165, ISafeProtocolFunctionHandler, ISafeProtocolPlugin} from "../../interfaces/Modules.sol";
import {UserOperation} from "./interfaces/IERC4337.sol";
import {ISafeProtocol4337Handler} from "./interfaces/ISafeProtocol4337Handler.sol";

contract SafeProtocol4337Module is ISafeProtocolFunctionHandler, ISafeProtocolPlugin {
uint256 private constant VALIDATION_SIG_SUCCESS = 0;
uint256 private constant VALIDATION_SIG_FAILURE = 0;

address payable public immutable entrypoint;

constructor(address payable _entrypoint) {
require(_entrypoint != address(0), "invalid entrypoint address");
entrypoint = _entrypoint;
}

/**
* @inheritdoc IERC165
*/
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return
interfaceId == type(IERC165).interfaceId ||
interfaceId == type(ISafeProtocolFunctionHandler).interfaceId ||
interfaceId == type(ISafeProtocolPlugin).interfaceId;
}

/**
* @inheritdoc ISafeProtocolFunctionHandler
*/
function handle(address account, address sender, uint256 value, bytes calldata data) external override returns (bytes memory result) {
require(sender == entrypoint, "unsupported entrypoint");
require(value == 0, "not payable");

ISafeProtocolManager manager = ISafeProtocolManager(msg.sender);
bytes4 selector = bytes4(data[:4]);
if (selector == ISafeProtocol4337Handler(account).validateUserOp.selector) {
(UserOperation memory userOp, bytes32 userOpHash, uint256 missingAccountFunds) = abi.decode(
data[4:],
(UserOperation, bytes32, uint256)
);
uint256 validationData = _validateUserOp(manager, account, userOp, userOpHash, missingAccountFunds);
result = abi.encode(validationData);
} else if (selector == ISafeProtocol4337Handler(account).executeUserOp.selector) {
(address to, uint256 opValue, bytes memory opData) = abi.decode(data[4:], (address, uint256, bytes));
_executeUserOp(manager, account, to, opValue, opData);
}
}

/**
* Validate account operation.
* @param manager the protocol manager.
* @param account the account.
* @param userOp the operation that is about to be executed.
* @param missingAccountFunds missing funds on the account's deposit in the entrypoint.
* @return validationData packaged validation data.
*/
function _validateUserOp(
ISafeProtocolManager manager,
address account,
UserOperation memory userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) internal returns (uint256 validationData) {
require(bytes4(userOp.callData) == ISafeProtocol4337Handler(account).executeUserOp.selector, "unsupported execution");

if (missingAccountFunds > 0) {
SafeTransaction memory transaction;
{
transaction.actions = new SafeProtocolAction[](1);
transaction.actions[0].to = entrypoint;
transaction.actions[0].value = missingAccountFunds;
transaction.nonce = userOp.nonce;
}
manager.executeTransaction(account, transaction);
}

try IAccount(account).checkSignatures(userOpHash, "", userOp.signature) {
validationData = VALIDATION_SIG_SUCCESS;
} catch {
validationData = VALIDATION_SIG_FAILURE;
}
}

/**
* Executes a account operation.
* @param manager the protocol manager.
* @param account the account.
* @param to target of the operation.
* @param value value of the operation.
* @param data calldata for the operation.
*/
function _executeUserOp(ISafeProtocolManager manager, address account, address to, uint256 value, bytes memory data) internal {
SafeTransaction memory transaction;
{
transaction.actions = new SafeProtocolAction[](1);
transaction.actions[0].to = payable(to);
transaction.actions[0].value = value;
transaction.actions[0].data = data;
}
manager.executeTransaction(account, transaction);
}

/**
* @inheritdoc ISafeProtocolFunctionHandler
*/
function metadataProvider()
external
view
override(ISafeProtocolFunctionHandler, ISafeProtocolPlugin)
returns (uint256 providerType, bytes memory location)
{}

/**
* @notice A funtion that returns name of the plugin
* @return name string name of the plugin
*/
function name() external pure override returns (string memory) {
return "Safe Protocol ERC-4337 Plugin";
}

/**
* @notice A function that returns version of the plugin
* @return version string version of the plugin
*/
function version() external pure override returns (string memory) {
return "1";
}

/**
* @notice A function that indicates permissions required by the.
* @dev Permissions types and value: EXECUTE_CALL = 1, CALL_TO_SELF = 2, EXECUTE_DELEGATECALL = 4.
* These values can be sumed to indicate multiple permissions. e.g. EXECUTE_CALL + CALL_TO_SELF = 3
* @return permissions Bit-based permissions required by the plugin.
*/
function requiresPermissions() external pure override returns (uint8 permissions) {
permissions = PLUGIN_PERMISSION_EXECUTE_CALL;
}
}
6 changes: 6 additions & 0 deletions contracts/modules/erc4337/interfaces/IERC4337.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.18;

import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol";
import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
import {UserOperation} from "@account-abstraction/contracts/interfaces/UserOperation.sol";
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.18;

import {IAccount} from "./IERC4337.sol";

interface ISafeProtocol4337Handler is IAccount {
function executeUserOp(address to, uint256 value, bytes calldata data) external;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"build:ts": "yarn rimraf dist && tsc -p tsconfig.prod.json"
},
"devDependencies": {
"@account-abstraction/contracts": "^0.6.0",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
"@nomicfoundation/hardhat-ethers": "^3.0.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
Expand Down
84 changes: 84 additions & 0 deletions test/modules/plugins/erc4337/SafeProtocol4337Plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import EntryPoint from "@account-abstraction/contracts/artifacts/EntryPoint.json";
import { Signer } from "ethers";
import { ethers, deployments } from "hardhat";

import { MODULE_TYPE_PLUGIN, MODULE_TYPE_FUNCTION_HANDLER, PLUGIN_PERMISSION_EXECUTE_CALL } from "../../../../src/utils/constants";

describe("SafeProtocol4337Module", async () => {
const setupTests = deployments.createFixture(async ({ deployments }) => {
await deployments.fixture();
const [deployer, owner, user, bundler] = await ethers.getSigners();

const registry = await ethers.deployContract("SafeProtocolRegistry", [owner.address]);
const manager = await ethers.deployContract("SafeProtocolManager", [owner.address, await registry.getAddress()]);

const entrypoint = await deployEntryPoint(deployer);
const module = await ethers.deployContract("SafeProtocol4337Module", [await entrypoint.getAddress()]);
const handler = await ethers.getContractAt("ISafeProtocol4337Handler", await module.getAddress());

const account = await ethers.deployContract("TestExecutor", [await manager.getAddress()]);
await account.setModule(await manager.getAddress());

await registry.connect(owner).addModule(await module.getAddress(), MODULE_TYPE_PLUGIN | MODULE_TYPE_FUNCTION_HANDLER);
await account.exec(
await account.getAddress(),
0,
manager.interface.encodeFunctionData("enablePlugin", [await module.getAddress(), PLUGIN_PERMISSION_EXECUTE_CALL]),
);
await account.exec(
await account.getAddress(),
0,
manager.interface.encodeFunctionData("setFunctionHandler", [
handler.validateUserOp.fragment.selector,
await handler.getAddress(),
]),
);
await account.exec(
await account.getAddress(),
0,
manager.interface.encodeFunctionData("setFunctionHandler", [
handler.executeUserOp.fragment.selector,
await handler.getAddress(),
]),
);

return { account, entrypoint, handler, user, bundler };
});

describe("handleOps", () => {
it("should validate and execute user operations", async () => {
const { account, entrypoint, handler, user, bundler } = await setupTests();

await user.sendTransaction({
to: await account.getAddress(),
value: ethers.parseEther("1.0"),
});

const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
await entrypoint.connect(bundler).handleOps(
[
{
sender: await account.getAddress(),
nonce: await entrypoint.getNonce(await account.getAddress(), 0),
initCode: "0x",
callData: handler.interface.encodeFunctionData("executeUserOp", [ethers.ZeroAddress, 0, "0x"]),
callGasLimit: 100000,
verificationGasLimit: 200000,
preVerificationGas: 100000,
maxFeePerGas: maxFeePerGas ?? 0,
maxPriorityFeePerGas: maxPriorityFeePerGas ?? 0,
paymasterAndData: "0x",
signature: "0x",
},
],
bundler.address,
);
});
});
});

async function deployEntryPoint(deployer: Signer) {
const { abi, bytecode } = EntryPoint;
const contract = await new ethers.ContractFactory(abi, bytecode, deployer).deploy();
return await ethers.getContractAt("IEntryPoint", await contract.getAddress());
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf"
integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==

"@account-abstraction/contracts@^0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@account-abstraction/contracts/-/contracts-0.6.0.tgz#7188a01839999226e6b2796328af338329543b76"
integrity sha512-8ooRJuR7XzohMDM4MV34I12Ci2bmxfE9+cixakRL7lA4BAwJKQ3ahvd8FbJa9kiwkUPCUNtj+/zxDQWYYalLMQ==

"@adraffy/[email protected]":
version "1.9.2"
resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz#60111a5d9db45b2e5cbb6231b0bb8d97e8659316"
Expand Down

0 comments on commit e7e4eb5

Please sign in to comment.