Skip to content

Commit

Permalink
Merge pull request #19 from rhinestonewtf/feature/no-safe-owner
Browse files Browse the repository at this point in the history
feat: add no safe owner contract
  • Loading branch information
zeroknots authored Feb 15, 2025
2 parents 22c4c59 + e6fafcb commit 83acdf5
Show file tree
Hide file tree
Showing 2 changed files with 287 additions and 0 deletions.
14 changes: 14 additions & 0 deletions src/utils/NoSafeOwner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;

contract NoSafeOwner {
// legacy ERC-1271 used by Safe 1.4.1 and lower
function isValidSignature(bytes calldata, bytes calldata) public pure returns (bytes4) {
return 0xffffffff;
}

// up-to-date ERC-1271
function isValidSignature(bytes32, bytes calldata) public pure returns (bytes4) {
return 0xffffffff;
}
}
273 changes: 273 additions & 0 deletions test/utils/NoSafeOwner.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "forge-std/Test.sol";
import { Safe7579 } from "src/Safe7579.sol";
import { ISafe7579 } from "src/ISafe7579.sol";
import { IERC7484 } from "src/interfaces/IERC7484.sol";
import "src/DataTypes.sol";
import { ModuleManager } from "src/core/ModuleManager.sol";
import { MockValidator } from "module-bases/mocks/MockValidator.sol";
import { MockRegistry } from "../mocks/MockRegistry.sol";
import { MockExecutor } from "../mocks/MockExecutor.sol";
import { MockFallback } from "../mocks/MockFallback.sol";
import { ExecutionLib } from "erc7579/lib/ExecutionLib.sol";
import { ModeLib } from "erc7579/lib/ModeLib.sol";
import { IERC7579Account, Execution } from "erc7579/interfaces/IERC7579Account.sol";
import { MockTarget } from "../mocks/MockTarget.sol";
import { NoSafeOwner } from "src/utils/NoSafeOwner.sol";

import { Safe } from "@safe-global/safe-contracts/contracts/Safe.sol";
import { Enum } from "@safe-global/safe-contracts/contracts/common/Enum.sol";
import {
SafeProxy,
SafeProxyFactory
} from "@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol";
import { LibClone } from "solady/utils/LibClone.sol";
import { Safe7579Launchpad } from "src/Safe7579Launchpad.sol";

import { Solarray } from "solarray/Solarray.sol";
import "../dependencies/EntryPoint.sol";

import { Simulator } from "@rhinestone/erc4337-validation/src/Simulator.sol";

contract ValidContractOwner {
function isValidSignature(bytes calldata, bytes calldata) public pure returns (bytes4) {
return 0x20c13b0b;
}

function isValidSignature(bytes32, bytes calldata) public pure returns (bytes4) {
return 0x1626ba7e;
}
}

