Skip to content

Commit

Permalink
chore: SimplePlusAccount init
Browse files Browse the repository at this point in the history
  • Loading branch information
jpgonzalezra committed May 17, 2024
1 parent 27f0508 commit 0656017
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 75 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"url": "https://github.com/jpgonzalezra"
},
"dependencies": {
"@openzeppelin/contracts": "^5.0.1"
"@openzeppelin/contracts": "5.0.2",
"@account-abstraction/contracts": "0.7.0"
},
"devDependencies": {
"forge-std": "github:foundry-rs/forge-std#v1.8.1",
Expand Down
3 changes: 3 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/
@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/
forge-std/=node_modules/forge-std/
@account-abstraction/contracts/=node_modules/@account-abstraction/contracts/
solady/=solady/src
20 changes: 10 additions & 10 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.25 <0.9.0;
// // SPDX-License-Identifier: UNLICENSED
// pragma solidity >=0.8.25 <0.9.0;

import { Foo } from "../src/Foo.sol";
// import { SimplePlusAccount } from "../src/SimplePlusAccount.sol";

import { BaseScript } from "./Base.s.sol";
// import { BaseScript } from "./Base.s.sol";

/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting
contract Deploy is BaseScript {
function run() public broadcast returns (Foo foo) {
foo = new Foo();
}
}
// /// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting
// contract Deploy is BaseScript {
// function run() public broadcast returns (Foo foo) {
// foo = new Foo();
// }
// }
8 changes: 0 additions & 8 deletions src/Foo.sol

This file was deleted.

136 changes: 136 additions & 0 deletions src/SimplePlusAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.25;

import { SimpleAccount, IEntryPoint } from "@account-abstraction/contracts/samples/SimpleAccount.sol";
import { SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED } from "@account-abstraction/contracts/core/Helpers.sol";
import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol";
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

contract SimplePlusAccount is SimpleAccount, IERC1271, EIP712 {
using ECDSA for bytes32;
using MessageHashUtils for bytes32;

bytes32 internal constant _MESSAGE_TYPEHASH = keccak256("SimplePlusAccount(bytes message)");

modifier onlyAuthorized() {
_onlyAuthorized();
_;
}

// @notice Signature types used for user operation validation and ERC-1271 signature validation.
enum SignatureType {
EOA,
CONTRACT,
CONTRACT_WITH_ADDR
}

error InvalidOwner(address newOwner);
error NotAuthorized();
error InvalidSignatureType();

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

constructor(IEntryPoint anEntryPoint) SimpleAccount(anEntryPoint) EIP712("SimplePlusAccount", "1") { }

/**
* @dev The _entryPoint member is immutable, to reduce gas consumption. To upgrade EntryPoint,
* a new implementation of SimpleAccount must be deployed with the new EntryPoint address, then upgrading
* the implementation by calling `upgradeTo()`
*/
function initialize(address anOwner) public virtual override initializer {
_initialize(anOwner);
}

/// @dev Revert if the caller is not any of:
/// 1. The entry point
/// 2. The account itself (when redirected through `execute`, etc.)
/// 3. An owner
function _onlyAuthorized() internal view {
if (msg.sender != address(entryPoint()) && msg.sender != address(this) && msg.sender != owner) {
revert NotAuthorized();
}
}

/// @notice Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current
/// owner or from the entry point via a user operation signed by the current owner.
/// @param newOwner The new owner.
function transferOwnership(address newOwner) external onlyAuthorized {
if (newOwner == address(0) || newOwner == address(this) || owner == newOwner) {
revert InvalidOwner(newOwner);
}
address oldOwner = owner;
owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}

/*
* @dev Validates the signature of a user operation.
*
* The signature is considered valid if:
* - It is signed by the owner's private key (when the owner is an EOA), or
* - It is a valid ERC-1271 signature from the owner (when the owner is a contract).
*
* Reverts if the signature is malformed or if the signature type is unrecognized.
*
* Note:
* - This function differs from `validateUserOp` in that it does **not** wrap the hash in an
* "Ethereum Signed Message" envelope before checking the signature for the EOA-owner case.
*/
function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) {
bytes32 structHash = keccak256(abi.encode(_MESSAGE_TYPEHASH, keccak256(abi.encode(hash))));
bytes32 replaySafeHash = MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash);
return _validateSignatureType(uint8(signature[0]), replaySafeHash, signature[1:]) == SIG_VALIDATION_SUCCESS
? this.isValidSignature.selector
: bytes4(0xffffffff);
}

function _validateSignature(
PackedUserOperation calldata userOp,
bytes32 userOpHash
)
internal
virtual
override
returns (uint256 validationData)
{
return _validateSignatureType(uint8(userOp.signature[0]), userOpHash, userOp.signature[1:]);
}

