Skip to content

validate user op #29

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
85 changes: 82 additions & 3 deletions src/7702/MinimalAccount.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
// 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";
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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another important thing we were missing :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite limited though. Just checking if recovered is this EOA.

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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this right? dont think signature prepends the address?

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];
Expand Down Expand Up @@ -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;

Expand Down
88 changes: 86 additions & 2 deletions test/MinimalAccount.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}

}