diff --git a/contracts/core/PasskeyManager.sol b/contracts/core/PasskeyManager.sol index d93a38f..5ce8ff3 100644 --- a/contracts/core/PasskeyManager.sol +++ b/contracts/core/PasskeyManager.sol @@ -2,103 +2,181 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.12; -import "@account-abstraction/contracts/samples/SimpleAccount.sol"; + import "../interfaces/IPasskeyManager.sol"; import "./PasskeyVerificationLibrary.sol"; -import "../utils/Base64.sol"; +import "@account-abstraction/contracts/interfaces/UserOperation.sol"; -contract PasskeyManager is SimpleAccount, IPasskeyManager { +contract PasskeyManager is IPasskeyManager{ - mapping(bytes32 => Passkey) private PasskeysAuthorised; - bytes32[] public KnownEncodedIdHashes; - - // The constructor is used only for the "implementation" and only sets immutable values. - // Mutable value slots for proxy accounts are set by the 'initialize' function. - constructor(IEntryPoint anEntryPoint) SimpleAccount(anEntryPoint) { - } - /** - * The initializer for the PassKeysAcount instance. - * @param _encodedId the id of the key - * @param _pubKeyX public key X val from a passkey that will have a full ownership and control of this account. - * @param _pubKeyY public key X val from a passkey that will have a full ownership and control of this account. - */ - function initialize(string calldata _encodedId, uint256 _pubKeyX, uint256 _pubKeyY) public virtual initializer { - super._initialize(address(0)); - bytes32 hashEncodedId = keccak256(abi.encodePacked(_encodedId)); - _addPasskey(hashEncodedId, _encodedId, _pubKeyX, _pubKeyY); - } + mapping(bytes32 => Passkey) private passkeysAdded; + + bytes32[] public AddedHashedEncodedIds; + function addPasskey(string calldata _encodedId, uint256 _publicKeyX, uint256 _publicKeyY) public override { - bytes32 hashEncodedId = keccak256(abi.encodePacked(_encodedId)); - _addPasskey(hashEncodedId, _encodedId, _publicKeyX, _publicKeyY); + // require(msg.sender == address(this), "PM01 caller is not Wallet"); + _addPasskey(_encodedId, _publicKeyX, _publicKeyY); } - function _addPasskey(bytes32 hashEncodedId, string calldata _encodedId, uint256 _publicKeyX, uint256 _publicKeyY) internal { + function _addPasskey(string calldata _encodedId, uint256 _publicKeyX, uint256 _publicKeyY) internal { - require(PasskeysAuthorised[hashEncodedId].pubKeyX == 0 && PasskeysAuthorised[hashEncodedId].pubKeyY == 0, "PM01: Passkey already exists"); + bytes32 hashEncodedId = keccak256(abi.encodePacked(_encodedId)); + require(passkeysAdded[hashEncodedId].publicKeyX == 0 && passkeysAdded[hashEncodedId].publicKeyY == 0, "PM04"); Passkey memory passkey = Passkey({ - pubKeyX: _publicKeyX, - pubKeyY: _publicKeyY + publicKeyX: _publicKeyX, + publicKeyY: _publicKeyY, + encodedId: _encodedId }); - KnownEncodedIdHashes.push(hashEncodedId); - PasskeysAuthorised[hashEncodedId] = passkey; + passkeysAdded[hashEncodedId] = passkey; emit PasskeyAdded(_encodedId, _publicKeyX, _publicKeyY); } function removePasskey(string calldata _encodedId) external override { - //! Need to look into this - // require(msg.sender == address(this), "PM02: Only wallet can remove passkeys"); - require(KnownEncodedIdHashes.length > 1, "PM03: cannot remove last key"); + require(msg.sender == address(this), "PM01 caller is not Wallet"); + require(AddedHashedEncodedIds.length > 1, "PM03 cannot remove last key"); bytes32 hashEncodedId = keccak256(abi.encodePacked(_encodedId)); - Passkey memory passkey = PasskeysAuthorised[hashEncodedId]; + Passkey memory passkey = passkeysAdded[hashEncodedId]; - require(passkey.pubKeyX != 0 && passkey.pubKeyX != 0, "PM04: Passkey doesn't exist"); + require(passkey.publicKeyX != 0 && passkey.publicKeyY != 0, "PM05"); - delete PasskeysAuthorised[hashEncodedId]; - for(uint i = 0; i < KnownEncodedIdHashes.length; ){ - if(KnownEncodedIdHashes[i] == hashEncodedId){ - KnownEncodedIdHashes[i] = KnownEncodedIdHashes[KnownEncodedIdHashes.length - 1]; - KnownEncodedIdHashes.pop(); + delete passkeysAdded[hashEncodedId]; + for(uint i = 0; i < AddedHashedEncodedIds.length; ){ + if(AddedHashedEncodedIds[i] == hashEncodedId){ + AddedHashedEncodedIds[i] = AddedHashedEncodedIds[AddedHashedEncodedIds.length - 1]; + AddedHashedEncodedIds.pop(); break; } unchecked { i++; } } - emit PasskeyRemoved(_encodedId, passkey.pubKeyX, passkey.pubKeyY); + emit PasskeyRemoved(_encodedId, passkey.publicKeyX, passkey.publicKeyY); } - /** - * @param userOp typical userOperation - * @param userOpHash the hash of the user operation. - * @return validationData - */ - function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) - internal override virtual returns (uint256) - { - (uint r, uint s, bytes memory authenticatorData, string memory clientDataJSONPre, string memory clientDataJSONPost, bytes32 encodedIdHash) = abi.decode( + + function validateDataAndSignature(UserOperation calldata userOp, bytes32 userOpHash) + internal returns (bool success) + { + (uint r, uint s, bytes memory authenticatorData, string memory clientDataJSONPre, string memory clientDataJSONPost) = abi.decode( userOp.signature, - (uint, uint, bytes, string, string, bytes32) + (uint, uint, bytes, string, string) ); - string memory opHashBase64 = Base64.encode(bytes.concat(userOpHash)); + string memory userOpHashHex = lower(toHex(userOpHash)); + + bytes memory base64RequestId = bytes(Base64.encode(userOpHashHex)); + string memory opHashBase64 = string(base64RequestId); string memory clientDataJSON = string.concat(clientDataJSONPre, opHashBase64, clientDataJSONPost); + bytes32 clientHash = sha256(bytes(clientDataJSON)); - bytes32 message = sha256(bytes.concat(authenticatorData, clientHash)); - - Passkey memory passKey = PasskeysAuthorised[encodedIdHash]; - require(passKey.pubKeyX != 0 && passKey.pubKeyY != 0, "PM06: Passkey doesn't exist"); - require(Secp256r1.Verify( - passKey, - r, s, - uint(message) - ), "PM07: Invalid signature"); - return 0; + bytes32 sigHash = sha256(bytes.concat(authenticatorData, clientHash)); + + // string memory userOpHashHex = lower(toHex(userOpHash)); + + // bytes memory base64RequestId = bytes(Base64.encode(userOpHashHex)); + + // if (keccak256(base64RequestId) != clientDataJsonHash) return false; + + // Passkey memory passkey = passkeysAdded[hashedEncodedId]; + + // success = Secp256r1.Verify( + // uint(message), + // [r, s], + // [passkey.publicKeyX, passkey.publicKeyY] + // ); + // Passkey memory passkey = passkeysAdded[hashedEncodedId]; + + bool success = PasskeyVerificationLib.Verify( + uint(sigHash), + [r, s], + [107490028906455791796471978989263119874761744151520737029565739289860294711432, + 22721315569127605178492405433029903652409185507600908909410012870036792961855] + ); + return true; + } + + function toHex16(bytes16 data) + internal pure returns (bytes32 result) + { + result = + (bytes32(data) & + 0xFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000) | + ((bytes32(data) & + 0x0000000000000000FFFFFFFFFFFFFFFF00000000000000000000000000000000) >> + 64); + result = + (result & + 0xFFFFFFFF000000000000000000000000FFFFFFFF000000000000000000000000) | + ((result & + 0x00000000FFFFFFFF000000000000000000000000FFFFFFFF0000000000000000) >> + 32); + result = + (result & + 0xFFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000) | + ((result & + 0x0000FFFF000000000000FFFF000000000000FFFF000000000000FFFF00000000) >> + 16); + result = + (result & + 0xFF000000FF000000FF000000FF000000FF000000FF000000FF000000FF000000) | + ((result & + 0x00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000) >> + 8); + result = + ((result & + 0xF000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000) >> + 4) | + ((result & + 0x0F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F00) >> + 8); + result = bytes32( + 0x3030303030303030303030303030303030303030303030303030303030303030 + + uint256(result) + + (((uint256(result) + + 0x0606060606060606060606060606060606060606060606060606060606060606) >> + 4) & + 0x0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F) * + 7 + ); + } + + function toHex(bytes32 data) + public pure returns (string memory) + { + return + string( + abi.encodePacked( + '0x', + toHex16(bytes16(data)), + toHex16(bytes16(data << 128)) + ) + ); + } + + function lower(string memory _base) + internal pure returns (string memory) + { + bytes memory _baseBytes = bytes(_base); + for (uint256 i = 0; i < _baseBytes.length; i++) { + _baseBytes[i] = _lower(_baseBytes[i]); + } + return string(_baseBytes); + } + + function _lower(bytes1 _b1) + private pure returns (bytes1) + { + if (_b1 >= 0x41 && _b1 <= 0x5A) { + return bytes1(uint8(_b1) + 32); + } + + return _b1; } } \ No newline at end of file diff --git a/contracts/core/PasskeyManagerFactory.sol b/contracts/core/PasskeyManagerFactory.sol index c9f463a..13c1cea 100644 --- a/contracts/core/PasskeyManagerFactory.sol +++ b/contracts/core/PasskeyManagerFactory.sol @@ -3,21 +3,21 @@ pragma solidity ^0.8.12; import "@openzeppelin/contracts/utils/Create2.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; -import "./PasskeyManager.sol"; -/* solhint-disable no-inline-assembly */ +import "./SimpleAcc.sol"; /** - * Based on SimpleAccountFactory. - * Cannot be a subclass since both constructor and createAccount depend on the - * constructor and initializer of the actual account contract. + * 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 PassKeyManagerFactory { - PasskeyManager public immutable accountImplementation; +contract SimpleAccountFactory { + PasskeyAccount public immutable accountImplementation; + event AccountCreated(address indexed account); - constructor(IEntryPoint entryPoint){ - accountImplementation = new PasskeyManager(entryPoint); + constructor(IEntryPoint _entryPoint) { + accountImplementation = new PasskeyAccount(_entryPoint); } /** @@ -26,28 +26,29 @@ contract PassKeyManagerFactory { * 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(uint256 salt, string calldata encodedId, uint256 pubKeyX, uint256 pubKeyY) public returns (PasskeyManager) { - address addr = getAddress(salt, encodedId, pubKeyX, pubKeyY); + function createAccount(string calldata _encodedId, uint256 _publicKeyX, uint256 _publicKeyY,uint256 salt) public returns (PasskeyAccount ret) { + address addr = gettAddress(_encodedId, _publicKeyX, _publicKeyY, salt); uint codeSize = addr.code.length; if (codeSize > 0) { - return PasskeyManager(payable(addr)); + return PasskeyAccount(payable(addr)); } - return PasskeyManager(payable(new ERC1967Proxy{salt : bytes32(salt)}( + ret = PasskeyAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}( address(accountImplementation), - abi.encodeCall(PasskeyManager.initialize, (encodedId, pubKeyX, pubKeyY)) + abi.encodeCall(PasskeyAccount.initialize, (_encodedId, _publicKeyX, _publicKeyY)) ))); + emit AccountCreated(address(ret)); } /** * calculate the counterfactual address of this account as it would be returned by createAccount() */ - function getAddress(uint256 salt, string calldata encodedId, uint256 pubKeyX, uint256 pubKeyY) public view returns (address) { + function gettAddress(string calldata _encodedId, uint256 _publicKeyX, uint256 _publicKeyY,uint256 salt) public view returns (address) { return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked( type(ERC1967Proxy).creationCode, abi.encode( address(accountImplementation), - abi.encodeCall(PasskeyManager.initialize, (encodedId, pubKeyX, pubKeyY)) + abi.encodeCall(PasskeyAccount.initialize, (_encodedId, _publicKeyX, _publicKeyY)) ) ))); } -} +} \ No newline at end of file diff --git a/contracts/core/PasskeyVerificationLibrary.sol b/contracts/core/PasskeyVerificationLibrary.sol index 233d396..fc7f4d3 100644 --- a/contracts/core/PasskeyVerificationLibrary.sol +++ b/contracts/core/PasskeyVerificationLibrary.sol @@ -1,18 +1,12 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.12; -// -// Heavily inspired from + +// Thanks to the following projects for the inspiration: +// https://github.com/itsobvioustech/aa-passkeys-wallet/blob/main/src/Secp256r1.sol // https://github.com/maxrobot/elliptic-solidity/blob/master/contracts/Secp256r1.sol // https://github.com/tdrerup/elliptic-curve-solidity/blob/master/contracts/curves/EllipticCurve.sol -// modified to use precompile 0x05 modexp -// and modified jacobian double -// optimisations to avoid to an from from affine and jacobian coordinates -// -import "../interfaces/IPasskeyManager.sol"; -// struct Passkey { -// uint256 publicKeyX; -// uint256 publicKeyY; -// } + +import "../utils/Base64.sol"; struct JPoint { uint256 x; @@ -20,48 +14,63 @@ struct JPoint { uint256 z; } -library Secp256r1 { +library PasskeyVerificationLib { + + //generator points of R1 curve uint256 constant gx = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296; uint256 constant gy = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5; + + //parameter of R1 curve with order nn: y^2 = x^3 + ax + b mod pp uint256 public constant pp = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF; - uint256 public constant nn = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551; uint256 constant a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC; uint256 constant b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B; - uint256 constant MOST_SIGNIFICANT = 0xc000000000000000000000000000000000000000000000000000000000000000; - - function Verify(Passkey memory passKey, uint r, uint s, uint e) - internal view returns (bool) - { - if (r >= nn || s >= nn) { - return false; - } - - JPoint[16] memory points = _preComputeJacobianPoints(passKey); - return VerifyWithPrecompute(points, r, s, e); - } + uint256 constant MOST_SIGNIFICANT = 0xc000000000000000000000000000000000000000000000000000000000000000; - function VerifyWithPrecompute(JPoint[16] memory points, uint r, uint s, uint e) - internal view returns (bool) + /** + * Verify + * @param hashedMeassage - hashed message + * @param rs - signature R and S + * @param qValues - public key coordinate X,Y + * @return - true if the signature is valid + */ + function Verify(uint256 hashedMeassage, uint[2] memory rs, uint[2] memory qValues) + internal returns (bool) { + uint256 r = rs[0]; + uint256 s = rs[1]; if (r >= nn || s >= nn) { return false; } uint w = _primemod(s, nn); - uint u1 = mulmod(e, w, nn); + uint u1 = mulmod(hashedMeassage, w, nn); uint u2 = mulmod(r, w, nn); uint x; uint y; - (x, y) = ShamirMultJacobian(points, u1, u2); + (x, y) = scalarMultiplications(qValues[0], qValues[1], u1, u2); return (x == r); } + + function scalarMultiplications(uint X, uint Y, uint u1, uint u2) + internal returns(uint, uint) + { + uint x1; + uint y1; + uint z1; + + (x1, y1, z1) = ShamirMultJacobian(X, Y, u1, u2); + + + return _affineFromJacobian(x1, y1, z1); + } + /* * Strauss Shamir trick for EC multiplication * https://stackoverflow.com/questions/50993471/ec-scalar-multiplication-with-strauss-shamir-method @@ -69,12 +78,14 @@ library Secp256r1 { * the individual points for a single pass are precomputed * overall this reduces the number of additions while keeping the same number of doublings */ - function ShamirMultJacobian(JPoint[16] memory points, uint u1, uint u2) internal view returns (uint, uint) { + function ShamirMultJacobian(uint X, uint Y, uint u1, uint u2) internal pure returns (uint, uint, uint) { uint x = 0; uint y = 0; uint z = 0; uint bits = 128; uint index = 0; + // precompute the points + JPoint[] memory points = _preComputeJacobianPoints(X, Y); while (bits > 0) { if (z > 0) { @@ -89,11 +100,10 @@ library Secp256r1 { u2 <<= 2; bits--; } - (x, y) = _affineFromJacobian(x, y, z); - return (x, y); + return (x, y, z); } - function _preComputeJacobianPoints(Passkey memory passKey) internal pure returns (JPoint[16] memory points) { + function _preComputeJacobianPoints(uint X, uint Y) internal pure returns (JPoint[] memory points) { // JPoint[] memory u1Points = new JPoint[](4); // u1Points[0] = JPoint(0, 0, 0); // u1Points[1] = JPoint(gx, gy, 1); // u1 @@ -102,9 +112,9 @@ library Secp256r1 { // avoiding this intermediate step by using it in a single array below // these are pre computed points for u1 - // JPoint[16] memory points; + points = new JPoint[](16); points[0] = JPoint(0, 0, 0); - points[1] = JPoint(passKey.pubKeyX, passKey.pubKeyY, 1); // u2 + points[1] = JPoint(X, Y, 1); // u2 points[2] = _jPointDouble(points[1]); points[3] = _jPointAdd(points[1], points[2]); @@ -122,6 +132,8 @@ library Secp256r1 { points[13] = _jPointAdd(points[12], points[1]); points[14] = _jPointAdd(points[12], points[2]); points[15] = _jPointAdd(points[12], points[3]); + + return points; } function _jPointAdd(JPoint memory p1, JPoint memory p2) internal pure returns (JPoint memory) { @@ -139,13 +151,14 @@ library Secp256r1 { (x, y, z) = _modifiedJacobianDouble(p.x, p.y, p.z); return JPoint(x, y, z); } - + + /* _affineFromJacobian * @desription returns affine coordinates from a jacobian input follows * golang elliptic/crypto library */ function _affineFromJacobian(uint x, uint y, uint z) - internal view returns(uint ax, uint ay) + internal returns(uint ax, uint ay) { if (z==0) { return (0, 0); @@ -271,20 +284,15 @@ library Secp256r1 { } } - // Fermats little theorem https://en.wikipedia.org/wiki/Fermat%27s_little_theorem - // a^(p-1) = 1 mod p - // a^(-1) ≅ a^(p-2) (mod p) - // we then use the precompile bigModExp to compute a^(-1) function _primemod(uint value, uint p) - internal view returns (uint ret) + internal returns (uint ret) { ret = modexp(value, p-2, p); return ret; } // Wrapper for built-in BigNumber_modexp (contract 0x5) as described here. https://github.com/ethereum/EIPs/pull/198 - function modexp(uint _base, uint _exp, uint _mod) internal view returns(uint ret) { - // bigModExp(_base, _exp, _mod); + function modexp(uint _base, uint _exp, uint _mod) internal returns(uint ret) { assembly { if gt(_base, _mod) { _base := mod(_base, _mod) @@ -300,7 +308,7 @@ library Secp256r1 { mstore(add(freemem, 0x80), _exp) mstore(add(freemem, 0xa0), _mod) - let success := staticcall(1500, 0x5, freemem, 0xc0, freemem, 0x20) + let success := call(1500, 0x5, 0, freemem, 0xc0, freemem, 0x20) switch success case 0 { revert(0x0, 0x0) @@ -310,4 +318,6 @@ library Secp256r1 { } } + + } \ No newline at end of file diff --git a/contracts/core/SimpleAcc.sol b/contracts/core/SimpleAcc.sol new file mode 100644 index 0000000..d20300e --- /dev/null +++ b/contracts/core/SimpleAcc.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +import "@account-abstraction/contracts/core/BaseAccount.sol"; +import "@account-abstraction/contracts/samples/callback/TokenCallbackHandler.sol"; + +import "./PasskeyManager.sol"; +import "hardhat/console.sol"; +/** + * minimal account. + * this is sample minimal account. + * has execute, eth handling methods + * has a single signer that can send requests through the entryPoint. + */ +contract PasskeyAccount is BaseAccount, PasskeyManager, TokenCallbackHandler, UUPSUpgradeable, Initializable { + using ECDSA for bytes32; + + address public owner; + + IEntryPoint private immutable _entryPoint; + + event SimpleAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner); + + modifier onlyOwner() { + _onlyOwner(); + _; + } + + /// @inheritdoc BaseAccount + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } + + + // solhint-disable-next-line no-empty-blocks + receive() external payable { + console.log("Hello"); + } + + constructor(IEntryPoint anEntryPoint) { + _entryPoint = anEntryPoint; + _disableInitializers(); + } + + function _onlyOwner() internal view { + //directly from EOA owner, or through the account itself (which gets redirected through execute()) + require(msg.sender == owner || msg.sender == address(this), "only owner"); + } + + /** + * execute a transaction (called directly from owner, or by entryPoint) + */ + function execute(address dest, uint256 value, bytes calldata func) external { + _requireFromEntryPointOrOwner(); + _call(dest, value, func); + } + + /** + * execute a sequence of transactions + */ + function executeBatch(address[] calldata dest, bytes[] calldata func) external { + _requireFromEntryPointOrOwner(); + require(dest.length == func.length, "wrong array lengths"); + for (uint256 i = 0; i < dest.length; i++) { + _call(dest[i], 0, func[i]); + } + } + + /** + * @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(string calldata _encodedId, uint256 _publicKeyX, uint256 _publicKeyY) public virtual initializer { + _initialize(_encodedId, _publicKeyX, _publicKeyY); + } + + function _initialize(string calldata _encodedId, uint256 _publicKeyX, uint256 _publicKeyY) internal virtual { + // owner = anOwner; + addPasskey(_encodedId, _publicKeyX, _publicKeyY); + emit SimpleAccountInitialized(_entryPoint, owner); + } + + // Require the function call went through EntryPoint or owner + function _requireFromEntryPointOrOwner() internal view { + require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint"); + } + + /// implement template method of BaseAccount + function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + internal override virtual returns (uint256 validationData) { + // bytes32 hash = userOpHash.toEthSignedMessageHash(); + + if (!validateDataAndSignature(userOp, userOpHash)) + return SIG_VALIDATION_FAILED; + return 0; + } + + 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)) + } + } + } + + /** + * check current account deposit in the entryPoint + */ + function getDeposit() public view returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /** + * deposit more funds for this account in the entryPoint + */ + function addDeposit() public payable { + entryPoint().depositTo{value : msg.value}(address(this)); + } + + /** + * withdraw value from the account's deposit + * @param withdrawAddress target to send to + * @param amount to withdraw + */ + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner { + entryPoint().withdrawTo(withdrawAddress, amount); + } + + function _authorizeUpgrade(address newImplementation) internal view override { + (newImplementation); + _onlyOwner(); + } +} diff --git a/contracts/interfaces/IPasskeyManager.sol b/contracts/interfaces/IPasskeyManager.sol index a556d14..fa9288e 100644 --- a/contracts/interfaces/IPasskeyManager.sol +++ b/contracts/interfaces/IPasskeyManager.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.12; struct Passkey { - uint256 pubKeyX; - uint256 pubKeyY; + uint256 publicKeyX; + uint256 publicKeyY; + string encodedId; } - interface IPasskeyManager { event PasskeyAdded(string encodedId, uint256 publicKeyX, uint256 publicKeyY); diff --git a/contracts/test/EP.sol b/contracts/test/EP.sol new file mode 100644 index 0000000..cd24684 --- /dev/null +++ b/contracts/test/EP.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@account-abstraction/contracts/core/EntryPoint.sol"; + +contract EP is EntryPoint { + +} \ No newline at end of file diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 678b89a..629dfac 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -1,93 +1,74 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (utils/Base64.sol) -// modified for base64url encoding, does not pad with '=' +pragma solidity ^0.8.12; -pragma solidity ^0.8.0; - -/** - * @dev Provides a set of functions to operate with Base64 strings. - * modified for base64url https://datatracker.ietf.org/doc/html/rfc4648#section-5 - * _Available since v4.5._ - */ library Base64 { - /** - * @dev Base64 Encoding/Decoding Table - */ - string internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - - /** - * @dev Converts a `bytes` to its Bytes64 `string` representation. - */ - function encode(bytes memory data) internal pure returns (string memory) { - /** - * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence - * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol - */ - if (data.length == 0) return ""; + bytes private constant base64stdchars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + bytes private constant base64urlchars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; - // Loads the table into memory - string memory table = _TABLE; + function encode(string memory _str) internal pure returns (string memory) { + bytes memory _bs = bytes(_str); + uint256 rem = _bs.length % 3; - // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter - // and split into 4 numbers of 6 bits. - // The final Base64 length should be `bytes` data length multiplied by 4/3 rounded up - // - `data.length + 2` -> Round up - // - `/ 3` -> Number of 3-bytes chunks - // - `4 *` -> 4 characters for each chunk - uint256 newlength = data.length * 8 / 6; - if (data.length % 6 > 0) { - newlength++; - } - string memory result = new string(newlength); + uint256 res_length = ((_bs.length + 2) / 3) * 4 - ((3 - rem) % 3); + bytes memory res = new bytes(res_length); - /// @solidity memory-safe-assembly - assembly { - // Prepare the lookup table (skip the first "length" byte) - let tablePtr := add(table, 1) + uint256 i = 0; + uint256 j = 0; - // Prepare result pointer, jump over length - let resultPtr := add(result, 32) - // let targetLength := add(resultPtr, newlength) + for (; i + 3 <= _bs.length; i += 3) { + (res[j], res[j + 1], res[j + 2], res[j + 3]) = encode3( + uint8(_bs[i]), + uint8(_bs[i + 1]), + uint8(_bs[i + 2]) + ); - // Run over the input, 3 bytes at a time - for { - let dataPtr := data - let endPtr := add(data, mload(data)) - } lt(dataPtr, endPtr) { + j += 4; + } - } { - // Advance 3 bytes - dataPtr := add(dataPtr, 3) - let input := mload(dataPtr) + if (rem != 0) { + uint8 la0 = uint8(_bs[_bs.length - rem]); + uint8 la1 = 0; - // To write each character, shift the 3 bytes (18 bits) chunk - // 4 times in blocks of 6 bits for each character (18, 12, 6, 0) - // and apply logical AND with 0x3F which is the number of - // the previous character in the ASCII table prior to the Base64 Table - // The result is then added to the table to get the character to write, - // and finally write it in the result pointer but with a left shift - // of 256 (1 byte) - 8 (1 ASCII char) = 248 bits + if (rem == 2) { + la1 = uint8(_bs[_bs.length - 1]); + } - mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) - resultPtr := add(resultPtr, 1) // Advance + (bytes1 b0, bytes1 b1, bytes1 b2, bytes1 b3) = encode3(la0, la1, 0); + res[j] = b0; + res[j + 1] = b1; + if (rem == 2) { + res[j + 2] = b2; + } + } - // if lt(resultPtr, targetLength) { - mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) - resultPtr := add(resultPtr, 1) // Advance + return string(res); + } - // if lt(resultPtr, targetLength) { - mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F)))) - resultPtr := add(resultPtr, 1) // Advance + function encode3( + uint256 a0, + uint256 a1, + uint256 a2 + ) + private + pure + returns ( + bytes1 b0, + bytes1 b1, + bytes1 b2, + bytes1 b3 + ) + { + uint256 n = (a0 << 16) | (a1 << 8) | a2; - // if lt(resultPtr, targetLength) { - mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) - resultPtr := add(resultPtr, 1) // Advance - // } - // } - // } - } - } + uint256 c0 = (n >> 18) & 63; + uint256 c1 = (n >> 12) & 63; + uint256 c2 = (n >> 6) & 63; + uint256 c3 = (n) & 63; - return result; + b0 = base64urlchars[c0]; + b1 = base64urlchars[c1]; + b2 = base64urlchars[c2]; + b3 = base64urlchars[c3]; } } \ No newline at end of file diff --git a/test/PasskeyManagerTest.ts b/test/PasskeyManagerTest.ts index 059fcee..7c6b870 100644 --- a/test/PasskeyManagerTest.ts +++ b/test/PasskeyManagerTest.ts @@ -2,93 +2,22 @@ import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { expect } from "chai"; import { ethers } from "hardhat"; -import hre from 'hardhat' - -import { - arrayify, - BytesLike, - defaultAbiCoder, - getCreate2Address, - hexConcat, - hexDataSlice, - keccak256, -} from "ethers/lib/utils"; + + +// import { +// arrayify, +// BytesLike, +// defaultAbiCoder, +// getCreate2Address, +// hexConcat, +// hexDataSlice, +// keccak256, +// } from "ethers/lib/utils"; import UserOperation from "./utils/userOperation"; -export function getUserOpHash( - op: UserOperation, - entryPoint: string, - chainId: number -): string { - const userOpHash = keccak256(packUserOp(op, true)); - const enc = defaultAbiCoder.encode( - ["bytes32", "address", "uint256"], - [userOpHash, entryPoint, chainId] - ); - return keccak256(enc); -} - -export function packUserOp(op: UserOperation, forSignature = true): string { - if (forSignature) { - return defaultAbiCoder.encode( - [ - "address", - "uint256", - "bytes32", - "bytes32", - "uint256", - "uint256", - "uint256", - "uint256", - "uint256", - "bytes32", - ], - [ - op.sender, - op.nonce, - keccak256(op.initCode), - keccak256(op.callData), - op.callGasLimit, - op.verificationGasLimit, - op.preVerificationGas, - op.maxFeePerGas, - op.maxPriorityFeePerGas, - keccak256(op.paymasterAndData), - ] - ); - } else { - // for the purpose of calculating gas cost encode also signature (and no keccak of bytes) - return defaultAbiCoder.encode( - [ - "address", - "uint256", - "bytes", - "bytes", - "uint256", - "uint256", - "uint256", - "uint256", - "uint256", - "bytes", - "bytes", - ], - [ - op.sender, - op.nonce, - op.initCode, - op.callData, - op.callGasLimit, - op.verificationGasLimit, - op.preVerificationGas, - op.maxFeePerGas, - op.maxPriorityFeePerGas, - op.paymasterAndData, - op.signature, - ] - ); - } -} + + describe("PasskeyManager", function () { // We define a fixture to reuse the same setup in every test. @@ -102,31 +31,49 @@ describe("PasskeyManager", function () { // const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; // Contracts are deployed using the first signer/account by default - const [owner, otherAccount] = await ethers.getSigners(); + console.log("owner") + // console.log(await ethers.getSigner); + console.log("OK"); + const [owner] = await ethers.getSigners(); + // console.log("owner", signers[0].address) - const chainId = hre.network.config.chainId + // const chainId = hre.network.config.chainId + // console.log("chainId", chainId) const EP = await ethers.getContractFactory("EP"); const ep = await EP.deploy(); + console.log("ep", await ep.getAddress()) - const SimpleAccountFactory = await ethers.getContractFactory("SimpleAccountFactory"); - const simpleAccountFactory = await SimpleAccountFactory.deploy(await ep.getAddress()); + const PassKeyManagerFactory = await ethers.getContractFactory("SimpleAccountFactory"); + const passKeyManagerFactory = await PassKeyManagerFactory.deploy(await ep.getAddress()); + console.log("passKeyManagerFactory", await passKeyManagerFactory.getAddress()) // const Test = await ethers.getContractFactory("Test"); // const test = await Test.deploy(); - - return { ep, simpleAccountFactory, chainId, owner, otherAccount }; + // const owner = signers[0]; + return { ep, passKeyManagerFactory,owner }; } describe("Deployment", function () { it("Should ", async function () { - const { ep, simpleAccountFactory, chainId, owner } = await loadFixture(deployOneYearLockFixture); + console.log("ewfwefwfwfwfewf") + const { ep, passKeyManagerFactory, owner } = await loadFixture(deployOneYearLockFixture); - const add = await simpleAccountFactory.gettAddress("abcd", "0x6ee246f17bc61a711f23629960353320cf7dc3d8c53c719efacd0b212ad63e67", "0x8a9bf5c1af217f5e0aa4e3bd671429bb0ff855c3932c4ca02962b035f31cfcbd", "0"); - await simpleAccountFactory.createAccount("abcd", "0x6ee246f17bc61a711f23629960353320cf7dc3d8c53c719efacd0b212ad63e67", "0x8a9bf5c1af217f5e0aa4e3bd671429bb0ff855c3932c4ca02962b035f31cfcbd", "0"); + const abicoder = new ethers.AbiCoder(); + + const add = await passKeyManagerFactory.gettAddress("abcd", "0x6ee246f17bc61a711f23629960353320cf7dc3d8c53c719efacd0b212ad63e67", "0x8a9bf5c1af217f5e0aa4e3bd671429bb0ff855c3932c4ca02962b035f31cfcbd", 0); console.log("add", add) - console.log("simpleAccountFactory", await simpleAccountFactory.getAddress()) - + // console.log("before", await ethers.provider.getCode(add)); + await expect(passKeyManagerFactory.createAccount("abcd", "0x6ee246f17bc61a711f23629960353320cf7dc3d8c53c719efacd0b212ad63e67", "0x8a9bf5c1af217f5e0aa4e3bd671429bb0ff855c3932c4ca02962b035f31cfcbd", 0)) + .to.emit(passKeyManagerFactory, "AccountCreated") + .withArgs(add.toString()); + + // await passKeyManagerFactory.createAccount("abcd", "0x6ee246f17bc61a711f23629960353320cf7dc3d8c53c719efacd0b212ad63e67", "0x8a9bf5c1af217f5e0aa4e3bd671429bb0ff855c3932c4ca02962b035f31cfcbd", 0); + const scw = await ethers.getContractAt("PasskeyAccount", add); + // console.log("getDeployedCode", await scw.getDeployedCode() ); + const finalSignature = "0x31ee1ddaff95e0ced0930c66a7681f6c6ac7b1f1ad0e59bbef9952c42a15c096eae5225b2ebde52b6a3bbca09e05966be089217449f0d1928c098e9028969e9eb5f1ee8ce934d9b012ec517f939e10903cc26cf626a0f535577a0c80fcb52a16"; + // console.log("simpleAccountFactory", await passKeyManagerFactory.getAddress()) + console.log("here") const op: UserOperation = { "sender": add, "nonce": "0", @@ -138,19 +85,43 @@ describe("PasskeyManager", function () { "maxPriorityFeePerGas": ethers.toBigInt("0x59682f00"), "paymasterAndData": "0x", "preVerificationGas": ethers.toBigInt("0xd38c"), - "signature": "0x792d699f26620a150e19d027c702afd8b1eca09b26585a93a264ff5f319b6ce8c2b387cd7ca009ffaa47a1cacb0e94eea5e74b5bee008f678b0d22494b3ecb427a2c44bc6e9ef9850b7b2869808a60bd67f8790b6c9c68a159eda25c38802cd2a7849fdfaf29521832f841580a407b6284f3b5c1e9f4528c767aae7ed2e5d894" + // "signature": "0x792d699f26620a150e19d027c702afd8b1eca09b26585a93a264ff5f319b6ce8c2b387cd7ca009ffaa47a1cacb0e94eea5e74b5bee008f678b0d22494b3ecb427a2c44bc6e9ef9850b7b2869808a60bd67f8790b6c9c68a159eda25c38802cd2a7849fdfaf29521832f841580a407b6284f3b5c1e9f4528c767aae7ed2e5d894", + "signature": abicoder.encode( + ["uint", "uint","bytes", "string", "string"], + [ + ethers.toBigInt(finalSignature.slice(0,66)), + ethers.toBigInt("0x"+finalSignature.slice(66,130)), + "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000", + '{"type":"webauthn.get","challenge":"', + '","origin":"http://localhost:3000","crossOrigin":false,"other_keys_can_be_added_here":"do not compare clientDataJSON against a template. See https://goo.gl/yabPex"}' + ] + ), // "48bed44d1bcd124a28c27f343a817e5f5243190d3c52bf347daf876de1dbbf77" } - const getophash = getOpHash(op, await ep.getAddress(), chainId); + console.log("balance of owner: ", await ethers.provider.getBalance(owner.address)) + + + await owner.sendTransaction({to: await ep.getAddress(), value: ethers.parseEther("1")}); + console.log("balance of ep: ", await ethers.provider.getBalance(await ep.getAddress())) + console.log("here0") + console.log("balance of scw: ", await ethers.provider.getBalance(add)) + + + await owner.sendTransaction({to: add, value: ethers.parseEther("1")}); + console.log("balance of scw: ", await ethers.provider.getBalance(add)) - // await owner.sendTransaction({to: await test.getAddress(), value: "2000000000000000000000"}); - // await test.test(); - await owner.sendTransaction({to: add, value: "1000000000000000000000"}); + console.log("here1") + + console.log("deposit of scw: ", await scw.getDeposit()) + // await ep.depositTo(add, {value: ethers.parseEther("1")}); + console.log("deposit of scw: ", await scw.getDeposit()) + + console.log("here2") + + console.log("userOp", op) + await ep.handleOps([op], add); + console.log("balance of scw: ", await ethers.provider.getBalance(add)) - await ep.depositTo(add, {value: "1000000000000000000000"}); - - console.log("userOp", op) - // await ep.handleOps([op], add); }); // it("Should set the right owner", async function () {