Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: 4337 compatibility #32

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions contracts/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@ NODE_URL=""
MNEMONIC=""
INFURA_KEY=""
ETHERSCAN_API_KEY=""

# (Optional) variables for the ERC4337 integration test
ERC4337_TEST_BUNDLER_URL=""
ERC4337_TEST_NODE_URL=""
ERC4337_TEST_SAFE_FACTORY_ADDRESS=""
ERC4337_TEST_SINGLETON_ADDRESS=""
ERC4337_TEST_MNEMONIC=""
# The address derived from the mnemonic must be an owner of the registry and the protocol manager
ERC4337_TEST_SAFE_CORE_PROTOCOL_MANAGER_ADDRESS=""
ERC4337_TEST_SAFE_CORE_PROTOCOL_REGISTRY=""
24 changes: 14 additions & 10 deletions contracts/contracts/Base.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.18;

import {ISafeProtocolPlugin} from "@safe-global/safe-core-protocol/contracts/interfaces/Integrations.sol";
import {ISafeProtocolPlugin} from "@safe-global/safe-core-protocol/contracts/interfaces/Modules.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

enum MetadataProviderType {
Expand All @@ -18,7 +18,7 @@ interface IMetadataProvider {
struct PluginMetadata {
string name;
string version;
bool requiresRootAccess;
uint8 permissions;
string iconUrl;
string appUrl;
}
Expand All @@ -29,17 +29,17 @@ library PluginMetadataOps {
abi.encodePacked(
uint8(0x00), // Format
uint8(0x00), // Format version
abi.encode(data.name, data.version, data.requiresRootAccess, data.iconUrl, data.appUrl) // Plugin Metadata
abi.encode(data.name, data.version, data.permissions, data.iconUrl, data.appUrl) // Plugin Metadata
);
}

function decode(bytes calldata data) internal pure returns (PluginMetadata memory) {
require(bytes16(data[0:2]) == bytes16(0x0000), "Unsupported format or format version");
(string memory name, string memory version, bool requiresRootAccess, string memory iconUrl, string memory appUrl) = abi.decode(
(string memory name, string memory version, uint8 permissions, string memory iconUrl, string memory appUrl) = abi.decode(
data[2:],
(string, string, bool, string, string)
(string, string, uint8, string, string)
);
return PluginMetadata(name, version, requiresRootAccess, iconUrl, appUrl);
return PluginMetadata(name, version, permissions, iconUrl, appUrl);
}
}

Expand All @@ -48,19 +48,23 @@ abstract contract BasePlugin is ISafeProtocolPlugin {

string public name;
string public version;
bool public immutable requiresRootAccess;
uint8 public permissions;
bytes32 public immutable metadataHash;

constructor(PluginMetadata memory metadata) {
name = metadata.name;
version = metadata.version;
requiresRootAccess = metadata.requiresRootAccess;
permissions = metadata.permissions;
metadataHash = keccak256(metadata.encode());
}

function supportsInterface(bytes4 interfaceId) external view override returns (bool) {
function supportsInterface(bytes4 interfaceId) external pure virtual override returns (bool) {
return interfaceId == type(ISafeProtocolPlugin).interfaceId || interfaceId == type(IERC165).interfaceId;
}

function requiresPermissions() external view virtual override returns (uint8) {
return permissions;
}
}

abstract contract BasePluginWithStoredMetadata is BasePlugin, IMetadataProvider {
Expand Down Expand Up @@ -92,7 +96,7 @@ abstract contract BasePluginWithEventMetadata is BasePlugin {
emit Metadata(metadataHash, metadata.encode());
}

function metadataProvider() public view override returns (uint256 providerType, bytes memory location) {
function metadataProvider() public view virtual override returns (uint256 providerType, bytes memory location) {
providerType = uint256(MetadataProviderType.Event);
location = abi.encode(address(this));
}
Expand Down
169 changes: 169 additions & 0 deletions contracts/contracts/ERC4337Plugin.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// SPDX-License-Identifier: LGPL-3.0-only
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 {MODULE_TYPE_PLUGIN} from "@safe-global/safe-core-protocol/contracts/common/Constants.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {BasePlugin, BasePluginWithEventMetadata, PluginMetadata, MetadataProviderType} from "./Base.sol";

interface ISafe {
function isOwner(address owner) external view returns (bool);

function enableModule(address module) external;

function setFallbackHandler(address handler) external;

function execTransactionFromModule(
address payable to,
uint256 value,
bytes calldata data,
uint8 operation
) external returns (bool success);

function execTransactionFromModuleReturnData(
address to,
uint256 value,
bytes memory data,
uint8 operation
) external returns (bool success, bytes memory returnData);

function enablePlugin(address plugin, uint8 permissions) external;

function setFunctionHandler(bytes4 selector, address functionHandler) external;

function checkSignatures(bytes32 dataHash, bytes memory, bytes memory signatures) external view;
}

interface IEntryPoint {
function getUserOpHash(UserOperation calldata userOp) external view returns (bytes32);

function depositTo(address account) external payable;

function balanceOf(address account) external view returns (uint256);
}

struct UserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
uint256 callGasLimit;
uint256 verificationGasLimit;
uint256 preVerificationGas;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes paymasterAndData;
bytes signature;
}

/**
* @title WhitelistPlugin maintains a mapping that stores information about accounts that are
* permitted to execute non-root transactions through a Safe account.
* @notice This plugin does not need Safe owner(s) confirmation(s) to execute Safe txs once enabled
* through a Safe{Core} Protocol Manager.
*/
contract ERC4337Plugin is ISafeProtocolFunctionHandler, BasePluginWithEventMetadata {
address public immutable PLUGIN_ADDRESS;
ISafeProtocolManager public immutable SAFE_PROTOCOL_MANAGER;
address public immutable SAFE_PROTOCOL_FUNCTION_HANDLER;

address payable public immutable ENTRY_POINT;
uint256 internal constant SIGNATURE_VALID = 0;
// value in case of signature failure, with no time-range.
// equivalent to _packValidationData(true,0,0);
uint256 internal constant SIGNATURE_VALIDATION_FAILED = 1;

constructor(
ISafeProtocolManager safeCoreProtocolManager,
address safeCoreProtocolFunctionHandler,
address payable entryPoint
) BasePluginWithEventMetadata(PluginMetadata({name: "ERC4337 Plugin", version: "1.0.0", permissions: 1, iconUrl: "", appUrl: ""})) {
PLUGIN_ADDRESS = address(this);
SAFE_PROTOCOL_MANAGER = safeCoreProtocolManager;
ENTRY_POINT = entryPoint;
SAFE_PROTOCOL_FUNCTION_HANDLER = safeCoreProtocolFunctionHandler;
}

function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external returns (uint256 validationResult) {
require(msg.sender == address(PLUGIN_ADDRESS), "Only plugin");
// validationResult = validateSignature(userOp, userOpHash);

if (missingAccountFunds != 0) {
ISafe(userOp.sender).execTransactionFromModule(ENTRY_POINT, missingAccountFunds, "", 0);
}
}

function execTransaction(address safe, address payable to, uint256 value, bytes calldata data) external {
require(msg.sender == address(PLUGIN_ADDRESS));

SafeProtocolAction[] memory actions = new SafeProtocolAction[](1);
actions[0] = SafeProtocolAction({to: to, value: value, data: data});

SAFE_PROTOCOL_MANAGER.executeTransaction(safe, SafeTransaction({actions: actions, nonce: 0, metadataHash: bytes32(0)}));
}

function validateSignature(UserOperation calldata userOp, bytes32 userOpHash) internal view returns (uint256 validationResult) {
try ISafe(payable(userOp.sender)).checkSignatures(userOpHash, "", userOp.signature) {
return SIGNATURE_VALID;
} catch {
return SIGNATURE_VALIDATION_FAILED;
}
}

function handle(address, address sender, uint256, bytes calldata data) external returns (bytes memory result) {
bytes4 selector = bytes4(data[0:4]);
require(sender == ENTRY_POINT, "Only entrypoint");
bool success;
if (selector == this.validateUserOp.selector) {
(success, result) = PLUGIN_ADDRESS.call(data);
} else if (selector == this.execTransaction.selector) {
(success, result) = PLUGIN_ADDRESS.call(data);
}

// // solhint-disable-next-line no-inline-assembly
// assembly {
// // use assembly to avoid converting result bytes to string
// if eq(success, 0) {
// revert(add(result, 32), mload(result))
// }
// }
}

function enableSafeCoreProtocolWith4337Plugin() public {
require(address(this) != PLUGIN_ADDRESS, "Only delegatecall");

ISafe safe = ISafe(address(this));
safe.setFallbackHandler(SAFE_PROTOCOL_FUNCTION_HANDLER);
safe.enableModule(address(SAFE_PROTOCOL_MANAGER));
safe.enableModule(address(PLUGIN_ADDRESS));

// ISafe(address(SAFE_PROTOCOL_MANAGER)).enablePlugin(PLUGIN_ADDRESS, MODULE_TYPE_PLUGIN);
safe.setFunctionHandler(this.validateUserOp.selector, PLUGIN_ADDRESS);
// safe.setFunctionHandler(this.execTransaction.selector, PLUGIN_ADDRESS);
}

function metadataProvider()
public
view
override(BasePluginWithEventMetadata, ISafeProtocolFunctionHandler)
returns (uint256 providerType, bytes memory location)
{
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;
}

receive() external payable {}
}
4 changes: 4 additions & 0 deletions contracts/contracts/Imports.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ pragma solidity ^0.8.18;
// Import the contract so hardhat compiles it, and we have the ABI available
import {MockContract} from "@safe-global/mock-contract/contracts/MockContract.sol";
import {TestSafeProtocolRegistryUnrestricted} from "@safe-global/safe-core-protocol/contracts/test/TestSafeProtocolRegistryUnrestricted.sol";
import {SafeProtocolManager} from "@safe-global/safe-core-protocol/contracts/SafeProtocolManager.sol";
import {Safe} from "@safe-global/safe-contracts/contracts/Safe.sol";
import {SafeProxyFactory} from "@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol";
import {SafeProxy} from "@safe-global/safe-contracts/contracts/proxies/SafeProxy.sol";

// ExecutableMockContract for testing

Expand Down
9 changes: 4 additions & 5 deletions contracts/contracts/Plugins.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -32,7 +31,7 @@ contract RelayPlugin is BasePluginWithEventMetadata {
PluginMetadata({
name: "Relay Plugin",
version: "1.0.0",
requiresRootAccess: false,
permissions: 1,
iconUrl: "",
appUrl: "https://5afe.github.io/safe-core-protocol-demo/#/relay/${plugin}"
})
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 3 additions & 9 deletions contracts/contracts/RecoveryWithDelayPlugin.sol
Original file line number Diff line number Diff line change
@@ -1,7 +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 {ISafeProtocolPlugin} from "@safe-global/safe-core-protocol/contracts/interfaces/Integrations.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";
Expand Down Expand Up @@ -52,11 +50,7 @@ contract RecoveryWithDelayPlugin is BasePluginWithEventMetadata {

constructor(
address _recoverer
)
BasePluginWithEventMetadata(
PluginMetadata({name: "Recovery Plugin", version: "1.0.0", requiresRootAccess: true, iconUrl: "", appUrl: ""})
)
{
) BasePluginWithEventMetadata(PluginMetadata({name: "Recovery Plugin", version: "1.0.0", permissions: 2, iconUrl: "", appUrl: ""})) {
recoverer = _recoverer;
}

Expand All @@ -81,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) {
Expand Down
18 changes: 7 additions & 11 deletions contracts/contracts/WhitelistPlugin.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// 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 {ISafeProtocolPlugin} from "@safe-global/safe-core-protocol/contracts/interfaces/Integrations.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 {SafeTransaction, SafeProtocolAction} from "@safe-global/safe-core-protocol/contracts/DataTypes.sol";
import {BasePluginWithEventMetadata, PluginMetadata} from "./Base.sol";

/**
Expand Down Expand Up @@ -31,9 +30,7 @@ contract WhitelistPlugin is BasePluginWithEventMetadata {
error CallerIsNotOwner(address safe, address caller);

constructor()
BasePluginWithEventMetadata(
PluginMetadata({name: "Whitelist Plugin", version: "1.0.0", requiresRootAccess: false, iconUrl: "", appUrl: ""})
)
BasePluginWithEventMetadata(PluginMetadata({name: "Whitelist Plugin", version: "1.0.0", permissions: 1, iconUrl: "", appUrl: ""}))
{}

/**
Expand All @@ -44,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);
Expand Down
Loading