diff --git a/src/7702/MinimalAccount.sol b/src/7702/MinimalAccount.sol index 6946079..a1834f1 100644 --- a/src/7702/MinimalAccount.sol +++ b/src/7702/MinimalAccount.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.26; +import "account-abstraction-v0.7/core/Helpers.sol"; +import {UserOperationLib} from "account-abstraction-v0.7/core/UserOperationLib.sol"; +import {IEntryPoint} from "account-abstraction-v0.7/interfaces/IEntryPoint.sol"; +import {PackedUserOperation} from "account-abstraction-v0.7/interfaces/PackedUserOperation.sol"; import {IERC1155Receiver, IERC165} from "../interface/IERC1155Receiver.sol"; import {IERC721Receiver} from "../interface/IERC721Receiver.sol"; import {SessionLib} from "../lib/SessionLib.sol"; @@ -8,6 +12,8 @@ import {ECDSA} from "solady/utils/ECDSA.sol"; import {EIP712} from "solady/utils/EIP712.sol"; import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol"; import {SignatureCheckerLib} from "solady/utils/SignatureCheckerLib.sol"; +import {Receiver} from "solady/accounts/Receiver.sol"; +import {SignatureCheckerLib} from "solady/utils/SignatureCheckerLib.sol"; struct Call { address target; @@ -50,7 +56,7 @@ library MinimalAccountStorage { } -contract MinimalAccount is EIP712, IERC721Receiver, IERC1155Receiver { +contract MinimalAccount is EIP712, Receiver, IERC721Receiver, IERC1155Receiver { using EnumerableSetLib for EnumerableSetLib.AddressSet; using ECDSA for bytes32; @@ -63,16 +69,24 @@ contract MinimalAccount is EIP712, IERC721Receiver, IERC1155Receiver { error UIDAlreadyProcessed(); // 0x26748493 error SessionExpired(); // 0x1fd05a4a error NoCallsToExecute(); // 0xf4a0814c + error SignatureValidationFailed(); // 0x2fdec18b + error InvalidSelector(); // 0x7352d91c + error NotFromEntrypoint(); // 0xca433742 event Executed(address indexed to, uint256 value, bytes data); event SessionCreated(address indexed signer, SessionLib.SessionSpec sessionSpec); + address immutable private entrypoint; bytes32 private constant CALL_TYPEHASH = keccak256("Call(address target,uint256 value,bytes data)"); bytes32 private constant WRAPPED_CALL_TYPEHASH = keccak256("WrappedCalls(Call[] calls,bytes32 uid)Call(address target,uint256 value,bytes data)"); + constructor (address _entrypoint) { + entrypoint = _entrypoint; + } + function execute(Call[] calldata calls) external payable { - if (msg.sender != address(this)) { + if (msg.sender != address(this) && msg.sender != entrypoint) { _validate(msg.sender, calls); } @@ -101,6 +115,71 @@ contract MinimalAccount is EIP712, IERC721Receiver, IERC1155Receiver { // === View functions // ====== + function entryPoint() public view virtual returns (IEntryPoint) { + return IEntryPoint(entrypoint); + } + + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external virtual returns (uint256 validationData) { + _requireFromEntryPoint(); + validationData = _validateSignature(userOp, userOpHash); + _payPrefund(missingAccountFunds); + } + + function isValidSignature( + bytes32 _hash, + bytes memory _signature + ) public view virtual returns (bytes4 magicValue) { + address recoveredSigner = _hash.recover(_signature); + + if (recoveredSigner == address(this)) { + return 0x1626ba7e; // ERC-1271 MagicValue + } + } + + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual returns (uint256 validationData) { + (address signer, bytes memory signature) = abi.decode(userOp.signature, (address, bytes)); + bytes32 hash = SignatureCheckerLib.toEthSignedMessageHash(userOpHash); + bool isValidSig = SignatureCheckerLib.isValidSignatureNow(signer, hash, signature); + + if (!isValidSig) { + revert SignatureValidationFailed(); + } + + bytes4 functionSelector = bytes4(userOp.callData[:4]); + + if(functionSelector != this.execute.selector) { + revert InvalidSelector(); + } + + Call[] memory calls = abi.decode(userOp.callData[4:], (Call[])); + if (signer != address(this)) { + _validate(signer, calls); + } + + return _packValidationData(ValidationData(address(0), 0, 0)); + } + + function _requireFromEntryPoint() internal view virtual { + if(msg.sender != entrypoint) { + revert NotFromEntrypoint(); + } + } + + function _payPrefund(uint256 missingAccountFunds) internal virtual { + if (missingAccountFunds != 0) { + (bool success, ) = payable(msg.sender).call{ value: missingAccountFunds, gas: type(uint256).max }(""); + (success); + //ignore failure (its EntryPoint's job to verify, not account.) + } + } + function getCallPoliciesForSigner(address signer) external view returns (SessionLib.CallSpec[] memory) { bytes32 sessionUid = _minimalAccountStorage().sessionIds[signer]; return _minimalAccountStorage().callPolicies[sessionUid]; @@ -204,7 +283,7 @@ contract MinimalAccount is EIP712, IERC721Receiver, IERC1155Receiver { } } - function _validate(address _signer, Call[] calldata calls) internal virtual { + function _validate(address _signer, Call[] memory calls) internal virtual { bytes32 sessionUid = _minimalAccountStorage().sessionIds[_signer]; uint256 length = calls.length; diff --git a/test/MinimalAccount.t.sol b/test/MinimalAccount.t.sol index ab1420a..131a81e 100644 --- a/test/MinimalAccount.t.sol +++ b/test/MinimalAccount.t.sol @@ -21,6 +21,7 @@ import "lib/forge-std/src/console.sol"; contract MinimalAccountTest is Test { + IEntryPoint public entrypoint; MockTarget public target; MockERC20 public erc20; MinimalAccount public eoa7702; @@ -37,10 +38,11 @@ contract MinimalAccountTest is Test { bytes32 internal domainSeparator; function setUp() public { - address impl = address(new MinimalAccount()); + entrypoint = IEntryPoint(EntryPointLib.deploy()); + address impl = address(new MinimalAccount(address(entrypoint))); vm.etch(eoa, impl.code); // etch code on eoa -- emulating authorization - eoa7702 = MinimalAccount(eoa); + eoa7702 = MinimalAccount(payable(eoa)); target = new MockTarget(); erc20 = new MockERC20(); @@ -72,6 +74,18 @@ contract MinimalAccountTest is Test { assertEq(fetchedTransferPolicies.length, spec.transferPolicies.length); } + function test_transferOwner() public { + Call[] memory calls = new Call[](1); + calls[0].target = address(0x12345); + calls[0].value = 100; + calls[0].data = ""; + + vm.prank(address(eoa7702)); + eoa7702.execute(calls); + + assertTrue(address(0x12345).balance == 100); + } + function test_transferPolicy() public { // 1. create session key @@ -484,6 +498,36 @@ contract MinimalAccountTest is Test { // console.logBool(success); // } + function test_execute_owner() public { + Call[] memory calls = new Call[](1); + calls[0].target = address(0x12345); + calls[0].value = 100; + calls[0].data = ""; + + // vm.prank(address(eoa7702)); + // eoa7702.execute(calls); + + bytes memory userOpCalldata = abi.encodeCall( + MinimalAccount.execute, + ( + calls + ) + ); + + uint256 nonce = getNonce(address(eoa7702)); + + PackedUserOperation memory userOp = getDefaultUserOp(); + userOp.sender = address(eoa7702); + userOp.nonce = nonce; + userOp.callData = userOpCalldata; + + PackedUserOperation[] memory userOps = _getSignedUserOp(eoa, eoaPKey, userOp); + + entrypoint.handleOps(userOps, payable(address(eoa7702))); + + assertTrue(address(0x12345).balance == 100); + } + // test utils function _getDefaultSessionSpec(address signer) internal returns (SessionLib.SessionSpec memory spec) { @@ -502,4 +546,44 @@ contract MinimalAccountTest is Test { eoa7702.createSessionWithSig(spec, signature); } + function getNonce(address _account) internal returns (uint256 nonce) { + nonce = entrypoint.getNonce(_account, 0); + } + + function getDefaultUserOp() internal returns (PackedUserOperation memory userOp) { + userOp = PackedUserOperation({ + sender: address(0), + nonce: 0, + 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") + }); + } + + function _getSignedUserOp(address signer, uint256 pkey, PackedUserOperation memory userOp) + internal + returns (PackedUserOperation[] memory) + { + // Sign UserOp + { + bytes32 opHash = entrypoint.getUserOpHash(userOp); + bytes32 msgHash = SignatureCheckerLib.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pkey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + userOp.signature = abi.encode(signer, userOpSignature); + } + + // Create userOps array + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + return userOps; + } + }