function _validateSignatureType(
uint8 signatureType,
bytes32 hash,
bytes memory signature
)
private
view
returns (uint256)
{
if (signature.length == 0) {
revert InvalidSignatureType();
}

if (signatureType == uint8(SignatureType.EOA)) {
return _validateEOASignature(hash, signature);
} else if (signatureType == uint8(SignatureType.CONTRACT)) {
return _validateContractSignature(hash, signature);
}

revert InvalidSignatureType();
}

function _validateEOASignature(bytes32 userOpHash, bytes memory signature) private view returns (uint256) {
bytes32 signedHash = userOpHash.toEthSignedMessageHash();
address recovered = signedHash.recover(signature);
return recovered == owner ? SIG_VALIDATION_SUCCESS : SIG_VALIDATION_FAILED;
}

function _validateContractSignature(bytes32 userOpHash, bytes memory signature) private view returns (uint256) {
return SignatureChecker.isValidERC1271SignatureNow(owner, userOpHash, signature)
? SIG_VALIDATION_SUCCESS
: SIG_VALIDATION_FAILED;
}
}
52 changes: 52 additions & 0 deletions src/SimplePlusAccountFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.25;

import "@openzeppelin/contracts/utils/Create2.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import "./SimplePlusAccount.sol";

/**
* A sample factory contract for SimplePlusAccount
* A UserOperations "initCode" holds the address of the factory, and a method call (to createAccount, in this sample factory).
* The factory's createAccount returns the target account address even if it is already installed.
* This way, the entryPoint.getSenderAddress() can be called either before or after the account is created.
*/
contract SimplePlusAccountFactory {
SimplePlusAccount public immutable accountImplementation;

constructor(IEntryPoint _entryPoint) {
accountImplementation = new SimplePlusAccount(_entryPoint);
}

/**
* create an account, and return its address.
* returns the address even if the account is already deployed.
* Note that during UserOperation execution, this method is called only if the account is not deployed.
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
*/
function createAccount(address owner,uint256 salt) public returns (SimplePlusAccount ret) {
address addr = getAddress(owner, salt);
uint256 codeSize = addr.code.length;
if (codeSize > 0) {
return SimplePlusAccount(payable(addr));
}
ret = SimplePlusAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}(
address(accountImplementation),
abi.encodeCall(SimplePlusAccount.initialize, (owner))
)));
}

/**
* calculate the counterfactual address of this account as it would be returned by createAccount()
*/
function getAddress(address owner,uint256 salt) public view returns (address) {
return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked(
type(ERC1967Proxy).creationCode,
abi.encode(
address(accountImplementation),
abi.encodeCall(SimplePlusAccount.initialize, (owner))
)
)));
}
}
56 changes: 56 additions & 0 deletions test/AccountTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.25;

import "forge-std/src/Test.sol";

import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol";
import { EntryPoint } from "@account-abstraction/contracts/core/EntryPoint.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

contract AccountTest is Test {
using ECDSA for bytes32;
using MessageHashUtils for bytes32;

function getSignedOp(
EntryPoint entryPoint,
uint8 sigType,
bytes memory callData,
uint256 privateKey,
address account
)
internal
view
returns (PackedUserOperation memory)
{
PackedUserOperation memory op = getUnsignedOp(callData, account);
bytes32 userOpHash = entryPoint.getUserOpHash(op);
bytes32 ethSignedMessageHash = userOpHash.toEthSignedMessageHash();
bytes memory signature = abi.encodePacked(sigType, sign(privateKey, ethSignedMessageHash));
op.signature = signature;
return op;
}

function getUnsignedOp(bytes memory callData, address account) internal pure returns (PackedUserOperation memory) {
uint128 verificationGasLimit = 1 << 24;
uint128 callGasLimit = 1 << 24;
uint128 maxPriorityFeePerGas = 1 << 8;
uint128 maxFeePerGas = 1 << 8;
return PackedUserOperation({
sender: account,
nonce: 0,
initCode: "",
callData: callData,
accountGasLimits: bytes32(uint256(verificationGasLimit) << 128 | callGasLimit),
preVerificationGas: 1 << 24,
gasFees: bytes32(uint256(maxPriorityFeePerGas) << 128 | maxFeePerGas),
paymasterAndData: "",
signature: ""
});
}

function sign(uint256 privateKey, bytes32 digest) internal pure returns (bytes memory) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
return abi.encodePacked(r, s, v);
}
}
56 changes: 0 additions & 56 deletions test/Foo.t.sol

This file was deleted.

Loading

0 comments on commit 0656017

Please sign in to comment.