-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
243 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
// 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 ISafeProtocolPlugin | ||
*/ | ||
function metadataProvider() | ||
external | ||
view | ||
override(ISafeProtocolFunctionHandler, ISafeProtocolPlugin) | ||
returns (uint256 providerType, bytes memory location) | ||
{} | ||
|
||
/** | ||
* @inheritdoc ISafeProtocolPlugin | ||
*/ | ||
function name() external pure override returns (string memory) { | ||
return "Safe Protocol ERC-4337 Plugin"; | ||
} | ||
|
||
/** | ||
* @inheritdoc ISafeProtocolPlugin | ||
*/ | ||
function version() external pure override returns (string memory) { | ||
return "1"; | ||
} | ||
|
||
/** | ||
* @inheritdoc ISafeProtocolPlugin | ||
*/ | ||
function requiresPermissions() external pure override returns (uint8 permissions) { | ||
permissions = PLUGIN_PERMISSION_EXECUTE_CALL; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
8 changes: 8 additions & 0 deletions
8
contracts/modules/erc4337/interfaces/ISafeProtocol4337Handler.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
test/modules/plugins/erc4337/SafeProtocol4337Plugin.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|