Skip to content

Commit

Permalink
feat: replace Safe Module by ERC4337 Account contract (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
yum0e authored Nov 2, 2023
1 parent ddfa540 commit b74b759
Show file tree
Hide file tree
Showing 23 changed files with 935 additions and 176 deletions.
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
[submodule "contracts/lib/p256-verifier"]
path = contracts/lib/p256-verifier
url = https://github.com/daimo-eth/p256-verifier
[submodule "contracts/lib/account-abstraction"]
path = contracts/lib/account-abstraction
url = https://github.com/eth-infinitism/account-abstraction
[submodule "contracts/lib/openzeppelin-contracts"]
path = contracts/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
2 changes: 2 additions & 0 deletions contracts/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
PRIVATE_KEY=
ETHERSCAN_API_KEY=

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ src = "src"
out = "out"
libs = ["lib"]

bytecode_hash="none"
solc_version="0.8.21"

[rpc_endpoints]
base_goerli = "https://goerli.base.org"
1 change: 1 addition & 0 deletions contracts/lib/account-abstraction
Submodule account-abstraction added at 94cf02
1 change: 1 addition & 0 deletions contracts/lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at 045704
4 changes: 3 additions & 1 deletion contracts/remappings.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
p256-verifier/=lib/p256-verifier/src/
p256-verifier/=lib/p256-verifier/src/
account-abstraction/=lib/account-abstraction/contracts/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
22 changes: 22 additions & 0 deletions contracts/script/SimpleAccountFactory.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

import "forge-std/Script.sol";
import "account-abstraction/interfaces/IEntryPoint.sol";
import {SimpleAccountFactory} from "../src/SimpleAccountFactory.sol";

contract DeploySimpleAccountFactory is Script {
function run() public {
vm.startBroadcast();

// From https://docs.stackup.sh/docs/entity-addresses#entrypoint
IEntryPoint entryPoint = IEntryPoint(
0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
);

SimpleAccountFactory factory = new SimpleAccountFactory(entryPoint);
console2.log("SimpleAccountFactory deployed at", address(factory));
vm.stopBroadcast();
}
}
194 changes: 194 additions & 0 deletions contracts/src/SimpleAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "forge-std/console2.sol";
import "account-abstraction/interfaces/IAccount.sol";
import "account-abstraction/interfaces/IEntryPoint.sol";
import "account-abstraction/core/Helpers.sol";
import {WebAuthn} from "src/WebAuthn.sol";
import "openzeppelin-contracts/contracts/interfaces/IERC1271.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

struct Signature {
bytes authenticatorData;
string clientDataJSON;
uint256 challengeLocation;
uint256 responseTypeLocation;
uint256 r;
uint256 s;
}

contract SimpleAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
struct Call {
address dest;
uint256 value;
bytes data;
}

struct PublicKey {
bytes32 X;
bytes32 Y;
}

IEntryPoint public immutable entryPoint;
PublicKey public publicKey;

event SimpleAccountInitialized(
IEntryPoint indexed entryPoint,
bytes32[2] pubKey
);

// Return value in case of signature failure, with no time-range.
// Equivalent to _packValidationData(true,0,0)
uint256 private constant _SIG_VALIDATION_FAILED = 1;

constructor(IEntryPoint _entryPoint) {
entryPoint = _entryPoint;
_disableInitializers();
}

/**
* @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(
bytes32[2] memory aPublicKey
) public virtual initializer {
_initialize(aPublicKey);
}

function _initialize(bytes32[2] memory aPublicKey) internal virtual {
publicKey = PublicKey(aPublicKey[0], aPublicKey[1]);
emit SimpleAccountInitialized(entryPoint, [publicKey.X, publicKey.Y]);
}

// solhint-disable-next-line no-empty-blocks
receive() external payable {}

function _onlyOwner() internal view {
//directly through the account itself (which gets redirected through execute())
require(msg.sender == address(this), "only account itself can call");
}

/// Execute multiple transactions atomically.
function executeBatch(Call[] calldata calls) external onlyEntryPoint {
for (uint256 i = 0; i < calls.length; i++) {
_call(calls[i].dest, calls[i].value, calls[i].data);
}
}

function _validateSignature(
bytes memory message,
bytes calldata signature
) private view returns (bool) {
Signature memory sig = abi.decode(signature, (Signature));

return
WebAuthn.verifySignature({
challenge: message,
authenticatorData: sig.authenticatorData,
requireUserVerification: false,
clientDataJSON: sig.clientDataJSON,
challengeLocation: sig.challengeLocation,
responseTypeLocation: sig.responseTypeLocation,
r: sig.r,
s: sig.s,
x: uint256(publicKey.X),
y: uint256(publicKey.Y)
});
}

function isValidSignature(
bytes32 message,
bytes calldata signature
) external view override returns (bytes4 magicValue) {
if (_validateSignature(abi.encodePacked(message), signature)) {
return IERC1271(this).isValidSignature.selector;
}
return 0xffffffff;
}

function _validateUserOpSignature(
UserOperation calldata userOp,
bytes32 userOpHash
) private view returns (uint256 validationData) {
bytes memory messageToVerify;
bytes calldata signature;
ValidationData memory returnIfValid;

uint256 sigLength = userOp.signature.length;
if (sigLength == 0) return _SIG_VALIDATION_FAILED;

uint8 version = uint8(userOp.signature[0]);
if (version == 1) {
if (sigLength < 7) return _SIG_VALIDATION_FAILED;
uint48 validUntil = uint48(bytes6(userOp.signature[1:7]));

signature = userOp.signature[7:]; // keySlot, signature
messageToVerify = abi.encodePacked(version, validUntil, userOpHash);
returnIfValid.validUntil = validUntil;
} else {
return _SIG_VALIDATION_FAILED;
}

if (_validateSignature(messageToVerify, signature)) {
return _packValidationData(returnIfValid);
}
return _SIG_VALIDATION_FAILED;
}

function _call(address target, uint256 value, bytes memory data) internal {
(bool success, bytes memory result) = target.call{value: value}(data);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
}

function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
)
external
virtual
override
onlyEntryPoint
returns (uint256 validationData)
{
// Note: `forge coverage` incorrectly marks this function and downstream
// as non-covered.
validationData = _validateUserOpSignature(userOp, userOpHash);
_payPrefund(missingAccountFunds);
}

function _payPrefund(uint256 missingAccountFunds) private {
if (missingAccountFunds != 0) {
(bool success, ) = payable(msg.sender).call{
value: missingAccountFunds,
gas: type(uint256).max
}("");
(success); // no-op; silence unused variable warning
}
}

modifier onlySelf() {
require(msg.sender == address(this), "only self");
_;
}

modifier onlyEntryPoint() {
require(msg.sender == address(entryPoint), "only entry point");
_;
}

/// UUPSUpsgradeable: only allow self-upgrade.
function _authorizeUpgrade(
address newImplementation
) internal view override onlySelf {
(newImplementation); // No-op; silence unused parameter warning
}
}
82 changes: 82 additions & 0 deletions contracts/src/SimpleAccountFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;

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

import "src/SimpleAccount.sol";

/**
* A sample factory contract for SimpleAccount
* 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 SimpleAccountFactory {
SimpleAccount public immutable accountImplem;
IEntryPoint public immutable entryPoint;

constructor(IEntryPoint _entryPoint) {
entryPoint = _entryPoint;
accountImplem = new SimpleAccount(_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(
bytes32[2] memory publicKey,
uint256 salt
) public payable returns (SimpleAccount) {
address addr = getAddress(publicKey, salt);

// Prefund the account with msg.value
if (msg.value > 0) {
entryPoint.depositTo{value: msg.value}(addr);
}

// Otherwise, no-op if the account is already deployed
uint codeSize = addr.code.length;
if (codeSize > 0) {
return SimpleAccount(payable(addr));
}

return
SimpleAccount(
payable(
new ERC1967Proxy{salt: bytes32(salt)}(
address(accountImplem),
abi.encodeCall(SimpleAccount.initialize, (publicKey))
)
)
);
}

/**
* Calculate the counterfactual address of this account as it would be returned by createAccount()
*/
function getAddress(
bytes32[2] memory publicKey,
uint256 salt
) public view returns (address) {
return
Create2.computeAddress(
bytes32(salt),
keccak256(
abi.encodePacked(
type(ERC1967Proxy).creationCode,
abi.encode(
address(accountImplem),
abi.encodeCall(
SimpleAccount.initialize,
(publicKey)
)
)
)
)
);
}
}
Loading

0 comments on commit b74b759

Please sign in to comment.