diff --git a/contracts/contracts/Base.sol b/contracts/contracts/Base.sol index 037ecd0..a43b6d1 100644 --- a/contracts/contracts/Base.sol +++ b/contracts/contracts/Base.sol @@ -58,7 +58,7 @@ abstract contract BasePlugin is ISafeProtocolPlugin { metadataHash = keccak256(metadata.encode()); } - function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + function supportsInterface(bytes4 interfaceId) external pure virtual override returns (bool) { return interfaceId == type(ISafeProtocolPlugin).interfaceId || interfaceId == type(IERC165).interfaceId; } diff --git a/contracts/contracts/ERC4337Plugin.sol b/contracts/contracts/ERC4337Plugin.sol index f4dd387..7133aa6 100644 --- a/contracts/contracts/ERC4337Plugin.sol +++ b/contracts/contracts/ERC4337Plugin.sol @@ -2,8 +2,12 @@ pragma solidity ^0.8.18; import {ISafeProtocolPlugin, ISafeProtocolFunctionHandler} from "@safe-global/safe-core-protocol/contracts/interfaces/Modules.sol"; +import {ISafeProtocolManager} from "@safe-global/safe-core-protocol/contracts/interfaces/Manager.sol"; import {SafeTransaction, SafeRootAccess, SafeProtocolAction} from "@safe-global/safe-core-protocol/contracts/DataTypes.sol"; -import {BasePluginWithEventMetadata, PluginMetadata, MetadataProviderType} from "./Base.sol"; +import {MODULE_TYPE_PLUGIN} from "@safe-global/safe-core-protocol/contracts/common/Constants.sol"; +import {BasePlugin, BasePluginWithEventMetadata, PluginMetadata, MetadataProviderType} from "./Base.sol"; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; interface ISafe { function isOwner(address owner) external view returns (bool); @@ -26,33 +30,11 @@ interface ISafe { uint8 operation ) external returns (bool success, bytes memory returnData); - function enablePlugin(address plugin, bool allowRootAccess) external; + function enablePlugin(address plugin, uint8 permissions) external; function setFunctionHandler(bytes4 selector, address functionHandler) external; } -interface ISafeProtocolManager { - /** - * @notice This function allows enabled plugins to execute non-delegate call transactions thorugh a Safe. - * It should validate the status of the plugin through the registry and allows only listed and non-flagged integrations to execute transactions. - * @param safe Address of a Safe account - * @param transaction SafeTransaction instance containing payload information about the transaction - * @return data Array of bytes types returned upon the successful execution of all the actions. The size of the array will be the same as the size of the actions - * in case of succcessful execution. Empty if the call failed. - */ - function executeTransaction(ISafe safe, SafeTransaction calldata transaction) external returns (bytes[] memory data); - - /** - * @notice This function allows enabled plugins to execute delegate call transactions thorugh a Safe. - * It should validate the status of the plugin through the registry and allows only listed and non-flagged integrations to execute transactions. - * @param safe Address of a Safe account - * @param rootAccess SafeTransaction instance containing payload information about the transaction - * @return data Arbitrary length bytes data returned upon the successful execution. The size of the array will be the same as the size of the actions - * in case of succcessful execution. Empty if the call failed. - */ - function executeRootAccess(ISafe safe, SafeRootAccess calldata rootAccess) external returns (bytes memory data); -} - struct UserOperation { address sender; uint256 nonce; @@ -103,6 +85,7 @@ contract ERC4337Plugin is ISafeProtocolFunctionHandler, BasePluginWithEventMetad require(msg.sender == address(PLUGIN_ADDRESS)); address payable safeAddress = payable(msg.sender); ISafe safe = ISafe(safeAddress); + require(safe.execTransactionFromModule(to, value, data, 0), "tx failed"); } @@ -120,10 +103,9 @@ contract ERC4337Plugin is ISafeProtocolFunctionHandler, BasePluginWithEventMetad require(address(this) != PLUGIN_ADDRESS, "Only delegatecall"); ISafe safe = ISafe(address(this)); - safe.setFallbackHandler(address(SAFE_PROTOCOL_MANAGER)); safe.enableModule(address(SAFE_PROTOCOL_MANAGER)); - safe.enablePlugin(PLUGIN_ADDRESS, false); + safe.enablePlugin(PLUGIN_ADDRESS, MODULE_TYPE_PLUGIN); safe.setFunctionHandler(this.validateUserOp.selector, PLUGIN_ADDRESS); safe.setFunctionHandler(this.execTransaction.selector, PLUGIN_ADDRESS); } @@ -138,7 +120,14 @@ contract ERC4337Plugin is ISafeProtocolFunctionHandler, BasePluginWithEventMetad override(BasePluginWithEventMetadata, ISafeProtocolFunctionHandler) returns (uint256 providerType, bytes memory location) { - providerType = uint256(MetadataProviderType.Contract); + providerType = uint256(MetadataProviderType.Event); location = abi.encode(address(this)); } + + function supportsInterface(bytes4 interfaceId) external pure override(BasePlugin, IERC165) returns (bool) { + return + interfaceId == type(ISafeProtocolPlugin).interfaceId || + interfaceId == type(IERC165).interfaceId || + interfaceId == type(ISafeProtocolFunctionHandler).interfaceId; + } } diff --git a/contracts/contracts/Plugins.sol b/contracts/contracts/Plugins.sol index 093c03d..62c5162 100644 --- a/contracts/contracts/Plugins.sol +++ b/contracts/contracts/Plugins.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.18; import {BasePluginWithEventMetadata, PluginMetadata} from "./Base.sol"; -import {ISafe} from "@safe-global/safe-core-protocol/contracts/interfaces/Accounts.sol"; import {ISafeProtocolManager} from "@safe-global/safe-core-protocol/contracts/interfaces/Manager.sol"; import {SafeTransaction, SafeProtocolAction} from "@safe-global/safe-core-protocol/contracts/DataTypes.sol"; import {_getFeeCollectorRelayContext, _getFeeTokenRelayContext, _getFeeRelayContext} from "@gelatonetwork/relay-context/contracts/GelatoRelayContext.sol"; @@ -47,12 +46,12 @@ contract RelayPlugin is BasePluginWithEventMetadata { emit MaxFeeUpdated(msg.sender, token, maxFee); } - function payFee(ISafeProtocolManager manager, ISafe safe, uint256 nonce) internal { + function payFee(ISafeProtocolManager manager, address safe, uint256 nonce) internal { address feeCollector = _getFeeCollectorRelayContext(); address feeToken = _getFeeTokenRelayContext(); uint256 fee = _getFeeRelayContext(); SafeProtocolAction[] memory actions = new SafeProtocolAction[](1); - uint256 maxFee = maxFeePerToken[address(safe)][feeToken]; + uint256 maxFee = maxFeePerToken[safe][feeToken]; if (fee > maxFee) revert FeeTooHigh(feeToken, fee); if (feeToken == NATIVE_TOKEN || feeToken == address(0)) { // If the native token is used for fee payment, then we directly send the fees to the fee collector @@ -81,7 +80,7 @@ contract RelayPlugin is BasePluginWithEventMetadata { if (!success) revert RelayExecutionFailure(data); } - function executeFromPlugin(ISafeProtocolManager manager, ISafe safe, bytes calldata data) external { + function executeFromPlugin(ISafeProtocolManager manager, address safe, bytes calldata data) external { if (trustedOrigin != address(0) && msg.sender != trustedOrigin) revert UntrustedOrigin(msg.sender); relayCall(address(safe), data); diff --git a/contracts/contracts/RecoveryWithDelayPlugin.sol b/contracts/contracts/RecoveryWithDelayPlugin.sol index e257653..bfcdbe0 100644 --- a/contracts/contracts/RecoveryWithDelayPlugin.sol +++ b/contracts/contracts/RecoveryWithDelayPlugin.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.18; -import {ISafe} from "@safe-global/safe-core-protocol/contracts/interfaces/Accounts.sol"; import {ISafeProtocolManager} from "@safe-global/safe-core-protocol/contracts/interfaces/Manager.sol"; import {BasePluginWithEventMetadata, PluginMetadata} from "./Base.sol"; import {SafeTransaction, SafeRootAccess, SafeProtocolAction} from "@safe-global/safe-core-protocol/contracts/DataTypes.sol"; @@ -76,13 +75,13 @@ contract RecoveryWithDelayPlugin is BasePluginWithEventMetadata { */ function executeFromPlugin( ISafeProtocolManager manager, - ISafe safe, + address safe, address prevOwner, address oldOwner, address newOwner, uint256 nonce ) external returns (bytes memory data) { - bytes32 txHash = getTransactionHash(address(manager), address(safe), prevOwner, oldOwner, newOwner, nonce); + bytes32 txHash = getTransactionHash(address(manager), safe, prevOwner, oldOwner, newOwner, nonce); Announcement memory announcement = announcements[txHash]; if (announcement.executed) { diff --git a/contracts/contracts/WhitelistPlugin.sol b/contracts/contracts/WhitelistPlugin.sol index 2fbefc2..20d8113 100644 --- a/contracts/contracts/WhitelistPlugin.sol +++ b/contracts/contracts/WhitelistPlugin.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.18; -import {ISafe} from "@safe-global/safe-core-protocol/contracts/interfaces/Accounts.sol"; + import {ISafeProtocolManager} from "@safe-global/safe-core-protocol/contracts/interfaces/Manager.sol"; import {SafeTransaction, SafeProtocolAction} from "@safe-global/safe-core-protocol/contracts/DataTypes.sol"; import {BasePluginWithEventMetadata, PluginMetadata} from "./Base.sol"; @@ -41,19 +41,18 @@ contract WhitelistPlugin is BasePluginWithEventMetadata { */ function executeFromPlugin( ISafeProtocolManager manager, - ISafe safe, + address safe, SafeTransaction calldata safetx ) external returns (bytes[] memory data) { - address safeAddress = address(safe); // Only Safe owners are allowed to execute transactions to whitelisted accounts. - if (!(OwnerManager(safeAddress).isOwner(msg.sender))) { - revert CallerIsNotOwner(safeAddress, msg.sender); + if (!(OwnerManager(safe).isOwner(msg.sender))) { + revert CallerIsNotOwner(safe, msg.sender); } SafeProtocolAction[] memory actions = safetx.actions; uint256 length = actions.length; for (uint256 i = 0; i < length; i++) { - if (!whitelistedAddresses[safeAddress][actions[i].to]) revert AddressNotWhiteListed(actions[i].to); + if (!whitelistedAddresses[safe][actions[i].to]) revert AddressNotWhiteListed(actions[i].to); } // Test: Any tx that updates whitelist of this contract should be blocked (data) = manager.executeTransaction(safe, safetx); diff --git a/contracts/package.json b/contracts/package.json index 623d639..7e9e504 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,74 +1,74 @@ { - "name": "@safe-global/safe-core-protocol-demo", - "version": "0.1.0-alpha.0", - "description": "'Demo of Safe{Core} Protocol contracts", - "main": "dist/index.js", - "repository": "git@github.com:5afe/safe-core-protocol-demo.git", - "author": "safe.global", - "license": "LGPL-3.0", - "scripts": { - "build": "hardhat compile", - "test": "hardhat test", - "coverage": "hardhat coverage", - "fmt:sol": "prettier 'contracts/**/*.sol' -w", - "fmt:ts": "prettier 'test/**/*.ts' -w", - "fmt": "yarn fmt:ts && yarn fmt:sol", - "lint": "yarn lint:sol && yarn lint:ts", - "lint:sol": "solhint 'contracts/**/*.sol'", - "lint:ts": "eslint 'test/**/*.ts' --max-warnings 0 --fix", - "typechain": "TS_NODE_TRANSPILE_ONLY=true hardhat typechain", - "postinstall": "yarn typechain", - "deploy": "hardhat deploy --network", - "register-plugin": "hardhat register-plugin --network", - "list-plugins": "hardhat list-plugins --network", - "prepack": "yarn build" - }, - "devDependencies": { - "@gelatonetwork/relay-context": "^2.1.0", - "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", - "@nomicfoundation/hardhat-ethers": "^3.0.4", - "@nomicfoundation/hardhat-network-helpers": "^1.0.9", - "@nomicfoundation/hardhat-toolbox": "^3.0.0", - "@nomicfoundation/hardhat-verify": "^1.1.1", - "@openzeppelin/contracts": "^4.9.3", - "@safe-global/mock-contract": "^4.1.0", - "@safe-global/safe-contracts": "^1.4.1-build.0", - "@safe-global/safe-core-protocol": "^0.2.1-alpha.0", - "@safe-global/safe-singleton-factory": "^1.0.15", - "@typechain/ethers-v6": "^0.5.0", - "@typechain/hardhat": "^9.0.0", - "@types/chai": "^4.3.6", - "@types/mocha": ">=9.1.0", - "@types/node": ">=20.5.9", - "@types/yargs": "^17.0.24", - "@typescript-eslint/eslint-plugin": "^6.6.0", - "@typescript-eslint/parser": "^6.6.0", - "chai": "^4.3.8", - "dotenv": "^16.3.1", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-no-only-tests": "^3.1.0", - "eslint-plugin-prettier": "^5.0.0", - "ethers": "^6.7.1", - "hardhat": "^2.17.2", - "hardhat-deploy": "^0.11.37", - "hardhat-gas-reporter": "^1.0.8", - "hardhat-typechain": "^0.3.5", - "prettier": "^3.0.3", - "prettier-plugin-solidity": "^1.1.3", - "solhint": "^3.6.2", - "solhint-plugin-prettier": "^0.0.5", - "solidity-coverage": "^0.8.4", - "ts-node": ">=8.0.0", - "typechain": "^8.3.1", - "typescript": "~5.2.2", - "yargs": "^17.7.2" - }, - "files": [ - "contracts", - "dist", - "scripts", - "test", - "artifacts" - ] -} \ No newline at end of file + "name": "@safe-global/safe-core-protocol-demo", + "version": "0.1.0-alpha.0", + "description": "'Demo of Safe{Core} Protocol contracts", + "main": "dist/index.js", + "repository": "git@github.com:5afe/safe-core-protocol-demo.git", + "author": "safe.global", + "license": "LGPL-3.0", + "scripts": { + "build": "hardhat compile", + "test": "hardhat test", + "coverage": "hardhat coverage", + "fmt:sol": "prettier 'contracts/**/*.sol' -w", + "fmt:ts": "prettier 'test/**/*.ts' -w", + "fmt": "yarn fmt:ts && yarn fmt:sol", + "lint": "yarn lint:sol && yarn lint:ts", + "lint:sol": "solhint 'contracts/**/*.sol'", + "lint:ts": "eslint 'test/**/*.ts' --max-warnings 0 --fix", + "typechain": "TS_NODE_TRANSPILE_ONLY=true hardhat typechain", + "postinstall": "yarn typechain", + "deploy": "hardhat deploy --network", + "register-plugin": "hardhat register-plugin --network", + "list-plugins": "hardhat list-plugins --network", + "prepack": "yarn build" + }, + "devDependencies": { + "@gelatonetwork/relay-context": "^2.1.0", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", + "@nomicfoundation/hardhat-ethers": "^3.0.4", + "@nomicfoundation/hardhat-network-helpers": "^1.0.9", + "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@nomicfoundation/hardhat-verify": "^1.1.1", + "@openzeppelin/contracts": "^4.9.3", + "@safe-global/mock-contract": "^4.1.0", + "@safe-global/safe-contracts": "^1.4.1-build.0", + "@safe-global/safe-core-protocol": "^0.3.0-alpha.1", + "@safe-global/safe-singleton-factory": "^1.0.15", + "@typechain/ethers-v6": "^0.5.0", + "@typechain/hardhat": "^9.0.0", + "@types/chai": "^4.3.6", + "@types/mocha": ">=9.1.0", + "@types/node": ">=20.5.9", + "@types/yargs": "^17.0.24", + "@typescript-eslint/eslint-plugin": "^6.6.0", + "@typescript-eslint/parser": "^6.6.0", + "chai": "^4.3.8", + "dotenv": "^16.3.1", + "eslint": "^8.48.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-no-only-tests": "^3.1.0", + "eslint-plugin-prettier": "^5.0.0", + "ethers": "^6.7.1", + "hardhat": "^2.17.2", + "hardhat-deploy": "^0.11.37", + "hardhat-gas-reporter": "^1.0.8", + "hardhat-typechain": "^0.3.5", + "prettier": "^3.0.3", + "prettier-plugin-solidity": "^1.1.3", + "solhint": "^3.6.2", + "solhint-plugin-prettier": "^0.0.5", + "solidity-coverage": "^0.8.4", + "ts-node": ">=8.0.0", + "typechain": "^8.3.1", + "typescript": "~5.2.2", + "yargs": "^17.7.2" + }, + "files": [ + "contracts", + "dist", + "scripts", + "test", + "artifacts" + ] +} diff --git a/contracts/src/utils/builder.ts b/contracts/src/utils/builder.ts index 2cdbb74..f42b919 100644 --- a/contracts/src/utils/builder.ts +++ b/contracts/src/utils/builder.ts @@ -1,7 +1,13 @@ import { AddressLike } from "ethers"; import { SafeRootAccess, SafeTransaction } from "./dataTypes"; -export const buildSingleTx = (address: AddressLike, value: bigint, data: string, nonce: bigint, metadataHash: Uint8Array | string): SafeTransaction => { +export const buildSingleTx = ( + address: AddressLike, + value: bigint, + data: string, + nonce: bigint, + metadataHash: Uint8Array | string, +): SafeTransaction => { return { actions: [ { @@ -15,7 +21,13 @@ export const buildSingleTx = (address: AddressLike, value: bigint, data: string, }; }; -export const buildRootTx = (address: AddressLike, value: bigint, data: string, nonce: bigint, metadataHash: Uint8Array | string): SafeRootAccess => { +export const buildRootTx = ( + address: AddressLike, + value: bigint, + data: string, + nonce: bigint, + metadataHash: Uint8Array | string, +): SafeRootAccess => { return { action: { to: address, diff --git a/contracts/src/utils/constants.ts b/contracts/src/utils/constants.ts index 4709414..3c1776d 100644 --- a/contracts/src/utils/constants.ts +++ b/contracts/src/utils/constants.ts @@ -1,9 +1,9 @@ export const SENTINEL_MODULES = "0x0000000000000000000000000000000000000001"; -export enum IntegrationType { - Plugin, - Hooks, - FunctionHandler, +export enum ModuleType { + Plugin = 1, + FunctionHandler = 2, + Hooks = 4, } export enum ExecutionType { diff --git a/contracts/test/ERC4337Plugin.spec.ts b/contracts/test/ERC4337Plugin.spec.ts index 1ffbfd5..0887d26 100644 --- a/contracts/test/ERC4337Plugin.spec.ts +++ b/contracts/test/ERC4337Plugin.spec.ts @@ -3,7 +3,7 @@ import { expect } from "chai"; import { loadPluginMetadata } from "../src/utils/metadata"; import { getProtocolManagerAddress } from "../src/utils/protocol"; import { deploySafe, getSafeProxyFactoryContractFactory, getSafeSingletonContractFactory } from "./utils/safe"; -import { IntegrationType } from "../src/utils/constants"; +import { ModuleType } from "../src/utils/constants"; const ERC4337_TEST_ENV_VARIABLES_DEFINED = typeof process.env.ERC4337_TEST_BUNDLER_URL !== "undefined" && @@ -19,6 +19,12 @@ const BUNDLER_URL = process.env.ERC4337_TEST_BUNDLER_URL; const NODE_URL = process.env.ERC4337_TEST_NODE_URL; const MNEMONIC = process.env.ERC4337_TEST_MNEMONIC; +// const uint8ToBytes32 = (number: number): Buffer => { +// const bytes = Buffer.alloc(32); +// bytes.writeUint8(number, 31); +// return bytes; +// }; + const randomAddress = "0x1234567890123456789012345678901234567890"; describe("ERC4337 Plugin", () => { @@ -35,8 +41,9 @@ describe("ERC4337 Plugin", () => { const erc4337Plugin = await ( await ethers.getContractFactory("ERC4337Plugin") ).deploy(safeProtocolManager.getAddress(), randomAddress); - await testRegistryDeployment.addModule(erc4337Plugin.getAddress(), IntegrationType.Plugin); - await testRegistryDeployment.addModule(erc4337Plugin.getAddress(), IntegrationType.FunctionHandler); + await testRegistryDeployment + .addModule(erc4337Plugin.getAddress(), ModuleType.Plugin + ModuleType.FunctionHandler) + .then((tx) => tx.wait(1)); const account = await (await ethers.getContractFactory("ExecutableMockContract")).deploy(); const proxyFactory = await (await (await getSafeProxyFactoryContractFactory()).deploy()).waitForDeployment(); @@ -61,7 +68,7 @@ describe("ERC4337 Plugin", () => { expect(await erc4337Plugin.permissions()).to.be.eq(1); }); - it("can retrieve meta data for module", async () => { + it.only("can retrieve metadata for the module", async () => { const { erc4337Plugin } = await setup(); expect(await loadPluginMetadata(hre, erc4337Plugin)).to.be.deep.eq({ name: "ERC4337 Plugin", @@ -83,5 +90,5 @@ describe("ERC4337 Plugin", () => { * 1. Deployment of the Safe with the ERC4337 module and handler is possible * 2. Executing a transaction is possible */ - itif("integration test", async () => { }); + itif("integration test", async () => {}); }); diff --git a/contracts/yarn.lock b/contracts/yarn.lock index e4d9a37..3d9f411 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -818,10 +818,10 @@ resolved "https://registry.yarnpkg.com/@safe-global/safe-contracts/-/safe-contracts-1.4.1-build.0.tgz#5d82e2f3fd8430b4589df992b9ee2c71386082fe" integrity sha512-TIpoKJtMqLcLFoid0cvpxo8YTcnRUj95MHvxzwgPbJPRONOckNS6ebgGyBBRDmnpxFh34IBpPUZ7JD+z2Cfbbg== -"@safe-global/safe-core-protocol@^0.2.1-alpha.0": - version "0.2.1-alpha.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-core-protocol/-/safe-core-protocol-0.2.1-alpha.0.tgz#eb9c792e34bcb3818ae44dc0edc3cea4a2466294" - integrity sha512-X7OILBxC1HhK7w8P70O9PsKrufD0w0fU+6rtf94w3aALmUMDywJr5g9mM5qoTAI/pRCjp3TzRkaw5yb3flJebw== +"@safe-global/safe-core-protocol@^0.3.0-alpha.1": + version "0.3.0-alpha.1" + resolved "https://registry.yarnpkg.com/@safe-global/safe-core-protocol/-/safe-core-protocol-0.3.0-alpha.1.tgz#222da478bfbdb2feddc17e33c3edd948fb6f674c" + integrity sha512-MTY7tH7SYe7kNaFCix6B9ZTKdRIthLfxuoOlY+OZhDX7vckmW8239Ab033kHBWWcpp8K0KsF2/N5iy9+drShOQ== "@safe-global/safe-singleton-factory@^1.0.15": version "1.0.15"