-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: replace Safe Module by ERC4337 Account contract (#22)
- Loading branch information
Showing
23 changed files
with
935 additions
and
176 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
PRIVATE_KEY= | ||
ETHERSCAN_API_KEY= |
39 changes: 39 additions & 0 deletions
39
contracts/broadcast/SimpleAccountFactory.s.sol/84531/dry-run/run-1698915913.json
Large diffs are not rendered by default.
Oops, something went wrong.
39 changes: 39 additions & 0 deletions
39
contracts/broadcast/SimpleAccountFactory.s.sol/84531/dry-run/run-1698915996.json
Large diffs are not rendered by default.
Oops, something went wrong.
39 changes: 39 additions & 0 deletions
39
contracts/broadcast/SimpleAccountFactory.s.sol/84531/dry-run/run-latest.json
Large diffs are not rendered by default.
Oops, something went wrong.
37 changes: 37 additions & 0 deletions
37
contracts/broadcast/SimpleAccountFactory.s.sol/84531/run-1698916310.json
Large diffs are not rendered by default.
Oops, something went wrong.
39 changes: 39 additions & 0 deletions
39
contracts/broadcast/SimpleAccountFactory.s.sol/84531/run-1698916336.json
Large diffs are not rendered by default.
Oops, something went wrong.
70 changes: 70 additions & 0 deletions
70
contracts/broadcast/SimpleAccountFactory.s.sol/84531/run-1698916418.json
Large diffs are not rendered by default.
Oops, something went wrong.
70 changes: 70 additions & 0 deletions
70
contracts/broadcast/SimpleAccountFactory.s.sol/84531/run-1698916452.json
Large diffs are not rendered by default.
Oops, something went wrong.
70 changes: 70 additions & 0 deletions
70
contracts/broadcast/SimpleAccountFactory.s.sol/84531/run-latest.json
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule account-abstraction
added at
94cf02
Submodule openzeppelin-contracts
added at
045704
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
) | ||
) | ||
) | ||
) | ||
); | ||
} | ||
} |
Oops, something went wrong.