contract NoSafeOwnerTest is Test {
using Simulator for PackedUserOperation; // or UserOperation

Safe7579 safe7579;
Safe singleton;
Safe safe;
SafeProxyFactory safeProxyFactory;
Safe7579Launchpad launchpad;

MockValidator defaultValidator;
MockExecutor defaultExecutor;
MockTarget target;
NoSafeOwner noSafeOwner;
ValidContractOwner validContractOwner;

IEntryPoint entrypoint;
bytes userOpInitCode;
IERC7484 registry;

struct Setup {
address singleton;
address signerFactory;
bytes signerData;
address setupTo;
bytes setupData;
address fallbackHandler;
}

function setUp() public virtual {
// Set up EntryPoint
entrypoint = etchEntrypoint();
singleton = new Safe();
safeProxyFactory = new SafeProxyFactory();
registry = new MockRegistry();
safe7579 = new Safe7579();
launchpad = new Safe7579Launchpad(address(entrypoint), registry);

// Set up Modules
defaultValidator = new MockValidator();
defaultExecutor = new MockExecutor();
target = new MockTarget();
noSafeOwner = new NoSafeOwner();
validContractOwner = new ValidContractOwner();

bytes32 salt;

ModuleInit[] memory validators = new ModuleInit[](1);
validators[0] = ModuleInit({ module: address(defaultValidator), initData: bytes("") });
ModuleInit[] memory executors = new ModuleInit[](1);
executors[0] = ModuleInit({ module: address(defaultExecutor), initData: bytes("") });
ModuleInit[] memory fallbacks = new ModuleInit[](0);
ModuleInit[] memory hooks = new ModuleInit[](0);

Safe7579Launchpad.InitData memory initData = Safe7579Launchpad.InitData({
singleton: address(singleton),
owners: Solarray.addresses(address(validContractOwner), address(noSafeOwner)),
threshold: 1,
setupTo: address(launchpad),
setupData: abi.encodeCall(
Safe7579Launchpad.initSafe7579,
(
address(safe7579),
executors,
fallbacks,
hooks,
Solarray.addresses(makeAddr("attester1"), makeAddr("attester2")),
2
)
),
safe7579: ISafe7579(safe7579),
validators: validators,
callData: abi.encodeCall(
IERC7579Account.execute,
(
ModeLib.encodeSimpleSingle(),
ExecutionLib.encodeSingle({
target: address(target),
value: 0,
callData: abi.encodeCall(MockTarget.set, (1337))
})
)
)
});
bytes32 initHash = launchpad.hash(initData);

bytes memory factoryInitializer =
abi.encodeCall(Safe7579Launchpad.preValidationSetup, (initHash, address(0), ""));

PackedUserOperation memory userOp =
getDefaultUserOp(address(safe), address(defaultValidator));

{
userOp.callData = abi.encodeCall(Safe7579Launchpad.setupSafe, (initData));
userOp.initCode = _initCode(factoryInitializer, salt);
}

address predict = launchpad.predictSafeAddress({
singleton: address(launchpad),
safeProxyFactory: address(safeProxyFactory),
creationCode: type(SafeProxy).creationCode,
salt: salt,
factoryInitializer: factoryInitializer
});
userOp.sender = predict;
assertEq(userOp.sender, predict);
userOp.signature = abi.encodePacked(
uint48(0), uint48(type(uint48).max), hex"4141414141414141414141414141414141"
);

bytes32 userOpHash = entrypoint.getUserOpHash(userOp);
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = userOp;
deal(address(userOp.sender), 1 ether);

userOp.simulateUserOp(address(entrypoint));
entrypoint.handleOps(userOps, payable(address(0x69)));

safe = Safe(payable(predict));

assertEq(target.value(), 1337);
}

function _initCode(
bytes memory initializer,
bytes32 salt
)
internal
view
returns (bytes memory _initCode)
{
_initCode = abi.encodePacked(
address(safeProxyFactory),
abi.encodeCall(
SafeProxyFactory.createProxyWithNonce,
(address(launchpad), initializer, uint256(salt))
)
);
}

function test_execSingle() public {
// Create calldata for the account to execute
bytes memory setValueOnTarget = abi.encodeCall(MockTarget.set, 1337);

// Encode the call into the calldata for the userOp
bytes memory userOpCalldata = abi.encodeCall(
IERC7579Account.execute,
(
ModeLib.encodeSimpleSingle(),
ExecutionLib.encodeSingle(address(target), uint256(0), setValueOnTarget)
)
);

PackedUserOperation memory userOp =
getDefaultUserOp(address(safe), address(defaultValidator));
userOp.initCode = userOpInitCode;
userOp.callData = userOpCalldata;

// Create userOps array
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = userOp;

// Send the userOp to the entrypoint
entrypoint.handleOps(userOps, payable(address(0x69)));

// Assert that the value was set ie that execution was successful
assertTrue(target.value() == 1337);
}

function test_makeSafeTransaction() public {
safe.execTransaction({
to: address(target),
value: 0,
data: abi.encodeWithSignature("set(uint256)", 1337),
operation: Enum.Operation.Call,
safeTxGas: 0,
baseGas: 0,
gasPrice: 0,
gasToken: address(0),
refundReceiver: payable(address(0)),
signatures: abi.encodePacked(
uint256(uint160(address(validContractOwner))),
uint256(65),
bytes1(0),
abi.encodePacked(uint256(65), keccak256("r"), keccak256("s"), uint8(1))
)
});
}

function test_makeSafeTransaction_revertWhen_NoSafeOwner() public {
vm.expectRevert(bytes("GS024"));
safe.execTransaction({
to: address(target),
value: 0,
data: abi.encodeWithSignature("set(uint256)", 1337),
operation: Enum.Operation.Call,
safeTxGas: 0,
baseGas: 0,
gasPrice: 0,
gasToken: address(0),
refundReceiver: payable(address(0)),
signatures: abi.encodePacked(
uint256(uint160(address(noSafeOwner))),
uint256(65),
bytes1(0),
abi.encodePacked(uint256(65), keccak256("r"), keccak256("s"), uint8(1))
)
});
}

function getDefaultUserOp(
address account,
address validator
)
internal
view
returns (PackedUserOperation memory userOp)
{
userOp = PackedUserOperation({
sender: account,
nonce: safe7579.getNonce(account, validator),
initCode: "",
callData: "",
accountGasLimits: bytes32(abi.encodePacked(uint128(2e6), uint128(2e6))),
preVerificationGas: 2e6,
gasFees: bytes32(abi.encodePacked(uint128(2e6), uint128(2e6))),
paymasterAndData: bytes(""),
signature: abi.encodePacked(hex"41414141")
});
}
}

0 comments on commit 83acdf5

Please sign in to comment.