Skip to content

Commit

Permalink
feature: add sig fallback to launchpad
Browse files Browse the repository at this point in the history
  • Loading branch information
kopy-kat committed Jul 1, 2024
1 parent ebef70c commit f3f45dc
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 116 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@rhinestone/module-bases": "github:rhinestonewtf/module-bases",
"@ERC4337/account-abstraction": "github:kopy-kat/account-abstraction#develop",
"@rhinestone/sentinellist": "github:rhinestonewtf/sentinellist",
"@rhinestone/checknsignatures": "github:rhinestonewtf/checknsignatures",
"@safe-global/safe-contracts": "^1.4.1",
"ds-test": "github:dapphub/ds-test",
"erc7579": "github:erc7579/erc7579-implementation",
Expand Down
36 changes: 31 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 2 additions & 6 deletions src/ISafe7579.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.20;

import "./DataTypes.sol";

Check warning on line 4 in src/ISafe7579.sol

View workflow job for this annotation

GitHub Actions / lint / forge-lint

global import of path ./DataTypes.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import { IERC7579Account } from "./interfaces//IERC7579Account.sol";

import { ISafeOp } from "./interfaces/ISafeOp.sol";
import { ModeCode } from "./lib/ModeLib.sol";
import { PackedUserOperation } from
"@ERC4337/account-abstraction/contracts/core/UserOperationLib.sol";
Expand All @@ -13,7 +13,7 @@ import { PackedUserOperation } from
* creates full ERC7579 compliance to Safe accounts
* @author rhinestone | zeroknots.eth, Konrad Kopp (@kopy-kat)
*/
interface ISafe7579 is IERC7579Account {
interface ISafe7579 is IERC7579Account, ISafeOp {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* Validation */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand Down Expand Up @@ -237,10 +237,6 @@ interface ISafe7579 is IERC7579Account {
function supportsModule(uint256 moduleTypeId) external pure returns (bool);
function accountId() external view returns (string memory accountImplementationId);

/**
* Domain Separator for EIP-712.
*/
function domainSeparator() external view returns (bytes32);
/**
* Safe7579 is using validator selection encoding in the userop nonce.
* to make it easier for SDKs / devs to integrate, this function can be
Expand Down
97 changes: 4 additions & 93 deletions src/Safe7579.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
import { ModuleInstallUtil } from "./utils/DCUtil.sol";
import { AccessControl } from "./core/AccessControl.sol";
import { Initializer } from "./core/Initializer.sol";
import { ISafeOp, SAFE_OP_TYPEHASH } from "./interfaces/ISafeOp.sol";
import { ISafe } from "./interfaces/ISafe.sol";
import { ISafe7579 } from "./ISafe7579.sol";
import {
Expand All @@ -34,6 +33,7 @@ import {
import { _packValidationData } from "@ERC4337/account-abstraction/contracts/core/Helpers.sol";
import { IEntryPoint } from "@ERC4337/account-abstraction/contracts/interfaces/IEntryPoint.sol";
import { IERC1271 } from "./interfaces/IERC1271.sol";
import { SafeOp } from "./core/SafeOp.sol";

uint256 constant MULTITYPE_MODULE = 0;

Expand All @@ -50,7 +50,7 @@ uint256 constant MULTITYPE_MODULE = 0;
* event emissions to be done via the SafeProxy as msg.sender using Safe's
* "executeTransactionFromModule" features.
*/
contract Safe7579 is ISafe7579, ISafeOp, AccessControl, Initializer {
contract Safe7579 is ISafe7579, SafeOp, AccessControl, Initializer {
using UserOperationLib for PackedUserOperation;
using ExecutionLib for bytes;

Expand Down Expand Up @@ -301,12 +301,8 @@ contract Safe7579 is ISafe7579, ISafeOp, AccessControl, Initializer {
view
returns (uint256 validationData)
{
(
bytes memory operationData,
uint48 validAfter,
uint48 validUntil,
bytes calldata signatures
) = _getSafeOp(userOp);
(bytes memory operationData, uint48 validAfter, uint48 validUntil, bytes memory signatures)
= getSafeOp(userOp, entryPoint());
try ISafe((msg.sender)).checkSignatures(keccak256(operationData), operationData, signatures)
{
// The timestamps are validated by the entry point,
Expand Down Expand Up @@ -530,91 +526,6 @@ contract Safe7579 is ISafe7579, ISafeOp, AccessControl, Initializer {
return string(abi.encodePacked("safe-", safeVersion, ".erc7579.v0.0.1"));
}

/**
* @dev Decodes an ERC-4337 user operation into a Safe operation.
* @param userOp The ERC-4337 user operation.
* @return operationData Encoded EIP-712 Safe operation data bytes used for signature
* verification.
* @return validAfter The timestamp the user operation is valid from.
* @return validUntil The timestamp the user operation is valid until.
* @return signatures The Safe owner signatures extracted from the user operation.
*/
function _getSafeOp(PackedUserOperation calldata userOp)
internal
view
returns (
bytes memory operationData,
uint48 validAfter,
uint48 validUntil,
bytes calldata signatures
)
{
// Extract additional Safe operation fields from the user operation signature which is
// encoded as:
// `abi.encodePacked(validAfter, validUntil, signatures)`
{
bytes calldata sig = userOp.signature;
validAfter = uint48(bytes6(sig[0:6]));
validUntil = uint48(bytes6(sig[6:12]));
signatures = sig[12:];
}

// It is important that **all** user operation fields are represented in the `SafeOp` data
// somehow, to prevent
// user operations from being submitted that do not fully respect the user preferences. The
// only exception is
// the `signature` bytes. Note that even `initCode` needs to be represented in the operation
// data, otherwise
// it can be replaced with a more expensive initialization that would charge the user
// additional fees.
{
// In order to work around Solidity "stack too deep" errors related to too many stack
// variables, manually
// encode the `SafeOp` fields into a memory `struct` for computing the EIP-712
// struct-hash. This works
// because the `EncodedSafeOpStruct` struct has no "dynamic" fields so its memory layout
// is identical to the
// result of `abi.encode`-ing the individual fields.
EncodedSafeOpStruct memory encodedSafeOp = EncodedSafeOpStruct({
typeHash: SAFE_OP_TYPEHASH,
safe: msg.sender,
nonce: userOp.nonce,
initCodeHash: keccak256(userOp.initCode),
callDataHash: keccak256(userOp.callData),
callGasLimit: userOp.unpackCallGasLimit(),
verificationGasLimit: userOp.unpackVerificationGasLimit(),
preVerificationGas: userOp.preVerificationGas,
maxFeePerGas: userOp.unpackMaxFeePerGas(),
maxPriorityFeePerGas: userOp.unpackMaxPriorityFeePerGas(),
paymasterAndDataHash: keccak256(userOp.paymasterAndData),
validAfter: validAfter,
validUntil: validUntil,
entryPoint: entryPoint()
});

bytes32 safeOpStructHash;
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
// Since the `encodedSafeOp` value's memory layout is identical to the result of
// `abi.encode`-ing the
// individual `SafeOp` fields, we can pass it directly to `keccak256`. Additionally,
// there are 14
// 32-byte fields to hash, for a length of `14 * 32 = 448` bytes.
safeOpStructHash := keccak256(encodedSafeOp, 448)
}

operationData =
abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), safeOpStructHash);
}
}

/**
* @inheritdoc ISafe7579
*/
function domainSeparator() public view returns (bytes32) {
return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, block.chainid, this));
}

/**
* @inheritdoc ISafe7579
*/
Expand Down
66 changes: 59 additions & 7 deletions src/Safe7579Launchpad.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import { ISafe } from "./interfaces/ISafe.sol";
import { ISafe7579 } from "./ISafe7579.sol";
import { IERC7484 } from "./interfaces/IERC7484.sol";
import "./DataTypes.sol";

Check warning on line 13 in src/Safe7579Launchpad.sol

View workflow job for this annotation

GitHub Actions / lint / forge-lint

global import of path ./DataTypes.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import { ISafeOp } from "./interfaces/ISafeOp.sol";
import { CheckSignatures } from "@rhinestone/checknsignatures/src/CheckNSignatures.sol";

import { IValidator } from "erc7579/interfaces/IERC7579Module.sol";

import { SafeStorage } from "@safe-global/safe-contracts/contracts/libraries/SafeStorage.sol";

import { MODULE_TYPE_VALIDATOR } from "erc7579/interfaces/IERC7579Module.sol";
import { LibSort } from "solady/utils/LibSort.sol";

/**
* Launchpad to deploy a Safe account and connect the Safe7579 adapter.
Expand All @@ -26,6 +29,9 @@ import { MODULE_TYPE_VALIDATOR } from "erc7579/interfaces/IERC7579Module.sol";
* @author rhinestone | zeroknots.eth
*/
contract Safe7579Launchpad is IAccount, SafeStorage {
using CheckSignatures for bytes32;
using LibSort for address[];

event ModuleInstalled(uint256 moduleTypeId, address module);

// keccak256("Safe7579Launchpad.initHash") - 1
Expand Down Expand Up @@ -210,14 +216,21 @@ contract Safe7579Launchpad is IAccount, SafeStorage {

if (validatorModule == validator) userOpValidatorInstalled = true;
}
// Ensure that the validator module selected in the userOp was
// part of the validators in InitData
if (!userOpValidatorInstalled) {
return _packValidationData({ sigFailed: true, validUntil: 0, validAfter: 0 });
}

// validate userOp with selected validation module.
validationData = IValidator(validator).validateUserOp(userOp, userOpHash);
if (userOpValidatorInstalled) {
// validate userOp with selected validation module.
validationData = IValidator(validator).validateUserOp(userOp, userOpHash);
} else {
// otherwise we fall back to safe-style validation, like in the safe7579
(bool validSig, uint48 validUntil, uint48 validAfter) =
_isValidSafeSigners(initData, userOp);

validationData = _packValidationData({
sigFailed: !validSig,
validUntil: validUntil,
validAfter: validAfter
});
}

// pay back gas to EntryPoint
if (missingAccountFunds > 0) {
Expand Down Expand Up @@ -347,4 +360,43 @@ contract Safe7579Launchpad is IAccount, SafeStorage {
)
);
}

function _isValidSafeSigners(
InitData memory safeSetupCallData,
PackedUserOperation calldata userOp
)
internal
view
returns (bool validSig, uint48 validUntil, uint48 validAfter)
{
bytes memory operationData;
bytes memory signatures;
// decode ERC4337 userOp into Safe operation.
(operationData, validAfter, validUntil, signatures) =
ISafeOp(safeSetupCallData.safe7579).getSafeOp(userOp, SUPPORTED_ENTRYPOINT);
bytes32 _hash = keccak256(operationData);

address[] memory signers = _hash.recoverNSignatures(signatures, safeSetupCallData.threshold);
signers.insertionSort();

address[] memory owners = safeSetupCallData.owners;

// sorting owners here instead of requiring sorted list for improved UX
owners.insertionSort();
owners.uniquifySorted();

uint256 ownersLength = owners.length;

uint256 validSigs;
for (uint256 i; i < ownersLength; i++) {
(bool found,) = signers.searchSorted(owners[i]);
if (found) {
validSigs++;
if (validSigs >= safeSetupCallData.threshold) {
return (true, validUntil, validAfter);
}
}
}
return (false, validUntil, validAfter);
}
}
Loading

0 comments on commit f3f45dc

Please sign in to comment.