From 621dadf84d191e9f84a79dcc870700c3293da0fb Mon Sep 17 00:00:00 2001 From: kbehouse Date: Thu, 11 May 2023 15:58:08 +0800 Subject: [PATCH 01/26] v1.3.0: init v1.3.0: init (with other requirement) --- contracts/BloctoAccount.sol | 90 ++ .../BloctoAccount4337/BloctoAccount4337.sol | 109 +++ .../BloctoAccount4337CloneableWallet.sol | 15 + contracts/BloctoAccountCloneableWallet.sol | 13 + contracts/BloctoAccountFactory.sol | 60 ++ contracts/BloctoAccountProxy.sol | 14 + .../CoreWallet/BytesExtractSignature.sol | 27 + contracts/CoreWallet/CoreWallet.sol | 919 ++++++++++++++++++ contracts/CoreWallet/README.md | 3 + contracts/TokenCallbackHandler.sol | 48 + contracts/test/TestBloctoAccountV2.sol | 29 + contracts/test/TestERC20.sol | 27 + contracts/test/TestUtil.sol | 14 + hardhat.config.ts | 15 +- package.json | 7 +- src/AASigner.ts | 411 ++++++++ src/Create2Factory.ts | 120 +++ src/runop.ts | 111 +++ test/bloctoaccount.test.ts | 160 +++ test/testutils.ts | 338 +++++++ 20 files changed, 2524 insertions(+), 6 deletions(-) create mode 100644 contracts/BloctoAccount.sol create mode 100644 contracts/BloctoAccount4337/BloctoAccount4337.sol create mode 100644 contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol create mode 100644 contracts/BloctoAccountCloneableWallet.sol create mode 100644 contracts/BloctoAccountFactory.sol create mode 100644 contracts/BloctoAccountProxy.sol create mode 100644 contracts/CoreWallet/BytesExtractSignature.sol create mode 100644 contracts/CoreWallet/CoreWallet.sol create mode 100644 contracts/CoreWallet/README.md create mode 100644 contracts/TokenCallbackHandler.sol create mode 100644 contracts/test/TestBloctoAccountV2.sol create mode 100644 contracts/test/TestERC20.sol create mode 100644 contracts/test/TestUtil.sol create mode 100644 src/AASigner.ts create mode 100644 src/Create2Factory.ts create mode 100644 src/runop.ts create mode 100644 test/bloctoaccount.test.ts create mode 100644 test/testutils.ts diff --git a/contracts/BloctoAccount.sol b/contracts/BloctoAccount.sol new file mode 100644 index 0000000..855c80f --- /dev/null +++ b/contracts/BloctoAccount.sol @@ -0,0 +1,90 @@ +// 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/proxy/utils/UUPSUpgradeable.sol"; +import "./TokenCallbackHandler.sol"; +import "./CoreWallet/CoreWallet.sol"; +// for test +import "hardhat/console.sol"; + +/** + * Blocto account. + */ +contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet { + /// @notice This is the version of this contract. + string public constant VERSION = "1.3.0"; + + //-----------------------------------------Method 1---------------------------------------------// + function _authorizeUpgrade(address newImplementation) internal view override { + (newImplementation); + require(msg.sender == address(this), "BloctoAccount: only self"); + } + + //-----------------------------------------Method 2---------------------------------------------// + // invoke cosigner check + modifier onlyInvokeCosigner( + uint8 v, + bytes32 r, + bytes32 s, + uint256 nonce, + address authorizedAddress, + bytes memory data + ) { + // check signature version + require(v == 27 || v == 28, "Invalid signature version."); + + // calculate hash + bytes32 operationHash = + keccak256(abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, nonce, authorizedAddress, data)); + + // recover signer + address signer = ecrecover(operationHash, v, r, s); + + // check for valid signature + require(signer != address(0), "Invalid signature."); + + // check nonce + require(nonce > nonces[signer], "must use valid nonce for signer"); + + // check signer + require(signer == authorizedAddress, "authorized addresses must be equal"); + + // Get cosigner + address requiredCosigner = address(authorizations[authVersion + uint256(uint160(signer))]); + + // The operation should be approved if the signer address has no cosigner (i.e. signer == cosigner) or + // if the actual cosigner matches the required cosigner. + require(requiredCosigner == signer || requiredCosigner == msg.sender, "Invalid authorization."); + + // increment nonce to prevent replay attacks + nonces[signer] = nonce; + + _; + } + + // upgrade contract by msg.sender is cosigner and sign message (v, r, s) by authorizedAddress + function invokeCosignerUpgrade( + uint8 v, + bytes32 r, + bytes32 s, + uint256 nonce, + address authorizedAddress, + address newImplementation + ) external onlyInvokeCosigner(v, r, s, nonce, authorizedAddress, abi.encodePacked(newImplementation)) { + _upgradeTo(newImplementation); + } + + //-----------------------------------------Method 3---------------------------------------------// + // modifier onlySelf() { + // require(msg.sender == address(this), "only self"); + // _; + // } + + // function upgradeTo(address newImplementation) external override onlyProxy onlySelf { + // _upgradeTo(newImplementation); + // } +} diff --git a/contracts/BloctoAccount4337/BloctoAccount4337.sol b/contracts/BloctoAccount4337/BloctoAccount4337.sol new file mode 100644 index 0000000..faec614 --- /dev/null +++ b/contracts/BloctoAccount4337/BloctoAccount4337.sol @@ -0,0 +1,109 @@ +// 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/proxy/utils/UUPSUpgradeable.sol"; +import "@account-abstraction/contracts/core/BaseAccount.sol"; + +import "../TokenCallbackHandler.sol"; +import "../CoreWallet/CoreWallet.sol"; + +/** + * Blocto account. + * compatibility for EIP-4337 and smart contract wallet with cosigner functionality (CoreWallet) + */ +contract BloctoAccount4337 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, BaseAccount { + /// @notice This is the version of this contract. + string public constant VERSION = "1.4.0"; + + IEntryPoint private _entryPoint; + + modifier onlySelf() { + require(msg.sender == address(this), "only self"); + _; + } + + // override from UUPSUpgradeable + function _authorizeUpgrade(address newImplementation) internal view override onlySelf { + (newImplementation); + require(msg.sender == address(this), "BloctoAccount: only self"); + } + + constructor(IEntryPoint anEntryPoint) { + _entryPoint = anEntryPoint; + } + /// @inheritdoc BaseAccount + + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } + + function setEntryPoint(address anEntryPoint) public onlySelf { + _entryPoint = IEntryPoint(anEntryPoint); + } + + /** + * execute a transaction (called directly by entryPoint) + */ + function execute(address dest, uint256 value, bytes calldata func) external { + _requireFromEntryPoint(); + _call(dest, value, func); + } + + /** + * execute a sequence of transactions + */ + function executeBatch(address[] calldata dest, bytes[] calldata func) external { + _requireFromEntryPoint(); + require(dest.length == func.length, "wrong array lengths"); + for (uint256 i = 0; i < dest.length; i++) { + _call(dest[i], 0, func[i]); + } + } + + /// implement template method of BaseAccount + function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + internal + virtual + override + returns (uint256 validationData) + { + bytes4 result = this.isValidSignature(userOpHash, userOp.signature); + if (result != IERC1271.isValidSignature.selector) { + 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 deposit to withdrawAddress by cosigner & authorizedAddress signature + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) external onlySelf { + entryPoint().withdrawTo(withdrawAddress, amount); + } +} diff --git a/contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol b/contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol new file mode 100644 index 0000000..1824956 --- /dev/null +++ b/contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "./BloctoAccount4337.sol"; + +/// @title BloctoAccountCloneableWallet Wallet +/// @notice This contract represents a complete but non working wallet. +contract BloctoAccount4337CloneableWallet is BloctoAccount4337 { + /// @dev An empty constructor that deploys a NON-FUNCTIONAL version + /// of `BloctoAccount` + + constructor(IEntryPoint anEntryPoint) BloctoAccount4337(anEntryPoint) { + initialized = true; + } +} diff --git a/contracts/BloctoAccountCloneableWallet.sol b/contracts/BloctoAccountCloneableWallet.sol new file mode 100644 index 0000000..a4973f2 --- /dev/null +++ b/contracts/BloctoAccountCloneableWallet.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "./BloctoAccount.sol"; + +/// @title BloctoAccountCloneableWallet Wallet +/// @notice This contract represents a complete but non working wallet. +contract BloctoAccountCloneableWallet is BloctoAccount { + /// @dev An empty constructor that deploys a NON-FUNCTIONAL version of `BloctoAccount` + constructor() { + initialized = true; + } +} diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol new file mode 100644 index 0000000..9c2090e --- /dev/null +++ b/contracts/BloctoAccountFactory.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +// import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./BloctoAccountProxy.sol"; +import "./BloctoAccount.sol"; + +// BloctoAccountFactory for creating BloctoAccountProxy +contract BloctoAccountFactory is Ownable { + /// @notice This is the version of this contract. + string public constant VERSION = "1.3.0"; + // address public accountImplementation; + address public bloctoAccountImplementation; + + event WalletCreated(address wallet, address authorizedAddress, bool full); + + constructor(address _bloctoAccountImplementation) { + bloctoAccountImplementation = _bloctoAccountImplementation; + } + + /** + * create an account, and return its address(BloctoAccount). + * 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 _authorizedAddress, address _cosigner, address _recoveryAddress, bytes32 _salt) + public + returns (BloctoAccount ret) + { + address addr = getAddress(_cosigner, _recoveryAddress, _salt); + uint256 codeSize = addr.code.length; + if (codeSize > 0) { + return BloctoAccount(payable(addr)); + } + bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); + // for consistent address + BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); + newProxy.initImplementation(bloctoAccountImplementation); + ret = BloctoAccount(payable(address(newProxy))); + ret.init(_authorizedAddress, _cosigner, _recoveryAddress); + emit WalletCreated(address(ret), _authorizedAddress, false); + } + + /** + * calculate the counterfactual address of this account as it would be returned by createAccount() + */ + function getAddress(address _cosigner, address _recoveryAddress, bytes32 _salt) public view returns (address) { + bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); + return Create2.computeAddress( + bytes32(salt), keccak256(abi.encodePacked(type(BloctoAccountProxy).creationCode, abi.encode(address(this)))) + ); + } + + function setImplementation(address _bloctoAccountImplementation) public onlyOwner { + bloctoAccountImplementation = _bloctoAccountImplementation; + } +} diff --git a/contracts/BloctoAccountProxy.sol b/contracts/BloctoAccountProxy.sol new file mode 100644 index 0000000..77a2479 --- /dev/null +++ b/contracts/BloctoAccountProxy.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +contract BloctoAccountProxy is ERC1967Proxy, Initializable { + constructor(address _logic) ERC1967Proxy(_logic, new bytes(0)) {} + + function initImplementation(address implementation) public initializer { + require(Address.isContract(implementation), "ERC1967: new implementation is not a contract"); + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = implementation; + } +} diff --git a/contracts/CoreWallet/BytesExtractSignature.sol b/contracts/CoreWallet/BytesExtractSignature.sol new file mode 100644 index 0000000..6bcb2ef --- /dev/null +++ b/contracts/CoreWallet/BytesExtractSignature.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +/// @title ECDSA is a library that contains useful methods for working with ECDSA signatures +library BytesExtractSignature { + /// @notice Extracts the r, s, and v components from the `sigData` field starting from the `offset` + /// @dev Note: does not do any bounds checking on the arguments! + /// @param sigData the signature data; could be 1 or more packed signatures. + /// @param offset the offset in sigData from which to start unpacking the signature components. + function extractSignature( + bytes memory sigData, + uint256 offset + ) internal pure returns (bytes32 r, bytes32 s, uint8 v) { + // Divide the signature in r, s and v variables + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + // solium-disable-next-line security/no-inline-assembly + assembly { + let dataPointer := add(sigData, offset) + r := mload(add(dataPointer, 0x20)) + s := mload(add(dataPointer, 0x40)) + v := byte(0, mload(add(dataPointer, 0x60))) + } + + return (r, s, v); + } +} diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol new file mode 100644 index 0000000..2a5cf62 --- /dev/null +++ b/contracts/CoreWallet/CoreWallet.sol @@ -0,0 +1,919 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "./BytesExtractSignature.sol"; +import "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/// @title Core Wallet +/// @notice A basic smart contract wallet with cosigner functionality. The notion of "cosigner" is +/// the simplest possible multisig solution, a two-of-two signature scheme. It devolves nicely +/// to "one-of-one" (i.e. singlesig) by simply having the cosigner set to the same value as +/// the main signer. +/// +/// Most "advanced" functionality (deadman's switch, multiday recovery flows, blacklisting, etc) +/// can be implemented externally to this smart contract, either as an additional smart contract +/// (which can be tracked as a signer without cosigner, or as a cosigner) or as an off-chain flow +/// using a public/private key pair as cosigner. Of course, the basic cosigning functionality could +/// also be implemented in this way, but (A) the complexity and gas cost of two-of-two multisig (as +/// implemented here) is negligable even if you don't need the cosigner functionality, and +/// (B) two-of-two multisig (as implemented here) handles a lot of really common use cases, most +/// notably third-party gas payment and off-chain blacklisting and fraud detection. +contract CoreWallet is IERC1271{ + using BytesExtractSignature for bytes; + using ECDSA for bytes32; + + /// @notice We require that presigned transactions use the EIP-191 signing format. + /// See that EIP for more info: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-191.md + bytes1 public constant EIP191_VERSION_DATA = bytes1(0); + bytes1 public constant EIP191_PREFIX = bytes1(0x19); + + /// @notice This is a sentinel value used to determine when a delegate is set to expose + /// support for an interface containing more than a single function. See `delegates` and + /// `setDelegate` for more information. + address public constant COMPOSITE_PLACEHOLDER = address(1); + + /// @notice A pre-shifted "1", used to increment the authVersion, so we can "prepend" + /// the authVersion to an address (for lookups in the authorizations mapping) + /// by using the '+' operator (which is cheaper than a shift and a mask). See the + /// comment on the `authorizations` variable for how this is used. + uint256 public constant AUTH_VERSION_INCREMENTOR = (1 << 160); + + /// @notice The pre-shifted authVersion (to get the current authVersion as an integer, + /// shift this value right by 160 bits). Starts as `1 << 160` (`AUTH_VERSION_INCREMENTOR`) + /// See the comment on the `authorizations` variable for how this is used. + uint256 public authVersion; + + /// @notice A mapping containing all of the addresses that are currently authorized to manage + /// the assets owned by this wallet. + /// + /// The keys in this mapping are authorized addresses with a version number prepended, + /// like so: (authVersion,96)(address,160). The current authVersion MUST BE included + /// for each look-up; this allows us to effectively clear the entire mapping of its + /// contents merely by incrementing the authVersion variable. (This is important for + /// the emergencyRecovery() method.) Inspired by https://ethereum.stackexchange.com/a/42540 + /// + /// The values in this mapping are 256bit words, whose lower 20 bytes constitute "cosigners" + /// for each address. If an address maps to itself, then that address is said to have no cosigner. + /// + /// The upper 12 bytes are reserved for future meta-data purposes. The meta-data could refer + /// to the key (authorized address) or the value (cosigner) of the mapping. + /// + /// Addresses that map to a non-zero cosigner in the current authVersion are called + /// "authorized addresses". + mapping(uint256 => address) public authorizations; + + /// @notice A per-key nonce value, incremented each time a transaction is processed with that key. + /// Used for replay prevention. The nonce value in the transaction must exactly equal the current + /// nonce value in the wallet for that key. (This mirrors the way Ethereum's transaction nonce works.) + mapping(address => uint256) public nonces; + + /// @notice A mapping tracking dynamically supported interfaces and their corresponding + /// implementation contracts. Keys are interface IDs and values are addresses of + /// contracts that are responsible for implementing the function corresponding to the + /// interface. + /// + /// Delegates are added (or removed) via the `setDelegate` method after the contract is + /// deployed, allowing support for new interfaces to be dynamically added after deployment. + /// When a delegate is added, its interface ID is considered "supported" under EIP165. + /// + /// For cases where an interface composed of more than a single function must be + /// supported, it is necessary to manually add the composite interface ID with + /// `setDelegate(interfaceId, COMPOSITE_PLACEHOLDER)`. Interface IDs added with the + /// COMPOSITE_PLACEHOLDER address are ignored when called and are only used to specify + /// supported interfaces. + mapping(bytes4 => address) public delegates; + + /// @notice A special address that is authorized to call `emergencyRecovery()`. That function + /// resets ALL authorization for this wallet, and must therefore be treated with utmost security. + /// Reasonable choices for recoveryAddress include: + /// - the address of a private key in cold storage + /// - a physically secured hardware wallet + /// - a multisig smart contract, possibly with a time-delayed challenge period + /// - the zero address, if you like performing without a safety net ;-) + address public recoveryAddress; + + /// @notice Used to track whether or not this contract instance has been initialized. This + /// is necessary since it is common for this wallet smart contract to be used as the "library + /// code" for an clone contract. See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1167.md + /// for more information about clone contracts. + bool public initialized; + + /// @notice Used to decorate methods that can only be called directly by the recovery address. + modifier onlyRecoveryAddress() { + require( + msg.sender == recoveryAddress, + "sender must be recovery address" + ); + _; + } + + /// @notice Used to decorate the `init` function so this can only be called one time. Necessary + /// since this contract will often be used as a "clone". (See above.) + modifier onlyOnce() { + require(!initialized, "must not already be initialized"); + initialized = true; + _; + } + + /// @notice Used to decorate methods that can only be called indirectly via an `invoke()` method. + /// In practice, it means that those methods can only be called by a signer/cosigner + /// pair that is currently authorized. Theoretically, we could factor out the + /// signer/cosigner verification code and use it explicitly in this modifier, but that + /// would either result in duplicated code, or additional overhead in the invoke() + /// calls (due to the stack manipulation for calling into the shared verification function). + /// Doing it this way makes calling the administration functions more expensive (since they + /// go through a explicit call() instead of just branching within the contract), but it + /// makes invoke() more efficient. We assume that invoke() will be used much, much more often + /// than any of the administration functions. + modifier onlyInvoked() { + require(msg.sender == address(this), "must be called from `invoke()`"); + _; + } + + /// @notice Emitted when an authorized address is added, removed, or modified. When an + /// authorized address is removed ("deauthorized"), cosigner will be address(0) in + /// this event. + /// + /// NOTE: When emergencyRecovery() is called, all existing addresses are deauthorized + /// WITHOUT Authorized(addr, 0) being emitted. If you are keeping an off-chain mirror of + /// authorized addresses, you must also watch for EmergencyRecovery events. + /// @dev hash is 0xf5a7f4fb8a92356e8c8c4ae7ac3589908381450500a7e2fd08c95600021ee889 + /// @param authorizedAddress the address to authorize or unauthorize + /// @param cosigner the 2-of-2 signatory (optional). + event Authorized(address authorizedAddress, address cosigner); + + /// @notice Emitted when an emergency recovery has been performed. If this event is fired, + /// ALL previously authorized addresses have been deauthorized and the only authorized + /// address is the authorizedAddress indicated in this event. + /// @dev hash is 0xe12d0bbeb1d06d7a728031056557140afac35616f594ef4be227b5b172a604b5 + /// @param authorizedAddress the new authorized address + /// @param cosigner the cosigning address for `authorizedAddress` + event EmergencyRecovery(address authorizedAddress, address cosigner); + + /// @notice Emitted when the recovery address changes. Either (but not both) of the + /// parameters may be zero. + /// @dev hash is 0x568ab3dedd6121f0385e007e641e74e1f49d0fa69cab2957b0b07c4c7de5abb6 + /// @param previousRecoveryAddress the previous recovery address + /// @param newRecoveryAddress the new recovery address + event RecoveryAddressChanged( + address previousRecoveryAddress, + address newRecoveryAddress + ); + + /// @dev Emitted when this contract receives a non-zero amount ether via the fallback function + /// (i.e. This event is not fired if the contract receives ether as part of a method invocation) + /// @param from the address which sent you ether + /// @param value the amount of ether sent + event Received(address from, uint value); + + /// @notice Emitted whenever a transaction is processed successfully from this wallet. Includes + /// both simple send ether transactions, as well as other smart contract invocations. + /// @dev hash is 0x101214446435ebbb29893f3348e3aae5ea070b63037a3df346d09d3396a34aee + /// @param hash The hash of the entire operation set. 0 is returned when emitted from `invoke0()`. + /// @param result A bitfield of the results of the operations. A bit of 0 means success, and 1 means failure. + /// @param numOperations A count of the number of operations processed + event InvocationSuccess( + bytes32 hash, + uint256 result, + uint256 numOperations + ); + + /// @notice Emitted when a delegate is added or removed. + /// @param interfaceId The interface ID as specified by EIP165 + /// @param delegate The address of the contract implementing the given function. If this is + /// COMPOSITE_PLACEHOLDER, we are indicating support for a composite interface. + event DelegateUpdated(bytes4 interfaceId, address delegate); + + /// @notice The shared initialization code used to setup the contract state regardless of whether or + /// not the clone pattern is being used. + /// @param _authorizedAddress the initial authorized address, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + function init( + address _authorizedAddress, + address _cosigner, + address _recoveryAddress + ) public onlyOnce { + require( + _authorizedAddress != _recoveryAddress, + "Do not use the recovery address as an authorized address." + ); + require( + _cosigner != _recoveryAddress, + "Do not use the recovery address as a cosigner." + ); + require( + _authorizedAddress != address(0), + "Authorized addresses must not be zero." + ); + require(_cosigner != address(0), "Initial cosigner must not be zero."); + + recoveryAddress = _recoveryAddress; + // set initial authorization value + authVersion = AUTH_VERSION_INCREMENTOR; + // add initial authorized address + authorizations[ + authVersion + uint256(uint160(_authorizedAddress)) + ] = _cosigner; + + emit Authorized(_authorizedAddress, _cosigner); + } + + function bytesToAddresses( + bytes memory bys + ) private pure returns (address[] memory addresses) { + addresses = new address[](bys.length / 20); + for (uint i = 0; i < bys.length; i += 20) { + address addr; + uint end = i + 20; + assembly { + addr := mload(add(bys, end)) + } + addresses[i / 20] = addr; + } + } + + function init2( + bytes memory _authorizedAddresses, + address _cosigner, + address _recoveryAddress + ) public onlyOnce { + address[] memory addresses = bytesToAddresses(_authorizedAddresses); + for (uint i = 0; i < addresses.length; i++) { + address _authorizedAddress = addresses[i]; + require( + _authorizedAddress != _recoveryAddress, + "Do not use the recovery address as an authorized address." + ); + require( + _cosigner != _recoveryAddress, + "Do not use the recovery address as a cosigner." + ); + require( + _authorizedAddress != address(0), + "Authorized addresses must not be zero." + ); + require( + _cosigner != address(0), + "Initial cosigner must not be zero." + ); + + recoveryAddress = _recoveryAddress; + // set initial authorization value + authVersion = AUTH_VERSION_INCREMENTOR; + // add initial authorized address + authorizations[ + authVersion + uint256(uint160(_authorizedAddress)) + ] = _cosigner; + + emit Authorized(_authorizedAddress, _cosigner); + } + } + + /// @notice The fallback function, invoked whenever we receive a transaction that doesn't call any of our + /// named functions. In particular, this method is called when we are the target of a simple send + /// transaction, when someone calls a method we have dynamically added a delegate for, or when someone + /// tries to call a function we don't implement, either statically or dynamically. + /// + /// A correct invocation of this method occurs in two cases: + /// - someone transfers ETH to this wallet (`msg.data.length` is 0) + /// - someone calls a delegated function (`msg.data.length` is greater than 0 and + /// `delegates[msg.sig]` is set) + /// In all other cases, this function will revert. + /// + /// NOTE: Some smart contracts send 0 eth as part of a more complex operation + /// (-cough- CryptoKitties -cough-); ideally, we'd `require(msg.value > 0)` here when + /// `msg.data.length == 0`, but to work with those kinds of smart contracts, we accept zero sends + /// and just skip logging in that case. + fallback() external payable { + if (msg.value > 0) { + emit Received(msg.sender, msg.value); + } + if (msg.data.length > 0) { + address delegate = delegates[msg.sig]; + require(delegate > COMPOSITE_PLACEHOLDER, "Invalid transaction"); + + // We have found a delegate contract that is responsible for the method signature of + // this call. Now, pass along the calldata of this CALL to the delegate contract. + assembly { + calldatacopy(0, 0, calldatasize()) + let result := staticcall( + gas(), + delegate, + 0, + calldatasize(), + 0, + 0 + ) + returndatacopy(0, 0, returndatasize()) + + // If the delegate reverts, we revert. If the delegate does not revert, we return the data + // returned by the delegate to the original caller. + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } + } + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} + + /// @notice Adds or removes dynamic support for an interface. Can be used in 3 ways: + /// - Add a contract "delegate" that implements a single function + /// - Remove delegate for a function + /// - Specify that an interface ID is "supported", without adding a delegate. This is + /// used for composite interfaces when the interface ID is not a single method ID. + /// @dev Must be called through `invoke` + /// @param _interfaceId The ID of the interface we are adding support for + /// @param _delegate Either: + /// - the address of a contract that implements the function specified by `_interfaceId` + /// for adding an implementation for a single function + /// - 0 for removing an existing delegate + /// - COMPOSITE_PLACEHOLDER for specifying support for a composite interface + function setDelegate( + bytes4 _interfaceId, + address _delegate + ) external onlyInvoked { + delegates[_interfaceId] = _delegate; + emit DelegateUpdated(_interfaceId, _delegate); + } + + /// @notice Configures an authorizable address. Can be used in four ways: + /// - Add a new signer/cosigner pair (cosigner must be non-zero) + /// - Set or change the cosigner for an existing signer (if authorizedAddress != cosigner) + /// - Remove the cosigning requirement for a signer (if authorizedAddress == cosigner) + /// - Remove a signer (if cosigner == address(0)) + /// @dev Must be called through `invoke()` + /// @param _authorizedAddress the address to configure authorization + /// @param _cosigner the corresponding cosigning address + function setAuthorized( + address _authorizedAddress, + address _cosigner + ) external onlyInvoked { + // TODO: Allowing a signer to remove itself is actually pretty terrible; it could result in the user + // removing their only available authorized key. Unfortunately, due to how the invocation forwarding + // works, we don't actually _know_ which signer was used to call this method, so there's no easy way + // to prevent this. + + // TODO: Allowing the backup key to be set as an authorized address bypasses the recovery mechanisms. + // Dapper can prevent this with offchain logic and the cosigner, but it would be nice to have + // this enforced by the smart contract logic itself. + + require( + _authorizedAddress != address(0), + "Authorized addresses must not be zero." + ); + require( + _authorizedAddress != recoveryAddress, + "Do not use the recovery address as an authorized address." + ); + require( + _cosigner == address(0) || _cosigner != recoveryAddress, + "Do not use the recovery address as a cosigner." + ); + + authorizations[ + authVersion + uint256(uint160(_authorizedAddress)) + ] = _cosigner; + emit Authorized(_authorizedAddress, _cosigner); + } + + /// @notice Performs an emergency recovery operation, removing all existing authorizations and setting + /// a sole new authorized address with optional cosigner. THIS IS A SCORCHED EARTH SOLUTION, and great + /// care should be taken to ensure that this method is never called unless it is a last resort. See the + /// comments above about the proper kinds of addresses to use as the recoveryAddress to ensure this method + /// is not trivially abused. + /// @param _authorizedAddress the new and sole authorized address + /// @param _cosigner the corresponding cosigner address, can be equal to _authorizedAddress + function emergencyRecovery( + address _authorizedAddress, + address _cosigner + ) external onlyRecoveryAddress { + require( + _authorizedAddress != address(0), + "Authorized addresses must not be zero." + ); + require( + _authorizedAddress != recoveryAddress, + "Do not use the recovery address as an authorized address." + ); + require(_cosigner != address(0), "The cosigner must not be zero."); + + // Incrementing the authVersion number effectively erases the authorizations mapping. See the comments + // on the authorizations variable (above) for more information. + authVersion += AUTH_VERSION_INCREMENTOR; + + // Store the new signer/cosigner pair as the only remaining authorized address + authorizations[ + authVersion + uint256(uint160(_authorizedAddress)) + ] = _cosigner; + emit EmergencyRecovery(_authorizedAddress, _cosigner); + } + + function emergencyRecovery2( + address _authorizedAddress, + address _cosigner, + address _recoveryAddress + ) external onlyRecoveryAddress { + require( + _authorizedAddress != address(0), + "Authorized addresses must not be zero." + ); + require( + _authorizedAddress != _recoveryAddress, + "Do not use the recovery address as an authorized address." + ); + require(_cosigner != address(0), "The cosigner must not be zero."); + + // Incrementing the authVersion number effectively erases the authorizations mapping. See the comments + // on the authorizations variable (above) for more information. + authVersion += AUTH_VERSION_INCREMENTOR; + + // Store the new signer/cosigner pair as the only remaining authorized address + authorizations[ + authVersion + uint256(uint160(_authorizedAddress)) + ] = _cosigner; + + // set new recovery address + address previous = recoveryAddress; + recoveryAddress = _recoveryAddress; + + emit RecoveryAddressChanged(previous, recoveryAddress); + emit EmergencyRecovery(_authorizedAddress, _cosigner); + } + + /// @notice Sets the recovery address, which can be zero (indicating that no recovery is possible) + /// Can be updated by any authorized address. This address should be set with GREAT CARE. See the + /// comments above about the proper kinds of addresses to use as the recoveryAddress to ensure this + /// mechanism is not trivially abused. + /// @dev Must be called through `invoke()` + /// @param _recoveryAddress the new recovery address + function setRecoveryAddress(address _recoveryAddress) external onlyInvoked { + require( + address( + authorizations[authVersion + uint256(uint160(_recoveryAddress))] + ) == address(0), + "Do not use an authorized address as the recovery address." + ); + + address previous = recoveryAddress; + recoveryAddress = _recoveryAddress; + + emit RecoveryAddressChanged(previous, recoveryAddress); + } + + /// @notice Allows ANY caller to recover gas by way of deleting old authorization keys after + /// a recovery operation. Anyone can call this method to delete the old unused storage and + /// get themselves a bit of gas refund in the bargin. + /// @dev keys must be known to caller or else nothing is refunded + /// @param _version the version of the mapping which you want to delete (unshifted) + /// @param _keys the authorization keys to delete + function recoverGas(uint256 _version, address[] calldata _keys) external { + // TODO: should this be 0xffffffffffffffffffffffff ? + require( + _version > 0 && _version < 0xffffffff, + "Invalid version number." + ); + + uint256 shiftedVersion = _version << 160; + + require( + shiftedVersion < authVersion, + "You can only recover gas from expired authVersions." + ); + + for (uint256 i = 0; i < _keys.length; ++i) { + delete ( + authorizations[shiftedVersion + uint256(uint160(_keys[i]))] + ); + } + } + + /// @notice Should return whether the signature provided is valid for the provided data + /// See https://github.com/ethereum/EIPs/issues/1271 + /// @dev This function meets the following conditions as per the EIP: + /// MUST return the bytes4 magic value `0x1626ba7e` when function passes. + /// MUST NOT modify state (using `STATICCALL` for solc < 0.5, `view` modifier for solc > 0.5) + /// MUST allow external calls + /// @param hash A 32 byte hash of the signed data. The actual hash that is hashed however is the + /// the following tightly packed arguments: `0x19,0x0,wallet_address,hash` + /// @param _signature Signature byte array associated with `_data` + /// @return Magic value `0x1626ba7e` upon success, 0 otherwise. + function isValidSignature( + bytes32 hash, + bytes calldata _signature + ) external view returns (bytes4) { + // We 'hash the hash' for the following reasons: + // 1. `hash` is not the hash of an Ethereum transaction + // 2. signature must target this wallet to avoid replaying the signature for another wallet + // with the same key + // 3. Gnosis does something similar: + // https://github.com/gnosis/safe-contracts/blob/102e632d051650b7c4b0a822123f449beaf95aed/contracts/GnosisSafe.sol + + bytes32 operationHash = keccak256( + abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, hash) + ); + + bytes32[2] memory r; + bytes32[2] memory s; + uint8[2] memory v; + address signer; + address cosigner; + + // extract 1 or 2 signatures depending on length + if (_signature.length == 65) { + (r[0], s[0], v[0]) = _signature.extractSignature(0); + signer = ecrecover(operationHash, v[0], r[0], s[0]); + cosigner = signer; + } else if (_signature.length == 130) { + (r[0], s[0], v[0]) = _signature.extractSignature(0); + (r[1], s[1], v[1]) = _signature.extractSignature(65); + signer = ecrecover(operationHash, v[0], r[0], s[0]); + cosigner = ecrecover(operationHash, v[1], r[1], s[1]); + } else { + return 0; + } + + // check for valid signature + if (signer == address(0)) { + return 0; + } + + // check for valid signature + if (cosigner == address(0)) { + return 0; + } + + // check to see if this is an authorized key + if ( + address(authorizations[authVersion + uint256(uint160(signer))]) != + cosigner + ) { + return 0; + } + + return IERC1271.isValidSignature.selector; + } + + /// @notice Query if this contract implements an interface. This function takes into account + /// interfaces we implement dynamically through delegates. For interfaces that are just a + /// single method, using `setDelegate` will result in that method's ID returning true from + /// `supportsInterface`. For composite interfaces that are composed of multiple functions, it is + /// necessary to add the interface ID manually with `setDelegate(interfaceID, + /// COMPOSITE_PLACEHOLDER)` + /// IN ADDITION to adding each function of the interface as usual. + /// @param interfaceID The interface identifier, as specified in ERC-165 + /// @dev Interface identification is specified in ERC-165. This function + /// uses less than 30,000 gas. + /// @return `true` if the contract implements `interfaceID` and + /// `interfaceID` is not 0xffffffff, `false` otherwise + // function supportsInterface( + // bytes4 interfaceID + // ) external view returns (bool) { + // // First check if the ID matches one of the interfaces we support statically. + // if ( + // interfaceID == this.supportsInterface.selector || // ERC165 + // // interfaceID == ERC721_RECEIVED_FINAL || // ERC721 Final + // // interfaceID == ERC721_RECEIVED_DRAFT || // ERC721 Draft + // interfaceID == ERC223_ID // ERC223 + // // interfaceID == ERC1155_TOKEN_RECIEVER || // ERC1155 Token Reciever + // // interfaceID == ERC1271_VALIDSIGNATURE // ERC1271 + // ) { + // return true; + // } + // // If we don't support the interface statically, check whether we have added + // // dynamic support for it. + // return uint256(delegates[interfaceID]) > 0; + // } + + /// @notice A version of `invoke()` that has no explicit signatures, and uses msg.sender + /// as both the signer and cosigner. Will only succeed if `msg.sender` is an authorized + /// signer for this wallet, with no cosigner, saving transaction size and gas in that case. + /// @param data The data containing the transactions to be invoked; see internalInvoke for details. + function invoke0(bytes calldata data) external { + // The nonce doesn't need to be incremented for transactions that don't include explicit signatures; + // the built-in nonce of the native ethereum transaction will protect against replay attacks, and we + // can save the gas that would be spent updating the nonce variable + + // The operation should be approved if the signer address has no cosigner (i.e. signer == cosigner) + require( + address( + authorizations[authVersion + uint256(uint160(msg.sender))] + ) == msg.sender, + "Invalid authorization." + ); + + internalInvoke(0, data); + } + + /// @notice A version of `invoke()` that has one explicit signature which is used to derive the authorized + /// address. Uses `msg.sender` as the cosigner. + /// @param v the v value for the signature; see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md + /// @param r the r value for the signature + /// @param s the s value for the signature + /// @param nonce the nonce value for the signature + /// @param authorizedAddress the address of the authorization key; this is used here so that cosigner signatures are interchangeable + /// between this function and `invoke2()` + /// @param data The data containing the transactions to be invoked; see internalInvoke for details. + function invoke1CosignerSends( + uint8 v, + bytes32 r, + bytes32 s, + uint256 nonce, + address authorizedAddress, + bytes calldata data + ) external { + // check signature version + require(v == 27 || v == 28, "Invalid signature version."); + + // calculate hash + bytes32 operationHash = keccak256( + abi.encodePacked( + EIP191_PREFIX, + EIP191_VERSION_DATA, + this, + nonce, + authorizedAddress, + data + ) + ); + + // recover signer + address signer = ecrecover(operationHash, v, r, s); + + // check for valid signature + require(signer != address(0), "Invalid signature."); + + // check nonce + require(nonce > nonces[signer], "must use valid nonce for signer"); + + // check signer + require( + signer == authorizedAddress, + "authorized addresses must be equal" + ); + + // Get cosigner + address requiredCosigner = address( + authorizations[authVersion + uint256(uint160(signer))] + ); + + // The operation should be approved if the signer address has no cosigner (i.e. signer == cosigner) or + // if the actual cosigner matches the required cosigner. + require( + requiredCosigner == signer || requiredCosigner == msg.sender, + "Invalid authorization." + ); + + // increment nonce to prevent replay attacks + nonces[signer] = nonce; + + // call internal function + internalInvoke(operationHash, data); + } + + /// @notice A version of `invoke()` that has one explicit signature which is used to derive the cosigning + /// address. Uses `msg.sender` as the authorized address. + /// @param v the v value for the signature; see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md + /// @param r the r value for the signature + /// @param s the s value for the signature + /// @param data The data containing the transactions to be invoked; see internalInvoke for details. + function invoke1SignerSends( + uint8 v, + bytes32 r, + bytes32 s, + bytes calldata data + ) external { + // check signature version + // `ecrecover` will in fact return 0 if given invalid + // so perhaps this check is redundant + require(v == 27 || v == 28, "Invalid signature version."); + + uint256 nonce = nonces[msg.sender]; + + // calculate hash + bytes32 operationHash = keccak256( + abi.encodePacked( + EIP191_PREFIX, + EIP191_VERSION_DATA, + this, + nonce, + msg.sender, + data + ) + ); + + // recover cosigner + address cosigner = ecrecover(operationHash, v, r, s); + + // check for valid signature + require(cosigner != address(0), "Invalid signature."); + + // Get required cosigner + address requiredCosigner = address( + authorizations[authVersion + uint256(uint160(msg.sender))] + ); + + // The operation should be approved if the signer address has no cosigner (i.e. signer == cosigner) or + // if the actual cosigner matches the required cosigner. + require( + requiredCosigner == cosigner || requiredCosigner == msg.sender, + "Invalid authorization." + ); + + // increment nonce to prevent replay attacks + nonces[msg.sender] = nonce + 1; + + internalInvoke(operationHash, data); + } + + /// @notice A version of `invoke()` that has two explicit signatures, the first is used to derive the authorized + /// address, the second to derive the cosigner. The value of `msg.sender` is ignored. + /// @param v the v values for the signatures + /// @param r the r values for the signatures + /// @param s the s values for the signatures + /// @param nonce the nonce value for the signature + /// @param authorizedAddress the address of the signer; forces the signature to be unique and tied to the signers nonce + /// @param data The data containing the transactions to be invoked; see internalInvoke for details. + function invoke2( + uint8[2] calldata v, + bytes32[2] calldata r, + bytes32[2] calldata s, + uint256 nonce, + address authorizedAddress, + bytes calldata data + ) external { + // check signature versions + // `ecrecover` will infact return 0 if given invalid + // so perhaps these checks are redundant + require(v[0] == 27 || v[0] == 28, "invalid signature version v[0]"); + require(v[1] == 27 || v[1] == 28, "invalid signature version v[1]"); + + bytes32 operationHash = keccak256( + abi.encodePacked( + EIP191_PREFIX, + EIP191_VERSION_DATA, + this, + nonce, + authorizedAddress, + data + ) + ); + + // recover signer and cosigner + address signer = ecrecover(operationHash, v[0], r[0], s[0]); + address cosigner = ecrecover(operationHash, v[1], r[1], s[1]); + + // check for valid signatures + require(signer != address(0), "Invalid signature for signer."); + require(cosigner != address(0), "Invalid signature for cosigner."); + + // check signer address + require( + signer == authorizedAddress, + "authorized addresses must be equal" + ); + + // check nonces + require(nonce > nonces[signer], "must use valid nonce for signer"); + + // Get Mapping + address requiredCosigner = address( + authorizations[authVersion + uint256(uint160(signer))] + ); + + // The operation should be approved if the signer address has no cosigner (i.e. signer == cosigner) or + // if the actual cosigner matches the required cosigner. + require( + requiredCosigner == signer || requiredCosigner == cosigner, + "Invalid authorization." + ); + + // increment nonce to prevent replay attacks + nonces[signer] = nonce; + + internalInvoke(operationHash, data); + } + + /// @dev Internal invoke call, + /// @param operationHash The hash of the operation + /// @param data The data to send to the `call()` operation + /// The data is prefixed with a global 1 byte revert flag + /// If revert is 1, then any revert from a `call()` operation is rethrown. + /// Otherwise, the error is recorded in the `result` field of the `InvocationSuccess` event. + /// Immediately following the revert byte (no padding), the data format is then is a series + /// of 1 or more tightly packed tuples: + /// `` + /// If `datalength == 0`, the data field must be omitted + function internalInvoke(bytes32 operationHash, bytes memory data) internal { + // keep track of the number of operations processed + uint256 numOps; + // keep track of the result of each operation as a bit + uint256 result; + + // We need to store a reference to this string as a variable so we can use it as an argument to + // the revert call from assembly. + string memory invalidLengthMessage = "Data field too short"; + string memory callFailed = "Call failed"; + + // At an absolute minimum, the data field must be at least 85 bytes + // + require(data.length >= 85, invalidLengthMessage); + + // Forward the call onto its actual target. Note that the target address can be `self` here, which is + // actually the required flow for modifying the configuration of the authorized keys and recovery address. + // + // The assembly code below loads data directly from memory, so the enclosing function must be marked `internal` + assembly { + // A cursor pointing to the revert flag, starts after the length field of the data object + let memPtr := add(data, 32) + + // The revert flag is the leftmost byte from memPtr + let revertFlag := byte(0, mload(memPtr)) + + // A pointer to the end of the data object + let endPtr := add(memPtr, mload(data)) + + // Now, memPtr is a cursor pointing to the beginning of the current sub-operation + memPtr := add(memPtr, 1) + + // Loop through data, parsing out the various sub-operations + for { + + } lt(memPtr, endPtr) { + + } { + // Load the length of the call data of the current operation + // 52 = to(20) + value(32) + let len := mload(add(memPtr, 52)) + + // Compute a pointer to the end of the current operation + // 84 = to(20) + value(32) + size(32) + let opEnd := add(len, add(memPtr, 84)) + + // Bail if the current operation's data overruns the end of the enclosing data buffer + // NOTE: Comment out this bit of code and uncomment the next section if you want + // the solidity-coverage tool to work. + // See https://github.com/sc-forks/solidity-coverage/issues/287 + if gt(opEnd, endPtr) { + // The computed end of this operation goes past the end of the data buffer. Not good! + revert( + add(invalidLengthMessage, 32), + mload(invalidLengthMessage) + ) + } + // NOTE: Code that is compatible with solidity-coverage + // switch gt(opEnd, endPtr) + // case 1 { + // revert(add(invalidLengthMessage, 32), mload(invalidLengthMessage)) + // } + + // This line of code packs in a lot of functionality! + // - load the target address from memPtr, the address is only 20-bytes but mload always grabs 32-bytes, + // so we have to shr by 12 bytes. + // - load the value field, stored at memPtr+20 + // - pass a pointer to the call data, stored at memPtr+84 + // - use the previously loaded len field as the size of the call data + // - make the call (passing all remaining gas to the child call) + // - check the result (0 == reverted) + if eq( + 0, + call( + gas(), + shr(96, mload(memPtr)), + mload(add(memPtr, 20)), + add(memPtr, 84), + len, + 0, + 0 + ) + ) { + switch revertFlag + case 1 { + revert(add(callFailed, 32), mload(callFailed)) + } + default { + // mark this operation as failed + // create the appropriate bit, 'or' with previous + result := or(result, exp(2, numOps)) + } + } + + // increment our counter + numOps := add(numOps, 1) + + // Update mem pointer to point to the next sub-operation + memPtr := opEnd + } + } + + // emit single event upon success + emit InvocationSuccess(operationHash, result, numOps); + } +} diff --git a/contracts/CoreWallet/README.md b/contracts/CoreWallet/README.md new file mode 100644 index 0000000..345f1ea --- /dev/null +++ b/contracts/CoreWallet/README.md @@ -0,0 +1,3 @@ +## Overview + +Fork CoreWallet from https://github.com/dapperlabs/dapper-contracts & https://github.com/portto/evm-contract-wallet diff --git a/contracts/TokenCallbackHandler.sol b/contracts/TokenCallbackHandler.sol new file mode 100644 index 0000000..60e0210 --- /dev/null +++ b/contracts/TokenCallbackHandler.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +/* solhint-disable no-empty-blocks */ + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; + +/** + * Token callback handler. + * Handles supported tokens' callbacks, allowing account receiving these tokens. + */ +contract TokenCallbackHandler is IERC777Recipient, IERC721Receiver, IERC1155Receiver { + function tokensReceived(address, address, address, uint256, bytes calldata, bytes calldata) + external + pure + override + {} + + function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } + + function onERC1155Received(address, address, uint256, uint256, bytes calldata) + external + pure + override + returns (bytes4) + { + return IERC1155Receiver.onERC1155Received.selector; + } + + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + pure + override + returns (bytes4) + { + return IERC1155Receiver.onERC1155BatchReceived.selector; + } + + function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) { + return interfaceId == type(IERC721Receiver).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId + || interfaceId == type(IERC165).interfaceId; + } +} diff --git a/contracts/test/TestBloctoAccountV2.sol b/contracts/test/TestBloctoAccountV2.sol new file mode 100644 index 0000000..3a4d665 --- /dev/null +++ b/contracts/test/TestBloctoAccountV2.sol @@ -0,0 +1,29 @@ +// 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 "../TokenCallbackHandler.sol"; +import "../CoreWallet/CoreWallet.sol"; + +/** + * Blocto account. + * compatibility for EIP-4337 and smart contract wallet with cosigner functionality (CoreWallet) + */ +contract TestBloctoAccountV2 is CoreWallet, UUPSUpgradeable, Initializable { + /// @notice This is the version of this contract. + string public constant VERSION = "1.3.1"; + + // override from UUPSUpgradeable, to prevent upgrades from this method + function _authorizeUpgrade(address newImplementation) internal pure override { + (newImplementation); + require(false, "BloctoAccount: cannot upgrade from _authorizeUpgrade"); + } +} diff --git a/contracts/test/TestERC20.sol b/contracts/test/TestERC20.sol new file mode 100644 index 0000000..a783f1e --- /dev/null +++ b/contracts/test/TestERC20.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; + +contract TestERC20 is ERC20, ERC20Burnable, AccessControl { + uint8 private immutable _decimals; + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) { + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setupRole(MINTER_ROLE, msg.sender); + + _decimals = decimals_; + } + + function mint(address to, uint256 amount) public { + require(hasRole(MINTER_ROLE, msg.sender)); + _mint(to, amount); + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } +} diff --git a/contracts/test/TestUtil.sol b/contracts/test/TestUtil.sol new file mode 100644 index 0000000..be6d4f9 --- /dev/null +++ b/contracts/test/TestUtil.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0 +// from: https://github.com/eth-infinitism/account-abstraction/tree/develop/contracts/test +pragma solidity ^0.8.12; + +import "@account-abstraction/contracts/interfaces/UserOperation.sol"; + +contract TestUtil { + using UserOperationLib for UserOperation; + + function packUserOp(UserOperation calldata op) external pure returns (bytes memory){ + return op.pack(); + } + +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 259b823..d887c41 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -33,6 +33,14 @@ function getNetwork (name: string): { url: string, accounts: { mnemonic: string // return getNetwork1(`wss://${name}.infura.io/ws/v3/${process.env.INFURA_ID}`) } +const optimizedComilerSettings = { + version: '0.8.17', + settings: { + optimizer: { enabled: true, runs: 1000000 }, + viaIR: true + } +} + // You need to export an object to set up your config // Go to https://hardhat.org/config/ to learn more @@ -43,7 +51,12 @@ const config: HardhatUserConfig = { settings: { optimizer: { enabled: true, runs: 1000000 } } - }] + }], + overrides: { + 'contracts/core/EntryPoint.sol': optimizedComilerSettings, + 'contracts/BloctoAccountCloneableWallet.sol': optimizedComilerSettings, + 'contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol': optimizedComilerSettings + } }, networks: { dev: { url: 'http://localhost:8545' }, diff --git a/package.json b/package.json index a344a7a..eece71d 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,7 @@ "lint:js": "eslint -f unix .", "lint-fix": "eslint -f unix . --fix", "lint:sol": "solhint -f unix \"contracts/**/*.sol\" --max-warnings 0", - "test": "hardhat test ", - "deploy-accountfactory": "hardhat run deploy/deploy-BloctoAccountFactory.ts", - "verify-accountfactory": "npx hardhat verify 0xf8915e6896fb575a95246793b150c56e2666da11 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 ", - "verify-verifyingpaymaster": "npx hardhat verify 0x214cC14b7312855c7F5FF156757B2eeeB938048D 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 0xec53Efb202a4427bae100776BA920A5938E9d509" + "test": "hardhat test" }, "devDependencies": { "@account-abstraction/contracts": "^0.6.0", @@ -58,4 +55,4 @@ "table": "^6.8.0", "typescript": "^4.3.5" } -} +} \ No newline at end of file diff --git a/src/AASigner.ts b/src/AASigner.ts new file mode 100644 index 0000000..8cee2fc --- /dev/null +++ b/src/AASigner.ts @@ -0,0 +1,411 @@ +import { BigNumber, Bytes, ethers, Event, Signer } from 'ethers' +import { zeroAddress } from 'ethereumjs-util' +import { BaseProvider, Provider, TransactionRequest } from '@ethersproject/providers' +import { Deferrable, resolveProperties } from '@ethersproject/properties' +import { + EntryPoint, + EntryPoint__factory, + ERC1967Proxy__factory, + SimpleAccount, + SimpleAccount__factory +} from '../typechain' +import { BytesLike, hexValue } from '@ethersproject/bytes' +import { TransactionResponse } from '@ethersproject/abstract-provider' +import { fillAndSign, getUserOpHash } from '../test/UserOp' +import { UserOperation } from '../test/UserOperation' +import { TransactionReceipt } from '@ethersproject/abstract-provider/src.ts/index' +import { clearInterval } from 'timers' +import { Create2Factory } from './Create2Factory' +import { getCreate2Address, hexConcat, Interface, keccak256 } from 'ethers/lib/utils' +import { HashZero } from '../test/testutils' + +export type SendUserOp = (userOp: UserOperation) => Promise + +export const debug = process.env.DEBUG != null + +/** + * send a request using rpc. + * + * @param provider - rpc provider that supports "eth_sendUserOperation" + */ +export function rpcUserOpSender (provider: ethers.providers.JsonRpcProvider, entryPointAddress: string): SendUserOp { + let chainId: number + + return async function (userOp) { + if (debug) { + console.log('sending eth_sendUserOperation', { + ...userOp, + initCode: (userOp.initCode ?? '').length, + callData: (userOp.callData ?? '').length + }, entryPointAddress) + } + if (chainId === undefined) { + chainId = await provider.getNetwork().then(net => net.chainId) + } + + const cleanUserOp = Object.keys(userOp).map(key => { + let val = (userOp as any)[key] + if (typeof val !== 'string' || !val.startsWith('0x')) { + val = hexValue(val) + } + return [key, val] + }) + .reduce((set, [k, v]) => ({ ...set, [k]: v }), {}) + await provider.send('eth_sendUserOperation', [cleanUserOp, entryPointAddress]).catch(e => { + throw e.error ?? e + }) + return undefined + } +} + +interface QueueSendUserOp extends SendUserOp { + lastQueueUpdate: number + queueSize: number + queue: { [sender: string]: UserOperation[] } + push: () => Promise + setInterval: (intervalMs: number) => void + cancelInterval: () => void + + _cancelInterval: any +} + +/** + * a SendUserOp that queue requests. need to call sendQueuedUserOps to create a bundle and send them. + * the returned object handles the queue of userops and also interval control. + */ +export function queueUserOpSender (entryPointAddress: string, signer: Signer, intervalMs = 3000): QueueSendUserOp { + const entryPoint = EntryPoint__factory.connect(entryPointAddress, signer) + + const ret = async function (userOp: UserOperation) { + if (ret.queue[userOp.sender] == null) { + ret.queue[userOp.sender] = [] + } + ret.queue[userOp.sender].push(userOp) + ret.lastQueueUpdate = Date.now() + ret.queueSize++ + } as QueueSendUserOp + + ret.queue = {} + ret.push = async function () { + await sendQueuedUserOps(ret, entryPoint) + } + ret.setInterval = function (intervalMs: number) { + ret.cancelInterval() + // eslint-disable-next-line @typescript-eslint/no-misused-promises + ret._cancelInterval = setInterval(ret.push, intervalMs) + } + ret.cancelInterval = function () { + if (ret._cancelInterval != null) { + clearInterval(ret._cancelInterval) + ret._cancelInterval = null + } + } + + if (intervalMs != null) { + ret.setInterval(intervalMs) + } + + return ret +} + +/** + * create a bundle from the queue and send it to the entrypoint. + * NOTE: only a single request from a given sender can be put into a bundle. + * @param queue + * @param entryPoint + */ + +let sending = false + +// after that much time with no new TX, send whatever you can. +const IDLE_TIME = 5000 + +// when reaching this theshold, don't wait anymore and send a bundle +const BUNDLE_SIZE_IMMEDIATE = 3 + +async function sendQueuedUserOps (queueSender: QueueSendUserOp, entryPoint: EntryPoint): Promise { + if (sending) { + console.log('sending in progress. waiting') + return + } + sending = true + try { + if (queueSender.queueSize < BUNDLE_SIZE_IMMEDIATE || queueSender.lastQueueUpdate + IDLE_TIME > Date.now()) { + console.log('queue too small/too young. waiting') + return + } + const ops: UserOperation[] = [] + const queue = queueSender.queue + Object.keys(queue).forEach(sender => { + const op = queue[sender].shift() + if (op != null) { + ops.push(op) + queueSender.queueSize-- + } + }) + if (ops.length === 0) { + console.log('no ops to send') + return + } + const signer = await (entryPoint.provider as any).getSigner().getAddress() + console.log('==== sending batch of ', ops.length) + const ret = await entryPoint.handleOps(ops, signer, { maxPriorityFeePerGas: 2e9 }) + console.log('handleop tx=', ret.hash) + const rcpt = await ret.wait() + console.log('events=', rcpt.events!.map(e => ({ name: e.event, args: e.args }))) + } finally { + sending = false + } +} + +/** + * send UserOp using handleOps, but locally. + * for testing: instead of connecting through RPC to a remote host, directly send the transaction + * @param entryPointAddress the entryPoint address to use. + * @param signer ethers provider to send the request (must have eth balance to send) + * @param beneficiary the account to receive the payment (from account/paymaster). defaults to the signer's address + */ +export function localUserOpSender (entryPointAddress: string, signer: Signer, beneficiary?: string): SendUserOp { + const entryPoint = EntryPoint__factory.connect(entryPointAddress, signer) + return async function (userOp) { + if (debug) { + console.log('sending', { + ...userOp, + initCode: userOp.initCode.length <= 2 ? userOp.initCode : `` + }) + } + const gasLimit = BigNumber.from(userOp.preVerificationGas).add(userOp.verificationGasLimit).add(userOp.callGasLimit) + console.log('calc gaslimit=', gasLimit.toString()) + const ret = await entryPoint.handleOps([userOp], beneficiary ?? await signer.getAddress(), { + maxPriorityFeePerGas: userOp.maxPriorityFeePerGas, + maxFeePerGas: userOp.maxFeePerGas + }) + await ret.wait() + return undefined + } +} + +export class AAProvider extends BaseProvider { + private readonly entryPoint: EntryPoint + + constructor (entryPointAddress: string, provider: Provider) { + super(provider.getNetwork()) + this.entryPoint = EntryPoint__factory.connect(entryPointAddress, provider) + } +} + +/** + * a signer that wraps account-abstraction. + */ +export class AASigner extends Signer { + _account?: SimpleAccount + + private _isPhantom = true + public entryPoint: EntryPoint + + private _chainId: Promise | undefined + + /** + * create account abstraction signer + * @param signer - the underlying signer. has no funds (=can't send TXs) + * @param entryPoint the entryPoint contract. used for read-only operations + * @param sendUserOp function to actually send the UserOp to the entryPoint. + * @param index - index of this account for this signer. + */ + constructor (readonly signer: Signer, readonly entryPointAddress: string, readonly sendUserOp: SendUserOp, readonly index = 0, readonly provider = signer.provider) { + super() + this.entryPoint = EntryPoint__factory.connect(entryPointAddress, signer) + } + + // connect to a specific pre-deployed address + // (note: in order to send transactions, the underlying signer address must be valid signer for this account (its owner) + async connectAccountAddress (address: string): Promise { + if (this._account != null) { + throw Error('already connected to account') + } + if (await this.provider!.getCode(address).then(code => code.length) <= 2) { + throw new Error('cannot connect to non-existing contract') + } + this._account = SimpleAccount__factory.connect(address, this.signer) + this._isPhantom = false + } + + connect (provider: Provider): Signer { + throw new Error('connect not implemented') + } + + async _deploymentAddress (): Promise { + return getCreate2Address(Create2Factory.contractAddress, HashZero, keccak256(await this._deploymentTransaction())) + } + + // TODO TODO: THERE IS UTILS.getAccountInitCode - why not use that? + async _deploymentTransaction (): Promise { + const implementationAddress = zeroAddress() // TODO: pass implementation in here + const ownerAddress = await this.signer.getAddress() + const initializeCall = new Interface(SimpleAccount__factory.abi).encodeFunctionData('initialize', [ownerAddress]) + return new ERC1967Proxy__factory(this.signer).getDeployTransaction(implementationAddress, initializeCall).data! + } + + async getAddress (): Promise { + await this.syncAccount() + return this._account!.address + } + + async signMessage (message: Bytes | string): Promise { + throw new Error('signMessage: unsupported by AA') + } + + async signTransaction (transaction: Deferrable): Promise { + throw new Error('signMessage: unsupported by AA') + } + + async getAccount (): Promise { + await this.syncAccount() + return this._account! + } + + // fabricate a response in a format usable by ethers users... + async userEventResponse (userOp: UserOperation): Promise { + const entryPoint = this.entryPoint + const userOpHash = getUserOpHash(userOp, entryPoint.address, await this._chainId!) + const provider = entryPoint.provider + const currentBLock = provider.getBlockNumber() + + let resolved = false + const waitPromise = new Promise((resolve, reject) => { + let listener = async function (this: any, ...param: any): Promise { + if (resolved) return + const event = arguments[arguments.length - 1] as Event + if (event.blockNumber <= await currentBLock) { + // not sure why this callback is called first for previously-mined block.. + console.log('ignore previous block', event.blockNumber) + return + } + if (event.args == null) { + console.error('got event without args', event) + return + } + if (event.args.userOpHash !== userOpHash) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions,@typescript-eslint/no-base-to-string + console.log(`== event with wrong userOpHash: sender/nonce: event.${event.args.sender}@${event.args.nonce.toString()}!= userOp.${userOp.sender}@${parseInt(userOp.nonce.toString())}`) + return + } + + const rcpt = await event.getTransactionReceipt() + console.log('got event with status=', event.args.success, 'gasUsed=', rcpt.gasUsed) + + // TODO: should use "userOpHash" as "transactionId" (but this has to be done in a provider, not a signer) + + // before returning the receipt, update the status from the event. + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!event.args.success) { + console.log('mark tx as failed') + rcpt.status = 0 + const revertReasonEvents = await entryPoint.queryFilter(entryPoint.filters.UserOperationRevertReason(userOp.sender), rcpt.blockHash) + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (revertReasonEvents[0]) { + console.log('rejecting with reason') + reject(new Error(`UserOp failed with reason: ${revertReasonEvents[0].args.revertReason}`) + ) + return + } + } + // eslint-disable-next-line @typescript-eslint/no-misused-promises + entryPoint.off('UserOperationEvent', listener) + resolve(rcpt) + resolved = true + } + listener = listener.bind(listener) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + entryPoint.on('UserOperationEvent', listener) + // for some reason, 'on' takes at least 2 seconds to be triggered on local network. so add a one-shot timer: + // eslint-disable-next-line @typescript-eslint/no-misused-promises + setTimeout(async () => await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(userOpHash)).then(query => { + if (query.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + listener(query[0]) + } + }), 500) + }) + const resp: TransactionResponse = { + hash: userOpHash, + confirmations: 0, + from: userOp.sender, + nonce: BigNumber.from(userOp.nonce).toNumber(), + gasLimit: BigNumber.from(userOp.callGasLimit), // ?? + value: BigNumber.from(0), + data: hexValue(userOp.callData), // should extract the actual called method from this "execFromSingleton()" call + chainId: await this._chainId!, + wait: async function (confirmations?: number): Promise { + return await waitPromise + } + } + return resp + } + + async sendTransaction (transaction: Deferrable): Promise { + const userOp = await this._createUserOperation(transaction) + // get response BEFORE sending request: the response waits for events, which might be triggered before the actual send returns. + const reponse = await this.userEventResponse(userOp) + await this.sendUserOp(userOp) + return reponse + } + + async syncAccount (): Promise { + if (this._account == null) { + const address = await this._deploymentAddress() + this._account = SimpleAccount__factory.connect(address, this.signer) + } + + this._chainId = this.provider?.getNetwork().then(net => net.chainId) + // once an account is deployed, it can no longer be a phantom. + // but until then, we need to re-check + if (this._isPhantom) { + const size = await this.signer.provider?.getCode(this._account.address).then(x => x.length) + // console.log(`== __isPhantom. addr=${this._account.address} re-checking code size. result = `, size) + this._isPhantom = size === 2 + // !await this.entryPoint.isContractDeployed(await this.getAddress()); + } + } + + // return true if account not yet created. + async isPhantom (): Promise { + await this.syncAccount() + return this._isPhantom + } + + async _createUserOperation (transaction: Deferrable): Promise { + const tx: TransactionRequest = await resolveProperties(transaction) + await this.syncAccount() + + let initCode: BytesLike | undefined + if (this._isPhantom) { + const initCallData = new Create2Factory(this.provider!).getDeployTransactionCallData(hexValue(await this._deploymentTransaction()), HashZero) + + initCode = hexConcat([ + Create2Factory.contractAddress, + initCallData + ]) + } + const execFromEntryPoint = await this._account!.populateTransaction.execute(tx.to!, tx.value ?? 0, tx.data!) + + let { gasPrice, maxPriorityFeePerGas, maxFeePerGas } = tx + // gasPrice is legacy, and overrides eip1559 values: + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (gasPrice) { + maxPriorityFeePerGas = gasPrice + maxFeePerGas = gasPrice + } + const userOp = await fillAndSign({ + sender: this._account!.address, + initCode, + nonce: initCode == null ? tx.nonce : this.index, + callData: execFromEntryPoint.data!, + callGasLimit: tx.gasLimit, + maxPriorityFeePerGas, + maxFeePerGas + }, this.signer, this.entryPoint) + + return userOp + } +} diff --git a/src/Create2Factory.ts b/src/Create2Factory.ts new file mode 100644 index 0000000..b0e78cf --- /dev/null +++ b/src/Create2Factory.ts @@ -0,0 +1,120 @@ +// from: https://github.com/Arachnid/deterministic-deployment-proxy +import { BigNumber, BigNumberish, ethers, Signer } from 'ethers' +import { arrayify, hexConcat, hexlify, hexZeroPad, keccak256 } from 'ethers/lib/utils' +import { Provider } from '@ethersproject/providers' +import { TransactionRequest } from '@ethersproject/abstract-provider' + +export class Create2Factory { + factoryDeployed = false + + // from: https://github.com/Arachnid/deterministic-deployment-proxy + static readonly contractAddress = '0x4e59b44847b379578588920ca78fbf26c0b4956c' + static readonly factoryTx = '0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222' + static readonly factoryDeployer = '0x3fab184622dc19b6109349b94811493bf2a45362' + static readonly deploymentGasPrice = 100e9 + static readonly deploymentGasLimit = 100000 + static readonly factoryDeploymentFee = (Create2Factory.deploymentGasPrice * Create2Factory.deploymentGasLimit).toString() + + constructor (readonly provider: Provider, + readonly signer = (provider as ethers.providers.JsonRpcProvider).getSigner()) { + } + + /** + * deploy a contract using our deterministic deployer. + * The deployer is deployed (unless it is already deployed) + * NOTE: this transaction will fail if already deployed. use getDeployedAddress to check it first. + * @param initCode delpoyment code. can be a hex string or factory.getDeploymentTransaction(..) + * @param salt specific salt for deployment + * @param gasLimit gas limit or 'estimate' to use estimateGas. by default, calculate gas based on data size. + */ + async deploy (initCode: string | TransactionRequest, salt: BigNumberish = 0, gasLimit?: BigNumberish | 'estimate'): Promise { + await this.deployFactory() + if (typeof initCode !== 'string') { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + initCode = (initCode as TransactionRequest).data!.toString() + } + + const addr = Create2Factory.getDeployedAddress(initCode, salt) + if (await this.provider.getCode(addr).then(code => code.length) > 2) { + return addr + } + + const deployTx = { + to: Create2Factory.contractAddress, + data: this.getDeployTransactionCallData(initCode, salt) + } + if (gasLimit === 'estimate') { + gasLimit = await this.signer.estimateGas(deployTx) + } + + // manual estimation (its bit larger: we don't know actual deployed code size) + if (gasLimit === undefined) { + gasLimit = arrayify(initCode) + .map(x => x === 0 ? 4 : 16) + .reduce((sum, x) => sum + x) + + 200 * initCode.length / 2 + // actual is usually somewhat smaller (only deposited code, not entire constructor) + 6 * Math.ceil(initCode.length / 64) + // hash price. very minor compared to deposit costs + 32000 + + 21000 + + // deployer requires some extra gas + gasLimit = Math.floor(gasLimit * 64 / 63) + } + + const ret = await this.signer.sendTransaction({ ...deployTx, gasLimit }) + await ret.wait() + if (await this.provider.getCode(addr).then(code => code.length) === 2) { + throw new Error('failed to deploy') + } + return addr + } + + getDeployTransactionCallData (initCode: string, salt: BigNumberish = 0): string { + const saltBytes32 = hexZeroPad(hexlify(salt), 32) + return hexConcat([ + saltBytes32, + initCode + ]) + } + + /** + * return the deployed address of this code. + * (the deployed address to be used by deploy() + * @param initCode + * @param salt + */ + static getDeployedAddress (initCode: string, salt: BigNumberish): string { + const saltBytes32 = hexZeroPad(hexlify(salt), 32) + return '0x' + keccak256(hexConcat([ + '0xff', + Create2Factory.contractAddress, + saltBytes32, + keccak256(initCode) + ])).slice(-40) + } + + // deploy the factory, if not already deployed. + async deployFactory (signer?: Signer): Promise { + if (await this._isFactoryDeployed()) { + return + } + await (signer ?? this.signer).sendTransaction({ + to: Create2Factory.factoryDeployer, + value: BigNumber.from(Create2Factory.factoryDeploymentFee) + }) + await this.provider.sendTransaction(Create2Factory.factoryTx) + if (!await this._isFactoryDeployed()) { + throw new Error('fatal: failed to deploy deterministic deployer') + } + } + + async _isFactoryDeployed (): Promise { + if (!this.factoryDeployed) { + const deployed = await this.provider.getCode(Create2Factory.contractAddress) + if (deployed.length > 2) { + this.factoryDeployed = true + } + } + return this.factoryDeployed + } +} diff --git a/src/runop.ts b/src/runop.ts new file mode 100644 index 0000000..d9fd0b2 --- /dev/null +++ b/src/runop.ts @@ -0,0 +1,111 @@ +// run a single op +// "yarn run runop [--network ...]" + +import hre, { ethers } from 'hardhat' +import { objdump } from '../test/testutils' +import { AASigner, localUserOpSender, rpcUserOpSender } from './AASigner' +import { TestCounter__factory, EntryPoint__factory } from '../typechain' +import '../test/aa.init' +import { parseEther } from 'ethers/lib/utils' +import { providers } from 'ethers' +import { TransactionReceipt } from '@ethersproject/abstract-provider/src.ts/index'; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { + console.log('net=', hre.network.name) + const aa_url = process.env.AA_URL + + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (aa_url == null && !process.env.FORCE_DEPLOY) { + await hre.run('deploy') + const chainId = await hre.getChainId() + if (chainId.match(/1337/) == null) { + console.log('chainid=', chainId) + await hre.run('etherscan-verify') + } + } + const [entryPointAddress, testCounterAddress] = await Promise.all([ + hre.deployments.get('EntryPoint').then(d => d.address), + hre.deployments.get('TestCounter').then(d => d.address) + ]) + + console.log('entryPointAddress:', entryPointAddress, 'testCounterAddress:', testCounterAddress) + const provider = ethers.provider + const ethersSigner = provider.getSigner(0) + const prefundAccountAddress = await ethersSigner.getAddress() + const prefundAccountBalance = await provider.getBalance(prefundAccountAddress) + console.log('using prefund account address', prefundAccountAddress, 'with balance', prefundAccountBalance.toString()) + + let sendUserOp + + if (aa_url != null) { + const newprovider = new providers.JsonRpcProvider(aa_url) + sendUserOp = rpcUserOpSender(newprovider, entryPointAddress) + const supportedEntryPoints: string[] = await newprovider.send('eth_supportedEntryPoints', []).then(ret => ret.map(ethers.utils.getAddress)) + console.log('node supported EntryPoints=', supportedEntryPoints) + if (!supportedEntryPoints.includes(entryPointAddress)) { + console.error('ERROR: node', aa_url, 'does not support our EntryPoint') + } + } else { sendUserOp = localUserOpSender(entryPointAddress, ethersSigner) } + + // index is unique for an account (so same owner can have multiple accounts, with different index + const index = parseInt(process.env.AA_INDEX ?? '0') + console.log('using account index (AA_INDEX)', index) + const aasigner = new AASigner(ethersSigner, entryPointAddress, sendUserOp, index) + // connect to pre-deployed account + // await aasigner.connectAccountAddress(accountAddress) + const myAddress = await aasigner.getAddress() + if (await provider.getBalance(myAddress) < parseEther('0.01')) { + console.log('prefund account') + await ethersSigner.sendTransaction({ to: myAddress, value: parseEther('0.01') }) + } + + // usually, an account will deposit for itself (that is, get created using eth, run "addDeposit" for itself + // and from there on will use deposit + // for testing, + const entryPoint = EntryPoint__factory.connect(entryPointAddress, ethersSigner) + console.log('account address=', myAddress) + let preDeposit = await entryPoint.balanceOf(myAddress) + console.log('current deposit=', preDeposit, 'current balance', await provider.getBalance(myAddress)) + + if (preDeposit.lte(parseEther('0.005'))) { + console.log('depositing for account') + await entryPoint.depositTo(myAddress, { value: parseEther('0.01') }) + preDeposit = await entryPoint.balanceOf(myAddress) + } + + const testCounter = TestCounter__factory.connect(testCounterAddress, aasigner) + + const prebalance = await provider.getBalance(myAddress) + console.log('balance=', prebalance.div(1e9).toString(), 'deposit=', preDeposit.div(1e9).toString()) + console.log('estimate direct call', { gasUsed: await testCounter.connect(ethersSigner).estimateGas.justemit().then(t => t.toNumber()) }) + const ret = await testCounter.justemit() + console.log('waiting for mine, hash (reqId)=', ret.hash) + const rcpt = await ret.wait() + const netname = await provider.getNetwork().then(net => net.name) + if (netname !== 'unknown') { + console.log('rcpt', rcpt.transactionHash, `https://dashboard.tenderly.co/tx/${netname}/${rcpt.transactionHash}/gas-usage`) + } + const gasPaid = prebalance.sub(await provider.getBalance(myAddress)) + const depositPaid = preDeposit.sub(await entryPoint.balanceOf(myAddress)) + console.log('paid (from balance)=', gasPaid.toNumber() / 1e9, 'paid (from deposit)', depositPaid.div(1e9).toString(), 'gasUsed=', rcpt.gasUsed) + const logs = await entryPoint.queryFilter('*' as any, rcpt.blockNumber) + console.log(logs.map((e: any) => ({ ev: e.event, ...objdump(e.args!) }))) + console.log('1st run gas used:', await evInfo(rcpt)) + + const ret1 = await testCounter.justemit() + const rcpt2 = await ret1.wait() + console.log('2nd run:', await evInfo(rcpt2)) + + async function evInfo (rcpt: TransactionReceipt): Promise { + // TODO: checking only latest block... + const block = rcpt.blockNumber + const ev = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(), block) + // if (ev.length === 0) return {} + return ev.map(event => { + const { nonce, actualGasUsed } = event.args + const gasUsed = rcpt.gasUsed.toNumber() + return { nonce: nonce.toNumber(), gasPaid, gasUsed: gasUsed, diff: gasUsed - actualGasUsed.toNumber() } + }) + } +})() diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts new file mode 100644 index 0000000..16705a5 --- /dev/null +++ b/test/bloctoaccount.test.ts @@ -0,0 +1,160 @@ +import { ethers } from 'hardhat' +import { Wallet, BigNumber } from 'ethers' +import { expect } from 'chai' +import { + BloctoAccount, + BloctoAccount__factory, + BloctoAccountCloneableWallet, + BloctoAccountCloneableWallet__factory, + BloctoAccount4337CloneableWallet__factory, + BloctoAccountFactory, + BloctoAccountFactory__factory, + TestERC20, + TestERC20__factory, + TestBloctoAccountV2, + TestBloctoAccountV2__factory +} from '../typechain' +import { EntryPoint } from '@account-abstraction/contracts' +import { + fund, + createAccount, + createAddress, + createAccountOwner, + deployEntryPoint, + getBalance, + isDeployed, + ONE_ETH, + TWO_ETH, + HashZero, + createAuthorizedCosignerRecoverWallet, + getSetEntryPointCode, + txData, + signMessage, + signUpgrade +} from './testutils' +// import { fillUserOpDefaults, getUserOpHash, signMessage, signUpgrade } from './UserOp' + +describe('BloctoAccount Upgrade Test', function () { + const ethersSigner = ethers.provider.getSigner() + + let authorizedWallet: Wallet + let cosignerWallet: Wallet + let recoverWallet: Wallet + + let implementation: string + let factory: BloctoAccountFactory + + let entryPoint: EntryPoint + + let erc20: TestERC20 + + async function testCreateAccount (salt: string): Promise { + const account = await createAccount( + ethersSigner, + await authorizedWallet.getAddress(), + await cosignerWallet.getAddress(), + await recoverWallet.getAddress(), + salt, + factory + ) + await fund(account) + return account + } + + before(async function () { + // v1 implementation + implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy()).address + + // account factory + factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation); + + // 3 wallet + [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() + await fund(cosignerWallet.address) + + // test erc20 + erc20 = await new TestERC20__factory(ethersSigner).deploy('Test ERC20', 'T20', 18) + + // 4337 + entryPoint = await deployEntryPoint() + }) + + describe('should upgrade with method2 invokeCosignerUpgrade', () => { + // random value for account + const AccountSalt = '0x4eb84e5765b53776863ffa7c4965af012ded5be4000000000000000000000001' + let account: BloctoAccount + let implementationV2: TestBloctoAccountV2 + + async function upgradeAccountToImplementationV2 (): Promise { + const authorizeInAccountNonce = (await account.nonces(authorizedWallet.address)).add(1) + const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosignerWallet) + const sign = await signUpgrade(authorizedWallet, account.address, authorizeInAccountNonce, implementationV2.address) + await accountLinkCosigner.invokeCosignerUpgrade(sign.v, sign.r, sign.s, authorizeInAccountNonce, authorizedWallet.address, implementationV2.address) + } + + before(async () => { + account = await testCreateAccount(AccountSalt) + implementationV2 = await new TestBloctoAccountV2__factory(ethersSigner).deploy() + }) + + it('version check', async () => { + expect(await account.VERSION()).to.eql('1.3.0') + await upgradeAccountToImplementationV2() + expect(await account.VERSION()).to.eql('1.3.1') + }) + }) + describe('should upgrade to 4337 with method1 upgradeTo', () => { + const AccountSalt = '0x4eb84e5765b53776863ffa7c4965af012ded5be4000000000000000000000002' + let account: BloctoAccount + let implementation4337: BloctoAccountCloneableWallet + + async function upgradeAccountToImplementation4337 (): Promise { + const authorizeInAccountNonce = (await account.nonces(authorizedWallet.address)).add(1) + const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosignerWallet) + const upgradeToData = txData(1, account.address, BigNumber.from(0), + account.interface.encodeFunctionData('upgradeTo', [implementation4337.address])) + + const sign = await signMessage(authorizedWallet, account.address, authorizeInAccountNonce, upgradeToData) + await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, authorizeInAccountNonce, authorizedWallet.address, upgradeToData) + } + + before(async () => { + account = await testCreateAccount(AccountSalt) + implementation4337 = await new BloctoAccount4337CloneableWallet__factory(ethersSigner).deploy(entryPoint.address) + await factory.setImplementation(implementation4337.address) + }) + + it('upgrade fail if not by contract self', async () => { + // upgrade revert even though upgrade by cosigner + await expect(account.connect(cosignerWallet).upgradeTo(implementation4337.address)) + .to.revertedWith('BloctoAccount: only self') + }) + + it('upgrade test', async () => { + expect(await account.VERSION()).to.eql('1.3.0') + await upgradeAccountToImplementation4337() + expect(await account.VERSION()).to.eql('1.4.0') + }) + + it('factory getAddress some be same', async () => { + const addrFromFacotry = await factory.getAddress( + await cosignerWallet.getAddress(), + await recoverWallet.getAddress(), + AccountSalt) + expect(addrFromFacotry).to.eql(account.address) + }) + + it('new account get new version', async () => { + const randomSalt = '0x33384e5765b53776863ffa7c4965af012ded5be4000000000000000000000005' + const accountNew = await createAccount( + ethersSigner, + await authorizedWallet.getAddress(), + await cosignerWallet.getAddress(), + await recoverWallet.getAddress(), + randomSalt, + factory + ) + expect(await accountNew.VERSION()).to.eql('1.4.0') + }) + }) +}) diff --git a/test/testutils.ts b/test/testutils.ts new file mode 100644 index 0000000..158cadf --- /dev/null +++ b/test/testutils.ts @@ -0,0 +1,338 @@ +import { ethers } from 'hardhat' +import { + arrayify, + hexConcat, + keccak256, + parseEther, + hexlify +} from 'ethers/lib/utils' +import { BigNumber, BigNumberish, Contract, ContractReceipt, Signer, Wallet } from 'ethers' +import { + IERC20, + BloctoAccount, + BloctoAccount__factory, + BloctoAccountFactory +} from '../typechain' + +import { EntryPoint, EntryPoint__factory } from '@account-abstraction/contracts' + +import { Bytes, BytesLike, hexZeroPad, concat } from '@ethersproject/bytes' +import { toUtf8Bytes } from '@ethersproject/strings' + +import { expect } from 'chai' +import { Create2Factory } from '../src/Create2Factory' + +export const AddressZero = ethers.constants.AddressZero +export const HashZero = ethers.constants.HashZero +export const ONE_ETH = parseEther('1') +export const TWO_ETH = parseEther('2') +export const FIVE_ETH = parseEther('5') + +export const tostr = (x: any): string => x != null ? x.toString() : 'null' + +export function tonumber (x: any): number { + try { + return parseFloat(x.toString()) + } catch (e: any) { + console.log('=== failed to parseFloat:', x, (e).message) + return NaN + } +} + +// just throw 1eth from account[0] to the given address (or contract instance) +export async function fund (contractOrAddress: string | Contract, amountEth = '1'): Promise { + let address: string + if (typeof contractOrAddress === 'string') { + address = contractOrAddress + } else { + address = contractOrAddress.address + } + await ethers.provider.getSigner().sendTransaction({ to: address, value: parseEther(amountEth) }) +} + +export async function getBalance (address: string): Promise { + const balance = await ethers.provider.getBalance(address) + return parseInt(balance.toString()) +} + +export async function getTokenBalance (token: IERC20, address: string): Promise { + const balance = await token.balanceOf(address) + return parseInt(balance.toString()) +} + +let counter = 0 + +export function createTmpAccount (): Wallet { + const privateKey = keccak256(Buffer.from(arrayify(BigNumber.from(++counter)))) + return new ethers.Wallet(privateKey, ethers.provider) + // return new ethers.Wallet('0x'.padEnd(66, privkeyBase), ethers.provider); +} + +// create non-random account, so gas calculations are deterministic +export function createAuthorizedCosignerRecoverWallet (): [Wallet, Wallet, Wallet] { + return [createTmpAccount(), createTmpAccount(), createTmpAccount()] +} + +export function createAddress (): string { + return createAccountOwner().address +} + +export function callDataCost (data: string): number { + return ethers.utils.arrayify(data) + .map(x => x === 0 ? 4 : 16) + .reduce((sum, x) => sum + x) +} + +export async function calcGasUsage (rcpt: ContractReceipt, entryPoint: EntryPoint, beneficiaryAddress?: string): Promise<{ actualGasCost: BigNumberish }> { + const actualGas = await rcpt.gasUsed + const logs = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(), rcpt.blockHash) + const { actualGasCost, actualGasUsed } = logs[0].args + console.log('\t== actual gasUsed (from tx receipt)=', actualGas.toString()) + console.log('\t== calculated gasUsed (paid to beneficiary)=', actualGasUsed) + const tx = await ethers.provider.getTransaction(rcpt.transactionHash) + console.log('\t== gasDiff', actualGas.toNumber() - actualGasUsed.toNumber() - callDataCost(tx.data)) + if (beneficiaryAddress != null) { + expect(await getBalance(beneficiaryAddress)).to.eq(actualGasCost.toNumber()) + } + return { actualGasCost } +} + +// helper function to create the initCode to deploy the account, using our account factory. +export function getAccountInitCode (authorizedAddress: string, cosignerAddress: string, recoveryAddress: string, factory: BloctoAccountFactory, salt = 0): BytesLike { + return hexConcat([ + factory.address, + factory.interface.encodeFunctionData('createAccount', [authorizedAddress, cosignerAddress, recoveryAddress, salt]) + ]) +} + +// given the parameters as AccountDeployer, return the resulting "counterfactual address" that it would create. +export async function getAccountAddress (authorizedAddress: string, cosignerAddress: string, recoveryAddress: string, + factory: BloctoAccountFactory, salt = 0): Promise { + return await factory.getAddress(authorizedAddress, cosignerAddress, recoveryAddress, salt) +} + +const panicCodes: { [key: number]: string } = { + // from https://docs.soliditylang.org/en/v0.8.0/control-structures.html + 0x01: 'assert(false)', + 0x11: 'arithmetic overflow/underflow', + 0x12: 'divide by zero', + 0x21: 'invalid enum value', + 0x22: 'storage byte array that is incorrectly encoded', + 0x31: '.pop() on an empty array.', + 0x32: 'array sout-of-bounds or negative index', + 0x41: 'memory overflow', + 0x51: 'zero-initialized variable of internal function type' +} + +// rethrow "cleaned up" exception. +// - stack trace goes back to method (or catch) line, not inner provider +// - attempt to parse revert data (needed for geth) +// use with ".catch(rethrow())", so that current source file/line is meaningful. +export function rethrow (): (e: Error) => void { + const callerStack = new Error().stack!.replace(/Error.*\n.*at.*\n/, '').replace(/.*at.* \(internal[\s\S]*/, '') + + if (arguments[0] != null) { + throw new Error('must use .catch(rethrow()), and NOT .catch(rethrow)') + } + return function (e: Error) { + const solstack = e.stack!.match(/((?:.* at .*\.sol.*\n)+)/) + const stack = (solstack != null ? solstack[1] : '') + callerStack + // const regex = new RegExp('error=.*"data":"(.*?)"').compile() + const found = /error=.*?"data":"(.*?)"/.exec(e.message) + let message: string + if (found != null) { + const data = found[1] + message = decodeRevertReason(data) ?? e.message + ' - ' + data.slice(0, 100) + } else { + message = e.message + } + const err = new Error(message) + err.stack = 'Error: ' + message + '\n' + stack + throw err + } +} + +export function decodeRevertReason (data: string, nullIfNoMatch = true): string | null { + const methodSig = data.slice(0, 10) + const dataParams = '0x' + data.slice(10) + + if (methodSig === '0x08c379a0') { + const [err] = ethers.utils.defaultAbiCoder.decode(['string'], dataParams) + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `Error(${err})` + } else if (methodSig === '0x00fa072b') { + const [opindex, paymaster, msg] = ethers.utils.defaultAbiCoder.decode(['uint256', 'address', 'string'], dataParams) + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `FailedOp(${opindex}, ${paymaster !== AddressZero ? paymaster : 'none'}, ${msg})` + } else if (methodSig === '0x4e487b71') { + const [code] = ethers.utils.defaultAbiCoder.decode(['uint256'], dataParams) + return `Panic(${panicCodes[code] ?? code} + ')` + } + if (!nullIfNoMatch) { + return data + } + return null +} + +let currentNode: string = '' + +// basic geth support +// - by default, has a single account. our code needs more. +export async function checkForGeth (): Promise { + // @ts-ignore + const provider = ethers.provider._hardhatProvider + + currentNode = await provider.request({ method: 'web3_clientVersion' }) + + console.log('node version:', currentNode) + // NOTE: must run geth with params: + // --http.api personal,eth,net,web3 + // --allow-insecure-unlock + if (currentNode.match(/geth/i) != null) { + for (let i = 0; i < 2; i++) { + const acc = await provider.request({ method: 'personal_newAccount', params: ['pass'] }).catch(rethrow) + await provider.request({ method: 'personal_unlockAccount', params: [acc, 'pass'] }).catch(rethrow) + await fund(acc, '10') + } + } +} + +// remove "array" members, convert values to strings. +// so Result obj like +// { '0': "a", '1': 20, first: "a", second: 20 } +// becomes: +// { first: "a", second: "20" } +export function objdump (obj: { [key: string]: any }): any { + return Object.keys(obj) + .filter(key => key.match(/^[\d_]/) == null) + .reduce((set, key) => ({ + ...set, + [key]: decodeRevertReason(obj[key].toString(), false) + }), {}) +} +/** + * process exception of ValidationResult + * usage: entryPoint.simulationResult(..).catch(simulationResultCatch) + */ +export function simulationResultCatch (e: any): any { + if (e.errorName !== 'ValidationResult') { + throw e + } + return e.errorArgs +} + +/** + * process exception of ValidationResultWithAggregation + * usage: entryPoint.simulationResult(..).catch(simulationResultWithAggregation) + */ +export function simulationResultWithAggregationCatch (e: any): any { + if (e.errorName !== 'ValidationResultWithAggregation') { + throw e + } + return e.errorArgs +} + +export async function deployEntryPoint (provider = ethers.provider): Promise { + const create2factory = new Create2Factory(provider) + const epf = new EntryPoint__factory(provider.getSigner()) + const addr = await create2factory.deploy(epf.bytecode, 0, process.env.COVERAGE != null ? 20e6 : 8e6) + return EntryPoint__factory.connect(addr, provider.getSigner()) +} + +export async function isDeployed (addr: string): Promise { + const code = await ethers.provider.getCode(addr) + return code.length > 2 +} + +// Deploys an implementation and a proxy pointing to this implementation +export async function createAccount ( + ethersSigner: Signer, + authorizedAddresses: string, + cosignerAddresses: string, + recoverAddresses: string, + salt: BytesLike, + accountFactory: BloctoAccountFactory +): Promise { + await accountFactory.createAccount(authorizedAddresses, cosignerAddresses, recoverAddresses, salt) + const accountAddress = await accountFactory.getAddress(cosignerAddresses, recoverAddresses, salt) + const account = BloctoAccount__factory.connect(accountAddress, ethersSigner) + return account +} + +// helper function to create the setEntryPointCode to set the account entryPoint address +export function getSetEntryPointCode (account: BloctoAccount, entryPointAddress: string): BytesLike { + return hexConcat([ + account.address, + account.interface.encodeFunctionData('setEntryPoint', [entryPointAddress]) + ]) +} + +// txData from https://github.com/dapperlabs/dapper-contracts/blob/master/test/wallet-utils.js +export const txData = (revert: number, to: string, amount: BigNumber, dataBuff: string): Uint8Array => { + // revert_flag (1), to (20), value (32), data length (32), data + const dataArr = [] + const revertBuff = Buffer.alloc(1) + // don't revert for now + revertBuff.writeUInt8(revert) + dataArr.push(revertBuff) + // 'to' is not padded (20 bytes) + dataArr.push(Buffer.from(to.replace('0x', ''), 'hex')) // address as string + // value (32 bytes) + dataArr.push(hexZeroPad(amount.toHexString(), 32)) + // data length (0) + // dataArr.push(utils.numToBuffer(dataBuff.length)) + const hex = Buffer.from(dataBuff.replace('0x', ''), 'hex') + dataArr.push(hexZeroPad(hexlify(hex.length), 32)) + if (hex.length > 0) { + dataArr.push(hex) + } + + return concat(dataArr) +} + +export const EIP191V0MessagePrefix = '\x19\x00' +export function hashMessageEIP191V0 (address: string, message: Bytes | string): string { + if (typeof (message) === 'string') { + message = toUtf8Bytes(message) + } + address = address.replace('0x', '') + + // const tx = concat([ + // toUtf8Bytes(EIP191V0MessagePrefix), + // Uint8Array.from(Buffer.from(address, 'hex')), + // message + // ]) + // console.log('hashMessageEIP191V0 tx', tx) + + return keccak256(concat([ + toUtf8Bytes(EIP191V0MessagePrefix), + Uint8Array.from(Buffer.from(address, 'hex')), + message + ])) +} + +export async function signUpgrade (signerWallet: Wallet, accountAddress: string, nonce: BigNumber, newImplementationAddress: string): Promise { + const nonceBytesLike = hexZeroPad(nonce.toHexString(), 32) + + const dataForHash = concat([ + nonceBytesLike, + signerWallet.address, + newImplementationAddress + ]) + // console.log('dataForHash: ', dataForHash) + const sign = signerWallet._signingKey().signDigest(hashMessageEIP191V0(accountAddress, dataForHash)) + return sign +} + +export async function signMessage (signerWallet: Wallet, accountAddress: string, nonce: BigNumber, data: Uint8Array): Promise { + const nonceBytesLike = hexZeroPad(nonce.toHexString(), 32) + + const dataForHash = concat([ + nonceBytesLike, + signerWallet.address, + data + ]) + // console.log('dataForHash: ', dataForHash) + const sign = signerWallet._signingKey().signDigest(hashMessageEIP191V0(accountAddress, dataForHash)) + return sign +} From 3263cf6bb56e4a2fa4c07cec0dd9ece3b5e629dc Mon Sep 17 00:00:00 2001 From: kbehouse Date: Fri, 12 May 2023 18:00:53 +0800 Subject: [PATCH 02/26] 4337 contract update & add deploy script --- README.md | 33 ++++- .../BloctoAccount4337/BloctoAccount4337.sol | 33 ++--- .../BloctoAccount4337CloneableWallet.sol | 2 +- .../BloctoAccount4337/VerifyingPaymaster.sol | 120 ++++++++++++++++++ ...deploy_BloctoAccount4337CloneableWallet.ts | 24 ++++ deploy/2_deploy_BloctoAccountFactory.ts | 25 ++++ deploy/3_deploy_VerifyingPaymaster.ts | 27 ++++ hardhat.config.ts | 8 +- package.json | 8 +- test/bloctoaccount.test.ts | 15 ++- 10 files changed, 260 insertions(+), 35 deletions(-) create mode 100644 contracts/BloctoAccount4337/VerifyingPaymaster.sol create mode 100644 deploy/1_deploy_BloctoAccount4337CloneableWallet.ts create mode 100644 deploy/2_deploy_BloctoAccountFactory.ts create mode 100644 deploy/3_deploy_VerifyingPaymaster.ts diff --git a/README.md b/README.md index f74dc74..43421ea 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,50 @@ # BloctoAccount & BloctoAccountFactory -## Test & Deploy +## Test test ``` -npx hardhat test test/entrypoint.test.ts +yarn test ``` + +## Deploy + +deploy BloctoAccountCloneableWallet + +``` +yarn deploy-bloctoaccountcloneable --network mumbai +``` + + deploy BloctoAccountFactory ``` -yarn deploy-accountfactory --network mumbai +yarn deploy-bloctoaccountfactory --network mumbai +``` + + +deploy VerifyingPaymaster +``` +yarn deploy-verifyingpaymaster --network mumbai ``` + +verify BloctoAccountCloneableWallet +``` +yarn verify-bloctoaccountcloneable --network mumbai +``` + + verify BloctoAccountFactory ``` yarn verify-accountfactory --network mumbai ``` +verify VerifyingPaymaster +``` +yarn verify-verifyingpaymaster --network mumbai +``` ## Acknowledgement diff --git a/contracts/BloctoAccount4337/BloctoAccount4337.sol b/contracts/BloctoAccount4337/BloctoAccount4337.sol index faec614..bfcb07d 100644 --- a/contracts/BloctoAccount4337/BloctoAccount4337.sol +++ b/contracts/BloctoAccount4337/BloctoAccount4337.sol @@ -19,7 +19,7 @@ contract BloctoAccount4337 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, /// @notice This is the version of this contract. string public constant VERSION = "1.4.0"; - IEntryPoint private _entryPoint; + address public constant EntryPointV060 = 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789; modifier onlySelf() { require(msg.sender == address(this), "only self"); @@ -29,20 +29,10 @@ contract BloctoAccount4337 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, // override from UUPSUpgradeable function _authorizeUpgrade(address newImplementation) internal view override onlySelf { (newImplementation); - require(msg.sender == address(this), "BloctoAccount: only self"); } - constructor(IEntryPoint anEntryPoint) { - _entryPoint = anEntryPoint; - } - /// @inheritdoc BaseAccount - function entryPoint() public view virtual override returns (IEntryPoint) { - return _entryPoint; - } - - function setEntryPoint(address anEntryPoint) public onlySelf { - _entryPoint = IEntryPoint(anEntryPoint); + return IEntryPoint(EntryPointV060); } /** @@ -64,6 +54,16 @@ contract BloctoAccount4337 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, } } + // internal call for execute and executeBatch + 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)) + } + } + } + /// implement template method of BaseAccount function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) internal @@ -79,15 +79,6 @@ contract BloctoAccount4337 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, 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 */ diff --git a/contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol b/contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol index 1824956..ee8fe8d 100644 --- a/contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol +++ b/contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol @@ -9,7 +9,7 @@ contract BloctoAccount4337CloneableWallet is BloctoAccount4337 { /// @dev An empty constructor that deploys a NON-FUNCTIONAL version /// of `BloctoAccount` - constructor(IEntryPoint anEntryPoint) BloctoAccount4337(anEntryPoint) { + constructor() { initialized = true; } } diff --git a/contracts/BloctoAccount4337/VerifyingPaymaster.sol b/contracts/BloctoAccount4337/VerifyingPaymaster.sol new file mode 100644 index 0000000..e24dcdb --- /dev/null +++ b/contracts/BloctoAccount4337/VerifyingPaymaster.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +/* solhint-disable reason-string */ +/* solhint-disable no-inline-assembly */ + +import "@account-abstraction/contracts/core/BasePaymaster.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +/** + * A sample paymaster that uses external service to decide whether to pay for the UserOp. + * The paymaster trusts an external signer to sign the transaction. + * The calling user must pass the UserOp to that external signer first, which performs + * whatever off-chain verification before signing the UserOp. + * Note that this signature is NOT a replacement for the account-specific signature: + * - the paymaster checks a signature to agree to PAY for GAS. + * - the account checks a signature to prove identity and account ownership. + */ + +contract VerifyingPaymaster is BasePaymaster { + using ECDSA for bytes32; + using UserOperationLib for UserOperation; + + address public verifyingSigner; + + uint256 private constant VALID_TIMESTAMP_OFFSET = 20; + + uint256 private constant SIGNATURE_OFFSET = 84; + + constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) { + verifyingSigner = _verifyingSigner; + } + + function setVerifyingSigner(address _verifyingSigner) public onlyOwner { + verifyingSigner = _verifyingSigner; + } + + mapping(address => uint256) public senderNonce; + + function pack(UserOperation calldata userOp) internal pure returns (bytes memory ret) { + // lighter signature scheme. must match UserOp.ts#packUserOp + bytes calldata pnd = userOp.paymasterAndData; + // copy directly the userOp from calldata up to (but not including) the paymasterAndData. + // this encoding depends on the ABI encoding of calldata, but is much lighter to copy + // than referencing each field separately. + assembly { + let ofs := userOp + let len := sub(sub(pnd.offset, ofs), 32) + ret := mload(0x40) + mstore(0x40, add(ret, add(len, 32))) + mstore(ret, len) + calldatacopy(add(ret, 32), ofs, len) + } + } + + /** + * return the hash we're going to sign off-chain (and validate on-chain) + * this method is called by the off-chain service, to sign the request. + * it is called on-chain from the validatePaymasterUserOp, to validate the signature. + * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", + * which will carry the signature itself. + */ + function getHash(UserOperation calldata userOp, uint48 validUntil, uint48 validAfter) + public + view + returns (bytes32) + { + //can't use userOp.hash(), since it contains also the paymasterAndData itself. + + return keccak256( + abi.encode( + pack(userOp), block.chainid, address(this), senderNonce[userOp.getSender()], validUntil, validAfter + ) + ); + } + + /** + * verify our external signer signed this request. + * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params + * paymasterAndData[:20] : address(this) + * paymasterAndData[20:84] : abi.encode(validUntil, validAfter) + * paymasterAndData[84:] : signature + */ + function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32, /*userOpHash*/ uint256 requiredPreFund) + internal + override + returns (bytes memory context, uint256 validationData) + { + (requiredPreFund); + + (uint48 validUntil, uint48 validAfter, bytes calldata signature) = + parsePaymasterAndData(userOp.paymasterAndData); + //ECDSA library supports both 64 and 65-byte long signatures. + // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA" + require( + signature.length == 64 || signature.length == 65, + "VerifyingPaymaster: invalid signature length in paymasterAndData" + ); + bytes32 hash = ECDSA.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter)); + senderNonce[userOp.getSender()]++; + + //don't revert on signature failure: return SIG_VALIDATION_FAILED + if (verifyingSigner != ECDSA.recover(hash, signature)) { + return ("", _packValidationData(true, validUntil, validAfter)); + } + + //no need for other on-chain validation: entire UserOp should have been checked + // by the external service prior to signing it. + return ("", _packValidationData(false, validUntil, validAfter)); + } + + function parsePaymasterAndData(bytes calldata paymasterAndData) + public + pure + returns (uint48 validUntil, uint48 validAfter, bytes calldata signature) + { + (validUntil, validAfter) = + abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET:SIGNATURE_OFFSET], (uint48, uint48)); + signature = paymasterAndData[SIGNATURE_OFFSET:]; + } +} diff --git a/deploy/1_deploy_BloctoAccount4337CloneableWallet.ts b/deploy/1_deploy_BloctoAccount4337CloneableWallet.ts new file mode 100644 index 0000000..6f197d2 --- /dev/null +++ b/deploy/1_deploy_BloctoAccount4337CloneableWallet.ts @@ -0,0 +1,24 @@ +import { ethers } from 'hardhat' + +const ContractName = 'BloctoAccount4337CloneableWallet' +const GasLimit = 6000000 + +async function main (): Promise { + // const lockedAmount = ethers.utils.parseEther("1"); + + const factory = await ethers.getContractFactory(ContractName) + const contract = await factory.deploy({ + gasLimit: GasLimit // set the gas limit to 6 million + }) + + await contract.deployed() + + console.log(`${ContractName} deployed to: ${contract.address}`) +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/deploy/2_deploy_BloctoAccountFactory.ts b/deploy/2_deploy_BloctoAccountFactory.ts new file mode 100644 index 0000000..593f119 --- /dev/null +++ b/deploy/2_deploy_BloctoAccountFactory.ts @@ -0,0 +1,25 @@ +import { ethers } from 'hardhat' + +const ContractName = 'BloctoAccountFactory' +const AccountToImplementation = '0x021DCa3104aa79f68EFEc784B56AFa382b1fd7b8' +const GasLimit = 6000000 + +async function main (): Promise { + // const lockedAmount = ethers.utils.parseEther("1"); + + const factory = await ethers.getContractFactory(ContractName) + const contract = await factory.deploy(AccountToImplementation, { + gasLimit: GasLimit // set the gas limit to 6 million + }) + + await contract.deployed() + + console.log(`${ContractName} deployed to: ${contract.address}`) +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/deploy/3_deploy_VerifyingPaymaster.ts b/deploy/3_deploy_VerifyingPaymaster.ts new file mode 100644 index 0000000..0ece30e --- /dev/null +++ b/deploy/3_deploy_VerifyingPaymaster.ts @@ -0,0 +1,27 @@ +import { ethers } from 'hardhat' + +const ContractName = 'VerifyingPaymaster' +// version 0.6.0 from https://mirror.xyz/erc4337official.eth/cSdZl9X-Hce71l_FzjVKQ5eN398ial7QmkDExmIIOQk +const EntryPointAddress = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' +const VerifyingSigner = '0x086443C6bA8165a684F3e316Da42D3A2F0a2330a' +const GasLimit = 6000000 + +async function main (): Promise { + // const lockedAmount = ethers.utils.parseEther("1"); + + const factory = await ethers.getContractFactory(ContractName) + const contract = await factory.deploy(EntryPointAddress, VerifyingSigner, { + gasLimit: GasLimit // set the gas limit to 6 million + }) + + await contract.deployed() + + console.log(`${ContractName} deployed to: ${contract.address}`) +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/hardhat.config.ts b/hardhat.config.ts index d887c41..4cbd663 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -13,7 +13,6 @@ const { POLYGONSCAN_API_KEY, // polygonscan API KEY BSCSCAN_API_KEY, // bscscan API KEY SNOWTRACE_API_KEY, // avalanche scan (snowtrace) API KEY - OPSCAN_API_KEY, // optimistic scan API KEY ARBSCAN_API_KEY // arbitrum scan API KEY } = process.env @@ -72,9 +71,7 @@ const config: HardhatUserConfig = { process.env.ETH_PRIVATE_KEY !== undefined ? [process.env.ETH_PRIVATE_KEY] : [], - chainId: 80001, - gas: 8000000, // 8M - gasPrice: 10000000000 // 10 gwei + chainId: 80001 } }, mocha: { @@ -91,8 +88,7 @@ const config: HardhatUserConfig = { avalanche: SNOWTRACE_API_KEY, goerli: ETHERSCAN_API_KEY, arbitrumOne: ARBSCAN_API_KEY, - arbitrumGoerli: ARBSCAN_API_KEY, - optimism: OPSCAN_API_KEY + arbitrumGoerli: ARBSCAN_API_KEY } } diff --git a/package.json b/package.json index eece71d..5ac021a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,13 @@ "lint:js": "eslint -f unix .", "lint-fix": "eslint -f unix . --fix", "lint:sol": "solhint -f unix \"contracts/**/*.sol\" --max-warnings 0", - "test": "hardhat test" + "test": "hardhat test", + "deploy-bloctoaccountcloneable": "hardhat run deploy/1_deploy_BloctoAccount4337CloneableWallet.ts", + "deploy-bloctoaccountfactory": "hardhat run deploy/2_deploy_BloctoAccountFactory.ts", + "deploy-verifyingpaymaster": "hardhat run deploy/3_deploy_VerifyingPaymaster.ts", + "verify-bloctoaccountcloneable": "npx hardhat verify 0x021dca3104aa79f68efec784b56afa382b1fd7b8", + "verify-accountfactory": "npx hardhat verify 0x7f0b3252e7d0199d8c94dd07325a2a1386f78fb1 0x021dca3104aa79f68efec784b56afa382b1fd7b8", + "verify-verifyingpaymaster": "npx hardhat verify 0xE671dEee9c758e642d50c32e13FD5fC6D42C98F1 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 0x086443C6bA8165a684F3e316Da42D3A2F0a2330a" }, "devDependencies": { "@account-abstraction/contracts": "^0.6.0", diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index 16705a5..c60e1fd 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -6,13 +6,16 @@ import { BloctoAccount__factory, BloctoAccountCloneableWallet, BloctoAccountCloneableWallet__factory, + BloctoAccount4337CloneableWallet, BloctoAccount4337CloneableWallet__factory, BloctoAccountFactory, BloctoAccountFactory__factory, TestERC20, TestERC20__factory, TestBloctoAccountV2, - TestBloctoAccountV2__factory + TestBloctoAccountV2__factory, + BloctoAccount4337, + BloctoAccount4337__factory } from '../typechain' import { EntryPoint } from '@account-abstraction/contracts' import { @@ -106,7 +109,8 @@ describe('BloctoAccount Upgrade Test', function () { describe('should upgrade to 4337 with method1 upgradeTo', () => { const AccountSalt = '0x4eb84e5765b53776863ffa7c4965af012ded5be4000000000000000000000002' let account: BloctoAccount - let implementation4337: BloctoAccountCloneableWallet + let account4337: BloctoAccount4337 + let implementation4337: BloctoAccount4337CloneableWallet async function upgradeAccountToImplementation4337 (): Promise { const authorizeInAccountNonce = (await account.nonces(authorizedWallet.address)).add(1) @@ -120,7 +124,7 @@ describe('BloctoAccount Upgrade Test', function () { before(async () => { account = await testCreateAccount(AccountSalt) - implementation4337 = await new BloctoAccount4337CloneableWallet__factory(ethersSigner).deploy(entryPoint.address) + implementation4337 = await new BloctoAccount4337CloneableWallet__factory(ethersSigner).deploy() await factory.setImplementation(implementation4337.address) }) @@ -133,6 +137,7 @@ describe('BloctoAccount Upgrade Test', function () { it('upgrade test', async () => { expect(await account.VERSION()).to.eql('1.3.0') await upgradeAccountToImplementation4337() + account4337 = BloctoAccount4337__factory.connect(account.address, ethersSigner) expect(await account.VERSION()).to.eql('1.4.0') }) @@ -156,5 +161,9 @@ describe('BloctoAccount Upgrade Test', function () { ) expect(await accountNew.VERSION()).to.eql('1.4.0') }) + + it('should be v060 address', async () => { + expect(await account4337.entryPoint()).to.eql(await implementation4337.EntryPointV060()) + }) }) }) From 36a266510828eadfbeffce22b2c99723f3b69054 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Tue, 16 May 2023 14:47:06 +0800 Subject: [PATCH 03/26] 4337: only for 4337 without temporarily versioin for upgrade --- README.md | 7 + contracts/BloctoAccount.sol | 139 ++++++++++-------- .../BloctoAccount4337CloneableWallet.sol | 15 -- contracts/BloctoAccountCloneableWallet.sol | 4 +- contracts/BloctoAccountFactory.sol | 2 +- .../VerifyingPaymaster.sol | 0 .../TestBloctoAccountCloneableWalletV140.sol | 13 ++ .../TestBloctoAccountV140.sol} | 39 +++-- contracts/test/TestBloctoAccountV2.sol | 29 ---- hardhat.config.ts | 2 +- package.json | 3 +- test/bloctoaccount.test.ts | 67 +++------ yarn.lock | 19 +++ 13 files changed, 166 insertions(+), 173 deletions(-) delete mode 100644 contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol rename contracts/{BloctoAccount4337 => Paymaster}/VerifyingPaymaster.sol (100%) create mode 100644 contracts/test/TestBloctoAccountCloneableWalletV140.sol rename contracts/{BloctoAccount4337/BloctoAccount4337.sol => test/TestBloctoAccountV140.sol} (71%) delete mode 100644 contracts/test/TestBloctoAccountV2.sol diff --git a/README.md b/README.md index 43421ea..9d4a77e 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,13 @@ verify VerifyingPaymaster yarn verify-verifyingpaymaster --network mumbai ``` +## Tool + +check storage layout +``` +npx hardhat check +``` + ## Acknowledgement this repo fork from https://github.com/eth-infinitism/account-abstraction \ No newline at end of file diff --git a/contracts/BloctoAccount.sol b/contracts/BloctoAccount.sol index 855c80f..796a851 100644 --- a/contracts/BloctoAccount.sol +++ b/contracts/BloctoAccount.sol @@ -6,85 +6,102 @@ pragma solidity ^0.8.12; /* solhint-disable reason-string */ import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@account-abstraction/contracts/core/BaseAccount.sol"; + import "./TokenCallbackHandler.sol"; import "./CoreWallet/CoreWallet.sol"; -// for test -import "hardhat/console.sol"; /** * Blocto account. + * compatibility for EIP-4337 and smart contract wallet with cosigner functionality (CoreWallet) */ -contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet { - /// @notice This is the version of this contract. +contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, BaseAccount { + /** + * This is the version of this contract. + */ string public constant VERSION = "1.3.0"; - //-----------------------------------------Method 1---------------------------------------------// - function _authorizeUpgrade(address newImplementation) internal view override { - (newImplementation); - require(msg.sender == address(this), "BloctoAccount: only self"); - } - - //-----------------------------------------Method 2---------------------------------------------// - // invoke cosigner check - modifier onlyInvokeCosigner( - uint8 v, - bytes32 r, - bytes32 s, - uint256 nonce, - address authorizedAddress, - bytes memory data - ) { - // check signature version - require(v == 27 || v == 28, "Invalid signature version."); + IEntryPoint private immutable _entryPoint; - // calculate hash - bytes32 operationHash = - keccak256(abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, nonce, authorizedAddress, data)); - - // recover signer - address signer = ecrecover(operationHash, v, r, s); - - // check for valid signature - require(signer != address(0), "Invalid signature."); + constructor(IEntryPoint anEntryPoint) { + _entryPoint = anEntryPoint; + } - // check nonce - require(nonce > nonces[signer], "must use valid nonce for signer"); + /** + * override from UUPSUpgradeable + */ + function _authorizeUpgrade(address newImplementation) internal view override onlyInvoked { + (newImplementation); + } - // check signer - require(signer == authorizedAddress, "authorized addresses must be equal"); + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } - // Get cosigner - address requiredCosigner = address(authorizations[authVersion + uint256(uint160(signer))]); + /** + * execute a transaction (called directly by entryPoint) + */ + function execute(address dest, uint256 value, bytes calldata func) external { + _requireFromEntryPoint(); + _call(dest, value, func); + } - // The operation should be approved if the signer address has no cosigner (i.e. signer == cosigner) or - // if the actual cosigner matches the required cosigner. - require(requiredCosigner == signer || requiredCosigner == msg.sender, "Invalid authorization."); + /** + * execute a sequence of transactions (called directly by entryPoint) + */ + function executeBatch(address[] calldata dest, bytes[] calldata func) external { + _requireFromEntryPoint(); + require(dest.length == func.length, "wrong array lengths"); + for (uint256 i = 0; i < dest.length; i++) { + _call(dest[i], 0, func[i]); + } + } - // increment nonce to prevent replay attacks - nonces[signer] = nonce; + /// internal call for execute and executeBatch + 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)) + } + } + } - _; + /// implement validate signature method of BaseAccount + function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + internal + virtual + override + returns (uint256 validationData) + { + bytes4 result = this.isValidSignature(userOpHash, userOp.signature); + if (result != IERC1271.isValidSignature.selector) { + return SIG_VALIDATION_FAILED; + } + + return 0; } - // upgrade contract by msg.sender is cosigner and sign message (v, r, s) by authorizedAddress - function invokeCosignerUpgrade( - uint8 v, - bytes32 r, - bytes32 s, - uint256 nonce, - address authorizedAddress, - address newImplementation - ) external onlyInvokeCosigner(v, r, s, nonce, authorizedAddress, abi.encodePacked(newImplementation)) { - _upgradeTo(newImplementation); + /** + * check current account deposit in the entryPoint StakeManager + */ + function getDeposit() public view returns (uint256) { + return entryPoint().balanceOf(address(this)); } - //-----------------------------------------Method 3---------------------------------------------// - // modifier onlySelf() { - // require(msg.sender == address(this), "only self"); - // _; - // } + /** + * deposit more funds for this account in the entryPoint StakeManager + */ + function addDeposit() public payable { + entryPoint().depositTo{value: msg.value}(address(this)); + } - // function upgradeTo(address newImplementation) external override onlyProxy onlySelf { - // _upgradeTo(newImplementation); - // } + /** + * withdraw deposit to withdrawAddress from entryPoint StakeManager + * @param withdrawAddress target to send to + * @param amount to withdraw + */ + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) external onlyInvoked { + entryPoint().withdrawTo(withdrawAddress, amount); + } } diff --git a/contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol b/contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol deleted file mode 100644 index ee8fe8d..0000000 --- a/contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.12; - -import "./BloctoAccount4337.sol"; - -/// @title BloctoAccountCloneableWallet Wallet -/// @notice This contract represents a complete but non working wallet. -contract BloctoAccount4337CloneableWallet is BloctoAccount4337 { - /// @dev An empty constructor that deploys a NON-FUNCTIONAL version - /// of `BloctoAccount` - - constructor() { - initialized = true; - } -} diff --git a/contracts/BloctoAccountCloneableWallet.sol b/contracts/BloctoAccountCloneableWallet.sol index a4973f2..64a0791 100644 --- a/contracts/BloctoAccountCloneableWallet.sol +++ b/contracts/BloctoAccountCloneableWallet.sol @@ -6,8 +6,8 @@ import "./BloctoAccount.sol"; /// @title BloctoAccountCloneableWallet Wallet /// @notice This contract represents a complete but non working wallet. contract BloctoAccountCloneableWallet is BloctoAccount { - /// @dev An empty constructor that deploys a NON-FUNCTIONAL version of `BloctoAccount` - constructor() { + /// @dev Cconstructor that deploys a NON-FUNCTIONAL version of `BloctoAccount` + constructor(IEntryPoint anEntryPoint) BloctoAccount(anEntryPoint) { initialized = true; } } diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol index 9c2090e..3ff9762 100644 --- a/contracts/BloctoAccountFactory.sol +++ b/contracts/BloctoAccountFactory.sol @@ -21,7 +21,7 @@ contract BloctoAccountFactory is Ownable { } /** - * create an account, and return its address(BloctoAccount). + * create an account, and return its BloctoAccount. * 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 diff --git a/contracts/BloctoAccount4337/VerifyingPaymaster.sol b/contracts/Paymaster/VerifyingPaymaster.sol similarity index 100% rename from contracts/BloctoAccount4337/VerifyingPaymaster.sol rename to contracts/Paymaster/VerifyingPaymaster.sol diff --git a/contracts/test/TestBloctoAccountCloneableWalletV140.sol b/contracts/test/TestBloctoAccountCloneableWalletV140.sol new file mode 100644 index 0000000..81d10d3 --- /dev/null +++ b/contracts/test/TestBloctoAccountCloneableWalletV140.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "./TestBloctoAccountV140.sol"; + +/// @title BloctoAccountCloneableWallet Wallet +/// @notice This contract represents a complete but non working wallet. +contract TestBloctoAccountCloneableWalletV140 is TestBloctoAccountV140 { + /// @dev Cconstructor that deploys a NON-FUNCTIONAL version of `TestBloctoAccountV140` + constructor(IEntryPoint anEntryPoint) TestBloctoAccountV140(anEntryPoint) { + initialized = true; + } +} diff --git a/contracts/BloctoAccount4337/BloctoAccount4337.sol b/contracts/test/TestBloctoAccountV140.sol similarity index 71% rename from contracts/BloctoAccount4337/BloctoAccount4337.sol rename to contracts/test/TestBloctoAccountV140.sol index bfcb07d..8773a04 100644 --- a/contracts/BloctoAccount4337/BloctoAccount4337.sol +++ b/contracts/test/TestBloctoAccountV140.sol @@ -15,24 +15,27 @@ import "../CoreWallet/CoreWallet.sol"; * Blocto account. * compatibility for EIP-4337 and smart contract wallet with cosigner functionality (CoreWallet) */ -contract BloctoAccount4337 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, BaseAccount { - /// @notice This is the version of this contract. +contract TestBloctoAccountV140 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, BaseAccount { + /** + * This is the version of this contract. + */ string public constant VERSION = "1.4.0"; - address public constant EntryPointV060 = 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789; + IEntryPoint private immutable _entryPoint; - modifier onlySelf() { - require(msg.sender == address(this), "only self"); - _; + constructor(IEntryPoint anEntryPoint) { + _entryPoint = anEntryPoint; } - // override from UUPSUpgradeable - function _authorizeUpgrade(address newImplementation) internal view override onlySelf { + /** + * override from UUPSUpgradeable + */ + function _authorizeUpgrade(address newImplementation) internal view override onlyInvoked { (newImplementation); } function entryPoint() public view virtual override returns (IEntryPoint) { - return IEntryPoint(EntryPointV060); + return _entryPoint; } /** @@ -44,7 +47,7 @@ contract BloctoAccount4337 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, } /** - * execute a sequence of transactions + * execute a sequence of transactions (called directly by entryPoint) */ function executeBatch(address[] calldata dest, bytes[] calldata func) external { _requireFromEntryPoint(); @@ -54,7 +57,7 @@ contract BloctoAccount4337 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, } } - // internal call for execute and executeBatch + /// internal call for execute and executeBatch function _call(address target, uint256 value, bytes memory data) internal { (bool success, bytes memory result) = target.call{value: value}(data); if (!success) { @@ -64,7 +67,7 @@ contract BloctoAccount4337 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, } } - /// implement template method of BaseAccount + /// implement validate signature method of BaseAccount function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) internal virtual @@ -80,21 +83,25 @@ contract BloctoAccount4337 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, } /** - * check current account deposit in the entryPoint + * check current account deposit in the entryPoint StakeManager */ function getDeposit() public view returns (uint256) { return entryPoint().balanceOf(address(this)); } /** - * deposit more funds for this account in the entryPoint + * deposit more funds for this account in the entryPoint StakeManager */ function addDeposit() public payable { entryPoint().depositTo{value: msg.value}(address(this)); } - // withdraw deposit to withdrawAddress by cosigner & authorizedAddress signature - function withdrawDepositTo(address payable withdrawAddress, uint256 amount) external onlySelf { + /** + * withdraw deposit to withdrawAddress from entryPoint StakeManager + * @param withdrawAddress target to send to + * @param amount to withdraw + */ + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) external onlyInvoked { entryPoint().withdrawTo(withdrawAddress, amount); } } diff --git a/contracts/test/TestBloctoAccountV2.sol b/contracts/test/TestBloctoAccountV2.sol deleted file mode 100644 index 3a4d665..0000000 --- a/contracts/test/TestBloctoAccountV2.sol +++ /dev/null @@ -1,29 +0,0 @@ -// 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 "../TokenCallbackHandler.sol"; -import "../CoreWallet/CoreWallet.sol"; - -/** - * Blocto account. - * compatibility for EIP-4337 and smart contract wallet with cosigner functionality (CoreWallet) - */ -contract TestBloctoAccountV2 is CoreWallet, UUPSUpgradeable, Initializable { - /// @notice This is the version of this contract. - string public constant VERSION = "1.3.1"; - - // override from UUPSUpgradeable, to prevent upgrades from this method - function _authorizeUpgrade(address newImplementation) internal pure override { - (newImplementation); - require(false, "BloctoAccount: cannot upgrade from _authorizeUpgrade"); - } -} diff --git a/hardhat.config.ts b/hardhat.config.ts index 4cbd663..e9e48cf 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -3,7 +3,7 @@ import '@typechain/hardhat' import { HardhatUserConfig } from 'hardhat/config' import '@nomiclabs/hardhat-etherscan' import '@openzeppelin/hardhat-upgrades' - +import 'hardhat-storage-layout' import 'solidity-coverage' import * as fs from 'fs' diff --git a/package.json b/package.json index 5ac021a..dbf16dc 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "ethereum-waffle": "^3.4.0", "ethers": "^5.4.2", "hardhat": "^2.6.6", + "hardhat-storage-layout": "^0.1.7", "solhint": "^3.3.7", "ts-generator": "^0.1.1", "ts-mocha": "^10.0.0", @@ -61,4 +62,4 @@ "table": "^6.8.0", "typescript": "^4.3.5" } -} \ No newline at end of file +} diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index c60e1fd..9e5966f 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -6,16 +6,12 @@ import { BloctoAccount__factory, BloctoAccountCloneableWallet, BloctoAccountCloneableWallet__factory, - BloctoAccount4337CloneableWallet, - BloctoAccount4337CloneableWallet__factory, BloctoAccountFactory, BloctoAccountFactory__factory, TestERC20, TestERC20__factory, - TestBloctoAccountV2, - TestBloctoAccountV2__factory, - BloctoAccount4337, - BloctoAccount4337__factory + TestBloctoAccountCloneableWalletV140, + TestBloctoAccountCloneableWalletV140__factory } from '../typechain' import { EntryPoint } from '@account-abstraction/contracts' import { @@ -65,8 +61,11 @@ describe('BloctoAccount Upgrade Test', function () { } before(async function () { + // 4337 + entryPoint = await deployEntryPoint() + // v1 implementation - implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy()).address + implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy(entryPoint.address)).address // account factory factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation); @@ -77,46 +76,19 @@ describe('BloctoAccount Upgrade Test', function () { // test erc20 erc20 = await new TestERC20__factory(ethersSigner).deploy('Test ERC20', 'T20', 18) - - // 4337 - entryPoint = await deployEntryPoint() }) - describe('should upgrade with method2 invokeCosignerUpgrade', () => { - // random value for account - const AccountSalt = '0x4eb84e5765b53776863ffa7c4965af012ded5be4000000000000000000000001' - let account: BloctoAccount - let implementationV2: TestBloctoAccountV2 - - async function upgradeAccountToImplementationV2 (): Promise { - const authorizeInAccountNonce = (await account.nonces(authorizedWallet.address)).add(1) - const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosignerWallet) - const sign = await signUpgrade(authorizedWallet, account.address, authorizeInAccountNonce, implementationV2.address) - await accountLinkCosigner.invokeCosignerUpgrade(sign.v, sign.r, sign.s, authorizeInAccountNonce, authorizedWallet.address, implementationV2.address) - } - - before(async () => { - account = await testCreateAccount(AccountSalt) - implementationV2 = await new TestBloctoAccountV2__factory(ethersSigner).deploy() - }) - - it('version check', async () => { - expect(await account.VERSION()).to.eql('1.3.0') - await upgradeAccountToImplementationV2() - expect(await account.VERSION()).to.eql('1.3.1') - }) - }) - describe('should upgrade to 4337 with method1 upgradeTo', () => { + describe('should upgrade to different version implementation', () => { const AccountSalt = '0x4eb84e5765b53776863ffa7c4965af012ded5be4000000000000000000000002' + const MockEntryPointV070 = '0x000000000000000000000000000000000000E070' let account: BloctoAccount - let account4337: BloctoAccount4337 - let implementation4337: BloctoAccount4337CloneableWallet + let implementationV140: TestBloctoAccountCloneableWalletV140 - async function upgradeAccountToImplementation4337 (): Promise { + async function upgradeAccountToV140 (): Promise { const authorizeInAccountNonce = (await account.nonces(authorizedWallet.address)).add(1) const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosignerWallet) const upgradeToData = txData(1, account.address, BigNumber.from(0), - account.interface.encodeFunctionData('upgradeTo', [implementation4337.address])) + account.interface.encodeFunctionData('upgradeTo', [implementationV140.address])) const sign = await signMessage(authorizedWallet, account.address, authorizeInAccountNonce, upgradeToData) await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, authorizeInAccountNonce, authorizedWallet.address, upgradeToData) @@ -124,20 +96,21 @@ describe('BloctoAccount Upgrade Test', function () { before(async () => { account = await testCreateAccount(AccountSalt) - implementation4337 = await new BloctoAccount4337CloneableWallet__factory(ethersSigner).deploy() - await factory.setImplementation(implementation4337.address) + // mock new entry point version 0.7.0 + implementationV140 = await new TestBloctoAccountCloneableWalletV140__factory(ethersSigner).deploy(MockEntryPointV070) + await factory.setImplementation(implementationV140.address) }) it('upgrade fail if not by contract self', async () => { // upgrade revert even though upgrade by cosigner - await expect(account.connect(cosignerWallet).upgradeTo(implementation4337.address)) - .to.revertedWith('BloctoAccount: only self') + await expect(account.connect(cosignerWallet).upgradeTo(implementationV140.address)) + .to.revertedWith('must be called from `invoke()') }) it('upgrade test', async () => { expect(await account.VERSION()).to.eql('1.3.0') - await upgradeAccountToImplementation4337() - account4337 = BloctoAccount4337__factory.connect(account.address, ethersSigner) + await upgradeAccountToV140() + // accountV140 = BloctoAccount__factory.connect(account.address, ethersSigner) expect(await account.VERSION()).to.eql('1.4.0') }) @@ -162,8 +135,8 @@ describe('BloctoAccount Upgrade Test', function () { expect(await accountNew.VERSION()).to.eql('1.4.0') }) - it('should be v060 address', async () => { - expect(await account4337.entryPoint()).to.eql(await implementation4337.EntryPointV060()) + it('should entrypoint be v070 address', async () => { + expect(await account.entryPoint()).to.eql(MockEntryPointV070) }) }) }) diff --git a/yarn.lock b/yarn.lock index 3cdcf6e..25d24fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3085,6 +3085,13 @@ concat-stream@^1.5.1: readable-stream "^2.2.2" typedarray "^0.0.6" +console-table-printer@^2.9.0: + version "2.11.1" + resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.11.1.tgz#c2dfe56e6343ea5bcfa3701a4be29fe912dbd9c7" + integrity sha512-8LfFpbF/BczoxPwo2oltto5bph8bJkGOATXsg3E9ddMJOGnWJciKHldx2zDj5XIBflaKzPfVCjOTl6tMh7lErg== + dependencies: + simple-wcswidth "^1.0.1" + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -5377,6 +5384,13 @@ hardhat-deploy@^0.11.23: qs "^6.9.4" zksync-web3 "^0.8.1" +hardhat-storage-layout@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/hardhat-storage-layout/-/hardhat-storage-layout-0.1.7.tgz#ad8a5afd8593ee51031eb1dd9476b4a2ed981785" + integrity sha512-q723g2iQnJpRdMC6Y8fbh/stG6MLHKNxa5jq/ohjtD5znOlOzQ6ojYuInY8V4o4WcPyG3ty4hzHYunLf66/1+A== + dependencies: + console-table-printer "^2.9.0" + hardhat@^2.6.6: version "2.12.4" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.12.4.tgz#e539ba58bee9ba1a1ced823bfdcec0b3c5a3e70f" @@ -8615,6 +8629,11 @@ simple-get@^2.7.0: once "^1.3.1" simple-concat "^1.0.0" +simple-wcswidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz#8ab18ac0ae342f9d9b629604e54d2aa1ecb018b2" + integrity sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg== + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" From 99082a32cc47e77f913aa7110c5d600717901868 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Wed, 17 May 2023 14:54:07 +0800 Subject: [PATCH 04/26] misc: update for clean code --- contracts/BloctoAccountCloneableWallet.sol | 2 +- ...ableWallet.ts => 1_deploy_BloctoAccountCloneableWallet.ts} | 2 +- test/bloctoaccount.test.ts | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) rename deploy/{1_deploy_BloctoAccount4337CloneableWallet.ts => 1_deploy_BloctoAccountCloneableWallet.ts} (91%) diff --git a/contracts/BloctoAccountCloneableWallet.sol b/contracts/BloctoAccountCloneableWallet.sol index 64a0791..ebe8b42 100644 --- a/contracts/BloctoAccountCloneableWallet.sol +++ b/contracts/BloctoAccountCloneableWallet.sol @@ -6,7 +6,7 @@ import "./BloctoAccount.sol"; /// @title BloctoAccountCloneableWallet Wallet /// @notice This contract represents a complete but non working wallet. contract BloctoAccountCloneableWallet is BloctoAccount { - /// @dev Cconstructor that deploys a NON-FUNCTIONAL version of `BloctoAccount` + /// @dev constructor that deploys a NON-FUNCTIONAL version of `BloctoAccount` constructor(IEntryPoint anEntryPoint) BloctoAccount(anEntryPoint) { initialized = true; } diff --git a/deploy/1_deploy_BloctoAccount4337CloneableWallet.ts b/deploy/1_deploy_BloctoAccountCloneableWallet.ts similarity index 91% rename from deploy/1_deploy_BloctoAccount4337CloneableWallet.ts rename to deploy/1_deploy_BloctoAccountCloneableWallet.ts index 6f197d2..930d0f5 100644 --- a/deploy/1_deploy_BloctoAccount4337CloneableWallet.ts +++ b/deploy/1_deploy_BloctoAccountCloneableWallet.ts @@ -1,6 +1,6 @@ import { ethers } from 'hardhat' -const ContractName = 'BloctoAccount4337CloneableWallet' +const ContractName = 'BloctoAccountCloneableWallet' const GasLimit = 6000000 async function main (): Promise { diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index 9e5966f..eeaa76a 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -45,8 +45,6 @@ describe('BloctoAccount Upgrade Test', function () { let entryPoint: EntryPoint - let erc20: TestERC20 - async function testCreateAccount (salt: string): Promise { const account = await createAccount( ethersSigner, @@ -75,7 +73,7 @@ describe('BloctoAccount Upgrade Test', function () { await fund(cosignerWallet.address) // test erc20 - erc20 = await new TestERC20__factory(ethersSigner).deploy('Test ERC20', 'T20', 18) + // erc20 = await new TestERC20__factory(ethersSigner).deploy('Test ERC20', 'T20', 18) }) describe('should upgrade to different version implementation', () => { From 99bb0b6928ca04ae717f7c8fe768e7e15d882e0d Mon Sep 17 00:00:00 2001 From: kbehouse Date: Mon, 22 May 2023 23:51:18 +0800 Subject: [PATCH 05/26] CoreWallet compatibility with 1.2.0 wallet --- contracts/BloctoAccountFactory.sol | 6 +- contracts/CoreWallet/CoreWallet.sol | 362 ++++++---------------------- test/testutils.ts | 4 +- 3 files changed, 80 insertions(+), 292 deletions(-) diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol index 3ff9762..0812401 100644 --- a/contracts/BloctoAccountFactory.sol +++ b/contracts/BloctoAccountFactory.sol @@ -26,7 +26,7 @@ contract BloctoAccountFactory is Ownable { * 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 _authorizedAddress, address _cosigner, address _recoveryAddress, bytes32 _salt) + function createAccount(address _authorizedAddress, address _cosigner, address _recoveryAddress, uint256 _salt) public returns (BloctoAccount ret) { @@ -40,14 +40,14 @@ contract BloctoAccountFactory is Ownable { BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); newProxy.initImplementation(bloctoAccountImplementation); ret = BloctoAccount(payable(address(newProxy))); - ret.init(_authorizedAddress, _cosigner, _recoveryAddress); + ret.init(_authorizedAddress, uint256(uint160(_cosigner)), _recoveryAddress); emit WalletCreated(address(ret), _authorizedAddress, false); } /** * calculate the counterfactual address of this account as it would be returned by createAccount() */ - function getAddress(address _cosigner, address _recoveryAddress, bytes32 _salt) public view returns (address) { + function getAddress(address _cosigner, address _recoveryAddress, uint256 _salt) public view returns (address) { bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); return Create2.computeAddress( bytes32(salt), keccak256(abi.encodePacked(type(BloctoAccountProxy).creationCode, abi.encode(address(this)))) diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol index 2a5cf62..aae6495 100644 --- a/contracts/CoreWallet/CoreWallet.sol +++ b/contracts/CoreWallet/CoreWallet.sol @@ -19,9 +19,9 @@ import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; /// implemented here) is negligable even if you don't need the cosigner functionality, and /// (B) two-of-two multisig (as implemented here) handles a lot of really common use cases, most /// notably third-party gas payment and off-chain blacklisting and fraud detection. -contract CoreWallet is IERC1271{ +contract CoreWallet is IERC1271 { using BytesExtractSignature for bytes; - using ECDSA for bytes32; + using ECDSA for bytes; /// @notice We require that presigned transactions use the EIP-191 signing format. /// See that EIP for more info: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-191.md @@ -61,7 +61,7 @@ contract CoreWallet is IERC1271{ /// /// Addresses that map to a non-zero cosigner in the current authVersion are called /// "authorized addresses". - mapping(uint256 => address) public authorizations; + mapping(uint256 => uint256) public authorizations; /// @notice A per-key nonce value, incremented each time a transaction is processed with that key. /// Used for replay prevention. The nonce value in the transaction must exactly equal the current @@ -101,10 +101,7 @@ contract CoreWallet is IERC1271{ /// @notice Used to decorate methods that can only be called directly by the recovery address. modifier onlyRecoveryAddress() { - require( - msg.sender == recoveryAddress, - "sender must be recovery address" - ); + require(msg.sender == recoveryAddress, "sender must be recovery address"); _; } @@ -141,7 +138,7 @@ contract CoreWallet is IERC1271{ /// @dev hash is 0xf5a7f4fb8a92356e8c8c4ae7ac3589908381450500a7e2fd08c95600021ee889 /// @param authorizedAddress the address to authorize or unauthorize /// @param cosigner the 2-of-2 signatory (optional). - event Authorized(address authorizedAddress, address cosigner); + event Authorized(address authorizedAddress, uint256 cosigner); /// @notice Emitted when an emergency recovery has been performed. If this event is fired, /// ALL previously authorized addresses have been deauthorized and the only authorized @@ -149,23 +146,20 @@ contract CoreWallet is IERC1271{ /// @dev hash is 0xe12d0bbeb1d06d7a728031056557140afac35616f594ef4be227b5b172a604b5 /// @param authorizedAddress the new authorized address /// @param cosigner the cosigning address for `authorizedAddress` - event EmergencyRecovery(address authorizedAddress, address cosigner); + event EmergencyRecovery(address authorizedAddress, uint256 cosigner); /// @notice Emitted when the recovery address changes. Either (but not both) of the /// parameters may be zero. /// @dev hash is 0x568ab3dedd6121f0385e007e641e74e1f49d0fa69cab2957b0b07c4c7de5abb6 /// @param previousRecoveryAddress the previous recovery address /// @param newRecoveryAddress the new recovery address - event RecoveryAddressChanged( - address previousRecoveryAddress, - address newRecoveryAddress - ); + event RecoveryAddressChanged(address previousRecoveryAddress, address newRecoveryAddress); /// @dev Emitted when this contract receives a non-zero amount ether via the fallback function /// (i.e. This event is not fired if the contract receives ether as part of a method invocation) /// @param from the address which sent you ether /// @param value the amount of ether sent - event Received(address from, uint value); + event Received(address from, uint256 value); /// @notice Emitted whenever a transaction is processed successfully from this wallet. Includes /// both simple send ether transactions, as well as other smart contract invocations. @@ -173,11 +167,7 @@ contract CoreWallet is IERC1271{ /// @param hash The hash of the entire operation set. 0 is returned when emitted from `invoke0()`. /// @param result A bitfield of the results of the operations. A bit of 0 means success, and 1 means failure. /// @param numOperations A count of the number of operations processed - event InvocationSuccess( - bytes32 hash, - uint256 result, - uint256 numOperations - ); + event InvocationSuccess(bytes32 hash, uint256 result, uint256 numOperations); /// @notice Emitted when a delegate is added or removed. /// @param interfaceId The interface ID as specified by EIP165 @@ -190,43 +180,26 @@ contract CoreWallet is IERC1271{ /// @param _authorizedAddress the initial authorized address, must not be zero! /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) - function init( - address _authorizedAddress, - address _cosigner, - address _recoveryAddress - ) public onlyOnce { - require( - _authorizedAddress != _recoveryAddress, - "Do not use the recovery address as an authorized address." - ); - require( - _cosigner != _recoveryAddress, - "Do not use the recovery address as a cosigner." - ); - require( - _authorizedAddress != address(0), - "Authorized addresses must not be zero." - ); - require(_cosigner != address(0), "Initial cosigner must not be zero."); + function init(address _authorizedAddress, uint256 _cosigner, address _recoveryAddress) public onlyOnce { + require(_authorizedAddress != _recoveryAddress, "Do not use the recovery address as an authorized address."); + require(address(uint160(_cosigner)) != _recoveryAddress, "Do not use the recovery address as a cosigner."); + require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); + require(address(uint160(_cosigner)) != address(0), "Initial cosigner must not be zero."); recoveryAddress = _recoveryAddress; // set initial authorization value authVersion = AUTH_VERSION_INCREMENTOR; // add initial authorized address - authorizations[ - authVersion + uint256(uint160(_authorizedAddress)) - ] = _cosigner; + authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; emit Authorized(_authorizedAddress, _cosigner); } - function bytesToAddresses( - bytes memory bys - ) private pure returns (address[] memory addresses) { - addresses = new address[](bys.length / 20); - for (uint i = 0; i < bys.length; i += 20) { + function bytesToAddresses(bytes memory bys) private pure returns (address[] memory addresses) { + addresses = new address[](bys.length/20); + for (uint256 i = 0; i < bys.length; i += 20) { address addr; - uint end = i + 20; + uint256 end = i + 20; assembly { addr := mload(add(bys, end)) } @@ -234,38 +207,20 @@ contract CoreWallet is IERC1271{ } } - function init2( - bytes memory _authorizedAddresses, - address _cosigner, - address _recoveryAddress - ) public onlyOnce { + function init2(bytes memory _authorizedAddresses, uint256 _cosigner, address _recoveryAddress) public onlyOnce { address[] memory addresses = bytesToAddresses(_authorizedAddresses); - for (uint i = 0; i < addresses.length; i++) { + for (uint256 i = 0; i < addresses.length; i++) { address _authorizedAddress = addresses[i]; - require( - _authorizedAddress != _recoveryAddress, - "Do not use the recovery address as an authorized address." - ); - require( - _cosigner != _recoveryAddress, - "Do not use the recovery address as a cosigner." - ); - require( - _authorizedAddress != address(0), - "Authorized addresses must not be zero." - ); - require( - _cosigner != address(0), - "Initial cosigner must not be zero." - ); + require(_authorizedAddress != _recoveryAddress, "Do not use the recovery address as an authorized address."); + require(address(uint160(_cosigner)) != _recoveryAddress, "Do not use the recovery address as a cosigner."); + require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); + require(address(uint160(_cosigner)) != address(0), "Initial cosigner must not be zero."); recoveryAddress = _recoveryAddress; // set initial authorization value authVersion = AUTH_VERSION_INCREMENTOR; // add initial authorized address - authorizations[ - authVersion + uint256(uint160(_authorizedAddress)) - ] = _cosigner; + authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; emit Authorized(_authorizedAddress, _cosigner); } @@ -298,25 +253,14 @@ contract CoreWallet is IERC1271{ // this call. Now, pass along the calldata of this CALL to the delegate contract. assembly { calldatacopy(0, 0, calldatasize()) - let result := staticcall( - gas(), - delegate, - 0, - calldatasize(), - 0, - 0 - ) + let result := staticcall(gas(), delegate, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) // If the delegate reverts, we revert. If the delegate does not revert, we return the data // returned by the delegate to the original caller. switch result - case 0 { - revert(0, returndatasize()) - } - default { - return(0, returndatasize()) - } + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } } } } @@ -336,10 +280,7 @@ contract CoreWallet is IERC1271{ /// for adding an implementation for a single function /// - 0 for removing an existing delegate /// - COMPOSITE_PLACEHOLDER for specifying support for a composite interface - function setDelegate( - bytes4 _interfaceId, - address _delegate - ) external onlyInvoked { + function setDelegate(bytes4 _interfaceId, address _delegate) external onlyInvoked { delegates[_interfaceId] = _delegate; emit DelegateUpdated(_interfaceId, _delegate); } @@ -352,10 +293,7 @@ contract CoreWallet is IERC1271{ /// @dev Must be called through `invoke()` /// @param _authorizedAddress the address to configure authorization /// @param _cosigner the corresponding cosigning address - function setAuthorized( - address _authorizedAddress, - address _cosigner - ) external onlyInvoked { + function setAuthorized(address _authorizedAddress, uint256 _cosigner) external onlyInvoked { // TODO: Allowing a signer to remove itself is actually pretty terrible; it could result in the user // removing their only available authorized key. Unfortunately, due to how the invocation forwarding // works, we don't actually _know_ which signer was used to call this method, so there's no easy way @@ -365,22 +303,14 @@ contract CoreWallet is IERC1271{ // Dapper can prevent this with offchain logic and the cosigner, but it would be nice to have // this enforced by the smart contract logic itself. + require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); + require(_authorizedAddress != recoveryAddress, "Do not use the recovery address as an authorized address."); require( - _authorizedAddress != address(0), - "Authorized addresses must not be zero." - ); - require( - _authorizedAddress != recoveryAddress, - "Do not use the recovery address as an authorized address." - ); - require( - _cosigner == address(0) || _cosigner != recoveryAddress, + address(uint160(_cosigner)) == address(0) || address(uint160(_cosigner)) != recoveryAddress, "Do not use the recovery address as a cosigner." ); - authorizations[ - authVersion + uint256(uint160(_authorizedAddress)) - ] = _cosigner; + authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; emit Authorized(_authorizedAddress, _cosigner); } @@ -391,54 +321,34 @@ contract CoreWallet is IERC1271{ /// is not trivially abused. /// @param _authorizedAddress the new and sole authorized address /// @param _cosigner the corresponding cosigner address, can be equal to _authorizedAddress - function emergencyRecovery( - address _authorizedAddress, - address _cosigner - ) external onlyRecoveryAddress { - require( - _authorizedAddress != address(0), - "Authorized addresses must not be zero." - ); - require( - _authorizedAddress != recoveryAddress, - "Do not use the recovery address as an authorized address." - ); - require(_cosigner != address(0), "The cosigner must not be zero."); + function emergencyRecovery(address _authorizedAddress, uint256 _cosigner) external onlyRecoveryAddress { + require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); + require(_authorizedAddress != recoveryAddress, "Do not use the recovery address as an authorized address."); + require(address(uint160(_cosigner)) != address(0), "The cosigner must not be zero."); // Incrementing the authVersion number effectively erases the authorizations mapping. See the comments // on the authorizations variable (above) for more information. authVersion += AUTH_VERSION_INCREMENTOR; // Store the new signer/cosigner pair as the only remaining authorized address - authorizations[ - authVersion + uint256(uint160(_authorizedAddress)) - ] = _cosigner; + authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; emit EmergencyRecovery(_authorizedAddress, _cosigner); } - function emergencyRecovery2( - address _authorizedAddress, - address _cosigner, - address _recoveryAddress - ) external onlyRecoveryAddress { - require( - _authorizedAddress != address(0), - "Authorized addresses must not be zero." - ); - require( - _authorizedAddress != _recoveryAddress, - "Do not use the recovery address as an authorized address." - ); - require(_cosigner != address(0), "The cosigner must not be zero."); + function emergencyRecovery2(address _authorizedAddress, uint256 _cosigner, address _recoveryAddress) + external + onlyRecoveryAddress + { + require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); + require(_authorizedAddress != _recoveryAddress, "Do not use the recovery address as an authorized address."); + require(address(uint160(_cosigner)) != address(0), "The cosigner must not be zero."); // Incrementing the authVersion number effectively erases the authorizations mapping. See the comments // on the authorizations variable (above) for more information. authVersion += AUTH_VERSION_INCREMENTOR; // Store the new signer/cosigner pair as the only remaining authorized address - authorizations[ - authVersion + uint256(uint160(_authorizedAddress)) - ] = _cosigner; + authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; // set new recovery address address previous = recoveryAddress; @@ -456,9 +366,7 @@ contract CoreWallet is IERC1271{ /// @param _recoveryAddress the new recovery address function setRecoveryAddress(address _recoveryAddress) external onlyInvoked { require( - address( - authorizations[authVersion + uint256(uint160(_recoveryAddress))] - ) == address(0), + address(uint160(authorizations[authVersion + uint256(uint160(_recoveryAddress))])) == address(0), "Do not use an authorized address as the recovery address." ); @@ -476,22 +384,14 @@ contract CoreWallet is IERC1271{ /// @param _keys the authorization keys to delete function recoverGas(uint256 _version, address[] calldata _keys) external { // TODO: should this be 0xffffffffffffffffffffffff ? - require( - _version > 0 && _version < 0xffffffff, - "Invalid version number." - ); + require(_version > 0 && _version < 0xffffffff, "Invalid version number."); uint256 shiftedVersion = _version << 160; - require( - shiftedVersion < authVersion, - "You can only recover gas from expired authVersions." - ); + require(shiftedVersion < authVersion, "You can only recover gas from expired authVersions."); for (uint256 i = 0; i < _keys.length; ++i) { - delete ( - authorizations[shiftedVersion + uint256(uint160(_keys[i]))] - ); + delete(authorizations[shiftedVersion + uint256(uint160(_keys[i]))]); } } @@ -505,20 +405,14 @@ contract CoreWallet is IERC1271{ /// the following tightly packed arguments: `0x19,0x0,wallet_address,hash` /// @param _signature Signature byte array associated with `_data` /// @return Magic value `0x1626ba7e` upon success, 0 otherwise. - function isValidSignature( - bytes32 hash, - bytes calldata _signature - ) external view returns (bytes4) { + function isValidSignature(bytes32 hash, bytes calldata _signature) external view returns (bytes4) { // We 'hash the hash' for the following reasons: // 1. `hash` is not the hash of an Ethereum transaction // 2. signature must target this wallet to avoid replaying the signature for another wallet // with the same key // 3. Gnosis does something similar: // https://github.com/gnosis/safe-contracts/blob/102e632d051650b7c4b0a822123f449beaf95aed/contracts/GnosisSafe.sol - - bytes32 operationHash = keccak256( - abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, hash) - ); + bytes32 operationHash = keccak256(abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, hash)); bytes32[2] memory r; bytes32[2] memory s; @@ -551,47 +445,13 @@ contract CoreWallet is IERC1271{ } // check to see if this is an authorized key - if ( - address(authorizations[authVersion + uint256(uint160(signer))]) != - cosigner - ) { + if (address(uint160(authorizations[authVersion + uint256(uint160(signer))])) != cosigner) { return 0; } return IERC1271.isValidSignature.selector; } - /// @notice Query if this contract implements an interface. This function takes into account - /// interfaces we implement dynamically through delegates. For interfaces that are just a - /// single method, using `setDelegate` will result in that method's ID returning true from - /// `supportsInterface`. For composite interfaces that are composed of multiple functions, it is - /// necessary to add the interface ID manually with `setDelegate(interfaceID, - /// COMPOSITE_PLACEHOLDER)` - /// IN ADDITION to adding each function of the interface as usual. - /// @param interfaceID The interface identifier, as specified in ERC-165 - /// @dev Interface identification is specified in ERC-165. This function - /// uses less than 30,000 gas. - /// @return `true` if the contract implements `interfaceID` and - /// `interfaceID` is not 0xffffffff, `false` otherwise - // function supportsInterface( - // bytes4 interfaceID - // ) external view returns (bool) { - // // First check if the ID matches one of the interfaces we support statically. - // if ( - // interfaceID == this.supportsInterface.selector || // ERC165 - // // interfaceID == ERC721_RECEIVED_FINAL || // ERC721 Final - // // interfaceID == ERC721_RECEIVED_DRAFT || // ERC721 Draft - // interfaceID == ERC223_ID // ERC223 - // // interfaceID == ERC1155_TOKEN_RECIEVER || // ERC1155 Token Reciever - // // interfaceID == ERC1271_VALIDSIGNATURE // ERC1271 - // ) { - // return true; - // } - // // If we don't support the interface statically, check whether we have added - // // dynamic support for it. - // return uint256(delegates[interfaceID]) > 0; - // } - /// @notice A version of `invoke()` that has no explicit signatures, and uses msg.sender /// as both the signer and cosigner. Will only succeed if `msg.sender` is an authorized /// signer for this wallet, with no cosigner, saving transaction size and gas in that case. @@ -603,9 +463,7 @@ contract CoreWallet is IERC1271{ // The operation should be approved if the signer address has no cosigner (i.e. signer == cosigner) require( - address( - authorizations[authVersion + uint256(uint160(msg.sender))] - ) == msg.sender, + address(uint160(authorizations[authVersion + uint256(uint160(msg.sender))])) == msg.sender, "Invalid authorization." ); @@ -633,16 +491,8 @@ contract CoreWallet is IERC1271{ require(v == 27 || v == 28, "Invalid signature version."); // calculate hash - bytes32 operationHash = keccak256( - abi.encodePacked( - EIP191_PREFIX, - EIP191_VERSION_DATA, - this, - nonce, - authorizedAddress, - data - ) - ); + bytes32 operationHash = + keccak256(abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, nonce, authorizedAddress, data)); // recover signer address signer = ecrecover(operationHash, v, r, s); @@ -654,22 +504,14 @@ contract CoreWallet is IERC1271{ require(nonce > nonces[signer], "must use valid nonce for signer"); // check signer - require( - signer == authorizedAddress, - "authorized addresses must be equal" - ); + require(signer == authorizedAddress, "authorized addresses must be equal"); // Get cosigner - address requiredCosigner = address( - authorizations[authVersion + uint256(uint160(signer))] - ); + address requiredCosigner = address(uint160(authorizations[authVersion + uint256(uint160(signer))])); // The operation should be approved if the signer address has no cosigner (i.e. signer == cosigner) or // if the actual cosigner matches the required cosigner. - require( - requiredCosigner == signer || requiredCosigner == msg.sender, - "Invalid authorization." - ); + require(requiredCosigner == signer || requiredCosigner == msg.sender, "Invalid authorization."); // increment nonce to prevent replay attacks nonces[signer] = nonce; @@ -684,12 +526,7 @@ contract CoreWallet is IERC1271{ /// @param r the r value for the signature /// @param s the s value for the signature /// @param data The data containing the transactions to be invoked; see internalInvoke for details. - function invoke1SignerSends( - uint8 v, - bytes32 r, - bytes32 s, - bytes calldata data - ) external { + function invoke1SignerSends(uint8 v, bytes32 r, bytes32 s, bytes calldata data) external { // check signature version // `ecrecover` will in fact return 0 if given invalid // so perhaps this check is redundant @@ -698,16 +535,8 @@ contract CoreWallet is IERC1271{ uint256 nonce = nonces[msg.sender]; // calculate hash - bytes32 operationHash = keccak256( - abi.encodePacked( - EIP191_PREFIX, - EIP191_VERSION_DATA, - this, - nonce, - msg.sender, - data - ) - ); + bytes32 operationHash = + keccak256(abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, nonce, msg.sender, data)); // recover cosigner address cosigner = ecrecover(operationHash, v, r, s); @@ -716,16 +545,11 @@ contract CoreWallet is IERC1271{ require(cosigner != address(0), "Invalid signature."); // Get required cosigner - address requiredCosigner = address( - authorizations[authVersion + uint256(uint160(msg.sender))] - ); + address requiredCosigner = address(uint160(authorizations[authVersion + uint256(uint160(msg.sender))])); // The operation should be approved if the signer address has no cosigner (i.e. signer == cosigner) or // if the actual cosigner matches the required cosigner. - require( - requiredCosigner == cosigner || requiredCosigner == msg.sender, - "Invalid authorization." - ); + require(requiredCosigner == cosigner || requiredCosigner == msg.sender, "Invalid authorization."); // increment nonce to prevent replay attacks nonces[msg.sender] = nonce + 1; @@ -755,16 +579,8 @@ contract CoreWallet is IERC1271{ require(v[0] == 27 || v[0] == 28, "invalid signature version v[0]"); require(v[1] == 27 || v[1] == 28, "invalid signature version v[1]"); - bytes32 operationHash = keccak256( - abi.encodePacked( - EIP191_PREFIX, - EIP191_VERSION_DATA, - this, - nonce, - authorizedAddress, - data - ) - ); + bytes32 operationHash = + keccak256(abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, nonce, authorizedAddress, data)); // recover signer and cosigner address signer = ecrecover(operationHash, v[0], r[0], s[0]); @@ -775,25 +591,17 @@ contract CoreWallet is IERC1271{ require(cosigner != address(0), "Invalid signature for cosigner."); // check signer address - require( - signer == authorizedAddress, - "authorized addresses must be equal" - ); + require(signer == authorizedAddress, "authorized addresses must be equal"); // check nonces require(nonce > nonces[signer], "must use valid nonce for signer"); // Get Mapping - address requiredCosigner = address( - authorizations[authVersion + uint256(uint160(signer))] - ); + address requiredCosigner = address(uint160(authorizations[authVersion + uint256(uint160(signer))])); // The operation should be approved if the signer address has no cosigner (i.e. signer == cosigner) or // if the actual cosigner matches the required cosigner. - require( - requiredCosigner == signer || requiredCosigner == cosigner, - "Invalid authorization." - ); + require(requiredCosigner == signer || requiredCosigner == cosigner, "Invalid authorization."); // increment nonce to prevent replay attacks nonces[signer] = nonce; @@ -844,11 +652,7 @@ contract CoreWallet is IERC1271{ memPtr := add(memPtr, 1) // Loop through data, parsing out the various sub-operations - for { - - } lt(memPtr, endPtr) { - - } { + for {} lt(memPtr, endPtr) {} { // Load the length of the call data of the current operation // 52 = to(20) + value(32) let len := mload(add(memPtr, 52)) @@ -863,10 +667,7 @@ contract CoreWallet is IERC1271{ // See https://github.com/sc-forks/solidity-coverage/issues/287 if gt(opEnd, endPtr) { // The computed end of this operation goes past the end of the data buffer. Not good! - revert( - add(invalidLengthMessage, 32), - mload(invalidLengthMessage) - ) + revert(add(invalidLengthMessage, 32), mload(invalidLengthMessage)) } // NOTE: Code that is compatible with solidity-coverage // switch gt(opEnd, endPtr) @@ -882,22 +683,9 @@ contract CoreWallet is IERC1271{ // - use the previously loaded len field as the size of the call data // - make the call (passing all remaining gas to the child call) // - check the result (0 == reverted) - if eq( - 0, - call( - gas(), - shr(96, mload(memPtr)), - mload(add(memPtr, 20)), - add(memPtr, 84), - len, - 0, - 0 - ) - ) { + if eq(0, call(gas(), shr(96, mload(memPtr)), mload(add(memPtr, 20)), add(memPtr, 84), len, 0, 0)) { switch revertFlag - case 1 { - revert(add(callFailed, 32), mload(callFailed)) - } + case 1 { revert(add(callFailed, 32), mload(callFailed)) } default { // mark this operation as failed // create the appropriate bit, 'or' with previous diff --git a/test/testutils.ts b/test/testutils.ts index 158cadf..74c7a46 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -108,7 +108,7 @@ export function getAccountInitCode (authorizedAddress: string, cosignerAddress: // given the parameters as AccountDeployer, return the resulting "counterfactual address" that it would create. export async function getAccountAddress (authorizedAddress: string, cosignerAddress: string, recoveryAddress: string, factory: BloctoAccountFactory, salt = 0): Promise { - return await factory.getAddress(authorizedAddress, cosignerAddress, recoveryAddress, salt) + return await factory.getAddress(cosignerAddress, recoveryAddress, salt) } const panicCodes: { [key: number]: string } = { @@ -250,7 +250,7 @@ export async function createAccount ( authorizedAddresses: string, cosignerAddresses: string, recoverAddresses: string, - salt: BytesLike, + salt: BigNumberish, accountFactory: BloctoAccountFactory ): Promise { await accountFactory.createAccount(authorizedAddresses, cosignerAddresses, recoverAddresses, salt) From 40fe3b109327adc02a2820782fcb1023e263128e Mon Sep 17 00:00:00 2001 From: kbehouse Date: Tue, 23 May 2023 11:20:26 +0800 Subject: [PATCH 06/26] wallet: add emit Received(msg.sender, msg.value); in receive() --- contracts/CoreWallet/CoreWallet.sol | 9 ++++++--- test/bloctoaccount.test.ts | 28 +++++++++++++++++++++++++--- test/testutils.ts | 4 ---- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol index aae6495..bd07b7d 100644 --- a/contracts/CoreWallet/CoreWallet.sol +++ b/contracts/CoreWallet/CoreWallet.sol @@ -231,8 +231,7 @@ contract CoreWallet is IERC1271 { /// transaction, when someone calls a method we have dynamically added a delegate for, or when someone /// tries to call a function we don't implement, either statically or dynamically. /// - /// A correct invocation of this method occurs in two cases: - /// - someone transfers ETH to this wallet (`msg.data.length` is 0) + /// A correct invocation of this method occurs in following case: /// - someone calls a delegated function (`msg.data.length` is greater than 0 and /// `delegates[msg.sig]` is set) /// In all other cases, this function will revert. @@ -266,7 +265,11 @@ contract CoreWallet is IERC1271 { } // solhint-disable-next-line no-empty-blocks - receive() external payable {} + receive() external payable { + if (msg.value > 0) { + emit Received(msg.sender, msg.value); + } + } /// @notice Adds or removes dynamic support for an interface. Can be used in 3 ways: /// - Add a contract "delegate" that implements a single function diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index eeaa76a..424a14c 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -45,13 +45,13 @@ describe('BloctoAccount Upgrade Test', function () { let entryPoint: EntryPoint - async function testCreateAccount (salt: string): Promise { + async function testCreateAccount (salt: number): Promise { const account = await createAccount( ethersSigner, await authorizedWallet.getAddress(), await cosignerWallet.getAddress(), await recoverWallet.getAddress(), - salt, + BigNumber.from(salt), factory ) await fund(account) @@ -76,8 +76,30 @@ describe('BloctoAccount Upgrade Test', function () { // erc20 = await new TestERC20__factory(ethersSigner).deploy('Test ERC20', 'T20', 18) }) + describe('wallet function', () => { + const AccountSalt = 123 + let account: BloctoAccount + before(async () => { + account = await testCreateAccount(AccountSalt) + }) + + it('should receive native token', async () => { + const beforeRecevive = await ethers.provider.getBalance(account.address) + const [owner] = await ethers.getSigners() + + const tx = await owner.sendTransaction({ + to: account.address, + value: ONE_ETH // Sends exactly 1.0 ether + }) + const receipt = await tx.wait() + const receivedSelector = ethers.utils.id('Received(address,uint256)') + expect(receipt.logs[0].topics[0]).to.equal(receivedSelector) + expect(await ethers.provider.getBalance(account.address)).to.equal(beforeRecevive.add(ONE_ETH)) + }) + }) + describe('should upgrade to different version implementation', () => { - const AccountSalt = '0x4eb84e5765b53776863ffa7c4965af012ded5be4000000000000000000000002' + const AccountSalt = 12345 const MockEntryPointV070 = '0x000000000000000000000000000000000000E070' let account: BloctoAccount let implementationV140: TestBloctoAccountCloneableWalletV140 diff --git a/test/testutils.ts b/test/testutils.ts index 74c7a46..b1c31d7 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -73,10 +73,6 @@ export function createAuthorizedCosignerRecoverWallet (): [Wallet, Wallet, Walle return [createTmpAccount(), createTmpAccount(), createTmpAccount()] } -export function createAddress (): string { - return createAccountOwner().address -} - export function callDataCost (data: string): number { return ethers.utils.arrayify(data) .map(x => x === 0 ? 4 : 16) From 07cf787738925cb79dbb1d0e4ea8405b4f654a6f Mon Sep 17 00:00:00 2001 From: kbehouse Date: Tue, 23 May 2023 15:24:28 +0800 Subject: [PATCH 07/26] test: add test/entrypoint/ and #simulateValidation --- test/entrypoint/UserOp.ts | 222 +++++++++++++++++ test/entrypoint/UserOperation.ts | 16 ++ test/entrypoint/aa.init.ts | 6 + test/entrypoint/chaiHelper.ts | 65 +++++ test/entrypoint/debugTx.ts | 26 ++ test/entrypoint/entrypoint.test.ts | 356 ++++++++++++++++++++++++++++ test/entrypoint/entrypoint_utils.ts | 29 +++ test/entrypoint/solidityTypes.ts | 10 + test/testutils.ts | 13 +- 9 files changed, 738 insertions(+), 5 deletions(-) create mode 100644 test/entrypoint/UserOp.ts create mode 100644 test/entrypoint/UserOperation.ts create mode 100644 test/entrypoint/aa.init.ts create mode 100644 test/entrypoint/chaiHelper.ts create mode 100644 test/entrypoint/debugTx.ts create mode 100644 test/entrypoint/entrypoint.test.ts create mode 100644 test/entrypoint/entrypoint_utils.ts create mode 100644 test/entrypoint/solidityTypes.ts diff --git a/test/entrypoint/UserOp.ts b/test/entrypoint/UserOp.ts new file mode 100644 index 0000000..0659fb9 --- /dev/null +++ b/test/entrypoint/UserOp.ts @@ -0,0 +1,222 @@ +import { + arrayify, + defaultAbiCoder, + hexDataSlice, + keccak256 +} from 'ethers/lib/utils' +import { BigNumber, Contract, Signer, Wallet } from 'ethers' +import { AddressZero, callDataCost, rethrow } from '../testutils' +import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util' +import { + EntryPoint +} from '../../typechain' +import { UserOperation } from './UserOperation' +import { Create2Factory } from '../../src/Create2Factory' + +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]) + } +} + +export function packUserOp1 (op: UserOperation): string { + return defaultAbiCoder.encode([ + 'address', // sender + 'uint256', // nonce + 'bytes32', // initCode + 'bytes32', // callData + 'uint256', // callGasLimit + 'uint256', // verificationGasLimit + 'uint256', // preVerificationGas + 'uint256', // maxFeePerGas + 'uint256', // maxPriorityFeePerGas + 'bytes32' // paymasterAndData + ], [ + op.sender, + op.nonce, + keccak256(op.initCode), + keccak256(op.callData), + op.callGasLimit, + op.verificationGasLimit, + op.preVerificationGas, + op.maxFeePerGas, + op.maxPriorityFeePerGas, + keccak256(op.paymasterAndData) + ]) +} + +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 const DefaultsForUserOp: UserOperation = { + sender: AddressZero, + nonce: 0, + initCode: '0x', + callData: '0x', + callGasLimit: 0, + verificationGasLimit: 150000, // default verification gas. will add create2 cost (3200+200*length) if initCode exists + preVerificationGas: 21000, // should also cover calldata cost. + maxFeePerGas: 0, + maxPriorityFeePerGas: 1e9, + paymasterAndData: '0x', + signature: '0x' +} + +export function signUserOp (op: UserOperation, signer: Wallet, entryPoint: string, chainId: number): UserOperation { + const message = getUserOpHash(op, entryPoint, chainId) + const msg1 = Buffer.concat([ + Buffer.from('\x19Ethereum Signed Message:\n32', 'ascii'), + Buffer.from(arrayify(message)) + ]) + + const sig = ecsign(keccak256_buffer(msg1), Buffer.from(arrayify(signer.privateKey))) + // that's equivalent of: await signer.signMessage(message); + // (but without "async" + const signedMessage1 = toRpcSig(sig.v, sig.r, sig.s) + return { + ...op, + signature: signedMessage1 + } +} + +export function fillUserOpDefaults (op: Partial, defaults = DefaultsForUserOp): UserOperation { + const partial: any = { ...op } + // we want "item:undefined" to be used from defaults, and not override defaults, so we must explicitly + // remove those so "merge" will succeed. + for (const key in partial) { + if (partial[key] == null) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete partial[key] + } + } + const filled = { ...defaults, ...partial } + return filled +} + +// helper to fill structure: +// - default callGasLimit to estimate call from entryPoint to account (TODO: add overhead) +// if there is initCode: +// - calculate sender by eth_call the deployment code +// - default verificationGasLimit estimateGas of deployment code plus default 100000 +// no initCode: +// - update nonce from account.getNonce() +// entryPoint param is only required to fill in "sender address when specifying "initCode" +// nonce: assume contract as "getNonce()" function, and fill in. +// sender - only in case of construction: fill sender from initCode. +// callGasLimit: VERY crude estimation (by estimating call to account, and add rough entryPoint overhead +// verificationGasLimit: hard-code default at 100k. should add "create2" cost +export async function fillUserOp (op: Partial, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { + const op1 = { ...op } + const provider = entryPoint?.provider + if (op.initCode != null) { + const initAddr = hexDataSlice(op1.initCode!, 0, 20) + const initCallData = hexDataSlice(op1.initCode!, 20) + if (op1.nonce == null) op1.nonce = 0 + if (op1.sender == null) { + // hack: if the init contract is our known deployer, then we know what the address would be, without a view call + if (initAddr.toLowerCase() === Create2Factory.contractAddress.toLowerCase()) { + const ctr = hexDataSlice(initCallData, 32) + const salt = hexDataSlice(initCallData, 0, 32) + op1.sender = Create2Factory.getDeployedAddress(ctr, salt) + } else { + // console.log('\t== not our deployer. our=', Create2Factory.contractAddress, 'got', initAddr) + if (provider == null) throw new Error('no entrypoint/provider') + op1.sender = await entryPoint!.callStatic.getSenderAddress(op1.initCode!).catch(e => e.errorArgs.sender) + } + } + if (op1.verificationGasLimit == null) { + if (provider == null) throw new Error('no entrypoint/provider') + const initEstimate = await provider.estimateGas({ + from: entryPoint?.address, + to: initAddr, + data: initCallData, + gasLimit: 10e6 + }) + op1.verificationGasLimit = BigNumber.from(DefaultsForUserOp.verificationGasLimit).add(initEstimate) + } + } + if (op1.nonce == null) { + if (provider == null) throw new Error('must have entryPoint to autofill nonce') + const c = new Contract(op.sender!, [`function ${getNonceFunction}() view returns(uint256)`], provider) + op1.nonce = await c[getNonceFunction]().catch(rethrow()) + } + if (op1.callGasLimit == null && op.callData != null) { + if (provider == null) throw new Error('must have entryPoint for callGasLimit estimate') + const gasEtimated = await provider.estimateGas({ + from: entryPoint?.address, + to: op1.sender, + data: op1.callData + }) + + // console.log('estim', op1.sender,'len=', op1.callData!.length, 'res=', gasEtimated) + // estimateGas assumes direct call from entryPoint. add wrapper cost. + op1.callGasLimit = gasEtimated // .add(55000) + } + if (op1.maxFeePerGas == null) { + if (provider == null) throw new Error('must have entryPoint to autofill maxFeePerGas') + const block = await provider.getBlock('latest') + op1.maxFeePerGas = block.baseFeePerGas!.add(op1.maxPriorityFeePerGas ?? DefaultsForUserOp.maxPriorityFeePerGas) + } + // TODO: this is exactly what fillUserOp below should do - but it doesn't. + // adding this manually + if (op1.maxPriorityFeePerGas == null) { + op1.maxPriorityFeePerGas = DefaultsForUserOp.maxPriorityFeePerGas + } + const op2 = fillUserOpDefaults(op1) + // eslint-disable-next-line @typescript-eslint/no-base-to-string + if (op2.preVerificationGas.toString() === '0') { + // TODO: we don't add overhead, which is ~21000 for a single TX, but much lower in a batch. + op2.preVerificationGas = callDataCost(packUserOp(op2, false)) + } + return op2 +} + +export async function fillAndSign (op: Partial, signer: Wallet | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { + const provider = entryPoint?.provider + const op2 = await fillUserOp(op, entryPoint, getNonceFunction) + + const chainId = await provider!.getNetwork().then(net => net.chainId) + const message = arrayify(getUserOpHash(op2, entryPoint!.address, chainId)) + + return { + ...op2, + signature: await signer.signMessage(message) + } +} + +export async function fillAndSignWithCoSigner (op: Partial, signer: Wallet | Signer, cosigner: Wallet | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { + const provider = entryPoint?.provider + const op2 = await fillUserOp(op, entryPoint) + + const chainId = await provider!.getNetwork().then(net => net.chainId) + const message = arrayify(getUserOpHash(op2, entryPoint!.address, chainId)) + + const signerSignature = await signer.signMessage(message) + const cosignerSignature = await cosigner.signMessage(message) + const signature = signerSignature + cosignerSignature.slice(2) + + return { + ...op2, + signature: signature + } +} diff --git a/test/entrypoint/UserOperation.ts b/test/entrypoint/UserOperation.ts new file mode 100644 index 0000000..8bed5ab --- /dev/null +++ b/test/entrypoint/UserOperation.ts @@ -0,0 +1,16 @@ +import * as typ from './solidityTypes' + +export interface UserOperation { + + sender: typ.address + nonce: typ.uint256 + initCode: typ.bytes + callData: typ.bytes + callGasLimit: typ.uint256 + verificationGasLimit: typ.uint256 + preVerificationGas: typ.uint256 + maxFeePerGas: typ.uint256 + maxPriorityFeePerGas: typ.uint256 + paymasterAndData: typ.bytes + signature: typ.bytes +} diff --git a/test/entrypoint/aa.init.ts b/test/entrypoint/aa.init.ts new file mode 100644 index 0000000..f796cdc --- /dev/null +++ b/test/entrypoint/aa.init.ts @@ -0,0 +1,6 @@ +import './chaiHelper' + +const ethers = require('ethers') +export const inspect_custom_symbol = Symbol.for('nodejs.util.inspect.custom') +// @ts-ignore +ethers.BigNumber.prototype[inspect_custom_symbol] = function () { return `BigNumber ${parseInt(this._hex)}` } diff --git a/test/entrypoint/chaiHelper.ts b/test/entrypoint/chaiHelper.ts new file mode 100644 index 0000000..f272168 --- /dev/null +++ b/test/entrypoint/chaiHelper.ts @@ -0,0 +1,65 @@ +// remap "eql" function to work nicely with EVM values. + +// cleanup "Result" object (returned on web3/ethers calls) +// remove "array" members, convert values to strings. +// so Result obj like +// { '0': "a", '1': 20, first: "a", second: 20 } +// becomes: +// { first: "a", second: "20" } +// map values inside object using mapping func. +import chai from 'chai' + +export function objValues (obj: { [key: string]: any }, mapFunc: (val: any, key?: string) => any): any { + return Object.keys(obj) + .filter(key => key.match(/^[\d_]/) == null) + .reduce((set, key) => ({ + ...set, + [key]: mapFunc(obj[key], key) + }), {}) +} + +/** + * cleanup a value of an object, for easier testing. + * - Result: this is an array which also contains named members. + * - obj.length*2 == Object.keys().length + * - remove the array elements, use just the named ones. + * - recursively handle inner members of object, arrays. + * - attempt toString. but if no normal value, recurse into fields. + */ +export function cleanValue (val: any): any { + if (val == null) return val + if (Array.isArray(val)) { + if (val.length * 2 === Object.keys(val).length) { + // "looks" like a Result object. + return objValues(val, cleanValue) + } + // its a plain array. map each array element + return val.map(val1 => cleanValue(val1)) + } + + const str = val.toString() + if (str !== '[object Object]') { return str } + + return objValues(val, cleanValue) +} + +// use cleanValue for comparing. MUCH easier, since numbers compare well with bignumbers, etc + +chai.Assertion.overwriteMethod('eql', (original) => { + // @ts-ignore + return function (this: any, expected: any) { + const _actual = cleanValue(this._obj) + const _expected = cleanValue(expected) + // original.apply(this,arguments) + this._obj = _actual + original.apply(this, [_expected]) + // assert.deepEqual(_actual, _expected) + // ctx.assert( + // _actual == _expected, + // 'expected #{act} to equal #{exp}', + // 'expected #{act} to be different from #{exp}', + // _expected, + // _actual + // ); + } +}) diff --git a/test/entrypoint/debugTx.ts b/test/entrypoint/debugTx.ts new file mode 100644 index 0000000..dc22598 --- /dev/null +++ b/test/entrypoint/debugTx.ts @@ -0,0 +1,26 @@ +import { ethers } from 'hardhat' + +export interface DebugLog { + pc: number + op: string + gasCost: number + depth: number + stack: string[] + memory: string[] +} + +export interface DebugTransactionResult { + gas: number + failed: boolean + returnValue: string + structLogs: DebugLog[] +} + +export async function debugTransaction (txHash: string, disableMemory = true, disableStorage = true): Promise { + const debugTx = async (hash: string): Promise => await ethers.provider.send('debug_traceTransaction', [hash, { + disableMemory, + disableStorage + }]) + + return await debugTx(txHash) +} diff --git a/test/entrypoint/entrypoint.test.ts b/test/entrypoint/entrypoint.test.ts new file mode 100644 index 0000000..5da558d --- /dev/null +++ b/test/entrypoint/entrypoint.test.ts @@ -0,0 +1,356 @@ +import './aa.init' +import { BigNumber, Event, Wallet } from 'ethers' +import { expect } from 'chai' +import { + EntryPoint, + BloctoAccount, + BloctoAccountFactory, + BloctoAccountCloneableWallet, + BloctoAccountCloneableWallet__factory, + BloctoAccountFactory, + BloctoAccountFactory__factory, + TestAggregatedAccount__factory, + TestAggregatedAccountFactory__factory, + TestCounter, + TestCounter__factory, + TestExpirePaymaster, + TestExpirePaymaster__factory, + TestExpiryAccount, + TestExpiryAccount__factory, + TestPaymasterAcceptAll, + TestPaymasterAcceptAll__factory, + TestRevertAccount__factory, + TestAggregatedAccount, + TestSignatureAggregator, + TestSignatureAggregator__factory, + MaliciousAccount__factory, + TestWarmColdAccount__factory +} from '../../typechain' +import { + AddressZero, + createAccountOwner, + fund, + checkForGeth, + rethrow, + tostr, + getAccountInitCode, + calcGasUsage, + ONE_ETH, + TWO_ETH, + deployEntryPoint, + getBalance, + createAddress, + getAccountAddress, + HashZero, + simulationResultCatch, + createAccount, + getAggregatedAccountInitCode, + simulationResultWithAggregationCatch, decodeRevertReason, + createAuthorizedCosignerRecoverWallet +} from '../testutils' +import { checkForBannedOps } from './entrypoint_utils' +import { DefaultsForUserOp, getUserOpHash, fillAndSignWithCoSigner } from './UserOp' +import { UserOperation } from './UserOperation' +import { PopulatedTransaction } from 'ethers/lib/ethers' +import { ethers } from 'hardhat' +import { arrayify, defaultAbiCoder, hexConcat, hexZeroPad, parseEther } from 'ethers/lib/utils' +import { BytesLike } from '@ethersproject/bytes' +import { toChecksumAddress } from 'ethereumjs-util' + +describe('EntryPoint', function () { + let entryPoint: EntryPoint + let BloctoAccountFactory: BloctoAccountFactory + + let authorizedWallet: Wallet + let cosignerWallet: Wallet + let recoverWallet: Wallet + const ethersSigner = ethers.provider.getSigner() + let account: BloctoAccount + + let implementation: string + let factory: BloctoAccountFactory + + const globalUnstakeDelaySec = 2 + const paymasterStake = ethers.utils.parseEther('2') + + before(async function () { + this.timeout(20000) + await checkForGeth() + + const chainId = await ethers.provider.getNetwork().then(net => net.chainId) + + entryPoint = await deployEntryPoint() + + // v1 implementation + implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy(entryPoint.address)).address + + // account factory + factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation); + + [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() + account = await createAccount( + ethersSigner, + await authorizedWallet.getAddress(), + await cosignerWallet.getAddress(), + await recoverWallet.getAddress(), + BigNumber.from(0), + factory + ) + await fund(account) + + // sanity: validate helper functions + const sampleOp = await fillAndSignWithCoSigner( + { sender: account.address }, + authorizedWallet, + cosignerWallet, + entryPoint + ) + expect(getUserOpHash(sampleOp, entryPoint.address, chainId)).to.eql(await entryPoint.getUserOpHash(sampleOp)) + }) + + describe('#simulateValidation', () => { + let account1: BloctoAccount + let authorizedWallet1: Wallet + let cosignerWallet1: Wallet + let recoverWallet1: Wallet + + before(async () => { + [authorizedWallet1, cosignerWallet1, recoverWallet1] = createAuthorizedCosignerRecoverWallet() + account1 = await createAccount( + ethersSigner, + await authorizedWallet1.getAddress(), + await cosignerWallet1.getAddress(), + await recoverWallet1.getAddress(), + 0, + factory) + }) + it('should fail if validateUserOp fails', async () => { + // using wrong nonce + const op = await fillAndSignWithCoSigner( + { sender: account.address, nonce: 1234 }, + authorizedWallet, + cosignerWallet, + entryPoint + ) + await expect(entryPoint.callStatic.simulateValidation(op)).to + .revertedWith('AA25 invalid account nonce') + }) + + it('should report signature failure without revert', async () => { + // (this is actually a feature of the wallet, not the entrypoint) + // using wrong owner for account1 + // (zero gas price so it doesn't fail on prefund) + const op = await fillAndSignWithCoSigner( + { sender: account1.address, maxFeePerGas: 0 }, + authorizedWallet, + cosignerWallet, + entryPoint + ) + + const { returnInfo } = await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) + expect(returnInfo.sigFailed).to.be.true + }) + + it('should revert if wallet not deployed (and no initcode)', async () => { + const op = await fillAndSignWithCoSigner( + { + sender: createAddress(), + nonce: 0, + verificationGasLimit: 1000 + }, + authorizedWallet, + cosignerWallet, + entryPoint + ) + + await expect(entryPoint.callStatic.simulateValidation(op)).to + .revertedWith('AA20 account not deployed') + }) + + it('should revert on oog if not enough verificationGas', async () => { + const op = await fillAndSignWithCoSigner( + { sender: account.address, verificationGasLimit: 1000 }, + authorizedWallet, + cosignerWallet, + entryPoint + ) + await expect(entryPoint.callStatic.simulateValidation(op)).to + .revertedWith('AA23 reverted (or OOG)') + }) + + it('should succeed if validateUserOp succeeds', async () => { + const op = await fillAndSignWithCoSigner( + { sender: account1.address }, + authorizedWallet1, + cosignerWallet1, + entryPoint + ) + await fund(account1) + await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) + }) + + it('should return empty context if no paymaster', async () => { + const op = await fillAndSignWithCoSigner( + { sender: account1.address, maxFeePerGas: 0 }, + authorizedWallet1, + cosignerWallet1, + entryPoint + ) + const { returnInfo } = await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) + expect(returnInfo.paymasterContext).to.eql('0x') + }) + + // it('should return stake of sender', async () => { + // const stakeValue = BigNumber.from(123) + // const unstakeDelay = 3 + // // const { proxy: account2 } = await createAccount(ethersSigner, await ethersSigner.getAddress(), entryPoint.address) + // const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + // const account2 = await createAccount( + // ethersSigner, + // await authorizedWallet2.getAddress(), + // await cosignerWallet2.getAddress(), + // await recoverWallet2.getAddress(), + // 0, + // factory) + + // await fund(account2) + // await account2.execute(entryPoint.address, stakeValue, entryPoint.interface.encodeFunctionData('addStake', [unstakeDelay])) + // // const op = await fillAndSign({ sender: account2.address }, ethersSigner, entryPoint) + // const op = await fillAndSignWithCoSigner( + // { sender: account2.address }, + // authorizedWallet2, + // cosignerWallet2, + // entryPoint + // ) + // const result = await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) + // expect(result.senderInfo).to.eql({ stake: stakeValue, unstakeDelaySec: unstakeDelay }) + // }) + + it('should prevent overflows: fail if any numeric value is more than 120 bits', async () => { + const op = await fillAndSignWithCoSigner( + { + preVerificationGas: BigNumber.from(2).pow(130), + sender: account1.address + }, + authorizedWallet1, + cosignerWallet1, + entryPoint + ) + + await expect( + entryPoint.callStatic.simulateValidation(op) + ).to.revertedWith('gas values overflow') + }) + + it('should fail creation for wrong sender', async () => { + const op1 = await fillAndSignWithCoSigner({ + initCode: getAccountInitCode(factory, authorizedWallet1.address, cosignerWallet1.address, recoverWallet1.address, 0), + sender: '0x'.padEnd(42, '1'), + verificationGasLimit: 3e6 + }, + authorizedWallet1, + cosignerWallet1, + entryPoint + ) + + await expect(entryPoint.callStatic.simulateValidation(op1)) + .to.revertedWith('AA14 initCode must return sender') + }) + + it('should report failure on insufficient verificationGas (OOG) for creation', async () => { + const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + const initCode = getAccountInitCode(factory, authorizedWallet2.address, cosignerWallet2.address, recoverWallet2.address) + const sender = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) + + const op0 = await fillAndSignWithCoSigner({ + initCode: initCode, + sender: sender, + verificationGasLimit: 8e5, + maxFeePerGas: 0 + }, + authorizedWallet2, + cosignerWallet2, + entryPoint + ) + + // must succeed with enough verification gas. + await expect(entryPoint.callStatic.simulateValidation(op0, { gasLimit: 1e6 })) + .to.revertedWith('ValidationResult') + + const op1 = await fillAndSignWithCoSigner({ + initCode: initCode, + sender: sender, + verificationGasLimit: 1e5, + maxFeePerGas: 0 + }, + authorizedWallet2, + cosignerWallet2, + entryPoint + ) + await expect(entryPoint.callStatic.simulateValidation(op1, { gasLimit: 1e6 })) + .to.revertedWith('AA13 initCode failed or OOG') + }) + + it('should succeed for creating an account', async () => { + const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + const initCode = getAccountInitCode(factory, authorizedWallet2.address, cosignerWallet2.address, recoverWallet2.address) + const sender = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) + + const op1 = await fillAndSignWithCoSigner({ + initCode: initCode, + sender: sender, + verificationGasLimit: 8e5, + maxFeePerGas: 0 + }, + authorizedWallet2, + cosignerWallet2, + entryPoint + ) + await fund(op1.sender) + + await entryPoint.callStatic.simulateValidation(op1).catch(simulationResultCatch) + }) + + it('should not call initCode from entrypoint', async () => { + // a possible attack: call an account's execFromEntryPoint through initCode. This might lead to stolen funds. + const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + const account = await createAccount( + ethersSigner, + await authorizedWallet2.getAddress(), + await cosignerWallet2.getAddress(), + await recoverWallet2.getAddress(), + 0, + factory) + + const sender = createAddress() + const op1 = await fillAndSignWithCoSigner({ + initCode: hexConcat([ + account.address, + account.interface.encodeFunctionData('execute', [sender, 0, '0x']) + ]), + sender: sender, + verificationGasLimit: 1e5, + maxFeePerGas: 0 + }, authorizedWallet2, cosignerWallet2, entryPoint) + + const error = await entryPoint.callStatic.simulateValidation(op1).catch(e => e) + expect(error.message).to.match(/initCode failed or OOG/, error) + }) + + it('should not use banned ops during simulateValidation', async () => { + const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + const sender = await getAccountAddress(factory, cosignerWallet2.address, recoverWallet2.address) + const initCode = getAccountInitCode(factory, authorizedWallet2.address, cosignerWallet2.address, recoverWallet2.address) + const op1 = await fillAndSignWithCoSigner({ + initCode: initCode, + sender: sender + }, authorizedWallet2, cosignerWallet2, entryPoint) + + await fund(op1.sender) + await entryPoint.simulateValidation(op1, { gasLimit: 10e6 }).catch(e => e) + const block = await ethers.provider.getBlock('latest') + const hash = block.transactions[0] + await checkForBannedOps(hash, false) + }) + }) +}) diff --git a/test/entrypoint/entrypoint_utils.ts b/test/entrypoint/entrypoint_utils.ts new file mode 100644 index 0000000..f6e9f8d --- /dev/null +++ b/test/entrypoint/entrypoint_utils.ts @@ -0,0 +1,29 @@ +import { debugTransaction } from './debugTx' +import { expect } from 'chai' + +export async function checkForBannedOps (txHash: string, checkPaymaster: boolean): Promise { + const tx = await debugTransaction(txHash) + const logs = tx.structLogs + const blockHash = logs.map((op, index) => ({ op: op.op, index })).filter(op => op.op === 'NUMBER') + expect(blockHash.length).to.equal(2, 'expected exactly 2 call to NUMBER (Just before and after validateUserOperation)') + const validateAccountOps = logs.slice(0, blockHash[0].index - 1) + const validatePaymasterOps = logs.slice(blockHash[0].index + 1) + const ops = validateAccountOps.filter(log => log.depth > 1).map(log => log.op) + const paymasterOps = validatePaymasterOps.filter(log => log.depth > 1).map(log => log.op) + + expect(ops).to.include('POP', 'not a valid ops list: ' + JSON.stringify(ops)) // sanity + const bannedOpCodes = new Set(['GAS', 'BASEFEE', 'GASPRICE', 'NUMBER']) + expect(ops.filter((op, index) => { + // don't ban "GAS" op followed by "*CALL" + if (op === 'GAS' && (ops[index + 1].match(/CALL/) != null)) { + return false + } + return bannedOpCodes.has(op) + })).to.eql([]) + if (checkPaymaster) { + expect(paymasterOps).to.include('POP', 'not a valid ops list: ' + JSON.stringify(paymasterOps)) // sanity + expect(paymasterOps).to.not.include('BASEFEE') + expect(paymasterOps).to.not.include('GASPRICE') + expect(paymasterOps).to.not.include('NUMBER') + } +} diff --git a/test/entrypoint/solidityTypes.ts b/test/entrypoint/solidityTypes.ts new file mode 100644 index 0000000..5026ef9 --- /dev/null +++ b/test/entrypoint/solidityTypes.ts @@ -0,0 +1,10 @@ +// define the same export types as used by export typechain/ethers +import { BigNumberish } from 'ethers' +import { BytesLike } from '@ethersproject/bytes' + +export type address = string +export type uint256 = BigNumberish +export type uint = BigNumberish +export type uint48 = BigNumberish +export type bytes = BytesLike +export type bytes32 = BytesLike diff --git a/test/testutils.ts b/test/testutils.ts index b1c31d7..847f34c 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -73,6 +73,10 @@ export function createAuthorizedCosignerRecoverWallet (): [Wallet, Wallet, Walle return [createTmpAccount(), createTmpAccount(), createTmpAccount()] } +export function createAddress (): string { + return createTmpAccount().address +} + export function callDataCost (data: string): number { return ethers.utils.arrayify(data) .map(x => x === 0 ? 4 : 16) @@ -94,17 +98,16 @@ export async function calcGasUsage (rcpt: ContractReceipt, entryPoint: EntryPoin } // helper function to create the initCode to deploy the account, using our account factory. -export function getAccountInitCode (authorizedAddress: string, cosignerAddress: string, recoveryAddress: string, factory: BloctoAccountFactory, salt = 0): BytesLike { +export function getAccountInitCode (factory: BloctoAccountFactory, authorizedAddress: string, cosignerAddress: string, recoveryAddress: string, salt = 0): BytesLike { return hexConcat([ factory.address, - factory.interface.encodeFunctionData('createAccount', [authorizedAddress, cosignerAddress, recoveryAddress, salt]) + factory.interface.encodeFunctionData('createAccount', [authorizedAddress, cosignerAddress, recoveryAddress, BigNumber.from(salt)]) ]) } // given the parameters as AccountDeployer, return the resulting "counterfactual address" that it would create. -export async function getAccountAddress (authorizedAddress: string, cosignerAddress: string, recoveryAddress: string, - factory: BloctoAccountFactory, salt = 0): Promise { - return await factory.getAddress(cosignerAddress, recoveryAddress, salt) +export async function getAccountAddress (factory: BloctoAccountFactory, cosignerAddress: string, recoveryAddress: string, salt = 0): Promise { + return await factory.getAddress(cosignerAddress, recoveryAddress, BigNumber.from(salt)) } const panicCodes: { [key: number]: string } = { From 5e2dd441053c1371ae89e08fdf3ec35c807dbe95 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Wed, 24 May 2023 11:31:29 +0800 Subject: [PATCH 08/26] BloctoAccountFactory.sol: add addStake,withdrawTo(stake) functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit style: 💄 addStake style style: 💄 addStake ditto --- contracts/BloctoAccountFactory.sol | 27 ++++++++++- .../0_deploy_Account_Factory-and-addStake.ts | 48 +++++++++++++++++++ hardhat.config.ts | 11 ++--- package.json | 4 +- 4 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 deploy/0_deploy_Account_Factory-and-addStake.ts diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol index 0812401..25e1930 100644 --- a/contracts/BloctoAccountFactory.sol +++ b/contracts/BloctoAccountFactory.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.12; import "@openzeppelin/contracts/utils/Create2.sol"; // import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; +import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import "./BloctoAccountProxy.sol"; import "./BloctoAccount.sol"; @@ -13,11 +14,13 @@ contract BloctoAccountFactory is Ownable { string public constant VERSION = "1.3.0"; // address public accountImplementation; address public bloctoAccountImplementation; + IEntryPoint public entryPoint; event WalletCreated(address wallet, address authorizedAddress, bool full); - constructor(address _bloctoAccountImplementation) { + constructor(address _bloctoAccountImplementation, IEntryPoint _entryPoint) { bloctoAccountImplementation = _bloctoAccountImplementation; + entryPoint = _entryPoint; } /** @@ -57,4 +60,26 @@ contract BloctoAccountFactory is Ownable { function setImplementation(address _bloctoAccountImplementation) public onlyOwner { bloctoAccountImplementation = _bloctoAccountImplementation; } + + function setEntrypoint(IEntryPoint _entrypoint) public onlyOwner { + entryPoint = _entrypoint; + } + + /** + * withdraw value from the deposit + * @param withdrawAddress target to send to + * @param amount to withdraw + */ + function withdrawTo(address payable withdrawAddress, uint256 amount) public onlyOwner { + entryPoint.withdrawTo(withdrawAddress, amount); + } + + /** + * add stake for this factory. + * This method can also carry eth value to add to the current stake. + * @param unstakeDelaySec - the unstake delay for this factory. Can only be increased. + */ + function addStake(uint32 unstakeDelaySec) external payable onlyOwner { + entryPoint.addStake{value: msg.value}(unstakeDelaySec); + } } diff --git a/deploy/0_deploy_Account_Factory-and-addStake.ts b/deploy/0_deploy_Account_Factory-and-addStake.ts new file mode 100644 index 0000000..06fe8fe --- /dev/null +++ b/deploy/0_deploy_Account_Factory-and-addStake.ts @@ -0,0 +1,48 @@ +import { EntryPoint__factory } from '@account-abstraction/contracts' +import { BigNumber } from 'ethers' +import { ethers } from 'hardhat' + +const BloctoAccountCloneableWallet = 'BloctoAccountCloneableWallet' +const BloctoAccountFactory = 'BloctoAccountFactory' +const EntryPoint = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' +const GasLimit = 6000000 + +async function main (): Promise { + // const lockedAmount = ethers.utils.parseEther("1"); + const [owner] = await ethers.getSigners() + console.log('deploy with accoiunt: ', owner.address) + + const factory = await ethers.getContractFactory(BloctoAccountCloneableWallet) + const walletCloneable = await factory.deploy(EntryPoint, { + gasLimit: GasLimit + }) + + await walletCloneable.deployed() + + console.log(`${BloctoAccountCloneableWallet} deployed to: ${walletCloneable.address}`) + + // account factory + const AccountFactory = await ethers.getContractFactory(BloctoAccountFactory) + const accountFactory = await AccountFactory.deploy(walletCloneable.address, EntryPoint, { + gasLimit: GasLimit + }) + + await accountFactory.deployed() + + console.log(`BloctoAccountFactory deployed to: ${accountFactory.address}`) + + // add stake + const tx = await accountFactory.addStake(BigNumber.from(86400 * 3650), { value: ethers.utils.parseEther('0.1') }) + await tx.wait() + + const entrypoint = EntryPoint__factory.connect(EntryPoint, ethers.provider) + const depositInfo = await entrypoint.getDepositInfo(accountFactory.address) + console.log(depositInfo) +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/hardhat.config.ts b/hardhat.config.ts index e9e48cf..35ca823 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -46,16 +46,11 @@ const optimizedComilerSettings = { const config: HardhatUserConfig = { solidity: { compilers: [{ - version: '0.8.15', + version: '0.8.17', settings: { optimizer: { enabled: true, runs: 1000000 } } - }], - overrides: { - 'contracts/core/EntryPoint.sol': optimizedComilerSettings, - 'contracts/BloctoAccountCloneableWallet.sol': optimizedComilerSettings, - 'contracts/BloctoAccount4337/BloctoAccount4337CloneableWallet.sol': optimizedComilerSettings - } + }] }, networks: { dev: { url: 'http://localhost:8545' }, @@ -66,7 +61,7 @@ const config: HardhatUserConfig = { proxy: getNetwork1('http://localhost:8545'), // mumbai: getNetwork1('https://polygon-testnet.public.blastapi.io'), mumbai: { - url: 'https://polygon-testnet.public.blastapi.io', + url: 'https://rpc.ankr.com/polygon_mumbai', accounts: process.env.ETH_PRIVATE_KEY !== undefined ? [process.env.ETH_PRIVATE_KEY] diff --git a/package.json b/package.json index dbf16dc..4df68a8 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "deploy-bloctoaccountfactory": "hardhat run deploy/2_deploy_BloctoAccountFactory.ts", "deploy-verifyingpaymaster": "hardhat run deploy/3_deploy_VerifyingPaymaster.ts", "verify-bloctoaccountcloneable": "npx hardhat verify 0x021dca3104aa79f68efec784b56afa382b1fd7b8", - "verify-accountfactory": "npx hardhat verify 0x7f0b3252e7d0199d8c94dd07325a2a1386f78fb1 0x021dca3104aa79f68efec784b56afa382b1fd7b8", + "verify-accountfactory": "npx hardhat verify 0xC261555D0e4623BF709d3f22Bf58A6275CE4d17D 0x66d4d44e4957F70dF0127b3de2dBaD9fF9058B5B 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", "verify-verifyingpaymaster": "npx hardhat verify 0xE671dEee9c758e642d50c32e13FD5fC6D42C98F1 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 0x086443C6bA8165a684F3e316Da42D3A2F0a2330a" }, "devDependencies": { @@ -62,4 +62,4 @@ "table": "^6.8.0", "typescript": "^4.3.5" } -} +} \ No newline at end of file From 316641fa11f262250af37d882e0373d75038fe07 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Thu, 25 May 2023 12:50:29 +0800 Subject: [PATCH 09/26] comment: add comment for contracts --- contracts/BloctoAccount.sol | 26 ++++++++++++++-- contracts/BloctoAccountCloneableWallet.sol | 5 ++- contracts/BloctoAccountFactory.sol | 9 +++++- contracts/BloctoAccountProxy.sol | 4 +++ hardhat.config.ts | 36 +++------------------- package.json | 4 +-- test/bloctoaccount.test.ts | 19 ++---------- test/entrypoint/entrypoint.test.ts | 2 +- 8 files changed, 49 insertions(+), 56 deletions(-) diff --git a/contracts/BloctoAccount.sol b/contracts/BloctoAccount.sol index 796a851..b5acb7d 100644 --- a/contracts/BloctoAccount.sol +++ b/contracts/BloctoAccount.sol @@ -23,23 +23,34 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas IEntryPoint private immutable _entryPoint; + /** + * constructor for BloctoAccount + * @param anEntryPoint entrypoint address + */ constructor(IEntryPoint anEntryPoint) { _entryPoint = anEntryPoint; } /** * override from UUPSUpgradeable + * @param newImplementation implementation address */ function _authorizeUpgrade(address newImplementation) internal view override onlyInvoked { (newImplementation); } + /** + * return entrypoint + */ function entryPoint() public view virtual override returns (IEntryPoint) { return _entryPoint; } /** * execute a transaction (called directly by entryPoint) + * @param dest dest call address + * @param value value to send + * @param func the func containing the transaction to be called */ function execute(address dest, uint256 value, bytes calldata func) external { _requireFromEntryPoint(); @@ -48,6 +59,8 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas /** * execute a sequence of transactions (called directly by entryPoint) + * @param dest sequence of dest call address + * @param func sequence of the func containing transactions to be called */ function executeBatch(address[] calldata dest, bytes[] calldata func) external { _requireFromEntryPoint(); @@ -57,7 +70,12 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas } } - /// internal call for execute and executeBatch + /** + * internal call for execute and executeBatch + * @param target target call address + * @param value value to send + * @param data the data containing the transaction to be called + */ function _call(address target, uint256 value, bytes memory data) internal { (bool success, bytes memory result) = target.call{value: value}(data); if (!success) { @@ -67,7 +85,11 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas } } - /// implement validate signature method of BaseAccount + /** + * implement validate signature method of BaseAccount from etnrypoint + * @param userOp user operation including signature for validating + * @param userOpHash user operation hash + */ function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) internal virtual diff --git a/contracts/BloctoAccountCloneableWallet.sol b/contracts/BloctoAccountCloneableWallet.sol index ebe8b42..f476a9a 100644 --- a/contracts/BloctoAccountCloneableWallet.sol +++ b/contracts/BloctoAccountCloneableWallet.sol @@ -6,7 +6,10 @@ import "./BloctoAccount.sol"; /// @title BloctoAccountCloneableWallet Wallet /// @notice This contract represents a complete but non working wallet. contract BloctoAccountCloneableWallet is BloctoAccount { - /// @dev constructor that deploys a NON-FUNCTIONAL version of `BloctoAccount` + /** + * constructor that deploys a NON-FUNCTIONAL version of `BloctoAccount` + * @param anEntryPoint entrypoint address + */ constructor(IEntryPoint anEntryPoint) BloctoAccount(anEntryPoint) { initialized = true; } diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol index 25e1930..d8fdabb 100644 --- a/contracts/BloctoAccountFactory.sol +++ b/contracts/BloctoAccountFactory.sol @@ -12,7 +12,6 @@ import "./BloctoAccount.sol"; contract BloctoAccountFactory is Ownable { /// @notice This is the version of this contract. string public constant VERSION = "1.3.0"; - // address public accountImplementation; address public bloctoAccountImplementation; IEntryPoint public entryPoint; @@ -57,10 +56,18 @@ contract BloctoAccountFactory is Ownable { ); } + /** + * set the implementation of the BloctoAccountProxy + * @param _bloctoAccountImplementation target to send to + */ function setImplementation(address _bloctoAccountImplementation) public onlyOwner { bloctoAccountImplementation = _bloctoAccountImplementation; } + /** + * set the entrypoint + * @param _entrypoint target entrypoint + */ function setEntrypoint(IEntryPoint _entrypoint) public onlyOwner { entryPoint = _entrypoint; } diff --git a/contracts/BloctoAccountProxy.sol b/contracts/BloctoAccountProxy.sol index 77a2479..ca87854 100644 --- a/contracts/BloctoAccountProxy.sol +++ b/contracts/BloctoAccountProxy.sol @@ -7,6 +7,10 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract BloctoAccountProxy is ERC1967Proxy, Initializable { constructor(address _logic) ERC1967Proxy(_logic, new bytes(0)) {} + /** + * initialize BloctoAccountProxy for adding the implementation address + * @param implementation implementation address + */ function initImplementation(address implementation) public initializer { require(Address.isContract(implementation), "ERC1967: new implementation is not a contract"); StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = implementation; diff --git a/hardhat.config.ts b/hardhat.config.ts index 35ca823..f4fe06b 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -6,7 +6,7 @@ import '@openzeppelin/hardhat-upgrades' import 'hardhat-storage-layout' import 'solidity-coverage' -import * as fs from 'fs' +// import * as fs from 'fs' const { ETHERSCAN_API_KEY, // etherscan API KEY @@ -16,30 +16,6 @@ const { ARBSCAN_API_KEY // arbitrum scan API KEY } = process.env -const mnemonicFileName = process.env.MNEMONIC_FILE ?? `${process.env.HOME}/.secret/testnet-mnemonic.txt` -let mnemonic = 'test '.repeat(11) + 'junk' -if (fs.existsSync(mnemonicFileName)) { mnemonic = fs.readFileSync(mnemonicFileName, 'ascii') } - -function getNetwork1 (url: string): { url: string, accounts: { mnemonic: string } } { - return { - url, - accounts: { mnemonic } - } -} - -function getNetwork (name: string): { url: string, accounts: { mnemonic: string } } { - return getNetwork1(`https://${name}.infura.io/v3/${process.env.INFURA_ID}`) - // return getNetwork1(`wss://${name}.infura.io/ws/v3/${process.env.INFURA_ID}`) -} - -const optimizedComilerSettings = { - version: '0.8.17', - settings: { - optimizer: { enabled: true, runs: 1000000 }, - viaIR: true - } -} - // You need to export an object to set up your config // Go to https://hardhat.org/config/ to learn more @@ -53,13 +29,9 @@ const config: HardhatUserConfig = { }] }, networks: { - dev: { url: 'http://localhost:8545' }, - // github action starts localgeth service, for gas calculations - localgeth: { url: 'http://localgeth:8545' }, - goerli: getNetwork('goerli'), - sepolia: getNetwork('sepolia'), - proxy: getNetwork1('http://localhost:8545'), - // mumbai: getNetwork1('https://polygon-testnet.public.blastapi.io'), + hardhat: { + allowUnlimitedContractSize: true + }, mumbai: { url: 'https://rpc.ankr.com/polygon_mumbai', accounts: diff --git a/package.json b/package.json index 4df68a8..4e06371 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bloctoaccount", "version": "0.6.0", - "description": "BloctoAccount & BloctoAccountFactory smart contract", + "description": "BloctoAccount & BloctoAccountFactory smart contracts", "scripts": { "clean": "rm -rf cache artifacts typechain typechain-types", "tsc": "tsc", @@ -13,7 +13,7 @@ "deploy-bloctoaccountcloneable": "hardhat run deploy/1_deploy_BloctoAccount4337CloneableWallet.ts", "deploy-bloctoaccountfactory": "hardhat run deploy/2_deploy_BloctoAccountFactory.ts", "deploy-verifyingpaymaster": "hardhat run deploy/3_deploy_VerifyingPaymaster.ts", - "verify-bloctoaccountcloneable": "npx hardhat verify 0x021dca3104aa79f68efec784b56afa382b1fd7b8", + "verify-bloctoaccountcloneable": "npx hardhat verify --contract contracts/BloctoAccountCloneableWallet.sol:BloctoAccountCloneableWallet 0x66d4d44e4957F70dF0127b3de2dBaD9fF9058B5B 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", "verify-accountfactory": "npx hardhat verify 0xC261555D0e4623BF709d3f22Bf58A6275CE4d17D 0x66d4d44e4957F70dF0127b3de2dBaD9fF9058B5B 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", "verify-verifyingpaymaster": "npx hardhat verify 0xE671dEee9c758e642d50c32e13FD5fC6D42C98F1 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 0x086443C6bA8165a684F3e316Da42D3A2F0a2330a" }, diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index 424a14c..a777136 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -4,12 +4,9 @@ import { expect } from 'chai' import { BloctoAccount, BloctoAccount__factory, - BloctoAccountCloneableWallet, BloctoAccountCloneableWallet__factory, BloctoAccountFactory, BloctoAccountFactory__factory, - TestERC20, - TestERC20__factory, TestBloctoAccountCloneableWalletV140, TestBloctoAccountCloneableWalletV140__factory } from '../typechain' @@ -17,21 +14,12 @@ import { EntryPoint } from '@account-abstraction/contracts' import { fund, createAccount, - createAddress, - createAccountOwner, deployEntryPoint, - getBalance, - isDeployed, ONE_ETH, - TWO_ETH, - HashZero, createAuthorizedCosignerRecoverWallet, - getSetEntryPointCode, txData, - signMessage, - signUpgrade + signMessage } from './testutils' -// import { fillUserOpDefaults, getUserOpHash, signMessage, signUpgrade } from './UserOp' describe('BloctoAccount Upgrade Test', function () { const ethersSigner = ethers.provider.getSigner() @@ -66,14 +54,11 @@ describe('BloctoAccount Upgrade Test', function () { implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy(entryPoint.address)).address // account factory - factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation); + factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation, entryPoint.address); // 3 wallet [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() await fund(cosignerWallet.address) - - // test erc20 - // erc20 = await new TestERC20__factory(ethersSigner).deploy('Test ERC20', 'T20', 18) }) describe('wallet function', () => { diff --git a/test/entrypoint/entrypoint.test.ts b/test/entrypoint/entrypoint.test.ts index 5da558d..d5f9ad3 100644 --- a/test/entrypoint/entrypoint.test.ts +++ b/test/entrypoint/entrypoint.test.ts @@ -85,7 +85,7 @@ describe('EntryPoint', function () { implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy(entryPoint.address)).address // account factory - factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation); + factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation, entryPoint.address); [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() account = await createAccount( From 315f1364acc93fa1e54c5cac2e4b2ba2d0a7f14c Mon Sep 17 00:00:00 2001 From: kbehouse Date: Thu, 25 May 2023 19:52:38 +0800 Subject: [PATCH 10/26] createAccount2: add create account with init2 for multiple auhtorizes address deploy: update deploy method --- README.md | 11 ++----- contracts/BloctoAccountFactory.sol | 29 +++++++++++++++++++ .../0_deploy_Account_Factory-and-addStake.ts | 4 +-- package.json | 1 + test/bloctoaccount.test.ts | 20 +++++++++++++ test/entrypoint/entrypoint.test.ts | 26 +++++++++++++++++ test/testutils.ts | 8 +++++ 7 files changed, 88 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9d4a77e..9b123d8 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,10 @@ yarn test ## Deploy -deploy BloctoAccountCloneableWallet +deploy BloctoAccountCloneableWallet, BloctoAccountFactory, and addStake to BloctoAccountFactory ``` -yarn deploy-bloctoaccountcloneable --network mumbai -``` - - -deploy BloctoAccountFactory - -``` -yarn deploy-bloctoaccountfactory --network mumbai +yarn deploy --network mumbai ``` diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol index d8fdabb..6cbea63 100644 --- a/contracts/BloctoAccountFactory.sol +++ b/contracts/BloctoAccountFactory.sol @@ -46,6 +46,35 @@ contract BloctoAccountFactory is Ownable { emit WalletCreated(address(ret), _authorizedAddress, false); } + function createAccount2( + bytes memory _authorizedAddresses, + address _cosigner, + address _recoveryAddress, + uint256 _salt + ) public returns (BloctoAccount ret) { + require( + _authorizedAddresses.length / 20 > 0 && _authorizedAddresses.length % 20 == 0, "invalid address byte array" + ); + + address addr = getAddress(_cosigner, _recoveryAddress, _salt); + uint256 codeSize = addr.code.length; + if (codeSize > 0) { + return BloctoAccount(payable(addr)); + } + bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); + // for consistent address + BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); + newProxy.initImplementation(bloctoAccountImplementation); + ret = BloctoAccount(payable(address(newProxy))); + ret.init2(_authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress); + + address firstAuthorizedAddress; + assembly { + firstAuthorizedAddress := mload(add(_authorizedAddresses, 20)) + } + emit WalletCreated(address(ret), firstAuthorizedAddress, false); + } + /** * calculate the counterfactual address of this account as it would be returned by createAccount() */ diff --git a/deploy/0_deploy_Account_Factory-and-addStake.ts b/deploy/0_deploy_Account_Factory-and-addStake.ts index 06fe8fe..c1dc61e 100644 --- a/deploy/0_deploy_Account_Factory-and-addStake.ts +++ b/deploy/0_deploy_Account_Factory-and-addStake.ts @@ -10,7 +10,7 @@ const GasLimit = 6000000 async function main (): Promise { // const lockedAmount = ethers.utils.parseEther("1"); const [owner] = await ethers.getSigners() - console.log('deploy with accoiunt: ', owner.address) + console.log('deploy with account: ', owner.address) const factory = await ethers.getContractFactory(BloctoAccountCloneableWallet) const walletCloneable = await factory.deploy(EntryPoint, { @@ -37,7 +37,7 @@ async function main (): Promise { const entrypoint = EntryPoint__factory.connect(EntryPoint, ethers.provider) const depositInfo = await entrypoint.getDepositInfo(accountFactory.address) - console.log(depositInfo) + console.log('stake: ', ethers.utils.formatUnits(depositInfo.stake), ', unstakeDelaySec: ', depositInfo.unstakeDelaySec) } // We recommend this pattern to be able to use async/await everywhere diff --git a/package.json b/package.json index 4e06371..f8590fb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "lint-fix": "eslint -f unix . --fix", "lint:sol": "solhint -f unix \"contracts/**/*.sol\" --max-warnings 0", "test": "hardhat test", + "deploy": "hardhat run deploy/0_deploy_Account_Factory-and-addStake.ts", "deploy-bloctoaccountcloneable": "hardhat run deploy/1_deploy_BloctoAccount4337CloneableWallet.ts", "deploy-bloctoaccountfactory": "hardhat run deploy/2_deploy_BloctoAccountFactory.ts", "deploy-verifyingpaymaster": "hardhat run deploy/3_deploy_VerifyingPaymaster.ts", diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index a777136..7973c52 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -13,6 +13,7 @@ import { import { EntryPoint } from '@account-abstraction/contracts' import { fund, + createTmpAccount, createAccount, deployEntryPoint, ONE_ETH, @@ -20,6 +21,7 @@ import { txData, signMessage } from './testutils' +import { hexConcat } from 'ethers/lib/utils' describe('BloctoAccount Upgrade Test', function () { const ethersSigner = ethers.provider.getSigner() @@ -81,6 +83,24 @@ describe('BloctoAccount Upgrade Test', function () { expect(receipt.logs[0].topics[0]).to.equal(receivedSelector) expect(await ethers.provider.getBalance(account.address)).to.equal(beforeRecevive.add(ONE_ETH)) }) + + it('should create account with multiple authorized address', async () => { + const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + const authorizedWallet22 = createTmpAccount() + + const addresses = hexConcat([authorizedWallet2.address, authorizedWallet22.address]) + const tx = await factory.createAccount2(addresses, cosignerWallet2.address, recoverWallet2.address, AccountSalt) + const receipt = await tx.wait() + + let findWalletCreated = false + receipt.events?.forEach((event) => { + if (event.event === 'WalletCreated' && + event.args?.authorizedAddress === authorizedWallet2.address) { + findWalletCreated = true + } + }) + expect(findWalletCreated).true + }) }) describe('should upgrade to different version implementation', () => { diff --git a/test/entrypoint/entrypoint.test.ts b/test/entrypoint/entrypoint.test.ts index d5f9ad3..269554f 100644 --- a/test/entrypoint/entrypoint.test.ts +++ b/test/entrypoint/entrypoint.test.ts @@ -34,6 +34,7 @@ import { rethrow, tostr, getAccountInitCode, + getAccountInitCode2, calcGasUsage, ONE_ETH, TWO_ETH, @@ -43,6 +44,7 @@ import { getAccountAddress, HashZero, simulationResultCatch, + createTmpAccount, createAccount, getAggregatedAccountInitCode, simulationResultWithAggregationCatch, decodeRevertReason, @@ -311,6 +313,30 @@ describe('EntryPoint', function () { await entryPoint.callStatic.simulateValidation(op1).catch(simulationResultCatch) }) + it('should succeed for creating an account with multiple authorize address', async () => { + const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + const authorizedWallet22 = createTmpAccount() + + const addresses = hexConcat([authorizedWallet2.address, authorizedWallet22.address]) + + const initCode = getAccountInitCode2(factory, addresses, cosignerWallet2.address, recoverWallet2.address) + const sender = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) + + const op1 = await fillAndSignWithCoSigner({ + initCode: initCode, + sender: sender, + verificationGasLimit: 8e5, + maxFeePerGas: 0 + }, + authorizedWallet2, + cosignerWallet2, + entryPoint + ) + await fund(op1.sender) + + await entryPoint.callStatic.simulateValidation(op1).catch(simulationResultCatch) + }) + it('should not call initCode from entrypoint', async () => { // a possible attack: call an account's execFromEntryPoint through initCode. This might lead to stolen funds. const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() diff --git a/test/testutils.ts b/test/testutils.ts index 847f34c..8f22627 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -105,6 +105,14 @@ export function getAccountInitCode (factory: BloctoAccountFactory, authorizedAdd ]) } +// helper function to create the initCode to deploy the account, using our account factory. +export function getAccountInitCode2 (factory: BloctoAccountFactory, authorizedAddresses: BytesLike, cosignerAddress: string, recoveryAddress: string, salt = 0): BytesLike { + return hexConcat([ + factory.address, + factory.interface.encodeFunctionData('createAccount2', [authorizedAddresses, cosignerAddress, recoveryAddress, BigNumber.from(salt)]) + ]) +} + // given the parameters as AccountDeployer, return the resulting "counterfactual address" that it would create. export async function getAccountAddress (factory: BloctoAccountFactory, cosignerAddress: string, recoveryAddress: string, salt = 0): Promise { return await factory.getAddress(cosignerAddress, recoveryAddress, BigNumber.from(salt)) From 3c22247c85f70060f5f9d38f5b5999fbea671246 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Mon, 5 Jun 2023 10:18:33 +0800 Subject: [PATCH 11/26] v1.3.0: executeBatch add value --- contracts/BloctoAccount.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/BloctoAccount.sol b/contracts/BloctoAccount.sol index b5acb7d..929ec9d 100644 --- a/contracts/BloctoAccount.sol +++ b/contracts/BloctoAccount.sol @@ -60,13 +60,14 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas /** * execute a sequence of transactions (called directly by entryPoint) * @param dest sequence of dest call address + * @param value sequence of value to send * @param func sequence of the func containing transactions to be called */ - function executeBatch(address[] calldata dest, bytes[] calldata func) external { + function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external { _requireFromEntryPoint(); require(dest.length == func.length, "wrong array lengths"); for (uint256 i = 0; i < dest.length; i++) { - _call(dest[i], 0, func[i]); + _call(dest[i], value[i], func[i]); } } From 1ccddd0eb9170ac803058e59823715c3e47f4760 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Wed, 7 Jun 2023 19:24:02 +0800 Subject: [PATCH 12/26] schnorr: schnorr init and test multiple sign success --- README.md | 6 + contracts/BloctoAccountFactory.sol | 73 ++++---- contracts/CoreWallet/CoreWallet.sol | 173 ++++++++++++------- package.json | 3 +- src/schnorrkel.js/README.md | 1 + src/schnorrkel.js/core/index.ts | 220 +++++++++++++++++++++++++ src/schnorrkel.js/core/types.ts | 28 ++++ src/schnorrkel.js/index.ts | 6 + src/schnorrkel.js/schnorrkel.ts | 124 ++++++++++++++ src/schnorrkel.js/types/index.ts | 4 + src/schnorrkel.js/types/key-pair.ts | 30 ++++ src/schnorrkel.js/types/key.ts | 15 ++ src/schnorrkel.js/types/nonce.ts | 18 ++ src/schnorrkel.js/types/signature.ts | 55 +++++++ src/schnorrkel.js/unsafe-schnorrkel.ts | 59 +++++++ test/bloctoaccount.test.ts | 4 + test/schnorrMultiSign.test.ts | 105 ++++++++++++ test/schnorrUtils.ts | 68 ++++++++ test/testutils.ts | 31 ++-- yarn.lock | 38 ++++- 20 files changed, 946 insertions(+), 115 deletions(-) create mode 100644 src/schnorrkel.js/README.md create mode 100644 src/schnorrkel.js/core/index.ts create mode 100644 src/schnorrkel.js/core/types.ts create mode 100644 src/schnorrkel.js/index.ts create mode 100644 src/schnorrkel.js/schnorrkel.ts create mode 100644 src/schnorrkel.js/types/index.ts create mode 100644 src/schnorrkel.js/types/key-pair.ts create mode 100644 src/schnorrkel.js/types/key.ts create mode 100644 src/schnorrkel.js/types/nonce.ts create mode 100644 src/schnorrkel.js/types/signature.ts create mode 100644 src/schnorrkel.js/unsafe-schnorrkel.ts create mode 100644 test/schnorrMultiSign.test.ts create mode 100644 test/schnorrUtils.ts diff --git a/README.md b/README.md index 9b123d8..11cbe0f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,12 @@ verify VerifyingPaymaster yarn verify-verifyingpaymaster --network mumbai ``` +## Schnorr Multi Sign Test + +``` +npx hardhat test test/schnorrMultiSign.test.ts +``` + ## Tool check storage layout diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol index 6cbea63..96e58fe 100644 --- a/contracts/BloctoAccountFactory.sol +++ b/contracts/BloctoAccountFactory.sol @@ -28,52 +28,53 @@ contract BloctoAccountFactory is Ownable { * 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 _authorizedAddress, address _cosigner, address _recoveryAddress, uint256 _salt) - public - returns (BloctoAccount ret) - { - address addr = getAddress(_cosigner, _recoveryAddress, _salt); - uint256 codeSize = addr.code.length; - if (codeSize > 0) { - return BloctoAccount(payable(addr)); - } + function createAccount( + address _authorizedAddress, + address _cosigner, + address _recoveryAddress, + uint256 _salt, + uint256 _mergedKeyIndexWithParity, + bytes32 _mergedKey + ) public onlyOwner returns (BloctoAccount ret) { bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); // for consistent address BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); newProxy.initImplementation(bloctoAccountImplementation); ret = BloctoAccount(payable(address(newProxy))); - ret.init(_authorizedAddress, uint256(uint160(_cosigner)), _recoveryAddress); + ret.init( + _authorizedAddress, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParity, _mergedKey + ); emit WalletCreated(address(ret), _authorizedAddress, false); } - function createAccount2( - bytes memory _authorizedAddresses, - address _cosigner, - address _recoveryAddress, - uint256 _salt - ) public returns (BloctoAccount ret) { - require( - _authorizedAddresses.length / 20 > 0 && _authorizedAddresses.length % 20 == 0, "invalid address byte array" - ); + // function createAccount2( + // bytes memory _authorizedAddresses, + // address _cosigner, + // address _recoveryAddress, + // uint256 _salt + // ) public returns (BloctoAccount ret) { + // require( + // _authorizedAddresses.length / 20 > 0 && _authorizedAddresses.length % 20 == 0, "invalid address byte array" + // ); - address addr = getAddress(_cosigner, _recoveryAddress, _salt); - uint256 codeSize = addr.code.length; - if (codeSize > 0) { - return BloctoAccount(payable(addr)); - } - bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); - // for consistent address - BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); - newProxy.initImplementation(bloctoAccountImplementation); - ret = BloctoAccount(payable(address(newProxy))); - ret.init2(_authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress); + // address addr = getAddress(_cosigner, _recoveryAddress, _salt); + // uint256 codeSize = addr.code.length; + // if (codeSize > 0) { + // return BloctoAccount(payable(addr)); + // } + // bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); + // // for consistent address + // BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); + // newProxy.initImplementation(bloctoAccountImplementation); + // ret = BloctoAccount(payable(address(newProxy))); + // ret.init2(_authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress); - address firstAuthorizedAddress; - assembly { - firstAuthorizedAddress := mload(add(_authorizedAddresses, 20)) - } - emit WalletCreated(address(ret), firstAuthorizedAddress, false); - } + // address firstAuthorizedAddress; + // assembly { + // firstAuthorizedAddress := mload(add(_authorizedAddresses, 20)) + // } + // emit WalletCreated(address(ret), firstAuthorizedAddress, false); + // } /** * calculate the counterfactual address of this account as it would be returned by createAccount() diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol index bd07b7d..c7bd84a 100644 --- a/contracts/CoreWallet/CoreWallet.sol +++ b/contracts/CoreWallet/CoreWallet.sol @@ -39,6 +39,9 @@ contract CoreWallet is IERC1271 { /// comment on the `authorizations` variable for how this is used. uint256 public constant AUTH_VERSION_INCREMENTOR = (1 << 160); + /// @notice Q constant for schnorr signature verify + uint256 internal constant Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; + /// @notice The pre-shifted authVersion (to get the current authVersion as an integer, /// shift this value right by 160 bits). Starts as `1 << 160` (`AUTH_VERSION_INCREMENTOR`) /// See the comment on the `authorizations` variable for how this is used. @@ -63,6 +66,9 @@ contract CoreWallet is IERC1271 { /// "authorized addresses". mapping(uint256 => uint256) public authorizations; + // (authVersion,96)(padding_0,153)(authKeyIdx,6)(parity,1) -> merged_ec_pubkey_x (256) + mapping(uint256 => bytes32) public mergedKeys; + /// @notice A per-key nonce value, incremented each time a transaction is processed with that key. /// Used for replay prevention. The nonce value in the transaction must exactly equal the current /// nonce value in the wallet for that key. (This mirrors the way Ethereum's transaction nonce works.) @@ -140,6 +146,8 @@ contract CoreWallet is IERC1271 { /// @param cosigner the 2-of-2 signatory (optional). event Authorized(address authorizedAddress, uint256 cosigner); + event AuthorizedMeregedKey(uint256 authorizedAddress, uint256 mergedKey); + /// @notice Emitted when an emergency recovery has been performed. If this event is fired, /// ALL previously authorized addresses have been deauthorized and the only authorized /// address is the authorizedAddress indicated in this event. @@ -180,19 +188,21 @@ contract CoreWallet is IERC1271 { /// @param _authorizedAddress the initial authorized address, must not be zero! /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) - function init(address _authorizedAddress, uint256 _cosigner, address _recoveryAddress) public onlyOnce { + function init( + address _authorizedAddress, + uint256 _cosigner, + address _recoveryAddress, + uint256 _mergedKeyIndexWithPairty, + bytes32 _mergedKey + ) public onlyOnce { require(_authorizedAddress != _recoveryAddress, "Do not use the recovery address as an authorized address."); require(address(uint160(_cosigner)) != _recoveryAddress, "Do not use the recovery address as a cosigner."); - require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); - require(address(uint160(_cosigner)) != address(0), "Initial cosigner must not be zero."); recoveryAddress = _recoveryAddress; // set initial authorization value authVersion = AUTH_VERSION_INCREMENTOR; // add initial authorized address - authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; - - emit Authorized(_authorizedAddress, _cosigner); + this.setAuthorized(_authorizedAddress, _cosigner, _mergedKeyIndexWithPairty, _mergedKey); } function bytesToAddresses(bytes memory bys) private pure returns (address[] memory addresses) { @@ -207,24 +217,18 @@ contract CoreWallet is IERC1271 { } } - function init2(bytes memory _authorizedAddresses, uint256 _cosigner, address _recoveryAddress) public onlyOnce { - address[] memory addresses = bytesToAddresses(_authorizedAddresses); - for (uint256 i = 0; i < addresses.length; i++) { - address _authorizedAddress = addresses[i]; - require(_authorizedAddress != _recoveryAddress, "Do not use the recovery address as an authorized address."); - require(address(uint160(_cosigner)) != _recoveryAddress, "Do not use the recovery address as a cosigner."); - require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); - require(address(uint160(_cosigner)) != address(0), "Initial cosigner must not be zero."); - - recoveryAddress = _recoveryAddress; - // set initial authorization value - authVersion = AUTH_VERSION_INCREMENTOR; - // add initial authorized address - authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; - - emit Authorized(_authorizedAddress, _cosigner); - } - } + // function init2(bytes memory _authorizedAddresses, uint256 _cosigner, address _recoveryAddress) public onlyOnce { + // address[] memory addresses = bytesToAddresses(_authorizedAddresses); + // recoveryAddress = _recoveryAddress; + // // set initial authorization value + // authVersion = AUTH_VERSION_INCREMENTOR; + // for (uint256 i = 0; i < addresses.length; i++) { + // address _authorizedAddress = addresses[i]; + // require(_authorizedAddress != _recoveryAddress, "Do not use the recovery address as an authorized address."); + // require(address(uint160(_cosigner)) != _recoveryAddress, "Do not use the recovery address as a cosigner."); + // setAuthorized(_authorizedAddress, _cosigner, 0, bytes32(0)); + // } + // } /// @notice The fallback function, invoked whenever we receive a transaction that doesn't call any of our /// named functions. In particular, this method is called when we are the target of a simple send @@ -296,27 +300,40 @@ contract CoreWallet is IERC1271 { /// @dev Must be called through `invoke()` /// @param _authorizedAddress the address to configure authorization /// @param _cosigner the corresponding cosigning address - function setAuthorized(address _authorizedAddress, uint256 _cosigner) external onlyInvoked { - // TODO: Allowing a signer to remove itself is actually pretty terrible; it could result in the user - // removing their only available authorized key. Unfortunately, due to how the invocation forwarding - // works, we don't actually _know_ which signer was used to call this method, so there's no easy way - // to prevent this. - - // TODO: Allowing the backup key to be set as an authorized address bypasses the recovery mechanisms. - // Dapper can prevent this with offchain logic and the cosigner, but it would be nice to have - // this enforced by the smart contract logic itself. - + /// @param _mergedIndexWithParity the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKey the corresponding mergedKey + function setAuthorized( + address _authorizedAddress, + uint256 _cosigner, + uint256 _mergedIndexWithParity, + bytes32 _mergedKey + ) external onlyInvoked { require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); require(_authorizedAddress != recoveryAddress, "Do not use the recovery address as an authorized address."); require( address(uint160(_cosigner)) == address(0) || address(uint160(_cosigner)) != recoveryAddress, "Do not use the recovery address as a cosigner." ); + require(_mergedKey != 0, "MergedKey must not be zero."); authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; + mergedKeys[authVersion + _mergedIndexWithParity] = _mergedKey; + emit Authorized(_authorizedAddress, _cosigner); } + /// @notice Configures an authorizable address to use a merged key. + /// @dev Must be called through `invoke()` + /// @param _meregedKeyIndex the merged key index + /// @param _meregedKey the corresponding merged authorized key & cosigner key by Schnorr + // function setMergedKey(uint256 _meregedKeyIndex, uint256 _meregedKey) external onlyInvoked { + // // require(_authorizedAddress != address(0), "Authorized address must not be zero."); + // require(_meregedKey != 0, "Merged key must not be zero."); + + // mergedKeys[authVersion + _meregedKeyIndex] = _meregedKey; + // emit AuthorizedMeregedKey(_meregedKeyIndex, _meregedKey); + // } + /// @notice Performs an emergency recovery operation, removing all existing authorizations and setting /// a sole new authorized address with optional cosigner. THIS IS A SCORCHED EARTH SOLUTION, and great /// care should be taken to ensure that this method is never called unless it is a last resort. See the @@ -398,61 +415,91 @@ contract CoreWallet is IERC1271 { } } + function calculateParity(uint256 boolsUintCopy) internal pure returns (uint8) { + // uint256 boolsUintCopy = boolsUint; + uint8 _count = 0; + for (uint8 i = 0; i < 255; i++) { + if (boolsUintCopy & 1 == 1) { + _count++; + } + boolsUintCopy >>= 1; + } + // console.log("count: %d", _count); + return (_count & 1) + 27; + } + + function verifySchnorr(bytes32 hash, bytes memory sig) internal view returns (bool) { + // px := public key x-coord + // e := schnorr signature challenge + // s := schnorr signature + // parity := public key y-coord parity (27 or 28) + (bytes32 e, bytes32 s, uint8 keyIndexWithParity) = sig.extractSignature(0); + bytes32 px = mergedKeys[authVersion + uint256(keyIndexWithParity)]; + uint8 parity = (keyIndexWithParity & 0x1) + 27; + + bytes32 sp = bytes32(Q - mulmod(uint256(s), uint256(px), Q)); + bytes32 ep = bytes32(Q - mulmod(uint256(e), uint256(px), Q)); + + require(sp != 0); + // the ecrecover precompile implementation checks that the `r` and `s` + // inputs are non-zero (in this case, `px` and `ep`), thus we don't need to + // check if they're zero. + address R = ecrecover(sp, parity, px, ep); + require(R != address(0), "ecrecover failed"); + // return e == keccak256(abi.encodePacked(R, uint8(parity), px, hash)) ? address(uint160(uint256(px))) : address(0); + return e == keccak256(abi.encodePacked(R, parity, px, hash)); + } + /// @notice Should return whether the signature provided is valid for the provided data /// See https://github.com/ethereum/EIPs/issues/1271 /// @dev This function meets the following conditions as per the EIP: /// MUST return the bytes4 magic value `0x1626ba7e` when function passes. /// MUST NOT modify state (using `STATICCALL` for solc < 0.5, `view` modifier for solc > 0.5) /// MUST allow external calls - /// @param hash A 32 byte hash of the signed data. The actual hash that is hashed however is the + /// @param _hash A 32 byte hash of the signed data. The actual hash that is hashed however is the /// the following tightly packed arguments: `0x19,0x0,wallet_address,hash` /// @param _signature Signature byte array associated with `_data` /// @return Magic value `0x1626ba7e` upon success, 0 otherwise. - function isValidSignature(bytes32 hash, bytes calldata _signature) external view returns (bytes4) { + function isValidSignature(bytes32 _hash, bytes calldata _signature) external view returns (bytes4) { + // return verifySchnorr(hash, _signature) ? IERC1271.isValidSignature.selector : bytes4(0); // We 'hash the hash' for the following reasons: // 1. `hash` is not the hash of an Ethereum transaction // 2. signature must target this wallet to avoid replaying the signature for another wallet // with the same key // 3. Gnosis does something similar: // https://github.com/gnosis/safe-contracts/blob/102e632d051650b7c4b0a822123f449beaf95aed/contracts/GnosisSafe.sol - bytes32 operationHash = keccak256(abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, hash)); + bytes32 operationHash = keccak256(abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, _hash)); - bytes32[2] memory r; - bytes32[2] memory s; - uint8[2] memory v; - address signer; - address cosigner; - - // extract 1 or 2 signatures depending on length if (_signature.length == 65) { - (r[0], s[0], v[0]) = _signature.extractSignature(0); - signer = ecrecover(operationHash, v[0], r[0], s[0]); - cosigner = signer; + return verifySchnorr(operationHash, _signature) ? IERC1271.isValidSignature.selector : bytes4(0); } else if (_signature.length == 130) { + bytes32[2] memory r; + bytes32[2] memory s; + uint8[2] memory v; + address signer; + address cosigner; (r[0], s[0], v[0]) = _signature.extractSignature(0); (r[1], s[1], v[1]) = _signature.extractSignature(65); signer = ecrecover(operationHash, v[0], r[0], s[0]); cosigner = ecrecover(operationHash, v[1], r[1], s[1]); - } else { - return 0; - } + // check for valid signature + if (signer == address(0)) { + return 0; + } - // check for valid signature - if (signer == address(0)) { - return 0; - } + // check for valid signature + if (cosigner == address(0)) { + return 0; + } - // check for valid signature - if (cosigner == address(0)) { - return 0; - } + // check to see if this is an authorized key + if (address(uint160(authorizations[authVersion + uint256(uint160(signer))])) != cosigner) { + return 0; + } - // check to see if this is an authorized key - if (address(uint160(authorizations[authVersion + uint256(uint160(signer))])) != cosigner) { - return 0; + return IERC1271.isValidSignature.selector; } - - return IERC1271.isValidSignature.selector; + return 0; } /// @notice A version of `invoke()` that has no explicit signatures, and uses msg.sender diff --git a/package.json b/package.json index f8590fb..e31f046 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "typechain": "^8.1.0" }, "dependencies": { + "@borislav.itskov/schnorrkel.js": "https://github.com/borislav-itskov/schnorrkel.js", "@gnosis.pm/safe-contracts": "^1.3.0", "@nomiclabs/hardhat-etherscan": "^3.1.7", "@openzeppelin/contracts": "^4.2.0", @@ -63,4 +64,4 @@ "table": "^6.8.0", "typescript": "^4.3.5" } -} \ No newline at end of file +} diff --git a/src/schnorrkel.js/README.md b/src/schnorrkel.js/README.md new file mode 100644 index 0000000..4ff1f2d --- /dev/null +++ b/src/schnorrkel.js/README.md @@ -0,0 +1 @@ +fork from: https://github.com/borislav-itskov/schnorrkel.js \ No newline at end of file diff --git a/src/schnorrkel.js/core/index.ts b/src/schnorrkel.js/core/index.ts new file mode 100644 index 0000000..b55248b --- /dev/null +++ b/src/schnorrkel.js/core/index.ts @@ -0,0 +1,220 @@ +import { randomBytes } from 'crypto' +import { ethers } from 'ethers' +import secp256k1 from 'secp256k1' +import ecurve from 'ecurve' +import elliptic from 'elliptic' +import bigi from 'bigi' +import { BN } from 'bn.js' + +import { InternalNoncePairs, InternalNonces, InternalPublicNonces, InternalSignature } from './types' +import { KeyPair } from '../types' + +const curve = ecurve.getCurveByName('secp256k1') +const n = curve?.n +const EC = elliptic.ec +const ec = new EC('secp256k1') +const generatorPoint = ec.g + +export const _generateL = (publicKeys: Uint8Array[]) => { + return ethers.utils.keccak256(_concatTypedArrays(publicKeys.sort())) +} + +export const _concatTypedArrays = (publicKeys: Uint8Array[]): Uint8Array => { + const c: Buffer = Buffer.alloc(publicKeys.reduce((partialSum, publicKey) => partialSum + publicKey.length, 0)) + publicKeys.map((publicKey, index) => c.set(publicKey, (index * publicKey.length))) + return new Uint8Array(c.buffer) +} + +export const _aCoefficient = (publicKey: Uint8Array, L: string): Uint8Array => { + return ethers.utils.arrayify(ethers.utils.solidityKeccak256( + ['bytes', 'bytes'], + [L, publicKey] + )) +} + +const _bCoefficient = (combinedPublicKey: Uint8Array, msgHash: string, publicNonces: InternalPublicNonces[]): Uint8Array => { + type KeyOf = keyof InternalPublicNonces + const arrayColumn = (arr: InternalPublicNonces[], n: KeyOf) => arr.map(x => x[n]) + const kPublicNonces = secp256k1.publicKeyCombine(arrayColumn(publicNonces, 'kPublic')) + const kTwoPublicNonces = secp256k1.publicKeyCombine(arrayColumn(publicNonces, 'kTwoPublic')) + + return ethers.utils.arrayify(ethers.utils.solidityKeccak256( + ['bytes', 'bytes32', 'bytes', 'bytes'], + [combinedPublicKey, msgHash, kPublicNonces, kTwoPublicNonces] + )) +} + +export const generateRandomKeys = () => { + let privKeyBytes: Buffer | undefined + do { + privKeyBytes = randomBytes(32) + } while (!secp256k1.privateKeyVerify(privKeyBytes)) + + const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKeyBytes)) + + const data = { + publicKey: pubKey, + privateKey: privKeyBytes + } + + return new KeyPair(data) +} + +export const _hashPrivateKey = (privateKey: Uint8Array): string => { + return ethers.utils.keccak256(privateKey) +} + +export const _generatePublicNonces = (privateKey: Buffer): { + privateNonceData: Pick + publicNonceData: InternalPublicNonces + hash: string +} => { + const hash = _hashPrivateKey(privateKey) + const nonce = _generateNonce() + + return { + hash, + privateNonceData: { + k: nonce.k, + kTwo: nonce.kTwo + }, + publicNonceData: { + kPublic: nonce.kPublic, + kTwoPublic: nonce.kTwoPublic + } + } +} + +const _generateNonce = (): InternalNoncePairs => { + const k = ethers.utils.randomBytes(32) + const kTwo = ethers.utils.randomBytes(32) + const kPublic = secp256k1.publicKeyCreate(k) + const kTwoPublic = secp256k1.publicKeyCreate(kTwo) + + return { + k, + kTwo, + kPublic, + kTwoPublic + } +} + +export const _multiSigSign = (nonces: InternalNonces, combinedPublicKey: Uint8Array, privateKey: Uint8Array, msg: string, publicKeys: Uint8Array[], publicNonces: InternalPublicNonces[]): InternalSignature => { + if (publicKeys.length < 2) { + throw Error('At least 2 public keys should be provided') + } + + const xHashed = _hashPrivateKey(privateKey) + if (!(xHashed in nonces) || Object.keys(nonces[xHashed]).length === 0) { + throw Error('Nonces should be exchanged before signing') + } + + const publicKey = secp256k1.publicKeyCreate(privateKey) + const L = _generateL(publicKeys) + const msgHash = _hashMessage(msg) + const a = _aCoefficient(publicKey, L) + const b = _bCoefficient(combinedPublicKey, msgHash, publicNonces) + + const effectiveNonces = publicNonces.map((batch) => { + return secp256k1.publicKeyCombine([batch.kPublic, secp256k1.publicKeyTweakMul(batch.kTwoPublic, b)]) + }) + const signerEffectiveNonce = secp256k1.publicKeyCombine([ + nonces[xHashed].kPublic, + secp256k1.publicKeyTweakMul(nonces[xHashed].kTwoPublic, b) + ]) + const inArray = effectiveNonces.filter(nonce => areBuffersSame(nonce, signerEffectiveNonce)).length != 0 + if (!inArray) { + throw Error('Passed nonces are invalid') + } + + const R = secp256k1.publicKeyCombine(effectiveNonces) + const e = challenge(R, msgHash, combinedPublicKey) + + const { k, kTwo } = nonces[xHashed] + + // xe = x * e + const xe = secp256k1.privateKeyTweakMul(privateKey, e) + + // xea = a * xe + const xea = secp256k1.privateKeyTweakMul(xe, a) + + // k + xea + const kPlusxea = secp256k1.privateKeyTweakAdd(xea, k) + + // kTwo * b + const kTwoMulB = secp256k1.privateKeyTweakMul(kTwo, b) + + // k + kTwoMulB + xea + const final = secp256k1.privateKeyTweakAdd(kPlusxea, kTwoMulB) + + return { + // s = k + xea mod(n) + signature: bigi.fromBuffer(final).mod(n).toBuffer(32), + challenge: e, + finalPublicNonce: R + } +} + +const areBuffersSame = (buf1: Uint8Array, buf2: Uint8Array): boolean => { + if (buf1.byteLength != buf2.byteLength) return false + + const dv1 = new Int8Array(buf1) + const dv2 = new Int8Array(buf2) + for (let i = 0; i != buf1.byteLength; i++) { + if (dv1[i] != dv2[i]) return false + } + + return true +} + +const challenge = (R: Uint8Array, msgHash: string, publicKey: Uint8Array): Uint8Array => { + // convert R to address + const R_uncomp = secp256k1.publicKeyConvert(R, false) + const R_addr = ethers.utils.arrayify(ethers.utils.keccak256(R_uncomp.slice(1, 65))).slice(12, 32) + + // e = keccak256(address(R) || compressed publicKey || msgHash) + return ethers.utils.arrayify( + ethers.utils.solidityKeccak256( + ['address', 'uint8', 'bytes32', 'bytes32'], + [R_addr, publicKey[0] + 27 - 2, publicKey.slice(1, 33), msgHash] + ) + ) +} + +export const _sumSigs = (signatures: Uint8Array[]): Buffer => { + let combined = bigi.fromBuffer(signatures[0]) + signatures.shift() + signatures.forEach(sig => { + combined = combined.add(bigi.fromBuffer(sig)) + }) + return combined.mod(n).toBuffer(32) +} + +export const _hashMessage = (message: string): string => { + // return ethers.utils.solidityKeccak256(['string'], [message]) + return message +} + +export const _sign = (privateKey: Uint8Array, msg: string): InternalSignature => { + const hash = _hashMessage(msg) + const publicKey = secp256k1.publicKeyCreate((privateKey as any)) + + // R = G * k + const k = ethers.utils.randomBytes(32) + const R = secp256k1.publicKeyCreate(k) + + // e = h(address(R) || compressed pubkey || m) + const e = challenge(R, hash, publicKey) + + // xe = x * e + const xe = secp256k1.privateKeyTweakMul((privateKey as any), e) + + // s = k + xe + const s = secp256k1.privateKeyTweakAdd(k, xe) + + return { + finalPublicNonce: R, + challenge: e, + signature: s + } +} diff --git a/src/schnorrkel.js/core/types.ts b/src/schnorrkel.js/core/types.ts new file mode 100644 index 0000000..6b83872 --- /dev/null +++ b/src/schnorrkel.js/core/types.ts @@ -0,0 +1,28 @@ +export interface InternalNoncePairs { + readonly k: Uint8Array, + readonly kTwo: Uint8Array, + readonly kPublic: Uint8Array, + readonly kTwoPublic: Uint8Array, +} + +export interface InternalPublicNonces { + readonly kPublic: Uint8Array, + readonly kTwoPublic: Uint8Array, +} + +export interface InternalSignature { + finalPublicNonce: Uint8Array, // the final public nonce + challenge: Uint8Array, // the schnorr challenge + signature: Uint8Array, // the signature +} + +export interface InternalNoncePairs { + readonly k: Uint8Array, + readonly kTwo: Uint8Array, + readonly kPublic: Uint8Array, + readonly kTwoPublic: Uint8Array, +} + +export type InternalNonces = { + [privateKey: string]: InternalNoncePairs +} \ No newline at end of file diff --git a/src/schnorrkel.js/index.ts b/src/schnorrkel.js/index.ts new file mode 100644 index 0000000..3325240 --- /dev/null +++ b/src/schnorrkel.js/index.ts @@ -0,0 +1,6 @@ +import Schnorrkel from './schnorrkel' +export { default as UnsafeSchnorrkel } from './unsafe-schnorrkel' + +export { Key, KeyPair, Signature, PublicNonces, Challenge, SignatureOutput, FinalPublicNonce } from './types' + +export default Schnorrkel \ No newline at end of file diff --git a/src/schnorrkel.js/schnorrkel.ts b/src/schnorrkel.js/schnorrkel.ts new file mode 100644 index 0000000..f4e8993 --- /dev/null +++ b/src/schnorrkel.js/schnorrkel.ts @@ -0,0 +1,124 @@ +import secp256k1 from 'secp256k1' + +import { Key, Nonces, PublicNonces, Signature, NoncePairs } from './types' + +import { _generateL, _aCoefficient, _generatePublicNonces, _multiSigSign, _hashPrivateKey, _sumSigs, _verify, _generatePk, _sign } from './core' +import { InternalNonces, InternalPublicNonces } from './core/types' +import { Challenge, FinalPublicNonce, SignatureOutput } from './types/signature' + +class Schnorrkel { + protected nonces: Nonces = {} + + private _setNonce(privateKey: Buffer): string { + const { publicNonceData, privateNonceData, hash } = _generatePublicNonces(privateKey) + + const mappedPublicNonce: PublicNonces = { + kPublic: new Key(Buffer.from(publicNonceData.kPublic)), + kTwoPublic: new Key(Buffer.from(publicNonceData.kTwoPublic)), + } + + const mappedPrivateNonce: Pick = { + k: new Key(Buffer.from(privateNonceData.k)), + kTwo: new Key(Buffer.from(privateNonceData.kTwo)) + } + + this.nonces[hash] = { ...mappedPrivateNonce, ...mappedPublicNonce } + return hash + } + + static getCombinedPublicKey(publicKeys: Array): Key { + if (publicKeys.length < 2) { + throw Error('At least 2 public keys should be provided') + } + + const bufferPublicKeys = publicKeys.map(publicKey => publicKey.buffer) + const L = _generateL(bufferPublicKeys) + + const modifiedKeys = bufferPublicKeys.map(publicKey => { + return secp256k1.publicKeyTweakMul(publicKey, _aCoefficient(publicKey, L)) + }) + + return new Key(Buffer.from(secp256k1.publicKeyCombine(modifiedKeys))) + } + + static getCombinedAddress(publicKeys: Array): string { + if (publicKeys.length < 2) throw Error('At least 2 public keys should be provided') + + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) + const px = _generatePk(combinedPublicKey.buffer) + return px + } + + generatePublicNonces(privateKey: Key): PublicNonces { + const hash = this._setNonce(privateKey.buffer) + const nonce = this.nonces[hash] + + return { + kPublic: nonce.kPublic, + kTwoPublic: nonce.kTwoPublic, + } + } + + private clearNonces(privateKey: Key): void { + const x = privateKey.buffer + const hash = _hashPrivateKey(x) + + delete this.nonces[hash] + } + + multiSigSign(privateKey: Key, msg: string, publicKeys: Key[], publicNonces: PublicNonces[]): SignatureOutput { + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) + const mappedPublicNonce: InternalPublicNonces[] = publicNonces.map(publicNonce => { + return { + kPublic: publicNonce.kPublic.buffer, + kTwoPublic: publicNonce.kTwoPublic.buffer, + } + }) + + const mappedNonces: InternalNonces = Object.fromEntries(Object.entries(this.nonces).map(([hash, nonce]) => { + return [ + hash, + { + k: nonce.k.buffer, + kTwo: nonce.kTwo.buffer, + kPublic: nonce.kPublic.buffer, + kTwoPublic: nonce.kTwoPublic.buffer, + } + ] + })) + + const musigData = _multiSigSign(mappedNonces, combinedPublicKey.buffer, privateKey.buffer, msg, publicKeys.map(key => key.buffer), mappedPublicNonce) + + // absolutely crucial to delete the nonces once a signature has been crafted with them. + // nonce reusae will lead to private key leakage! + this.clearNonces(privateKey) + + return { + signature: new Signature(Buffer.from(musigData.signature)), + finalPublicNonce: new FinalPublicNonce(Buffer.from(musigData.finalPublicNonce)), + challenge: new Challenge(Buffer.from(musigData.challenge)), + } + } + + static sign(privateKey: Key, msg: string): SignatureOutput { + const output = _sign(privateKey.buffer, msg) + + return { + signature: new Signature(Buffer.from(output.signature)), + finalPublicNonce: new FinalPublicNonce(Buffer.from(output.finalPublicNonce)), + challenge: new Challenge(Buffer.from(output.challenge)), + } + } + + static sumSigs(signatures: Signature[]): Signature { + const mappedSignatures = signatures.map(signature => signature.buffer) + const sum = _sumSigs(mappedSignatures) + return new Signature(Buffer.from(sum)) + } + + static verify(signature: Signature, msg: string, finalPublicNonce: FinalPublicNonce, publicKey: Key): boolean { + return _verify(signature.buffer, msg, finalPublicNonce.buffer, publicKey.buffer) + } +} + +export default Schnorrkel \ No newline at end of file diff --git a/src/schnorrkel.js/types/index.ts b/src/schnorrkel.js/types/index.ts new file mode 100644 index 0000000..0b86beb --- /dev/null +++ b/src/schnorrkel.js/types/index.ts @@ -0,0 +1,4 @@ +export { Key } from './key' +export { KeyPair } from './key-pair' +export { PublicNonces, Nonces, NoncePairs } from './nonce' +export { Signature, Challenge, SignatureOutput, FinalPublicNonce } from './signature' diff --git a/src/schnorrkel.js/types/key-pair.ts b/src/schnorrkel.js/types/key-pair.ts new file mode 100644 index 0000000..d29a552 --- /dev/null +++ b/src/schnorrkel.js/types/key-pair.ts @@ -0,0 +1,30 @@ +import { Key } from './key' + +export class KeyPair { + privateKey: Key + publicKey: Key + + constructor({ publicKey, privateKey }: { publicKey: Buffer, privateKey: Buffer }) { + this.privateKey = new Key(privateKey) + this.publicKey = new Key(publicKey) + } + + static fromJson(params: string): KeyPair { + try { + const data = JSON.parse(params) + const publicKey = Key.fromHex(data.publicKey) + const privateKey = Key.fromHex(data.privateKey) + + return new KeyPair({ publicKey: publicKey.buffer, privateKey: privateKey.buffer }) + } catch (error) { + throw new Error('Invalid JSON') + } + } + + toJson(): string { + return JSON.stringify({ + publicKey: this.publicKey.toHex(), + privateKey: this.privateKey.toHex(), + }) + } +} diff --git a/src/schnorrkel.js/types/key.ts b/src/schnorrkel.js/types/key.ts new file mode 100644 index 0000000..04aa6b6 --- /dev/null +++ b/src/schnorrkel.js/types/key.ts @@ -0,0 +1,15 @@ +export class Key { + readonly buffer: Buffer + + constructor(buffer: Buffer) { + this.buffer = buffer + } + + toHex(): string { + return this.buffer.toString('hex') + } + + static fromHex(hex: string): Key { + return new Key(Buffer.from(hex, 'hex')) + } +} diff --git a/src/schnorrkel.js/types/nonce.ts b/src/schnorrkel.js/types/nonce.ts new file mode 100644 index 0000000..ecbbfe6 --- /dev/null +++ b/src/schnorrkel.js/types/nonce.ts @@ -0,0 +1,18 @@ +import { Key } from './key' + +export interface NoncePairs { + readonly k: Key, + readonly kTwo: Key, + readonly kPublic: Key, + readonly kTwoPublic: Key, +} + +export interface PublicNonces { + readonly kPublic: Key, + readonly kTwoPublic: Key, +} + + +export type Nonces = { + [privateKey: string]: NoncePairs +} \ No newline at end of file diff --git a/src/schnorrkel.js/types/signature.ts b/src/schnorrkel.js/types/signature.ts new file mode 100644 index 0000000..4d56c11 --- /dev/null +++ b/src/schnorrkel.js/types/signature.ts @@ -0,0 +1,55 @@ + +export interface SignatureOutput { + finalPublicNonce: FinalPublicNonce, // the final public nonce + challenge: Challenge, // the schnorr challenge + signature: Signature, // the signature +} + +export class FinalPublicNonce { + readonly buffer: Buffer + + constructor(buffer: Buffer) { + this.buffer = buffer + } + + toHex(): string { + return this.buffer.toString('hex') + } + + static fromHex(hex: string): FinalPublicNonce { + return new FinalPublicNonce(Buffer.from(hex, 'hex')) + } +} + +export class Challenge { + readonly buffer: Buffer + + constructor(buffer: Buffer) { + this.buffer = buffer + } + + toHex(): string { + return this.buffer.toString('hex') + } + + static fromHex(hex: string): FinalPublicNonce { + return new FinalPublicNonce(Buffer.from(hex, 'hex')) + } +} + +export class Signature { + readonly buffer: Buffer + + constructor(buffer: Buffer) { + this.buffer = buffer + } + + toHex(): string { + return this.buffer.toString('hex') + } + + static fromHex(hex: string): FinalPublicNonce { + return new FinalPublicNonce(Buffer.from(hex, 'hex')) + } +} + diff --git a/src/schnorrkel.js/unsafe-schnorrkel.ts b/src/schnorrkel.js/unsafe-schnorrkel.ts new file mode 100644 index 0000000..44fb884 --- /dev/null +++ b/src/schnorrkel.js/unsafe-schnorrkel.ts @@ -0,0 +1,59 @@ +import { Key } from './types' + +import { _generateL, _aCoefficient, _generatePublicNonces, _multiSigSign, _hashPrivateKey, _sumSigs, _verify, _generatePk, _sign } from './core' +import Schnorrkel from './schnorrkel' + +class UnsafeSchnorrkel extends Schnorrkel { + static fromJson(json: string): UnsafeSchnorrkel { + interface JsonData { + nonces: { + [hash: string]: { + k: string, + kTwo: string, + kPublic: string, + kTwoPublic: string, + } + } + } + try { + const jsonData = JSON.parse(json) as JsonData + const noncesEntries = Object.entries(jsonData.nonces).map(([hash, nonce]) => { + return [ + hash, + { + k: Key.fromHex(nonce.k), + kTwo: Key.fromHex(nonce.kTwo), + kPublic: Key.fromHex(nonce.kPublic), + kTwoPublic: Key.fromHex(nonce.kTwoPublic), + } + ] + }) + + const schnorrkel = new UnsafeSchnorrkel() + schnorrkel.nonces = Object.fromEntries(noncesEntries) + return schnorrkel + } catch (error) { + throw new Error('Invalid JSON') + } + } + + toJson() { + const nonces = Object.fromEntries(Object.entries(this.nonces).map(([hash, nonce]) => { + return [ + hash, + { + k: nonce.k.toHex(), + kTwo: nonce.kTwo.toHex(), + kPublic: nonce.kPublic.toHex(), + kTwoPublic: nonce.kTwoPublic.toHex(), + } + ] + })) + + return JSON.stringify({ + nonces, + }) + } +} + +export default UnsafeSchnorrkel \ No newline at end of file diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index 7973c52..749c3ae 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -70,6 +70,10 @@ describe('BloctoAccount Upgrade Test', function () { account = await testCreateAccount(AccountSalt) }) + it('test gas', async () => { + + }) + it('should receive native token', async () => { const beforeRecevive = await ethers.provider.getBalance(account.address) const [owner] = await ethers.getSigners() diff --git a/test/schnorrMultiSign.test.ts b/test/schnorrMultiSign.test.ts new file mode 100644 index 0000000..2e3a223 --- /dev/null +++ b/test/schnorrMultiSign.test.ts @@ -0,0 +1,105 @@ +// update from https://github.com/borislav-itskov/schnorrkel.js +import { ethers } from 'hardhat' +import { Wallet, BigNumber } from 'ethers' +import { expect } from 'chai' +import { + BloctoAccount, + BloctoAccount__factory, + BloctoAccountCloneableWallet__factory, + BloctoAccountFactory, + BloctoAccountFactory__factory, + TestBloctoAccountCloneableWalletV140, + TestBloctoAccountCloneableWalletV140__factory +} from '../typechain' +import { EntryPoint } from '@account-abstraction/contracts' +import { + fund, + createTmpAccount, + createAccount, + deployEntryPoint, + ONE_ETH, + createAuthorizedCosignerRecoverWallet, + txData, + signMessage, + logBytes, + hashMessageEIP191V0 +} from './testutils' + +// import Schnorrkel, { Key, PublicNonces, SignatureOutput } from '@borislav.itskov/schnorrkel.js/src/index' +import Schnorrkel, { Key, PublicNonces, SignatureOutput } from '../src/schnorrkel.js/index' +import { DefaultSigner } from './schnorrUtils' + +const ERC1271_MAGICVALUE_BYTES32 = '0x1626ba7e' + +describe('Schnorr MultiSign Test', function () { + const ethersSigner = ethers.provider.getSigner() + + let authorizedWallet: Wallet + let cosignerWallet: Wallet + let recoverWallet: Wallet + // let account: BloctoAccount + + let implementation: string + let factory: BloctoAccountFactory + + before(async function () { + // deploy entry point (only for fill address) + const entryPoint = await deployEntryPoint() + + // v1 implementation + implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy(entryPoint.address)).address + + // account factory + factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation, entryPoint.address); + + // 3 wallet + [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() + }) + + it('should generate a schnorr musig2 and validate it on the blockchain', async () => { + // create account + // for only 1 byte + const mergedKeyIndex = 0 + + const signerOne = new DefaultSigner(authorizedWallet) + const signerTwo = new DefaultSigner(cosignerWallet) + const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] + const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) + const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) + // because of the parity byte is 2, 3 so sub 2 + const pxIndexWithPairty = combinedPublicKey.buffer.slice(0, 1).readInt8() - 2 + mergedKeyIndex + const account = await createAccount( + ethersSigner, + await authorizedWallet.getAddress(), + await cosignerWallet.getAddress(), + await recoverWallet.getAddress(), + BigNumber.from(123), + pxIndexWithPairty, + px, + factory + ) + + // multisig + const msg = 'just a test message' + + const msgKeccak256 = ethers.utils.solidityKeccak256(['string'], [msg]) + const msgEIP191V0 = hashMessageEIP191V0(account.address, msgKeccak256) + // note: following line multiSignMessage ignore hash message + const { signature: sigOne, challenge: e } = signerOne.multiSignMessage(msgEIP191V0, publicKeys, publicNonces) + const { signature: sigTwo } = signerTwo.multiSignMessage(msgEIP191V0, publicKeys, publicNonces) + const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) + + // wrap the result + // e (bytes32), s (bytes32), pxIndexWithPairty (uint8) + // pxIndexWithPairty (7 bit for pxIndex, 1 bit for parity) + const hexPxIndexWithPairty = ethers.utils.hexlify(pxIndexWithPairty).slice(-2) + const abiCoder = new ethers.utils.AbiCoder() + const sigData = abiCoder.encode(['bytes32', 'bytes32'], [ + e.buffer, + sSummed.buffer + ]) + hexPxIndexWithPairty + const result = await account.isValidSignature(msgKeccak256, sigData) + expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) + }) +}) diff --git a/test/schnorrUtils.ts b/test/schnorrUtils.ts new file mode 100644 index 0000000..d5c1a3c --- /dev/null +++ b/test/schnorrUtils.ts @@ -0,0 +1,68 @@ +// import { Key, PublicNonces } from '@borislav.itskov/schnorrkel.js/src/types' +// fork from https://github.com/borislav-itskov/schnorrkel.js +// import Schnorrkel, { Key, PublicNonces, SignatureOutput } from '@borislav.itskov/schnorrkel.js/src/index' +import Schnorrkel, { Key, PublicNonces, SignatureOutput } from '../src/schnorrkel.js/index' +import { Wallet } from 'ethers' +import { ethers } from 'hardhat' +import secp256k1 from 'secp256k1' + +const schnorrkel = new Schnorrkel() + +export class KeyPair { + privateKey: Key + publicKey: Key + + constructor ({ publicKey, privateKey }: { publicKey: Buffer, privateKey: Buffer }) { + this.privateKey = new Key(privateKey) + this.publicKey = new Key(publicKey) + } + + static fromJson (params: string): KeyPair { + try { + const data = JSON.parse(params) + const publicKey = Key.fromHex(data.publicKey) + const privateKey = Key.fromHex(data.privateKey) + + return new KeyPair({ publicKey: publicKey.buffer, privateKey: privateKey.buffer }) + } catch (error) { + throw new Error('Invalid JSON') + } + } + + toJson (): string { + return JSON.stringify({ + publicKey: this.publicKey.toHex(), + privateKey: this.privateKey.toHex() + }) + } +} + +export class DefaultSigner { + #privateKey: Key + #publicKey: Key + + constructor (w: Wallet) { + const pubKey = Buffer.from(secp256k1.publicKeyCreate(ethers.utils.arrayify(w.privateKey))) + + const data = { + publicKey: pubKey, + privateKey: Buffer.from(ethers.utils.arrayify(w.privateKey)) + } + const keyPair = new KeyPair(data) + + this.#privateKey = keyPair.privateKey + this.#publicKey = keyPair.publicKey + } + + getPublicKey (): Key { + return this.#publicKey + } + + getPublicNonces (): PublicNonces { + return schnorrkel.generatePublicNonces(this.#privateKey) + } + + multiSignMessage (msg: string, publicKeys: Key[], publicNonces: PublicNonces[]): SignatureOutput { + return schnorrkel.multiSigSign(this.#privateKey, msg, publicKeys, publicNonces) + } +} diff --git a/test/testutils.ts b/test/testutils.ts index 8f22627..26859b0 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -1,4 +1,4 @@ -import { ethers } from 'hardhat' +import { ethers, config } from 'hardhat' import { arrayify, hexConcat, @@ -60,12 +60,16 @@ export async function getTokenBalance (token: IERC20, address: string): Promise< return parseInt(balance.toString()) } -let counter = 0 +let counter = -1 export function createTmpAccount (): Wallet { const privateKey = keccak256(Buffer.from(arrayify(BigNumber.from(++counter)))) return new ethers.Wallet(privateKey, ethers.provider) - // return new ethers.Wallet('0x'.padEnd(66, privkeyBase), ethers.provider); + // const accounts: any = config.networks.hardhat.accounts + // console.log('accounts.path: ', accounts.path) + // console.log('accounts.mnemonic: ', accounts.mnemonic) + // counter++ + // return ethers.Wallet.fromMnemonic(accounts.mnemonic, accounts.path + `/${counter}`) } // create non-random account, so gas calculations are deterministic @@ -258,9 +262,14 @@ export async function createAccount ( cosignerAddresses: string, recoverAddresses: string, salt: BigNumberish, + mergedKeyIndexWithParity: number, + mergedKey: string, accountFactory: BloctoAccountFactory ): Promise { - await accountFactory.createAccount(authorizedAddresses, cosignerAddresses, recoverAddresses, salt) + const tx = await accountFactory.createAccount(authorizedAddresses, cosignerAddresses, recoverAddresses, salt, mergedKeyIndexWithParity, mergedKey) + // console.log('tx: ', tx) + const receipt = await tx.wait() + console.log('receipt gasUsed: ', receipt.gasUsed) const accountAddress = await accountFactory.getAddress(cosignerAddresses, recoverAddresses, salt) const account = BloctoAccount__factory.connect(accountAddress, ethersSigner) return account @@ -299,18 +308,8 @@ export const txData = (revert: number, to: string, amount: BigNumber, dataBuff: export const EIP191V0MessagePrefix = '\x19\x00' export function hashMessageEIP191V0 (address: string, message: Bytes | string): string { - if (typeof (message) === 'string') { - message = toUtf8Bytes(message) - } address = address.replace('0x', '') - // const tx = concat([ - // toUtf8Bytes(EIP191V0MessagePrefix), - // Uint8Array.from(Buffer.from(address, 'hex')), - // message - // ]) - // console.log('hashMessageEIP191V0 tx', tx) - return keccak256(concat([ toUtf8Bytes(EIP191V0MessagePrefix), Uint8Array.from(Buffer.from(address, 'hex')), @@ -343,3 +342,7 @@ export async function signMessage (signerWallet: Wallet, accountAddress: string, const sign = signerWallet._signingKey().signDigest(hashMessageEIP191V0(accountAddress, dataForHash)) return sign } + +export function logBytes (uint8: Uint8Array): string { + return Buffer.from(uint8).toString('hex') + '(' + uint8.length.toString() + ')' +} diff --git a/yarn.lock b/yarn.lock index 25d24fd..9eca7e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,6 +28,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@borislav.itskov/schnorrkel.js@https://github.com/borislav-itskov/schnorrkel.js": + version "2.0.0" + resolved "https://github.com/borislav-itskov/schnorrkel.js#357a1b56c0294d1e71b84d07d7ee898cbf5719d4" + dependencies: + bigi "^1.4.2" + ecurve "^1.0.6" + ethers "^5.7.2" + secp256k1 "^5.0.0" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -2366,6 +2375,11 @@ bech32@1.1.4: resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== +bigi@^1.1.0, bigi@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825" + integrity sha512-ddkU+dFIuEIW8lE7ZwdIAf2UPoM90eaprg5m3YXAVVTmKlqV/9BX4A2M8BOK2yOq6/VgZFVhK6QAxJebhlbhzw== + bigint-crypto-utils@^3.0.23: version "3.1.7" resolved "https://registry.yarnpkg.com/bigint-crypto-utils/-/bigint-crypto-utils-3.1.7.tgz#c4c1b537c7c1ab7aadfaecf3edfd45416bf2c651" @@ -3548,6 +3562,14 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecurve@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/ecurve/-/ecurve-1.0.6.tgz#dfdabbb7149f8d8b78816be5a7d5b83fcf6de797" + integrity sha512-/BzEjNfiSuB7jIWKcS/z8FK9jNjmEWvUV2YZ4RLSmcDtP7Lq0m6FvDuSnJpBlDpGRpfRQeTLGLBI8H+kEv0r+w== + dependencies: + bigi "^1.1.0" + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -4452,7 +4474,7 @@ ethereumjs-wallet@^1.0.1: utf8 "^3.0.0" uuid "^8.3.2" -ethers@^5.0.1, ethers@^5.0.2, ethers@^5.4.2, ethers@^5.5.2, ethers@^5.5.3: +ethers@^5.0.1, ethers@^5.0.2, ethers@^5.4.2, ethers@^5.5.2, ethers@^5.5.3, ethers@^5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -7213,6 +7235,11 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + node-emoji@^1.10.0: version "1.11.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" @@ -8451,6 +8478,15 @@ secp256k1@^4.0.1: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" +secp256k1@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-5.0.0.tgz#be6f0c8c7722e2481e9773336d351de8cddd12f7" + integrity sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA== + dependencies: + elliptic "^6.5.4" + node-addon-api "^5.0.0" + node-gyp-build "^4.2.0" + seedrandom@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.1.tgz#eb3dde015bcf55df05a233514e5df44ef9dce083" From 574647a4666b446f14f1ee0dac62bc45f08d9bb7 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Thu, 8 Jun 2023 09:31:55 +0800 Subject: [PATCH 13/26] v1.4.0: add test 'check none zero mergedKeyIndex' --- test/schnorrMultiSign.test.ts | 79 ++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/test/schnorrMultiSign.test.ts b/test/schnorrMultiSign.test.ts index 2e3a223..0d3a382 100644 --- a/test/schnorrMultiSign.test.ts +++ b/test/schnorrMultiSign.test.ts @@ -1,32 +1,20 @@ // update from https://github.com/borislav-itskov/schnorrkel.js import { ethers } from 'hardhat' -import { Wallet, BigNumber } from 'ethers' +import { BigNumber } from 'ethers' import { expect } from 'chai' import { - BloctoAccount, - BloctoAccount__factory, BloctoAccountCloneableWallet__factory, BloctoAccountFactory, - BloctoAccountFactory__factory, - TestBloctoAccountCloneableWalletV140, - TestBloctoAccountCloneableWalletV140__factory + BloctoAccountFactory__factory } from '../typechain' -import { EntryPoint } from '@account-abstraction/contracts' import { - fund, - createTmpAccount, createAccount, deployEntryPoint, - ONE_ETH, createAuthorizedCosignerRecoverWallet, - txData, - signMessage, - logBytes, hashMessageEIP191V0 } from './testutils' -// import Schnorrkel, { Key, PublicNonces, SignatureOutput } from '@borislav.itskov/schnorrkel.js/src/index' -import Schnorrkel, { Key, PublicNonces, SignatureOutput } from '../src/schnorrkel.js/index' +import Schnorrkel from '../src/schnorrkel.js/index' import { DefaultSigner } from './schnorrUtils' const ERC1271_MAGICVALUE_BYTES32 = '0x1626ba7e' @@ -34,11 +22,6 @@ const ERC1271_MAGICVALUE_BYTES32 = '0x1626ba7e' describe('Schnorr MultiSign Test', function () { const ethersSigner = ethers.provider.getSigner() - let authorizedWallet: Wallet - let cosignerWallet: Wallet - let recoverWallet: Wallet - // let account: BloctoAccount - let implementation: string let factory: BloctoAccountFactory @@ -50,17 +33,63 @@ describe('Schnorr MultiSign Test', function () { implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy(entryPoint.address)).address // account factory - factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation, entryPoint.address); - - // 3 wallet - [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() + factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation, entryPoint.address) }) it('should generate a schnorr musig2 and validate it on the blockchain', async () => { // create account // for only 1 byte - const mergedKeyIndex = 0 + const mergedKeyIndex = 0 << 1 + + const [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() + const signerOne = new DefaultSigner(authorizedWallet) + const signerTwo = new DefaultSigner(cosignerWallet) + const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] + const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) + const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) + // because of the parity byte is 2, 3 so sub 2 + const pxIndexWithPairty = combinedPublicKey.buffer.slice(0, 1).readInt8() - 2 + mergedKeyIndex + const account = await createAccount( + ethersSigner, + await authorizedWallet.getAddress(), + await cosignerWallet.getAddress(), + await recoverWallet.getAddress(), + BigNumber.from(123), + pxIndexWithPairty, + px, + factory + ) + + // multisig + const msg = 'just a test message' + + const msgKeccak256 = ethers.utils.solidityKeccak256(['string'], [msg]) + const msgEIP191V0 = hashMessageEIP191V0(account.address, msgKeccak256) + // note: following line multiSignMessage ignore hash message + const { signature: sigOne, challenge: e } = signerOne.multiSignMessage(msgEIP191V0, publicKeys, publicNonces) + const { signature: sigTwo } = signerTwo.multiSignMessage(msgEIP191V0, publicKeys, publicNonces) + const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) + // wrap the result + // e (bytes32), s (bytes32), pxIndexWithPairty (uint8) + // pxIndexWithPairty (7 bit for pxIndex, 1 bit for parity) + const hexPxIndexWithPairty = ethers.utils.hexlify(pxIndexWithPairty).slice(-2) + const abiCoder = new ethers.utils.AbiCoder() + const sigData = abiCoder.encode(['bytes32', 'bytes32'], [ + e.buffer, + sSummed.buffer + ]) + hexPxIndexWithPairty + const result = await account.isValidSignature(msgKeccak256, sigData) + expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) + }) + + it('check none zero mergedKeyIndex', async () => { + // create account + // for only 1 byte + const mergedKeyIndex = 2 << 1 + // following same as test 'should generate a schnorr musig2 and validate it on the blockchain' + const [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() const signerOne = new DefaultSigner(authorizedWallet) const signerTwo = new DefaultSigner(cosignerWallet) const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] From 5a5db5c21def1f853c3ff0dcb68483de1e0de216 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Thu, 8 Jun 2023 10:56:55 +0800 Subject: [PATCH 14/26] v1.4.0: update _mergedKeyIndexWithParity to uint8 --- contracts/BloctoAccountFactory.sol | 2 +- contracts/CoreWallet/CoreWallet.sol | 8 ++++---- test/schnorrMultiSign.test.ts | 24 ++++++++++++------------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol index 96e58fe..6c44485 100644 --- a/contracts/BloctoAccountFactory.sol +++ b/contracts/BloctoAccountFactory.sol @@ -33,7 +33,7 @@ contract BloctoAccountFactory is Ownable { address _cosigner, address _recoveryAddress, uint256 _salt, - uint256 _mergedKeyIndexWithParity, + uint8 _mergedKeyIndexWithParity, bytes32 _mergedKey ) public onlyOwner returns (BloctoAccount ret) { bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol index c7bd84a..e5f7653 100644 --- a/contracts/CoreWallet/CoreWallet.sol +++ b/contracts/CoreWallet/CoreWallet.sol @@ -66,7 +66,7 @@ contract CoreWallet is IERC1271 { /// "authorized addresses". mapping(uint256 => uint256) public authorizations; - // (authVersion,96)(padding_0,153)(authKeyIdx,6)(parity,1) -> merged_ec_pubkey_x (256) + // (authVersion,96)(padding_0,152)(authKeyIdx,7)(parity,1) -> merged_ec_pubkey_x (256) mapping(uint256 => bytes32) public mergedKeys; /// @notice A per-key nonce value, incremented each time a transaction is processed with that key. @@ -192,7 +192,7 @@ contract CoreWallet is IERC1271 { address _authorizedAddress, uint256 _cosigner, address _recoveryAddress, - uint256 _mergedKeyIndexWithPairty, + uint8 _mergedKeyIndexWithParity, bytes32 _mergedKey ) public onlyOnce { require(_authorizedAddress != _recoveryAddress, "Do not use the recovery address as an authorized address."); @@ -202,7 +202,7 @@ contract CoreWallet is IERC1271 { // set initial authorization value authVersion = AUTH_VERSION_INCREMENTOR; // add initial authorized address - this.setAuthorized(_authorizedAddress, _cosigner, _mergedKeyIndexWithPairty, _mergedKey); + this.setAuthorized(_authorizedAddress, _cosigner, _mergedKeyIndexWithParity, _mergedKey); } function bytesToAddresses(bytes memory bys) private pure returns (address[] memory addresses) { @@ -305,7 +305,7 @@ contract CoreWallet is IERC1271 { function setAuthorized( address _authorizedAddress, uint256 _cosigner, - uint256 _mergedIndexWithParity, + uint8 _mergedIndexWithParity, bytes32 _mergedKey ) external onlyInvoked { require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); diff --git a/test/schnorrMultiSign.test.ts b/test/schnorrMultiSign.test.ts index 0d3a382..907150c 100644 --- a/test/schnorrMultiSign.test.ts +++ b/test/schnorrMultiSign.test.ts @@ -49,14 +49,14 @@ describe('Schnorr MultiSign Test', function () { const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) // because of the parity byte is 2, 3 so sub 2 - const pxIndexWithPairty = combinedPublicKey.buffer.slice(0, 1).readInt8() - 2 + mergedKeyIndex + const pxIndexWithParity = combinedPublicKey.buffer.slice(0, 1).readInt8() - 2 + mergedKeyIndex const account = await createAccount( ethersSigner, await authorizedWallet.getAddress(), await cosignerWallet.getAddress(), await recoverWallet.getAddress(), BigNumber.from(123), - pxIndexWithPairty, + pxIndexWithParity, px, factory ) @@ -72,14 +72,14 @@ describe('Schnorr MultiSign Test', function () { const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) // wrap the result - // e (bytes32), s (bytes32), pxIndexWithPairty (uint8) - // pxIndexWithPairty (7 bit for pxIndex, 1 bit for parity) - const hexPxIndexWithPairty = ethers.utils.hexlify(pxIndexWithPairty).slice(-2) + // e (bytes32), s (bytes32), pxIndexWithParity (uint8) + // pxIndexWithParity (7 bit for pxIndex, 1 bit for parity) + const hexPxIndexWithParity = ethers.utils.hexlify(pxIndexWithParity).slice(-2) const abiCoder = new ethers.utils.AbiCoder() const sigData = abiCoder.encode(['bytes32', 'bytes32'], [ e.buffer, sSummed.buffer - ]) + hexPxIndexWithPairty + ]) + hexPxIndexWithParity const result = await account.isValidSignature(msgKeccak256, sigData) expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) }) @@ -97,14 +97,14 @@ describe('Schnorr MultiSign Test', function () { const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) // because of the parity byte is 2, 3 so sub 2 - const pxIndexWithPairty = combinedPublicKey.buffer.slice(0, 1).readInt8() - 2 + mergedKeyIndex + const pxIndexWithParity = combinedPublicKey.buffer.slice(0, 1).readInt8() - 2 + mergedKeyIndex const account = await createAccount( ethersSigner, await authorizedWallet.getAddress(), await cosignerWallet.getAddress(), await recoverWallet.getAddress(), BigNumber.from(123), - pxIndexWithPairty, + pxIndexWithParity, px, factory ) @@ -120,14 +120,14 @@ describe('Schnorr MultiSign Test', function () { const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) // wrap the result - // e (bytes32), s (bytes32), pxIndexWithPairty (uint8) - // pxIndexWithPairty (7 bit for pxIndex, 1 bit for parity) - const hexPxIndexWithPairty = ethers.utils.hexlify(pxIndexWithPairty).slice(-2) + // e (bytes32), s (bytes32), pxIndexWithParity (uint8) + // pxIndexWithParity (7 bit for pxIndex, 1 bit for parity) + const hexPxIndexWithParity = ethers.utils.hexlify(pxIndexWithParity).slice(-2) const abiCoder = new ethers.utils.AbiCoder() const sigData = abiCoder.encode(['bytes32', 'bytes32'], [ e.buffer, sSummed.buffer - ]) + hexPxIndexWithPairty + ]) + hexPxIndexWithParity const result = await account.isValidSignature(msgKeccak256, sigData) expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) }) From 3f6922a8020bdba63260e3fce571c128bbc9c344 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Thu, 8 Jun 2023 12:15:30 +0800 Subject: [PATCH 15/26] v1.4.0: add bit for know isSchnorr or not --- contracts/CoreWallet/CoreWallet.sol | 53 +++++++++++++++++------------ test/schnorrMultiSign.test.ts | 9 +++-- test/testutils.ts | 4 +-- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol index e5f7653..f8cdee5 100644 --- a/contracts/CoreWallet/CoreWallet.sol +++ b/contracts/CoreWallet/CoreWallet.sol @@ -66,7 +66,8 @@ contract CoreWallet is IERC1271 { /// "authorized addresses". mapping(uint256 => uint256) public authorizations; - // (authVersion,96)(padding_0,152)(authKeyIdx,7)(parity,1) -> merged_ec_pubkey_x (256) + // (authVersion,96)(padding_0,152)(isSchnorr,1) (authKeyIdx,6)(parity,1) -> merged_ec_pubkey_x (256) + // isisSchnorr: 1 -> schnnor, 0 -> not schnorr mapping(uint256 => bytes32) public mergedKeys; /// @notice A per-key nonce value, incremented each time a transaction is processed with that key. @@ -470,36 +471,46 @@ contract CoreWallet is IERC1271 { // https://github.com/gnosis/safe-contracts/blob/102e632d051650b7c4b0a822123f449beaf95aed/contracts/GnosisSafe.sol bytes32 operationHash = keccak256(abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, _hash)); - if (_signature.length == 65) { + if (_signature.length == 65 && (_signature[64] & 0x80) > 0) { return verifySchnorr(operationHash, _signature) ? IERC1271.isValidSignature.selector : bytes4(0); + } + + bytes32[2] memory r; + bytes32[2] memory s; + uint8[2] memory v; + address signer; + address cosigner; + + // extract 1 or 2 signatures depending on length + if (_signature.length == 65) { + (r[0], s[0], v[0]) = _signature.extractSignature(0); + signer = ecrecover(operationHash, v[0], r[0], s[0]); + cosigner = signer; } else if (_signature.length == 130) { - bytes32[2] memory r; - bytes32[2] memory s; - uint8[2] memory v; - address signer; - address cosigner; (r[0], s[0], v[0]) = _signature.extractSignature(0); (r[1], s[1], v[1]) = _signature.extractSignature(65); signer = ecrecover(operationHash, v[0], r[0], s[0]); cosigner = ecrecover(operationHash, v[1], r[1], s[1]); - // check for valid signature - if (signer == address(0)) { - return 0; - } + } else { + return 0; + } - // check for valid signature - if (cosigner == address(0)) { - return 0; - } + // check for valid signature + if (signer == address(0)) { + return 0; + } - // check to see if this is an authorized key - if (address(uint160(authorizations[authVersion + uint256(uint160(signer))])) != cosigner) { - return 0; - } + // check for valid signature + if (cosigner == address(0)) { + return 0; + } - return IERC1271.isValidSignature.selector; + // check to see if this is an authorized key + if (address(uint160(authorizations[authVersion + uint256(uint160(signer))])) != cosigner) { + return 0; } - return 0; + + return IERC1271.isValidSignature.selector; } /// @notice A version of `invoke()` that has no explicit signatures, and uses msg.sender diff --git a/test/schnorrMultiSign.test.ts b/test/schnorrMultiSign.test.ts index 907150c..5f6aee5 100644 --- a/test/schnorrMultiSign.test.ts +++ b/test/schnorrMultiSign.test.ts @@ -38,9 +38,8 @@ describe('Schnorr MultiSign Test', function () { it('should generate a schnorr musig2 and validate it on the blockchain', async () => { // create account - // for only 1 byte - const mergedKeyIndex = 0 << 1 - + // for only 1 byte, (isSchnorr,1)(authKeyIdx,6)(parity,1) + const mergedKeyIndex = 128 + (0 << 1) const [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() const signerOne = new DefaultSigner(authorizedWallet) const signerTwo = new DefaultSigner(cosignerWallet) @@ -86,8 +85,8 @@ describe('Schnorr MultiSign Test', function () { it('check none zero mergedKeyIndex', async () => { // create account - // for only 1 byte - const mergedKeyIndex = 2 << 1 + // for only 1 byte, (isSchnorr,1)(authKeyIdx,6)(parity,1) + const mergedKeyIndex = 128 + (2 << 1) // following same as test 'should generate a schnorr musig2 and validate it on the blockchain' const [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() const signerOne = new DefaultSigner(authorizedWallet) diff --git a/test/testutils.ts b/test/testutils.ts index 26859b0..e7af0ea 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -60,7 +60,7 @@ export async function getTokenBalance (token: IERC20, address: string): Promise< return parseInt(balance.toString()) } -let counter = -1 +let counter = 0 // Math.floor(Math.random() * 5000) export function createTmpAccount (): Wallet { const privateKey = keccak256(Buffer.from(arrayify(BigNumber.from(++counter)))) @@ -269,7 +269,7 @@ export async function createAccount ( const tx = await accountFactory.createAccount(authorizedAddresses, cosignerAddresses, recoverAddresses, salt, mergedKeyIndexWithParity, mergedKey) // console.log('tx: ', tx) const receipt = await tx.wait() - console.log('receipt gasUsed: ', receipt.gasUsed) + // console.log('createAccount gasUsed: ', receipt.gasUsed) const accountAddress = await accountFactory.getAddress(cosignerAddresses, recoverAddresses, salt) const account = BloctoAccount__factory.connect(accountAddress, ethersSigner) return account From 7346f9b8bad630962b10fadb21f72af86ea87bd9 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Tue, 13 Jun 2023 11:48:04 +0800 Subject: [PATCH 16/26] v1.4.0: add createAccount2, comment, update test --- contracts/BloctoAccount.sol | 2 +- contracts/BloctoAccountCloneableWallet.sol | 6 +- contracts/BloctoAccountFactory.sol | 120 +++++++------ contracts/BloctoAccountProxy.sol | 6 +- contracts/CoreWallet/CoreWallet.sol | 54 +++--- ... TestBloctoAccountCloneableWalletV200.sol} | 6 +- ...ountV140.sol => TestBloctoAccountV200.sol} | 4 +- .../0_deploy_Account_Factory-and-addStake.ts | 21 ++- deploy/4_verify.ts | 31 ++++ package.json | 6 +- test/bloctoaccount.test.ts | 57 +++--- test/entrypoint/entrypoint.test.ts | 169 ++---------------- test/testutils.ts | 18 +- 13 files changed, 218 insertions(+), 282 deletions(-) rename contracts/test/{TestBloctoAccountCloneableWalletV140.sol => TestBloctoAccountCloneableWalletV200.sol} (65%) rename contracts/test/{TestBloctoAccountV140.sol => TestBloctoAccountV200.sol} (96%) create mode 100644 deploy/4_verify.ts diff --git a/contracts/BloctoAccount.sol b/contracts/BloctoAccount.sol index 929ec9d..32c2e6e 100644 --- a/contracts/BloctoAccount.sol +++ b/contracts/BloctoAccount.sol @@ -19,7 +19,7 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas /** * This is the version of this contract. */ - string public constant VERSION = "1.3.0"; + string public constant VERSION = "1.4.0"; IEntryPoint private immutable _entryPoint; diff --git a/contracts/BloctoAccountCloneableWallet.sol b/contracts/BloctoAccountCloneableWallet.sol index f476a9a..11ec8f1 100644 --- a/contracts/BloctoAccountCloneableWallet.sol +++ b/contracts/BloctoAccountCloneableWallet.sol @@ -6,10 +6,8 @@ import "./BloctoAccount.sol"; /// @title BloctoAccountCloneableWallet Wallet /// @notice This contract represents a complete but non working wallet. contract BloctoAccountCloneableWallet is BloctoAccount { - /** - * constructor that deploys a NON-FUNCTIONAL version of `BloctoAccount` - * @param anEntryPoint entrypoint address - */ + /// @notice constructor that deploys a NON-FUNCTIONAL version of `BloctoAccount` + /// @param anEntryPoint entrypoint address constructor(IEntryPoint anEntryPoint) BloctoAccount(anEntryPoint) { initialized = true; } diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol index 6c44485..9bb6ec1 100644 --- a/contracts/BloctoAccountFactory.sol +++ b/contracts/BloctoAccountFactory.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.12; import "@openzeppelin/contracts/utils/Create2.sol"; -// import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import "./BloctoAccountProxy.sol"; @@ -10,24 +9,33 @@ import "./BloctoAccount.sol"; // BloctoAccountFactory for creating BloctoAccountProxy contract BloctoAccountFactory is Ownable { - /// @notice This is the version of this contract. - string public constant VERSION = "1.3.0"; + /// @notice this is the version of this contract. + string public constant VERSION = "1.4.0"; + /// @notice the implementation address for BloctoAccountCloneableWallet address public bloctoAccountImplementation; + /// @notice the address from EIP-4337 official implementation IEntryPoint public entryPoint; event WalletCreated(address wallet, address authorizedAddress, bool full); + /// @notice constructor + /// @param _bloctoAccountImplementation the implementation address for BloctoAccountCloneableWallet + /// @param _entryPoint the entrypoint address from EIP-4337 official implementation constructor(address _bloctoAccountImplementation, IEntryPoint _entryPoint) { bloctoAccountImplementation = _bloctoAccountImplementation; entryPoint = _entryPoint; } - /** - * create an account, and return its BloctoAccount. - * 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 - */ + /// @notice create an account, and return its BloctoAccount. + /// 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 + /// @param _authorizedAddress the initial authorized address, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _salt salt for create account (used for address calculation in create2) + /// @param _mergedKeyIndexWithParity the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKey the corresponding mergedKey (using Schnorr merged key) function createAccount( address _authorizedAddress, address _cosigner, @@ -47,38 +55,43 @@ contract BloctoAccountFactory is Ownable { emit WalletCreated(address(ret), _authorizedAddress, false); } - // function createAccount2( - // bytes memory _authorizedAddresses, - // address _cosigner, - // address _recoveryAddress, - // uint256 _salt - // ) public returns (BloctoAccount ret) { - // require( - // _authorizedAddresses.length / 20 > 0 && _authorizedAddresses.length % 20 == 0, "invalid address byte array" - // ); - - // address addr = getAddress(_cosigner, _recoveryAddress, _salt); - // uint256 codeSize = addr.code.length; - // if (codeSize > 0) { - // return BloctoAccount(payable(addr)); - // } - // bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); - // // for consistent address - // BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); - // newProxy.initImplementation(bloctoAccountImplementation); - // ret = BloctoAccount(payable(address(newProxy))); - // ret.init2(_authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress); - - // address firstAuthorizedAddress; - // assembly { - // firstAuthorizedAddress := mload(add(_authorizedAddresses, 20)) - // } - // emit WalletCreated(address(ret), firstAuthorizedAddress, false); - // } + /// @notice create an account with multiple authorized addresses, and return its BloctoAccount. + /// returns the address even if the account is already deployed. + /// @param _authorizedAddresses the initial authorized addresses, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _salt salt for create account (used for address calculation in create2) + /// @param _mergedKeyIndexWithParitys the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKeys the corresponding mergedKey + function createAccount2( + address[] calldata _authorizedAddresses, + address _cosigner, + address _recoveryAddress, + uint256 _salt, + uint8[] calldata _mergedKeyIndexWithParitys, + bytes32[] calldata _mergedKeys + ) public onlyOwner returns (BloctoAccount ret) { + address addr = getAddress(_cosigner, _recoveryAddress, _salt); + uint256 codeSize = addr.code.length; + if (codeSize > 0) { + return BloctoAccount(payable(addr)); + } + bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); + // for consistent address + BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); + newProxy.initImplementation(bloctoAccountImplementation); + ret = BloctoAccount(payable(address(newProxy))); + ret.init2( + _authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParitys, _mergedKeys + ); + // emit event only with _authorizedAddresses[0] + emit WalletCreated(address(ret), _authorizedAddresses[0], true); + } - /** - * calculate the counterfactual address of this account as it would be returned by createAccount() - */ + /// @notice calculate the counterfactual address of this account as it would be returned by createAccount() + /// @param _cosigner the initial cosigning address + /// @param _recoveryAddress the initial recovery address for the wallet + /// @param _salt salt for create account (used for address calculation in create2) function getAddress(address _cosigner, address _recoveryAddress, uint256 _salt) public view returns (address) { bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); return Create2.computeAddress( @@ -86,36 +99,27 @@ contract BloctoAccountFactory is Ownable { ); } - /** - * set the implementation of the BloctoAccountProxy - * @param _bloctoAccountImplementation target to send to - */ + /// @notice set the implementation + /// @param _bloctoAccountImplementation update the implementation address of BloctoAccountCloneableWallet for createAccount and createAccount2 function setImplementation(address _bloctoAccountImplementation) public onlyOwner { bloctoAccountImplementation = _bloctoAccountImplementation; } - /** - * set the entrypoint - * @param _entrypoint target entrypoint - */ + /// @notice set the entrypoint + /// @param _entrypoint target entrypoint function setEntrypoint(IEntryPoint _entrypoint) public onlyOwner { entryPoint = _entrypoint; } - /** - * withdraw value from the deposit - * @param withdrawAddress target to send to - * @param amount to withdraw - */ + /// @notice withdraw value from the deposit + /// @param withdrawAddress target to send to + /// @param amount to withdraw function withdrawTo(address payable withdrawAddress, uint256 amount) public onlyOwner { entryPoint.withdrawTo(withdrawAddress, amount); } - /** - * add stake for this factory. - * This method can also carry eth value to add to the current stake. - * @param unstakeDelaySec - the unstake delay for this factory. Can only be increased. - */ + /// @notice add stake in etnrypoint for this factory to avoid bundler reject + /// @param unstakeDelaySec - the unstake delay for this factory. Can only be increased. function addStake(uint32 unstakeDelaySec) external payable onlyOwner { entryPoint.addStake{value: msg.value}(unstakeDelaySec); } diff --git a/contracts/BloctoAccountProxy.sol b/contracts/BloctoAccountProxy.sol index ca87854..9328a2d 100644 --- a/contracts/BloctoAccountProxy.sol +++ b/contracts/BloctoAccountProxy.sol @@ -7,10 +7,8 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract BloctoAccountProxy is ERC1967Proxy, Initializable { constructor(address _logic) ERC1967Proxy(_logic, new bytes(0)) {} - /** - * initialize BloctoAccountProxy for adding the implementation address - * @param implementation implementation address - */ + /// @notice initialize BloctoAccountProxy for adding the implementation address + /// @param implementation implementation address function initImplementation(address implementation) public initializer { require(Address.isContract(implementation), "ERC1967: new implementation is not a contract"); StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = implementation; diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol index f8cdee5..f1c4ce3 100644 --- a/contracts/CoreWallet/CoreWallet.sol +++ b/contracts/CoreWallet/CoreWallet.sol @@ -67,7 +67,7 @@ contract CoreWallet is IERC1271 { mapping(uint256 => uint256) public authorizations; // (authVersion,96)(padding_0,152)(isSchnorr,1) (authKeyIdx,6)(parity,1) -> merged_ec_pubkey_x (256) - // isisSchnorr: 1 -> schnnor, 0 -> not schnorr + // isSchnorr: 1 -> schnorr, 0 -> not schnorr mapping(uint256 => bytes32) public mergedKeys; /// @notice A per-key nonce value, incremented each time a transaction is processed with that key. @@ -147,7 +147,7 @@ contract CoreWallet is IERC1271 { /// @param cosigner the 2-of-2 signatory (optional). event Authorized(address authorizedAddress, uint256 cosigner); - event AuthorizedMeregedKey(uint256 authorizedAddress, uint256 mergedKey); + event AuthorizedMeregedKey(uint256 authorizedAddress, bytes32 mergedKey); /// @notice Emitted when an emergency recovery has been performed. If this event is fired, /// ALL previously authorized addresses have been deauthorized and the only authorized @@ -189,6 +189,8 @@ contract CoreWallet is IERC1271 { /// @param _authorizedAddress the initial authorized address, must not be zero! /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _mergedKeyIndexWithParity the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKey the corresponding mergedKey (using Schnorr merged key) function init( address _authorizedAddress, uint256 _cosigner, @@ -218,18 +220,31 @@ contract CoreWallet is IERC1271 { } } - // function init2(bytes memory _authorizedAddresses, uint256 _cosigner, address _recoveryAddress) public onlyOnce { - // address[] memory addresses = bytesToAddresses(_authorizedAddresses); - // recoveryAddress = _recoveryAddress; - // // set initial authorization value - // authVersion = AUTH_VERSION_INCREMENTOR; - // for (uint256 i = 0; i < addresses.length; i++) { - // address _authorizedAddress = addresses[i]; - // require(_authorizedAddress != _recoveryAddress, "Do not use the recovery address as an authorized address."); - // require(address(uint160(_cosigner)) != _recoveryAddress, "Do not use the recovery address as a cosigner."); - // setAuthorized(_authorizedAddress, _cosigner, 0, bytes32(0)); - // } - // } + /// @notice The shared initialization code used to setup the contract state regardless of whether or + /// not the clone pattern is being used. + /// @param _authorizedAddresses the initial authorized addresses, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _mergedKeyIndexWithParitys the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKeys the corresponding mergedKey + function init2( + address[] calldata _authorizedAddresses, + uint256 _cosigner, + address _recoveryAddress, + uint8[] calldata _mergedKeyIndexWithParitys, + bytes32[] calldata _mergedKeys + ) public onlyOnce { + require(_authorizedAddresses.length > 0, "invalid _authorizedAddresses array"); + require(_authorizedAddresses.length == _mergedKeyIndexWithParitys.length, "Array length not match with."); + require(_authorizedAddresses.length == _mergedKeys.length, "Array length not match."); + recoveryAddress = _recoveryAddress; + // set initial authorization value + authVersion = AUTH_VERSION_INCREMENTOR; + for (uint256 i = 0; i < _authorizedAddresses.length; i++) { + address _authorizedAddress = _authorizedAddresses[i]; + this.setAuthorized(_authorizedAddress, _cosigner, _mergedKeyIndexWithParitys[i], _mergedKeys[i]); + } + } /// @notice The fallback function, invoked whenever we receive a transaction that doesn't call any of our /// named functions. In particular, this method is called when we are the target of a simple send @@ -327,13 +342,12 @@ contract CoreWallet is IERC1271 { /// @dev Must be called through `invoke()` /// @param _meregedKeyIndex the merged key index /// @param _meregedKey the corresponding merged authorized key & cosigner key by Schnorr - // function setMergedKey(uint256 _meregedKeyIndex, uint256 _meregedKey) external onlyInvoked { - // // require(_authorizedAddress != address(0), "Authorized address must not be zero."); - // require(_meregedKey != 0, "Merged key must not be zero."); + function setMergedKey(uint256 _meregedKeyIndex, bytes32 _meregedKey) external onlyInvoked { + require(_meregedKey != 0, "Merged key must not be zero."); - // mergedKeys[authVersion + _meregedKeyIndex] = _meregedKey; - // emit AuthorizedMeregedKey(_meregedKeyIndex, _meregedKey); - // } + mergedKeys[authVersion + _meregedKeyIndex] = _meregedKey; + emit AuthorizedMeregedKey(_meregedKeyIndex, _meregedKey); + } /// @notice Performs an emergency recovery operation, removing all existing authorizations and setting /// a sole new authorized address with optional cosigner. THIS IS A SCORCHED EARTH SOLUTION, and great diff --git a/contracts/test/TestBloctoAccountCloneableWalletV140.sol b/contracts/test/TestBloctoAccountCloneableWalletV200.sol similarity index 65% rename from contracts/test/TestBloctoAccountCloneableWalletV140.sol rename to contracts/test/TestBloctoAccountCloneableWalletV200.sol index 81d10d3..e8ff7be 100644 --- a/contracts/test/TestBloctoAccountCloneableWalletV140.sol +++ b/contracts/test/TestBloctoAccountCloneableWalletV200.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.12; -import "./TestBloctoAccountV140.sol"; +import "./TestBloctoAccountV200.sol"; /// @title BloctoAccountCloneableWallet Wallet /// @notice This contract represents a complete but non working wallet. -contract TestBloctoAccountCloneableWalletV140 is TestBloctoAccountV140 { +contract TestBloctoAccountCloneableWalletV200 is TestBloctoAccountV200 { /// @dev Cconstructor that deploys a NON-FUNCTIONAL version of `TestBloctoAccountV140` - constructor(IEntryPoint anEntryPoint) TestBloctoAccountV140(anEntryPoint) { + constructor(IEntryPoint anEntryPoint) TestBloctoAccountV200(anEntryPoint) { initialized = true; } } diff --git a/contracts/test/TestBloctoAccountV140.sol b/contracts/test/TestBloctoAccountV200.sol similarity index 96% rename from contracts/test/TestBloctoAccountV140.sol rename to contracts/test/TestBloctoAccountV200.sol index 8773a04..18f1e17 100644 --- a/contracts/test/TestBloctoAccountV140.sol +++ b/contracts/test/TestBloctoAccountV200.sol @@ -15,11 +15,11 @@ import "../CoreWallet/CoreWallet.sol"; * Blocto account. * compatibility for EIP-4337 and smart contract wallet with cosigner functionality (CoreWallet) */ -contract TestBloctoAccountV140 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, BaseAccount { +contract TestBloctoAccountV200 is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, BaseAccount { /** * This is the version of this contract. */ - string public constant VERSION = "1.4.0"; + string public constant VERSION = "2.0.0"; IEntryPoint private immutable _entryPoint; diff --git a/deploy/0_deploy_Account_Factory-and-addStake.ts b/deploy/0_deploy_Account_Factory-and-addStake.ts index c1dc61e..a909620 100644 --- a/deploy/0_deploy_Account_Factory-and-addStake.ts +++ b/deploy/0_deploy_Account_Factory-and-addStake.ts @@ -1,6 +1,6 @@ import { EntryPoint__factory } from '@account-abstraction/contracts' import { BigNumber } from 'ethers' -import { ethers } from 'hardhat' +import hre, { ethers } from 'hardhat' const BloctoAccountCloneableWallet = 'BloctoAccountCloneableWallet' const BloctoAccountFactory = 'BloctoAccountFactory' @@ -12,8 +12,8 @@ async function main (): Promise { const [owner] = await ethers.getSigners() console.log('deploy with account: ', owner.address) - const factory = await ethers.getContractFactory(BloctoAccountCloneableWallet) - const walletCloneable = await factory.deploy(EntryPoint, { + const BloctoAccountCloneableWalletContract = await ethers.getContractFactory(BloctoAccountCloneableWallet) + const walletCloneable = await BloctoAccountCloneableWalletContract.deploy(EntryPoint, { gasLimit: GasLimit }) @@ -38,6 +38,21 @@ async function main (): Promise { const entrypoint = EntryPoint__factory.connect(EntryPoint, ethers.provider) const depositInfo = await entrypoint.getDepositInfo(accountFactory.address) console.log('stake: ', ethers.utils.formatUnits(depositInfo.stake), ', unstakeDelaySec: ', depositInfo.unstakeDelaySec) + + // verify BloctoAccountCloneableWallet + await hre.run('verify:verify', { + address: walletCloneable.address, + constructorArguments: [ + EntryPoint + ] + }) + // verify BloctoAccountFactory + await hre.run('verify:verify', { + address: accountFactory.address, + constructorArguments: [ + walletCloneable.address, EntryPoint + ] + }) } // We recommend this pattern to be able to use async/await everywhere diff --git a/deploy/4_verify.ts b/deploy/4_verify.ts new file mode 100644 index 0000000..95d32b9 --- /dev/null +++ b/deploy/4_verify.ts @@ -0,0 +1,31 @@ +import hre from 'hardhat' + +const BloctoAccountCloneableWalletAddr = '0x409bAa86c5B901Cd9fA35317f519c260a0e6231b' +const BloctoAccountFactoryAddr = '0x1522Db12e80fA5827ca462Ba6C317c63d38A4Bca' +const EntryPoint = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' + +async function main (): Promise { + // verify BloctoAccountCloneableWallet + await hre.run('verify:verify', { + address: BloctoAccountCloneableWalletAddr, + contract: 'contracts/BloctoAccountCloneableWallet.sol:BloctoAccountCloneableWallet', + constructorArguments: [ + EntryPoint + ] + }) + // verify BloctoAccountFactory + await hre.run('verify:verify', { + address: BloctoAccountFactoryAddr, + contract: 'contracts/BloctoAccountFactory.sol:BloctoAccountFactory', + constructorArguments: [ + BloctoAccountCloneableWalletAddr, EntryPoint + ] + }) +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/package.json b/package.json index e31f046..8067de8 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "deploy-bloctoaccountcloneable": "hardhat run deploy/1_deploy_BloctoAccount4337CloneableWallet.ts", "deploy-bloctoaccountfactory": "hardhat run deploy/2_deploy_BloctoAccountFactory.ts", "deploy-verifyingpaymaster": "hardhat run deploy/3_deploy_VerifyingPaymaster.ts", + "verify": "hardhat run deploy/4_verify.ts", "verify-bloctoaccountcloneable": "npx hardhat verify --contract contracts/BloctoAccountCloneableWallet.sol:BloctoAccountCloneableWallet 0x66d4d44e4957F70dF0127b3de2dBaD9fF9058B5B 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", - "verify-accountfactory": "npx hardhat verify 0xC261555D0e4623BF709d3f22Bf58A6275CE4d17D 0x66d4d44e4957F70dF0127b3de2dBaD9fF9058B5B 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "verify-bloctoaccountproxy": "npx hardhat verify --contract contracts/BloctoAccountProxy.sol:BloctoAccountProxy 0xa38dB698ec2Ec25ac7b2919fD69F49a7A975a724 0xee8EED6701EfD1d4de6B3930859919C966c6B00C ", + "verify-bloctoaccountfactory": "npx hardhat verify 0xC261555D0e4623BF709d3f22Bf58A6275CE4d17D 0x66d4d44e4957F70dF0127b3de2dBaD9fF9058B5B 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", "verify-verifyingpaymaster": "npx hardhat verify 0xE671dEee9c758e642d50c32e13FD5fC6D42C98F1 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 0x086443C6bA8165a684F3e316Da42D3A2F0a2330a" }, "devDependencies": { @@ -64,4 +66,4 @@ "table": "^6.8.0", "typescript": "^4.3.5" } -} +} \ No newline at end of file diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index 749c3ae..b0ca308 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -7,8 +7,8 @@ import { BloctoAccountCloneableWallet__factory, BloctoAccountFactory, BloctoAccountFactory__factory, - TestBloctoAccountCloneableWalletV140, - TestBloctoAccountCloneableWalletV140__factory + TestBloctoAccountCloneableWalletV200, + TestBloctoAccountCloneableWalletV200__factory } from '../typechain' import { EntryPoint } from '@account-abstraction/contracts' import { @@ -19,9 +19,9 @@ import { ONE_ETH, createAuthorizedCosignerRecoverWallet, txData, - signMessage + signMessage, + getMergedKey } from './testutils' -import { hexConcat } from 'ethers/lib/utils' describe('BloctoAccount Upgrade Test', function () { const ethersSigner = ethers.provider.getSigner() @@ -35,13 +35,17 @@ describe('BloctoAccount Upgrade Test', function () { let entryPoint: EntryPoint - async function testCreateAccount (salt: number): Promise { + async function testCreateAccount (salt = 0, mergedKeyIndex = 0): Promise { + const [px, pxIndexWithParity] = getMergedKey(authorizedWallet, cosignerWallet, mergedKeyIndex) + const account = await createAccount( ethersSigner, await authorizedWallet.getAddress(), await cosignerWallet.getAddress(), await recoverWallet.getAddress(), BigNumber.from(salt), + pxIndexWithParity, + px, factory ) await fund(account) @@ -92,8 +96,15 @@ describe('BloctoAccount Upgrade Test', function () { const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() const authorizedWallet22 = createTmpAccount() - const addresses = hexConcat([authorizedWallet2.address, authorizedWallet22.address]) - const tx = await factory.createAccount2(addresses, cosignerWallet2.address, recoverWallet2.address, AccountSalt) + const [px, pxIndexWithParity] = getMergedKey(authorizedWallet, cosignerWallet, 0) + const [px2, pxIndexWithParity2] = getMergedKey(authorizedWallet2, cosignerWallet2, 1) + + const tx = await factory.createAccount2([authorizedWallet2.address, authorizedWallet22.address], + cosignerWallet2.address, recoverWallet2.address, + 754264557, // random salt + [pxIndexWithParity, pxIndexWithParity2], + [px, px2]) + const receipt = await tx.wait() let findWalletCreated = false @@ -111,13 +122,13 @@ describe('BloctoAccount Upgrade Test', function () { const AccountSalt = 12345 const MockEntryPointV070 = '0x000000000000000000000000000000000000E070' let account: BloctoAccount - let implementationV140: TestBloctoAccountCloneableWalletV140 + let implementationV200: TestBloctoAccountCloneableWalletV200 - async function upgradeAccountToV140 (): Promise { + async function upgradeAccountToV200 (): Promise { const authorizeInAccountNonce = (await account.nonces(authorizedWallet.address)).add(1) const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosignerWallet) const upgradeToData = txData(1, account.address, BigNumber.from(0), - account.interface.encodeFunctionData('upgradeTo', [implementationV140.address])) + account.interface.encodeFunctionData('upgradeTo', [implementationV200.address])) const sign = await signMessage(authorizedWallet, account.address, authorizeInAccountNonce, upgradeToData) await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, authorizeInAccountNonce, authorizedWallet.address, upgradeToData) @@ -126,21 +137,19 @@ describe('BloctoAccount Upgrade Test', function () { before(async () => { account = await testCreateAccount(AccountSalt) // mock new entry point version 0.7.0 - implementationV140 = await new TestBloctoAccountCloneableWalletV140__factory(ethersSigner).deploy(MockEntryPointV070) - await factory.setImplementation(implementationV140.address) + implementationV200 = await new TestBloctoAccountCloneableWalletV200__factory(ethersSigner).deploy(MockEntryPointV070) + await factory.setImplementation(implementationV200.address) }) it('upgrade fail if not by contract self', async () => { // upgrade revert even though upgrade by cosigner - await expect(account.connect(cosignerWallet).upgradeTo(implementationV140.address)) + await expect(account.connect(cosignerWallet).upgradeTo(implementationV200.address)) .to.revertedWith('must be called from `invoke()') }) it('upgrade test', async () => { - expect(await account.VERSION()).to.eql('1.3.0') - await upgradeAccountToV140() - // accountV140 = BloctoAccount__factory.connect(account.address, ethersSigner) - expect(await account.VERSION()).to.eql('1.4.0') + await upgradeAccountToV200() + expect(await account.VERSION()).to.eql('2.0.0') }) it('factory getAddress some be same', async () => { @@ -152,16 +161,10 @@ describe('BloctoAccount Upgrade Test', function () { }) it('new account get new version', async () => { - const randomSalt = '0x33384e5765b53776863ffa7c4965af012ded5be4000000000000000000000005' - const accountNew = await createAccount( - ethersSigner, - await authorizedWallet.getAddress(), - await cosignerWallet.getAddress(), - await recoverWallet.getAddress(), - randomSalt, - factory - ) - expect(await accountNew.VERSION()).to.eql('1.4.0') + const randomSalt = 54326346 + const accountNew = await testCreateAccount(randomSalt) + + expect(await accountNew.VERSION()).to.eql('2.0.0') }) it('should entrypoint be v070 address', async () => { diff --git a/test/entrypoint/entrypoint.test.ts b/test/entrypoint/entrypoint.test.ts index 269554f..e2bab98 100644 --- a/test/entrypoint/entrypoint.test.ts +++ b/test/entrypoint/entrypoint.test.ts @@ -8,23 +8,7 @@ import { BloctoAccountCloneableWallet, BloctoAccountCloneableWallet__factory, BloctoAccountFactory, - BloctoAccountFactory__factory, - TestAggregatedAccount__factory, - TestAggregatedAccountFactory__factory, - TestCounter, - TestCounter__factory, - TestExpirePaymaster, - TestExpirePaymaster__factory, - TestExpiryAccount, - TestExpiryAccount__factory, - TestPaymasterAcceptAll, - TestPaymasterAcceptAll__factory, - TestRevertAccount__factory, - TestAggregatedAccount, - TestSignatureAggregator, - TestSignatureAggregator__factory, - MaliciousAccount__factory, - TestWarmColdAccount__factory + BloctoAccountFactory__factory } from '../../typechain' import { AddressZero, @@ -46,9 +30,8 @@ import { simulationResultCatch, createTmpAccount, createAccount, - getAggregatedAccountInitCode, - simulationResultWithAggregationCatch, decodeRevertReason, - createAuthorizedCosignerRecoverWallet + createAuthorizedCosignerRecoverWallet, + getMergedKey } from '../testutils' import { checkForBannedOps } from './entrypoint_utils' import { DefaultsForUserOp, getUserOpHash, fillAndSignWithCoSigner } from './UserOp' @@ -90,12 +73,15 @@ describe('EntryPoint', function () { factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation, entryPoint.address); [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() + const [px, pxIndexWithParity] = getMergedKey(authorizedWallet, cosignerWallet, 0) account = await createAccount( ethersSigner, await authorizedWallet.getAddress(), await cosignerWallet.getAddress(), await recoverWallet.getAddress(), - BigNumber.from(0), + 0, + pxIndexWithParity, + px, factory ) await fund(account) @@ -118,12 +104,15 @@ describe('EntryPoint', function () { before(async () => { [authorizedWallet1, cosignerWallet1, recoverWallet1] = createAuthorizedCosignerRecoverWallet() + const [px, pxIndexWithParity] = getMergedKey(authorizedWallet, cosignerWallet, 0) account1 = await createAccount( ethersSigner, await authorizedWallet1.getAddress(), await cosignerWallet1.getAddress(), await recoverWallet1.getAddress(), 0, + pxIndexWithParity, + px, factory) }) it('should fail if validateUserOp fails', async () => { @@ -202,32 +191,6 @@ describe('EntryPoint', function () { expect(returnInfo.paymasterContext).to.eql('0x') }) - // it('should return stake of sender', async () => { - // const stakeValue = BigNumber.from(123) - // const unstakeDelay = 3 - // // const { proxy: account2 } = await createAccount(ethersSigner, await ethersSigner.getAddress(), entryPoint.address) - // const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() - // const account2 = await createAccount( - // ethersSigner, - // await authorizedWallet2.getAddress(), - // await cosignerWallet2.getAddress(), - // await recoverWallet2.getAddress(), - // 0, - // factory) - - // await fund(account2) - // await account2.execute(entryPoint.address, stakeValue, entryPoint.interface.encodeFunctionData('addStake', [unstakeDelay])) - // // const op = await fillAndSign({ sender: account2.address }, ethersSigner, entryPoint) - // const op = await fillAndSignWithCoSigner( - // { sender: account2.address }, - // authorizedWallet2, - // cosignerWallet2, - // entryPoint - // ) - // const result = await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) - // expect(result.senderInfo).to.eql({ stake: stakeValue, unstakeDelaySec: unstakeDelay }) - // }) - it('should prevent overflows: fail if any numeric value is more than 120 bits', async () => { const op = await fillAndSignWithCoSigner( { @@ -244,108 +207,18 @@ describe('EntryPoint', function () { ).to.revertedWith('gas values overflow') }) - it('should fail creation for wrong sender', async () => { - const op1 = await fillAndSignWithCoSigner({ - initCode: getAccountInitCode(factory, authorizedWallet1.address, cosignerWallet1.address, recoverWallet1.address, 0), - sender: '0x'.padEnd(42, '1'), - verificationGasLimit: 3e6 - }, - authorizedWallet1, - cosignerWallet1, - entryPoint - ) - - await expect(entryPoint.callStatic.simulateValidation(op1)) - .to.revertedWith('AA14 initCode must return sender') - }) - - it('should report failure on insufficient verificationGas (OOG) for creation', async () => { - const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() - const initCode = getAccountInitCode(factory, authorizedWallet2.address, cosignerWallet2.address, recoverWallet2.address) - const sender = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) - - const op0 = await fillAndSignWithCoSigner({ - initCode: initCode, - sender: sender, - verificationGasLimit: 8e5, - maxFeePerGas: 0 - }, - authorizedWallet2, - cosignerWallet2, - entryPoint - ) - - // must succeed with enough verification gas. - await expect(entryPoint.callStatic.simulateValidation(op0, { gasLimit: 1e6 })) - .to.revertedWith('ValidationResult') - - const op1 = await fillAndSignWithCoSigner({ - initCode: initCode, - sender: sender, - verificationGasLimit: 1e5, - maxFeePerGas: 0 - }, - authorizedWallet2, - cosignerWallet2, - entryPoint - ) - await expect(entryPoint.callStatic.simulateValidation(op1, { gasLimit: 1e6 })) - .to.revertedWith('AA13 initCode failed or OOG') - }) - - it('should succeed for creating an account', async () => { - const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() - const initCode = getAccountInitCode(factory, authorizedWallet2.address, cosignerWallet2.address, recoverWallet2.address) - const sender = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) - - const op1 = await fillAndSignWithCoSigner({ - initCode: initCode, - sender: sender, - verificationGasLimit: 8e5, - maxFeePerGas: 0 - }, - authorizedWallet2, - cosignerWallet2, - entryPoint - ) - await fund(op1.sender) - - await entryPoint.callStatic.simulateValidation(op1).catch(simulationResultCatch) - }) - - it('should succeed for creating an account with multiple authorize address', async () => { - const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() - const authorizedWallet22 = createTmpAccount() - - const addresses = hexConcat([authorizedWallet2.address, authorizedWallet22.address]) - - const initCode = getAccountInitCode2(factory, addresses, cosignerWallet2.address, recoverWallet2.address) - const sender = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) - - const op1 = await fillAndSignWithCoSigner({ - initCode: initCode, - sender: sender, - verificationGasLimit: 8e5, - maxFeePerGas: 0 - }, - authorizedWallet2, - cosignerWallet2, - entryPoint - ) - await fund(op1.sender) - - await entryPoint.callStatic.simulateValidation(op1).catch(simulationResultCatch) - }) - it('should not call initCode from entrypoint', async () => { // a possible attack: call an account's execFromEntryPoint through initCode. This might lead to stolen funds. const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + const [px, pxIndexWithParity] = getMergedKey(authorizedWallet2, cosignerWallet2, 0) const account = await createAccount( ethersSigner, await authorizedWallet2.getAddress(), await cosignerWallet2.getAddress(), await recoverWallet2.getAddress(), 0, + pxIndexWithParity, + px, factory) const sender = createAddress() @@ -362,21 +235,5 @@ describe('EntryPoint', function () { const error = await entryPoint.callStatic.simulateValidation(op1).catch(e => e) expect(error.message).to.match(/initCode failed or OOG/, error) }) - - it('should not use banned ops during simulateValidation', async () => { - const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() - const sender = await getAccountAddress(factory, cosignerWallet2.address, recoverWallet2.address) - const initCode = getAccountInitCode(factory, authorizedWallet2.address, cosignerWallet2.address, recoverWallet2.address) - const op1 = await fillAndSignWithCoSigner({ - initCode: initCode, - sender: sender - }, authorizedWallet2, cosignerWallet2, entryPoint) - - await fund(op1.sender) - await entryPoint.simulateValidation(op1, { gasLimit: 10e6 }).catch(e => e) - const block = await ethers.provider.getBlock('latest') - const hash = block.transactions[0] - await checkForBannedOps(hash, false) - }) }) }) diff --git a/test/testutils.ts b/test/testutils.ts index e7af0ea..6cdcd9d 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -21,6 +21,8 @@ import { toUtf8Bytes } from '@ethersproject/strings' import { expect } from 'chai' import { Create2Factory } from '../src/Create2Factory' +import Schnorrkel from '../src/schnorrkel.js/index' +import { DefaultSigner } from './schnorrUtils' export const AddressZero = ethers.constants.AddressZero export const HashZero = ethers.constants.HashZero @@ -102,7 +104,7 @@ export async function calcGasUsage (rcpt: ContractReceipt, entryPoint: EntryPoin } // helper function to create the initCode to deploy the account, using our account factory. -export function getAccountInitCode (factory: BloctoAccountFactory, authorizedAddress: string, cosignerAddress: string, recoveryAddress: string, salt = 0): BytesLike { +export function getAccountInitCode (factory: BloctoAccountFactory, authorizedAddress: string, cosignerAddress: string, recoveryAddress: string, salt, pxIndexWithParity, px): BytesLike { return hexConcat([ factory.address, factory.interface.encodeFunctionData('createAccount', [authorizedAddress, cosignerAddress, recoveryAddress, BigNumber.from(salt)]) @@ -268,7 +270,7 @@ export async function createAccount ( ): Promise { const tx = await accountFactory.createAccount(authorizedAddresses, cosignerAddresses, recoverAddresses, salt, mergedKeyIndexWithParity, mergedKey) // console.log('tx: ', tx) - const receipt = await tx.wait() + // const receipt = await tx.wait() // console.log('createAccount gasUsed: ', receipt.gasUsed) const accountAddress = await accountFactory.getAddress(cosignerAddresses, recoverAddresses, salt) const account = BloctoAccount__factory.connect(accountAddress, ethersSigner) @@ -346,3 +348,15 @@ export async function signMessage (signerWallet: Wallet, accountAddress: string, export function logBytes (uint8: Uint8Array): string { return Buffer.from(uint8).toString('hex') + '(' + uint8.length.toString() + ')' } + +export function getMergedKey (wallet1: Wallet, wallet2: Wallet, mergedKeyIndex: number): [px: string, pxIndexWithParity: number] { + mergedKeyIndex = 128 + (mergedKeyIndex << 1) + const signerOne = new DefaultSigner(wallet1) + const signerTwo = new DefaultSigner(wallet2) + const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) + const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) + const pxIndexWithParity = combinedPublicKey.buffer.slice(0, 1).readInt8() - 2 + mergedKeyIndex + + return [px, pxIndexWithParity] +} From a8c592a82ef9205805dab1db834533cf61f6d278 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Wed, 14 Jun 2023 18:17:43 +0800 Subject: [PATCH 17/26] chore: delete unused bytesToAddresses() --- contracts/CoreWallet/CoreWallet.sol | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol index f1c4ce3..c72cf41 100644 --- a/contracts/CoreWallet/CoreWallet.sol +++ b/contracts/CoreWallet/CoreWallet.sol @@ -208,18 +208,6 @@ contract CoreWallet is IERC1271 { this.setAuthorized(_authorizedAddress, _cosigner, _mergedKeyIndexWithParity, _mergedKey); } - function bytesToAddresses(bytes memory bys) private pure returns (address[] memory addresses) { - addresses = new address[](bys.length/20); - for (uint256 i = 0; i < bys.length; i += 20) { - address addr; - uint256 end = i + 20; - assembly { - addr := mload(add(bys, end)) - } - addresses[i / 20] = addr; - } - } - /// @notice The shared initialization code used to setup the contract state regardless of whether or /// not the clone pattern is being used. /// @param _authorizedAddresses the initial authorized addresses, must not be zero! From e532874dd7aa41ec6617727575d02db9ee98e3f1 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Thu, 15 Jun 2023 16:18:01 +0800 Subject: [PATCH 18/26] v1.4.0: merged key maybe zero --- contracts/CoreWallet/CoreWallet.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol index c72cf41..51cc95c 100644 --- a/contracts/CoreWallet/CoreWallet.sol +++ b/contracts/CoreWallet/CoreWallet.sol @@ -318,7 +318,6 @@ contract CoreWallet is IERC1271 { address(uint160(_cosigner)) == address(0) || address(uint160(_cosigner)) != recoveryAddress, "Do not use the recovery address as a cosigner." ); - require(_mergedKey != 0, "MergedKey must not be zero."); authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; mergedKeys[authVersion + _mergedIndexWithParity] = _mergedKey; @@ -331,8 +330,6 @@ contract CoreWallet is IERC1271 { /// @param _meregedKeyIndex the merged key index /// @param _meregedKey the corresponding merged authorized key & cosigner key by Schnorr function setMergedKey(uint256 _meregedKeyIndex, bytes32 _meregedKey) external onlyInvoked { - require(_meregedKey != 0, "Merged key must not be zero."); - mergedKeys[authVersion + _meregedKeyIndex] = _meregedKey; emit AuthorizedMeregedKey(_meregedKeyIndex, _meregedKey); } From 6f653a3f2080cb5d6f977319fe890abcca334e70 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Thu, 15 Jun 2023 18:27:33 +0800 Subject: [PATCH 19/26] v1.4.0: let BloctoAccountFactory be upgradeable --- contracts/BloctoAccountFactory.sol | 10 +- .../test/TestBloctoAccountFactoryV200.sol | 127 ++++++++++++++ package.json | 4 +- test/bloctoaccount.test.ts | 35 ++-- test/entrypoint/entrypoint.test.ts | 3 +- test/schnorrMultiSign.test.ts | 6 +- yarn.lock | 163 ++++++++++++++++-- 7 files changed, 309 insertions(+), 39 deletions(-) create mode 100644 contracts/test/TestBloctoAccountFactoryV200.sol diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol index 9bb6ec1..635d700 100644 --- a/contracts/BloctoAccountFactory.sol +++ b/contracts/BloctoAccountFactory.sol @@ -2,13 +2,14 @@ pragma solidity ^0.8.12; import "@openzeppelin/contracts/utils/Create2.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import "./BloctoAccountProxy.sol"; import "./BloctoAccount.sol"; // BloctoAccountFactory for creating BloctoAccountProxy -contract BloctoAccountFactory is Ownable { +contract BloctoAccountFactory is Initializable, OwnableUpgradeable { /// @notice this is the version of this contract. string public constant VERSION = "1.4.0"; /// @notice the implementation address for BloctoAccountCloneableWallet @@ -18,10 +19,11 @@ contract BloctoAccountFactory is Ownable { event WalletCreated(address wallet, address authorizedAddress, bool full); - /// @notice constructor + /// @notice initialize /// @param _bloctoAccountImplementation the implementation address for BloctoAccountCloneableWallet /// @param _entryPoint the entrypoint address from EIP-4337 official implementation - constructor(address _bloctoAccountImplementation, IEntryPoint _entryPoint) { + function initialize(address _bloctoAccountImplementation, IEntryPoint _entryPoint) public initializer { + __Ownable_init_unchained(); bloctoAccountImplementation = _bloctoAccountImplementation; entryPoint = _entryPoint; } diff --git a/contracts/test/TestBloctoAccountFactoryV200.sol b/contracts/test/TestBloctoAccountFactoryV200.sol new file mode 100644 index 0000000..6e07bc8 --- /dev/null +++ b/contracts/test/TestBloctoAccountFactoryV200.sol @@ -0,0 +1,127 @@ +// 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 "../BloctoAccountFactory.sol"; + +/// Test Blocto account Factory +contract TestBloctoAccountFactoryV200 is Initializable, OwnableUpgradeable { + /// @notice this is the version of this contract. + string public constant VERSION = "2.0.0"; + /// @notice the implementation address for BloctoAccountCloneableWallet + address public bloctoAccountImplementation; + /// @notice the address from EIP-4337 official implementation + IEntryPoint public entryPoint; + + event WalletCreated(address wallet, address authorizedAddress, bool full); + + /// @notice initialize + /// @param _bloctoAccountImplementation the implementation address for BloctoAccountCloneableWallet + /// @param _entryPoint the entrypoint address from EIP-4337 official implementation + function initialize(address _bloctoAccountImplementation, IEntryPoint _entryPoint) public initializer { + __Ownable_init_unchained(); + bloctoAccountImplementation = _bloctoAccountImplementation; + entryPoint = _entryPoint; + } + + /// @notice create an account, and return its BloctoAccount. + /// 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 + /// @param _authorizedAddress the initial authorized address, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _salt salt for create account (used for address calculation in create2) + /// @param _mergedKeyIndexWithParity the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKey the corresponding mergedKey (using Schnorr merged key) + function createAccount( + address _authorizedAddress, + address _cosigner, + address _recoveryAddress, + uint256 _salt, + uint8 _mergedKeyIndexWithParity, + bytes32 _mergedKey + ) public onlyOwner returns (BloctoAccount ret) { + bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); + // for consistent address + BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); + newProxy.initImplementation(bloctoAccountImplementation); + ret = BloctoAccount(payable(address(newProxy))); + ret.init( + _authorizedAddress, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParity, _mergedKey + ); + emit WalletCreated(address(ret), _authorizedAddress, false); + } + + /// @notice create an account with multiple authorized addresses, and return its BloctoAccount. + /// returns the address even if the account is already deployed. + /// @param _authorizedAddresses the initial authorized addresses, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _salt salt for create account (used for address calculation in create2) + /// @param _mergedKeyIndexWithParitys the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKeys the corresponding mergedKey + function createAccount2( + address[] calldata _authorizedAddresses, + address _cosigner, + address _recoveryAddress, + uint256 _salt, + uint8[] calldata _mergedKeyIndexWithParitys, + bytes32[] calldata _mergedKeys + ) public onlyOwner returns (BloctoAccount ret) { + address addr = getAddress(_cosigner, _recoveryAddress, _salt); + uint256 codeSize = addr.code.length; + if (codeSize > 0) { + return BloctoAccount(payable(addr)); + } + bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); + // for consistent address + BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); + newProxy.initImplementation(bloctoAccountImplementation); + ret = BloctoAccount(payable(address(newProxy))); + ret.init2( + _authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParitys, _mergedKeys + ); + // emit event only with _authorizedAddresses[0] + emit WalletCreated(address(ret), _authorizedAddresses[0], true); + } + + /// @notice calculate the counterfactual address of this account as it would be returned by createAccount() + /// @param _cosigner the initial cosigning address + /// @param _recoveryAddress the initial recovery address for the wallet + /// @param _salt salt for create account (used for address calculation in create2) + function getAddress(address _cosigner, address _recoveryAddress, uint256 _salt) public view returns (address) { + bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); + return Create2.computeAddress( + bytes32(salt), keccak256(abi.encodePacked(type(BloctoAccountProxy).creationCode, abi.encode(address(this)))) + ); + } + + /// @notice set the implementation + /// @param _bloctoAccountImplementation update the implementation address of BloctoAccountCloneableWallet for createAccount and createAccount2 + function setImplementation(address _bloctoAccountImplementation) public onlyOwner { + bloctoAccountImplementation = _bloctoAccountImplementation; + } + + /// @notice set the entrypoint + /// @param _entrypoint target entrypoint + function setEntrypoint(IEntryPoint _entrypoint) public onlyOwner { + entryPoint = _entrypoint; + } + + /// @notice withdraw value from the deposit + /// @param withdrawAddress target to send to + /// @param amount to withdraw + function withdrawTo(address payable withdrawAddress, uint256 amount) public onlyOwner { + entryPoint.withdrawTo(withdrawAddress, amount); + } + + /// @notice add stake in etnrypoint for this factory to avoid bundler reject + /// @param unstakeDelaySec - the unstake delay for this factory. Can only be increased. + function addStake(uint32 unstakeDelaySec) external payable onlyOwner { + entryPoint.addStake{value: msg.value}(unstakeDelaySec); + } +} diff --git a/package.json b/package.json index 8067de8..a44642b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@account-abstraction/contracts": "^0.6.0", "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-waffle": "^2.0.1", - "@openzeppelin/hardhat-upgrades": "^1.23.0", + "@openzeppelin/hardhat-upgrades": "^1.28.0", "@typechain/ethers-v5": "^10.1.0", "@types/chai": "^4.2.21", "@types/node": "^16.4.12", @@ -66,4 +66,4 @@ "table": "^6.8.0", "typescript": "^4.3.5" } -} \ No newline at end of file +} diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index b0ca308..ff53e9f 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -5,8 +5,6 @@ import { BloctoAccount, BloctoAccount__factory, BloctoAccountCloneableWallet__factory, - BloctoAccountFactory, - BloctoAccountFactory__factory, TestBloctoAccountCloneableWalletV200, TestBloctoAccountCloneableWalletV200__factory } from '../typechain' @@ -22,6 +20,7 @@ import { signMessage, getMergedKey } from './testutils' +import '@openzeppelin/hardhat-upgrades' describe('BloctoAccount Upgrade Test', function () { const ethersSigner = ethers.provider.getSigner() @@ -53,6 +52,9 @@ describe('BloctoAccount Upgrade Test', function () { } before(async function () { + // 3 wallet + [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() + await fund(cosignerWallet.address) // 4337 entryPoint = await deployEntryPoint() @@ -60,11 +62,8 @@ describe('BloctoAccount Upgrade Test', function () { implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy(entryPoint.address)).address // account factory - factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation, entryPoint.address); - - // 3 wallet - [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() - await fund(cosignerWallet.address) + const BloctoAccountFactory = await ethers.getContractFactory('BloctoAccountFactory') + factory = await upgrades.deployProxy(BloctoAccountFactory, [implementation, entryPoint.address], { initializer: 'initialize' }) }) describe('wallet function', () => { @@ -74,10 +73,6 @@ describe('BloctoAccount Upgrade Test', function () { account = await testCreateAccount(AccountSalt) }) - it('test gas', async () => { - - }) - it('should receive native token', async () => { const beforeRecevive = await ethers.provider.getBalance(account.address) const [owner] = await ethers.getSigners() @@ -118,7 +113,7 @@ describe('BloctoAccount Upgrade Test', function () { }) }) - describe('should upgrade to different version implementation', () => { + describe('should upgrade account to different version implementation', () => { const AccountSalt = 12345 const MockEntryPointV070 = '0x000000000000000000000000000000000000E070' let account: BloctoAccount @@ -171,4 +166,20 @@ describe('BloctoAccount Upgrade Test', function () { expect(await account.entryPoint()).to.eql(MockEntryPointV070) }) }) + + describe('should upgrade factory to different version implementation', () => { + const TestSalt = 135341 + + it('new factory get new version but same acccount address', async () => { + const beforeAccountAddr = await factory.getAddress(await cosignerWallet.getAddress(), await recoverWallet.getAddress(), TestSalt) + + const UpgradeContract = await ethers.getContractFactory('TestBloctoAccountFactoryV200') + const factoryV200 = await upgrades.upgradeProxy(factory.address, UpgradeContract) + + expect(await factoryV200.VERSION()).to.eql('2.0.0') + + const afterAccountAddr = await factoryV200.getAddress(await cosignerWallet.getAddress(), await recoverWallet.getAddress(), TestSalt) + expect(beforeAccountAddr).to.eql(afterAccountAddr) + }) + }) }) diff --git a/test/entrypoint/entrypoint.test.ts b/test/entrypoint/entrypoint.test.ts index e2bab98..9f05a91 100644 --- a/test/entrypoint/entrypoint.test.ts +++ b/test/entrypoint/entrypoint.test.ts @@ -70,7 +70,8 @@ describe('EntryPoint', function () { implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy(entryPoint.address)).address // account factory - factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation, entryPoint.address); + const BloctoAccountFactory = await ethers.getContractFactory('BloctoAccountFactory') + factory = await upgrades.deployProxy(BloctoAccountFactory, [implementation, entryPoint.address], { initializer: 'initialize' }); [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() const [px, pxIndexWithParity] = getMergedKey(authorizedWallet, cosignerWallet, 0) diff --git a/test/schnorrMultiSign.test.ts b/test/schnorrMultiSign.test.ts index 5f6aee5..b93c5ba 100644 --- a/test/schnorrMultiSign.test.ts +++ b/test/schnorrMultiSign.test.ts @@ -4,8 +4,7 @@ import { BigNumber } from 'ethers' import { expect } from 'chai' import { BloctoAccountCloneableWallet__factory, - BloctoAccountFactory, - BloctoAccountFactory__factory + BloctoAccountFactory } from '../typechain' import { createAccount, @@ -33,7 +32,8 @@ describe('Schnorr MultiSign Test', function () { implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy(entryPoint.address)).address // account factory - factory = await new BloctoAccountFactory__factory(ethersSigner).deploy(implementation, entryPoint.address) + const BloctoAccountFactory = await ethers.getContractFactory('BloctoAccountFactory') + factory = await upgrades.deployProxy(BloctoAccountFactory, [implementation, entryPoint.address], { initializer: 'initialize' }) }) it('should generate a schnorr musig2 and validate it on the blockchain', async () => { diff --git a/yarn.lock b/yarn.lock index 9eca7e6..1f4fd34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,38 @@ resolved "https://registry.yarnpkg.com/@account-abstraction/contracts/-/contracts-0.6.0.tgz#7188a01839999226e6b2796328af338329543b76" integrity sha512-8ooRJuR7XzohMDM4MV34I12Ci2bmxfE9+cixakRL7lA4BAwJKQ3ahvd8FbJa9kiwkUPCUNtj+/zxDQWYYalLMQ== +"@aws-crypto/sha256-js@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz#02acd1a1fda92896fc5a28ec7c6e164644ea32fc" + integrity sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g== + dependencies: + "@aws-crypto/util" "^1.2.2" + "@aws-sdk/types" "^3.1.0" + tslib "^1.11.1" + +"@aws-crypto/util@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-1.2.2.tgz#b28f7897730eb6538b21c18bd4de22d0ea09003c" + integrity sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg== + dependencies: + "@aws-sdk/types" "^3.1.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-sdk/types@^3.1.0": + version "3.347.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.347.0.tgz#4affe91de36ef227f6375d64a6efda8d4ececd5d" + integrity sha512-GkCMy79mdjU9OTIe5KT58fI/6uqdf8UmMdWqVHmFJ+UpEzOci7L/uw4sOXWo7xpPzLs6cJ7s5ouGZW4GRPmHFA== + dependencies: + tslib "^2.5.0" + +"@aws-sdk/util-utf8-browser@^3.0.0": + version "3.259.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz#3275a6f5eb334f96ca76635b961d3c50259fd9ff" + integrity sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw== + dependencies: + tslib "^2.3.1" + "@babel/code-frame@^7.0.0": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" @@ -143,7 +175,7 @@ "@ethersproject/properties" ">=5.0.0-beta.131" "@ethersproject/strings" ">=5.0.0-beta.130" -"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.0.9", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.7.0": +"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.0.9", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.6.3", "@ethersproject/abi@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== @@ -811,26 +843,51 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.0.tgz#6854c37df205dd2c056bdfa1b853f5d732109109" integrity sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw== -"@openzeppelin/hardhat-upgrades@^1.23.0": - version "1.23.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-1.23.0.tgz#294c81cc101da697761b8fda257b8229a5fb582f" - integrity sha512-hn0PYJuQG+jkuseNpYX+Rn90c9wVm8QRFRWJua6Q2BODwLKxThDb4l2bQMtdLvscKt2CS8ZFIfdvgKG/ejrgwg== - dependencies: - "@openzeppelin/upgrades-core" "^1.25.0" +"@openzeppelin/defender-base-client@^1.46.0": + version "1.46.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/defender-base-client/-/defender-base-client-1.46.0.tgz#aa5177f8fbad23fd03d78f3dbe06664bbe9333ff" + integrity sha512-EMnVBcfE6ZN5yMxfaxrFF3eqyGp2RQp3oSRSRP+R3yuCRJf8VCc2ArdZf1QPmQQzbq70nl8EZa03mmAqPauNlQ== + dependencies: + amazon-cognito-identity-js "^6.0.1" + async-retry "^1.3.3" + axios "^0.21.2" + lodash "^4.17.19" + node-fetch "^2.6.0" + +"@openzeppelin/hardhat-upgrades@^1.28.0": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-1.28.0.tgz#6361f313a8a879d8a08a5e395acf0933bc190950" + integrity sha512-7sb/Jf+X+uIufOBnmHR0FJVWuxEs2lpxjJnLNN6eCJCP8nD0v+Ot5lTOW2Qb/GFnh+fLvJtEkhkowz4ZQ57+zQ== + dependencies: + "@openzeppelin/defender-base-client" "^1.46.0" + "@openzeppelin/platform-deploy-client" "^0.8.0" + "@openzeppelin/upgrades-core" "^1.27.0" chalk "^4.1.0" debug "^4.1.1" proper-lockfile "^4.1.1" -"@openzeppelin/upgrades-core@^1.25.0": - version "1.25.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/upgrades-core/-/upgrades-core-1.25.0.tgz#4f540e2043b98f8b59a4e8a988b9aeb5b5f23a35" - integrity sha512-vSxOSm1k+P156nNm15ydhOmSPGC37mnl092FMVOH+eGaoqLjr2Za6ULVjDMFzvMnG+sGE1UoDOqBNPfTm0ch8w== +"@openzeppelin/platform-deploy-client@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/platform-deploy-client/-/platform-deploy-client-0.8.0.tgz#af6596275a19c283d6145f0128cc1247d18223c1" + integrity sha512-POx3AsnKwKSV/ZLOU/gheksj0Lq7Is1q2F3pKmcFjGZiibf+4kjGxr4eSMrT+2qgKYZQH1ZLQZ+SkbguD8fTvA== + dependencies: + "@ethersproject/abi" "^5.6.3" + "@openzeppelin/defender-base-client" "^1.46.0" + axios "^0.21.2" + lodash "^4.17.19" + node-fetch "^2.6.0" + +"@openzeppelin/upgrades-core@^1.27.0": + version "1.27.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/upgrades-core/-/upgrades-core-1.27.0.tgz#43f05c3e45b21bdc583488aa42297fbb0d065d17" + integrity sha512-FBIuFPKiRNMhW09HS8jkmV5DueGfxO2wp/kmCa0m0SMDyX4ROumgy/4Ao0/yH8/JZZPDiH1q3EnTRn+B7TGYgg== dependencies: cbor "^8.0.0" chalk "^4.1.0" compare-versions "^5.0.0" debug "^4.1.1" ethereumjs-util "^7.0.3" + minimist "^1.2.7" proper-lockfile "^4.1.1" solidity-ast "^0.4.15" @@ -1500,6 +1557,17 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" +amazon-cognito-identity-js@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.2.0.tgz#99e96666944429cb8f67b62e4cf7ad77fbe71ad0" + integrity sha512-9Fxrp9+MtLdsJvqOwSaE3ll+pneICeuE3pwj2yDkiyGNWuHx97b8bVLR2bOgfDmDJnY0Hq8QoeXtwdM4aaXJjg== + dependencies: + "@aws-crypto/sha256-js" "1.2.2" + buffer "4.9.2" + fast-base64-decode "^1.0.0" + isomorphic-unfetch "^3.0.0" + js-cookie "^2.2.1" + amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" @@ -1756,6 +1824,13 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-retry@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + async@1.x, async@^1.4.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -1800,7 +1875,7 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -axios@^0.21.1: +axios@^0.21.1, axios@^0.21.2: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== @@ -2345,7 +2420,7 @@ base-x@^3.0.2, base-x@^3.0.8: dependencies: safe-buffer "^5.0.1" -base64-js@^1.3.1: +base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -2614,6 +2689,15 @@ buffer-xor@^2.0.1: dependencies: safe-buffer "^5.1.1" +buffer@4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + buffer@^5.0.5, buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -4666,6 +4750,11 @@ fake-merkle-patricia-tree@^1.0.1: dependencies: checkpoint-store "^1.1.0" +fast-base64-decode@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" + integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -5674,7 +5763,7 @@ idna-uts46-hx@^2.3.1: dependencies: punycode "2.1.0" -ieee754@^1.1.13, ieee754@^1.2.1: +ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -6117,7 +6206,7 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== -isarray@1.0.0, isarray@~1.0.0: +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== @@ -6139,11 +6228,24 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +isomorphic-unfetch@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" + integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q== + dependencies: + node-fetch "^2.6.1" + unfetch "^4.2.0" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== +js-cookie@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" + integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== + js-sdsl@^4.1.4: version "4.2.0" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" @@ -6645,7 +6747,7 @@ lodash@4.17.20: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.17.4: +lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6971,6 +7073,11 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== +minimist@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minipass@^2.6.0, minipass@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" @@ -7255,6 +7362,13 @@ node-environment-flags@1.0.6: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" +node-fetch@^2.6.0: + version "2.6.11" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" + integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -8322,6 +8436,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -9450,11 +9569,16 @@ tsconfig-paths@^3.14.1, tsconfig-paths@^3.5.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.3.1, tslib@^2.5.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" + integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== + tsort@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786" @@ -9658,6 +9782,11 @@ undici@^5.4.0: dependencies: busboy "^1.6.0" +unfetch@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" + integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" From 22ce6ea0732c4e8bdec7ca08cb6c911ee9108e25 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Thu, 15 Jun 2023 18:43:44 +0800 Subject: [PATCH 20/26] v1.4.0: deploy update for factory upgradeable --- deploy/0_deploy_Account_Factory-and-addStake.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/deploy/0_deploy_Account_Factory-and-addStake.ts b/deploy/0_deploy_Account_Factory-and-addStake.ts index a909620..1f7e0e5 100644 --- a/deploy/0_deploy_Account_Factory-and-addStake.ts +++ b/deploy/0_deploy_Account_Factory-and-addStake.ts @@ -22,10 +22,9 @@ async function main (): Promise { console.log(`${BloctoAccountCloneableWallet} deployed to: ${walletCloneable.address}`) // account factory - const AccountFactory = await ethers.getContractFactory(BloctoAccountFactory) - const accountFactory = await AccountFactory.deploy(walletCloneable.address, EntryPoint, { - gasLimit: GasLimit - }) + const BloctoAccountFactoryContract = await ethers.getContractFactory(BloctoAccountFactory) + const accountFactory = await upgrades.deployProxy(BloctoAccountFactoryContract, ['0x515E96E561837Db9080E254db2Afd14B89D1ef68', EntryPoint], + { initializer: 'initialize', gasLimit: GasLimit }) await accountFactory.deployed() From 7bf711145fb94f36f909a7b7faef903cfa76667a Mon Sep 17 00:00:00 2001 From: kbehouse Date: Mon, 19 Jun 2023 08:47:24 +0800 Subject: [PATCH 21/26] v1.4.0: cosigner == 0 need mergedkey==0 --- contracts/CoreWallet/CoreWallet.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol index 51cc95c..6398451 100644 --- a/contracts/CoreWallet/CoreWallet.sol +++ b/contracts/CoreWallet/CoreWallet.sol @@ -315,7 +315,8 @@ contract CoreWallet is IERC1271 { require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); require(_authorizedAddress != recoveryAddress, "Do not use the recovery address as an authorized address."); require( - address(uint160(_cosigner)) == address(0) || address(uint160(_cosigner)) != recoveryAddress, + (address(uint160(_cosigner)) == address(0) && _mergedKey == 0) + || address(uint160(_cosigner)) != recoveryAddress, "Do not use the recovery address as a cosigner." ); From 07fb23ca14d9ea71572843bfef4d8b91745914db Mon Sep 17 00:00:00 2001 From: kbehouse Date: Mon, 19 Jun 2023 11:29:18 +0800 Subject: [PATCH 22/26] v1.4.0: use minimal proxy with storage --- contracts/BloctoAccount.sol | 22 ++++++++ contracts/BloctoAccountFactory.sol | 31 ++++++----- contracts/BloctoAccountProxy.sol | 39 ++++++++++---- contracts/CoreWallet/CoreWallet.sol | 11 +++- .../test/TestBloctoAccountFactoryV200.sol | 30 ++++++----- contracts/test/TestBloctoAccountV200.sol | 51 +++++++++++++++++-- hardhat.config.ts | 3 -- package.json | 2 +- test/bloctoaccount.test.ts | 22 +++++--- test/testutils.ts | 4 +- 10 files changed, 161 insertions(+), 54 deletions(-) diff --git a/contracts/BloctoAccount.sol b/contracts/BloctoAccount.sol index 32c2e6e..a2ecef6 100644 --- a/contracts/BloctoAccount.sol +++ b/contracts/BloctoAccount.sol @@ -127,4 +127,26 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas function withdrawDepositTo(address payable withdrawAddress, uint256 amount) external onlyInvoked { entryPoint().withdrawTo(withdrawAddress, amount); } + + /// @notice initialized _IMPLEMENTATION_SLOT + bool public initializedImplementation = false; + + /// @notice Used to decorate the `init` function so this can only be called one time. Necessary + /// since this contract will often be used as a "clone". (See above.) + modifier onlyOnceInitImplementation() { + require(!initializedImplementation, "must not already be initialized"); + initializedImplementation = true; + _; + } + + /// @notice initialize BloctoAccountProxy for adding the implementation address + /// @param implementation implementation address + function initImplementation(address implementation) public onlyOnceInitImplementation { + require(Address.isContract(implementation), "ERC1967: new implementation is not a contract"); + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = implementation; + } + + function disableInitImplementation() public { + initializedImplementation = true; + } } diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol index 635d700..b0d68e9 100644 --- a/contracts/BloctoAccountFactory.sol +++ b/contracts/BloctoAccountFactory.sol @@ -12,7 +12,9 @@ import "./BloctoAccount.sol"; contract BloctoAccountFactory is Initializable, OwnableUpgradeable { /// @notice this is the version of this contract. string public constant VERSION = "1.4.0"; - /// @notice the implementation address for BloctoAccountCloneableWallet + /// @notice the init implementation address of BloctoAccountCloneableWallet, never change for cosistent address + address public initImplementation; + /// @notice the implementation address of BloctoAccountCloneableWallet address public bloctoAccountImplementation; /// @notice the address from EIP-4337 official implementation IEntryPoint public entryPoint; @@ -24,6 +26,7 @@ contract BloctoAccountFactory is Initializable, OwnableUpgradeable { /// @param _entryPoint the entrypoint address from EIP-4337 official implementation function initialize(address _bloctoAccountImplementation, IEntryPoint _entryPoint) public initializer { __Ownable_init_unchained(); + initImplementation = _bloctoAccountImplementation; bloctoAccountImplementation = _bloctoAccountImplementation; entryPoint = _entryPoint; } @@ -47,10 +50,12 @@ contract BloctoAccountFactory is Initializable, OwnableUpgradeable { bytes32 _mergedKey ) public onlyOwner returns (BloctoAccount ret) { bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); - // for consistent address - BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); - newProxy.initImplementation(bloctoAccountImplementation); + // to be consistent address + BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(initImplementation); ret = BloctoAccount(payable(address(newProxy))); + // to save gas, first deploy using disableInitImplementation() + // to be consistent address, (after) first upgrade need to call initImplementation + ret.disableInitImplementation(); ret.init( _authorizedAddress, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParity, _mergedKey ); @@ -73,16 +78,15 @@ contract BloctoAccountFactory is Initializable, OwnableUpgradeable { uint8[] calldata _mergedKeyIndexWithParitys, bytes32[] calldata _mergedKeys ) public onlyOwner returns (BloctoAccount ret) { - address addr = getAddress(_cosigner, _recoveryAddress, _salt); - uint256 codeSize = addr.code.length; - if (codeSize > 0) { - return BloctoAccount(payable(addr)); - } bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); - // for consistent address - BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); - newProxy.initImplementation(bloctoAccountImplementation); + // to be consistent address + BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(initImplementation); + ret = BloctoAccount(payable(address(newProxy))); + // to save gas, first deploy use disableInitImplementation() + // to be consistent address, (after) first upgrade need to call initImplementation() + // ret.initImplementation(bloctoAccountImplementation); + ret.disableInitImplementation(); ret.init2( _authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParitys, _mergedKeys ); @@ -97,7 +101,8 @@ contract BloctoAccountFactory is Initializable, OwnableUpgradeable { function getAddress(address _cosigner, address _recoveryAddress, uint256 _salt) public view returns (address) { bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); return Create2.computeAddress( - bytes32(salt), keccak256(abi.encodePacked(type(BloctoAccountProxy).creationCode, abi.encode(address(this)))) + bytes32(salt), + keccak256(abi.encodePacked(type(BloctoAccountProxy).creationCode, abi.encode(address(initImplementation)))) ); } diff --git a/contracts/BloctoAccountProxy.sol b/contracts/BloctoAccountProxy.sol index 9328a2d..841e507 100644 --- a/contracts/BloctoAccountProxy.sol +++ b/contracts/BloctoAccountProxy.sol @@ -1,16 +1,37 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.12; -import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +contract BloctoAccountProxy { + /// @notice constructor for setting the implementation address + /// @param implementation the initial implementation(logic) addresses, must not be zero! + constructor(address implementation) { + assembly { + sstore(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc, implementation) + } + } + + /// @notice Fallback function that delegates calls to the address + /// @dev update from "@openzeppelin/contracts/proxy/Proxy.sol" + fallback() external payable virtual { + assembly { + let implementation := sload(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc) + // if eq(implementation, 0) { implementation := 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 } + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) -contract BloctoAccountProxy is ERC1967Proxy, Initializable { - constructor(address _logic) ERC1967Proxy(_logic, new bytes(0)) {} + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) - /// @notice initialize BloctoAccountProxy for adding the implementation address - /// @param implementation implementation address - function initImplementation(address implementation) public initializer { - require(Address.isContract(implementation), "ERC1967: new implementation is not a contract"); - StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = implementation; + switch result + // delegatecall returns 0 on error. + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } } } diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol index 6398451..74da472 100644 --- a/contracts/CoreWallet/CoreWallet.sol +++ b/contracts/CoreWallet/CoreWallet.sol @@ -198,6 +198,7 @@ contract CoreWallet is IERC1271 { uint8 _mergedKeyIndexWithParity, bytes32 _mergedKey ) public onlyOnce { + require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); require(_authorizedAddress != _recoveryAddress, "Do not use the recovery address as an authorized address."); require(address(uint160(_cosigner)) != _recoveryAddress, "Do not use the recovery address as a cosigner."); @@ -205,7 +206,9 @@ contract CoreWallet is IERC1271 { // set initial authorization value authVersion = AUTH_VERSION_INCREMENTOR; // add initial authorized address - this.setAuthorized(_authorizedAddress, _cosigner, _mergedKeyIndexWithParity, _mergedKey); + authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; + mergedKeys[authVersion + _mergedKeyIndexWithParity] = _mergedKey; + emit Authorized(_authorizedAddress, _cosigner); } /// @notice The shared initialization code used to setup the contract state regardless of whether or @@ -230,7 +233,11 @@ contract CoreWallet is IERC1271 { authVersion = AUTH_VERSION_INCREMENTOR; for (uint256 i = 0; i < _authorizedAddresses.length; i++) { address _authorizedAddress = _authorizedAddresses[i]; - this.setAuthorized(_authorizedAddress, _cosigner, _mergedKeyIndexWithParitys[i], _mergedKeys[i]); + require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); + authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; + mergedKeys[authVersion + _mergedKeyIndexWithParitys[i]] = _mergedKeys[i]; + + emit Authorized(_authorizedAddress, _cosigner); } } diff --git a/contracts/test/TestBloctoAccountFactoryV200.sol b/contracts/test/TestBloctoAccountFactoryV200.sol index 6e07bc8..9eaddb5 100644 --- a/contracts/test/TestBloctoAccountFactoryV200.sol +++ b/contracts/test/TestBloctoAccountFactoryV200.sol @@ -11,7 +11,9 @@ import "../BloctoAccountFactory.sol"; contract TestBloctoAccountFactoryV200 is Initializable, OwnableUpgradeable { /// @notice this is the version of this contract. string public constant VERSION = "2.0.0"; - /// @notice the implementation address for BloctoAccountCloneableWallet + /// @notice the init implementation address of BloctoAccountCloneableWallet, never change for cosistent address + address public initImplementation; + /// @notice the implementation address of BloctoAccountCloneableWallet address public bloctoAccountImplementation; /// @notice the address from EIP-4337 official implementation IEntryPoint public entryPoint; @@ -23,6 +25,7 @@ contract TestBloctoAccountFactoryV200 is Initializable, OwnableUpgradeable { /// @param _entryPoint the entrypoint address from EIP-4337 official implementation function initialize(address _bloctoAccountImplementation, IEntryPoint _entryPoint) public initializer { __Ownable_init_unchained(); + initImplementation = _bloctoAccountImplementation; bloctoAccountImplementation = _bloctoAccountImplementation; entryPoint = _entryPoint; } @@ -46,10 +49,12 @@ contract TestBloctoAccountFactoryV200 is Initializable, OwnableUpgradeable { bytes32 _mergedKey ) public onlyOwner returns (BloctoAccount ret) { bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); - // for consistent address - BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); - newProxy.initImplementation(bloctoAccountImplementation); + // to be consistent address + BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(initImplementation); ret = BloctoAccount(payable(address(newProxy))); + // to save gas, first deploy don't need to call initImplementation + // to be consistent address, (after) first upgrade need to call initImplementation + ret.initImplementation(bloctoAccountImplementation); ret.init( _authorizedAddress, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParity, _mergedKey ); @@ -72,16 +77,14 @@ contract TestBloctoAccountFactoryV200 is Initializable, OwnableUpgradeable { uint8[] calldata _mergedKeyIndexWithParitys, bytes32[] calldata _mergedKeys ) public onlyOwner returns (BloctoAccount ret) { - address addr = getAddress(_cosigner, _recoveryAddress, _salt); - uint256 codeSize = addr.code.length; - if (codeSize > 0) { - return BloctoAccount(payable(addr)); - } bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); - // for consistent address - BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(address(this)); - newProxy.initImplementation(bloctoAccountImplementation); + // to be consistent address + BloctoAccountProxy newProxy = new BloctoAccountProxy{salt: salt}(initImplementation); + ret = BloctoAccount(payable(address(newProxy))); + // to save gas, first deploy don't need to call initImplementation + // to be consistent address, (after) first upgrade need to call initImplementation + ret.initImplementation(bloctoAccountImplementation); ret.init2( _authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParitys, _mergedKeys ); @@ -96,7 +99,8 @@ contract TestBloctoAccountFactoryV200 is Initializable, OwnableUpgradeable { function getAddress(address _cosigner, address _recoveryAddress, uint256 _salt) public view returns (address) { bytes32 salt = keccak256(abi.encodePacked(_salt, _cosigner, _recoveryAddress)); return Create2.computeAddress( - bytes32(salt), keccak256(abi.encodePacked(type(BloctoAccountProxy).creationCode, abi.encode(address(this)))) + bytes32(salt), + keccak256(abi.encodePacked(type(BloctoAccountProxy).creationCode, abi.encode(address(initImplementation)))) ); } diff --git a/contracts/test/TestBloctoAccountV200.sol b/contracts/test/TestBloctoAccountV200.sol index 18f1e17..83b6077 100644 --- a/contracts/test/TestBloctoAccountV200.sol +++ b/contracts/test/TestBloctoAccountV200.sol @@ -23,23 +23,34 @@ contract TestBloctoAccountV200 is UUPSUpgradeable, TokenCallbackHandler, CoreWal IEntryPoint private immutable _entryPoint; + /** + * constructor for BloctoAccount + * @param anEntryPoint entrypoint address + */ constructor(IEntryPoint anEntryPoint) { _entryPoint = anEntryPoint; } /** * override from UUPSUpgradeable + * @param newImplementation implementation address */ function _authorizeUpgrade(address newImplementation) internal view override onlyInvoked { (newImplementation); } + /** + * return entrypoint + */ function entryPoint() public view virtual override returns (IEntryPoint) { return _entryPoint; } /** * execute a transaction (called directly by entryPoint) + * @param dest dest call address + * @param value value to send + * @param func the func containing the transaction to be called */ function execute(address dest, uint256 value, bytes calldata func) external { _requireFromEntryPoint(); @@ -48,16 +59,24 @@ contract TestBloctoAccountV200 is UUPSUpgradeable, TokenCallbackHandler, CoreWal /** * execute a sequence of transactions (called directly by entryPoint) + * @param dest sequence of dest call address + * @param value sequence of value to send + * @param func sequence of the func containing transactions to be called */ - function executeBatch(address[] calldata dest, bytes[] calldata func) external { + function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external { _requireFromEntryPoint(); require(dest.length == func.length, "wrong array lengths"); for (uint256 i = 0; i < dest.length; i++) { - _call(dest[i], 0, func[i]); + _call(dest[i], value[i], func[i]); } } - /// internal call for execute and executeBatch + /** + * internal call for execute and executeBatch + * @param target target call address + * @param value value to send + * @param data the data containing the transaction to be called + */ function _call(address target, uint256 value, bytes memory data) internal { (bool success, bytes memory result) = target.call{value: value}(data); if (!success) { @@ -67,7 +86,11 @@ contract TestBloctoAccountV200 is UUPSUpgradeable, TokenCallbackHandler, CoreWal } } - /// implement validate signature method of BaseAccount + /** + * implement validate signature method of BaseAccount from etnrypoint + * @param userOp user operation including signature for validating + * @param userOpHash user operation hash + */ function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) internal virtual @@ -104,4 +127,24 @@ contract TestBloctoAccountV200 is UUPSUpgradeable, TokenCallbackHandler, CoreWal function withdrawDepositTo(address payable withdrawAddress, uint256 amount) external onlyInvoked { entryPoint().withdrawTo(withdrawAddress, amount); } + + /// @notice initialized _IMPLEMENTATION_SLOT + /// @dev for first depoloy, set it to true for prevent call initImplementation + /// @dev (after) first upgrade, set it to false and immediately setting initImplementation + bool public initializedImplementation = false; + + /// @notice Used to decorate the `init` function so this can only be called one time. Necessary + /// since this contract will often be used as a "clone". (See above.) + modifier onlyOnceInitImplementation() { + require(!initializedImplementation, "must not already be initialized"); + initializedImplementation = true; + _; + } + + /// @notice initialize BloctoAccountProxy for adding the implementation address + /// @param implementation implementation address + function initImplementation(address implementation) public onlyOnceInitImplementation { + require(Address.isContract(implementation), "ERC1967: new implementation is not a contract"); + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = implementation; + } } diff --git a/hardhat.config.ts b/hardhat.config.ts index f4fe06b..960f7b2 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -6,8 +6,6 @@ import '@openzeppelin/hardhat-upgrades' import 'hardhat-storage-layout' import 'solidity-coverage' -// import * as fs from 'fs' - const { ETHERSCAN_API_KEY, // etherscan API KEY POLYGONSCAN_API_KEY, // polygonscan API KEY @@ -18,7 +16,6 @@ const { // You need to export an object to set up your config // Go to https://hardhat.org/config/ to learn more - const config: HardhatUserConfig = { solidity: { compilers: [{ diff --git a/package.json b/package.json index a44642b..9f8ce2e 100644 --- a/package.json +++ b/package.json @@ -66,4 +66,4 @@ "table": "^6.8.0", "typescript": "^4.3.5" } -} +} \ No newline at end of file diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index ff53e9f..f095b15 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -60,7 +60,6 @@ describe('BloctoAccount Upgrade Test', function () { // v1 implementation implementation = (await new BloctoAccountCloneableWallet__factory(ethersSigner).deploy(entryPoint.address)).address - // account factory const BloctoAccountFactory = await ethers.getContractFactory('BloctoAccountFactory') factory = await upgrades.deployProxy(BloctoAccountFactory, [implementation, entryPoint.address], { initializer: 'initialize' }) @@ -68,12 +67,9 @@ describe('BloctoAccount Upgrade Test', function () { describe('wallet function', () => { const AccountSalt = 123 - let account: BloctoAccount - before(async () => { - account = await testCreateAccount(AccountSalt) - }) it('should receive native token', async () => { + const account = await testCreateAccount(AccountSalt) const beforeRecevive = await ethers.provider.getBalance(account.address) const [owner] = await ethers.getSigners() @@ -133,7 +129,19 @@ describe('BloctoAccount Upgrade Test', function () { account = await testCreateAccount(AccountSalt) // mock new entry point version 0.7.0 implementationV200 = await new TestBloctoAccountCloneableWalletV200__factory(ethersSigner).deploy(MockEntryPointV070) - await factory.setImplementation(implementationV200.address) + // await factory.setImplementation(implementationV200.address) + }) + + it('new factory get new version and same acccount address', async () => { + const beforeAccountAddr = await factory.getAddress(await cosignerWallet.getAddress(), await recoverWallet.getAddress(), AccountSalt) + const UpgradeContract = await ethers.getContractFactory('TestBloctoAccountFactoryV200') + const factoryV200 = await upgrades.upgradeProxy(factory.address, UpgradeContract) + + factory.setImplementation(implementationV200.address) + expect(await factory.VERSION()).to.eql('2.0.0') + + const afterAccountAddr = await factoryV200.getAddress(await cosignerWallet.getAddress(), await recoverWallet.getAddress(), AccountSalt) + expect(beforeAccountAddr).to.eql(afterAccountAddr) }) it('upgrade fail if not by contract self', async () => { @@ -147,7 +155,7 @@ describe('BloctoAccount Upgrade Test', function () { expect(await account.VERSION()).to.eql('2.0.0') }) - it('factory getAddress some be same', async () => { + it('factory getAddress sould be same', async () => { const addrFromFacotry = await factory.getAddress( await cosignerWallet.getAddress(), await recoverWallet.getAddress(), diff --git a/test/testutils.ts b/test/testutils.ts index 6cdcd9d..ae0f13b 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -270,8 +270,8 @@ export async function createAccount ( ): Promise { const tx = await accountFactory.createAccount(authorizedAddresses, cosignerAddresses, recoverAddresses, salt, mergedKeyIndexWithParity, mergedKey) // console.log('tx: ', tx) - // const receipt = await tx.wait() - // console.log('createAccount gasUsed: ', receipt.gasUsed) + const receipt = await tx.wait() + console.log('createAccount gasUsed: ', receipt.gasUsed) const accountAddress = await accountFactory.getAddress(cosignerAddresses, recoverAddresses, salt) const account = BloctoAccount__factory.connect(accountAddress, ethersSigner) return account From 4f54526b66ccb61de8461c20a7dc31b020b7671c Mon Sep 17 00:00:00 2001 From: kbehouse Date: Mon, 19 Jun 2023 17:52:05 +0800 Subject: [PATCH 23/26] v1.4.0: update deploy script --- .gitignore | 1 + .../0_deploy_Account_Factory-and-addStake.ts | 6 +- deploy/{4_verify.ts => 0_verify.ts} | 22 +++-- .../1_deploy_BloctoAccountCloneableWallet.ts | 24 ------ ...ster.ts => 1_deploy_VerifyingPaymaster.ts} | 0 deploy/2_2_createMultipleSchnorrAccount.ts | 55 ++++++++++++ deploy/2_createSchnorrAccount.ts | 84 +++++++++++++++++++ deploy/2_deploy_BloctoAccountFactory.ts | 25 ------ test/bloctoaccount.test.ts | 2 +- 9 files changed, 159 insertions(+), 60 deletions(-) rename deploy/{4_verify.ts => 0_verify.ts} (51%) delete mode 100644 deploy/1_deploy_BloctoAccountCloneableWallet.ts rename deploy/{3_deploy_VerifyingPaymaster.ts => 1_deploy_VerifyingPaymaster.ts} (100%) create mode 100644 deploy/2_2_createMultipleSchnorrAccount.ts create mode 100644 deploy/2_createSchnorrAccount.ts delete mode 100644 deploy/2_deploy_BloctoAccountFactory.ts diff --git a/.gitignore b/.gitignore index 22b05d9..1cfbed4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules log/ .vscode/ +.openzeppelin/ #Hardhat files cache artifacts diff --git a/deploy/0_deploy_Account_Factory-and-addStake.ts b/deploy/0_deploy_Account_Factory-and-addStake.ts index 1f7e0e5..24d7657 100644 --- a/deploy/0_deploy_Account_Factory-and-addStake.ts +++ b/deploy/0_deploy_Account_Factory-and-addStake.ts @@ -1,6 +1,7 @@ import { EntryPoint__factory } from '@account-abstraction/contracts' import { BigNumber } from 'ethers' import hre, { ethers } from 'hardhat' +import { getImplementationAddress } from '@openzeppelin/upgrades-core' const BloctoAccountCloneableWallet = 'BloctoAccountCloneableWallet' const BloctoAccountFactory = 'BloctoAccountFactory' @@ -23,7 +24,7 @@ async function main (): Promise { // account factory const BloctoAccountFactoryContract = await ethers.getContractFactory(BloctoAccountFactory) - const accountFactory = await upgrades.deployProxy(BloctoAccountFactoryContract, ['0x515E96E561837Db9080E254db2Afd14B89D1ef68', EntryPoint], + const accountFactory = await upgrades.deployProxy(BloctoAccountFactoryContract, [walletCloneable.address, EntryPoint], { initializer: 'initialize', gasLimit: GasLimit }) await accountFactory.deployed() @@ -46,8 +47,9 @@ async function main (): Promise { ] }) // verify BloctoAccountFactory + const accountFactoryImplAddress = await getImplementationAddress(ethers.provider, accountFactory.address) await hre.run('verify:verify', { - address: accountFactory.address, + address: accountFactoryImplAddress, constructorArguments: [ walletCloneable.address, EntryPoint ] diff --git a/deploy/4_verify.ts b/deploy/0_verify.ts similarity index 51% rename from deploy/4_verify.ts rename to deploy/0_verify.ts index 95d32b9..336692b 100644 --- a/deploy/4_verify.ts +++ b/deploy/0_verify.ts @@ -1,7 +1,8 @@ -import hre from 'hardhat' +import { getImplementationAddress } from '@openzeppelin/upgrades-core' +import hre, { ethers } from 'hardhat' -const BloctoAccountCloneableWalletAddr = '0x409bAa86c5B901Cd9fA35317f519c260a0e6231b' -const BloctoAccountFactoryAddr = '0x1522Db12e80fA5827ca462Ba6C317c63d38A4Bca' +const BloctoAccountCloneableWalletAddr = '0x592D3167Cbb926379c1527f078F22E82FfAFdAa3' +const BloctoAccountFactoryAddr = '0x4b0C9eCC8A4577525688232977A346c1232a377E' const EntryPoint = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' async function main (): Promise { @@ -13,14 +14,19 @@ async function main (): Promise { EntryPoint ] }) - // verify BloctoAccountFactory + + // verify BloctoAccountFactory (if proxy) + const accountFactoryImplAddress = await getImplementationAddress(ethers.provider, BloctoAccountFactoryAddr) await hre.run('verify:verify', { address: BloctoAccountFactoryAddr, - contract: 'contracts/BloctoAccountFactory.sol:BloctoAccountFactory', - constructorArguments: [ - BloctoAccountCloneableWalletAddr, EntryPoint - ] + contract: 'contracts/BloctoAccountFactory.sol:BloctoAccountFactory' }) + + // verify BloctoAccountFactory (if not proxy) + // await hre.run('verify:verify', { + // address: BloctoAccountFactoryAddr, + // contract: 'contracts/BloctoAccountFactory.sol:BloctoAccountFactory' + // }) } // We recommend this pattern to be able to use async/await everywhere diff --git a/deploy/1_deploy_BloctoAccountCloneableWallet.ts b/deploy/1_deploy_BloctoAccountCloneableWallet.ts deleted file mode 100644 index 930d0f5..0000000 --- a/deploy/1_deploy_BloctoAccountCloneableWallet.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ethers } from 'hardhat' - -const ContractName = 'BloctoAccountCloneableWallet' -const GasLimit = 6000000 - -async function main (): Promise { - // const lockedAmount = ethers.utils.parseEther("1"); - - const factory = await ethers.getContractFactory(ContractName) - const contract = await factory.deploy({ - gasLimit: GasLimit // set the gas limit to 6 million - }) - - await contract.deployed() - - console.log(`${ContractName} deployed to: ${contract.address}`) -} - -// We recommend this pattern to be able to use async/await everywhere -// and properly handle errors. -main().catch((error) => { - console.error(error) - process.exitCode = 1 -}) diff --git a/deploy/3_deploy_VerifyingPaymaster.ts b/deploy/1_deploy_VerifyingPaymaster.ts similarity index 100% rename from deploy/3_deploy_VerifyingPaymaster.ts rename to deploy/1_deploy_VerifyingPaymaster.ts diff --git a/deploy/2_2_createMultipleSchnorrAccount.ts b/deploy/2_2_createMultipleSchnorrAccount.ts new file mode 100644 index 0000000..be1544f --- /dev/null +++ b/deploy/2_2_createMultipleSchnorrAccount.ts @@ -0,0 +1,55 @@ +// update from https://github.com/borislav-itskov/schnorrkel.js +import { ethers } from 'hardhat' +import { + createAuthorizedCosignerRecoverWallet, + getMergedKey +} from '../test/testutils' + +const FactoryAddress = '0x0194b9278b1f2ED8b2Ab5070382EAB890C2B199f' + +const RecoverAddress = '0x0c558b2735286533b834bd1172bcA43DBD2970f7' + +const ethersSigner = ethers.provider.getSigner() + +const SALT = 4521523 + +async function main (): Promise { + // const lockedAmount = ethers.utils.parseEther("1"); + const AccountFactory = await ethers.getContractFactory('BloctoAccountFactory') + const factory = await AccountFactory.attach(FactoryAddress) + + const [authorizedWallet, cosignerWallet] = createAuthorizedCosignerRecoverWallet() + const [authorizedWallet2, cosignerWallet2] = createAuthorizedCosignerRecoverWallet() + // const signerOne = new DefaultSigner(authorizedWallet) + // const signerTwo = new DefaultSigner(cosignerWallet) + // const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] + // const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] + // const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) + // const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) + // because of the parity byte is 2, 3 so sub 2 + // const pxIndexWithParity = combinedPublicKey.buffer.slice(0, 1).readInt8() - 2 + mergedKeyIndex + + const [px, pxIndexWithParity] = getMergedKey(authorizedWallet, cosignerWallet, 0) + const [px2, pxIndexWithParity2] = getMergedKey(authorizedWallet2, cosignerWallet2, 1) + + console.log('authorizedWallet.getAddress(): ', await authorizedWallet.getAddress(), ', cosignerWallet.getAddress()', await cosignerWallet.getAddress()) + + console.log('ethersSigner address: ', await ethersSigner.getAddress()) + console.log('factory.address', factory.address) + + const tx = await factory.createAccount2([authorizedWallet.address, authorizedWallet2.address], + cosignerWallet.address, RecoverAddress, + SALT, // random salt + [pxIndexWithParity, pxIndexWithParity2], + [px, px2]) + + console.log('after createAccount2') + const receipt = await tx.wait() + console.log(receipt) +} +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/deploy/2_createSchnorrAccount.ts b/deploy/2_createSchnorrAccount.ts new file mode 100644 index 0000000..411db6d --- /dev/null +++ b/deploy/2_createSchnorrAccount.ts @@ -0,0 +1,84 @@ +// update from https://github.com/borislav-itskov/schnorrkel.js +import { ethers } from 'hardhat' +import { BigNumber } from 'ethers' +import { expect } from 'chai' +import { + createAccount, + createAuthorizedCosignerRecoverWallet, + hashMessageEIP191V0 +} from '../test/testutils' + +import Schnorrkel from '../src/schnorrkel.js/index' +import { DefaultSigner } from '../test/schnorrUtils' + +const ERC1271_MAGICVALUE_BYTES32 = '0x1626ba7e' + +const FactoryAddress = '0x0194b9278b1f2ED8b2Ab5070382EAB890C2B199f' + +const RecoverAddress = '0x0c558b2735286533b834bd1172bcA43DBD2970f7' + +const ethersSigner = ethers.provider.getSigner() + +// multisig +const msg = 'just a test message' + +const SALT = 3521523151 + +async function main (): Promise { + // const lockedAmount = ethers.utils.parseEther("1"); + const AccountFactory = await ethers.getContractFactory('BloctoAccountFactory') + const factory = await AccountFactory.attach(FactoryAddress) + + const mergedKeyIndex = 128 + (0 << 1) + const [authorizedWallet, cosignerWallet] = createAuthorizedCosignerRecoverWallet() + const signerOne = new DefaultSigner(authorizedWallet) + const signerTwo = new DefaultSigner(cosignerWallet) + const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] + const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) + const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) + // because of the parity byte is 2, 3 so sub 2 + const pxIndexWithParity = combinedPublicKey.buffer.slice(0, 1).readInt8() - 2 + mergedKeyIndex + + console.log('authorizedWallet.getAddress(): ', await authorizedWallet.getAddress(), ', cosignerWallet.getAddress()', await cosignerWallet.getAddress()) + + console.log('ethersSigner address: ', await ethersSigner.getAddress()) + console.log('factory.address', factory.address) + const account = await createAccount( + ethersSigner, + await authorizedWallet.getAddress(), + await cosignerWallet.getAddress(), + RecoverAddress, + BigNumber.from(SALT), + pxIndexWithParity, + px, + factory + ) + + console.log('account create success! SCW Address: ', account.address) + + const msgKeccak256 = ethers.utils.solidityKeccak256(['string'], [msg]) + const msgEIP191V0 = hashMessageEIP191V0(account.address, msgKeccak256) + // note: following line multiSignMessage ignore hash message + const { signature: sigOne, challenge: e } = signerOne.multiSignMessage(msgEIP191V0, publicKeys, publicNonces) + const { signature: sigTwo } = signerTwo.multiSignMessage(msgEIP191V0, publicKeys, publicNonces) + const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) + + // wrap the result + // e (bytes32), s (bytes32), pxIndexWithParity (uint8) + // pxIndexWithParity (7 bit for pxIndex, 1 bit for parity) + const hexPxIndexWithParity = ethers.utils.hexlify(pxIndexWithParity).slice(-2) + const abiCoder = new ethers.utils.AbiCoder() + const sigData = abiCoder.encode(['bytes32', 'bytes32'], [ + e.buffer, + sSummed.buffer + ]) + hexPxIndexWithParity + const result = await account.isValidSignature(msgKeccak256, sigData) + expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) +} +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/deploy/2_deploy_BloctoAccountFactory.ts b/deploy/2_deploy_BloctoAccountFactory.ts deleted file mode 100644 index 593f119..0000000 --- a/deploy/2_deploy_BloctoAccountFactory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ethers } from 'hardhat' - -const ContractName = 'BloctoAccountFactory' -const AccountToImplementation = '0x021DCa3104aa79f68EFEc784B56AFa382b1fd7b8' -const GasLimit = 6000000 - -async function main (): Promise { - // const lockedAmount = ethers.utils.parseEther("1"); - - const factory = await ethers.getContractFactory(ContractName) - const contract = await factory.deploy(AccountToImplementation, { - gasLimit: GasLimit // set the gas limit to 6 million - }) - - await contract.deployed() - - console.log(`${ContractName} deployed to: ${contract.address}`) -} - -// We recommend this pattern to be able to use async/await everywhere -// and properly handle errors. -main().catch((error) => { - console.error(error) - process.exitCode = 1 -}) diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index f095b15..79a87ef 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -97,7 +97,7 @@ describe('BloctoAccount Upgrade Test', function () { [px, px2]) const receipt = await tx.wait() - + console.log('createAccount with multiple authorized address gasUsed: ', receipt.gasUsed) let findWalletCreated = false receipt.events?.forEach((event) => { if (event.event === 'WalletCreated' && From db94589561b29427b126cf4cea5f8c04f41a2989 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Tue, 20 Jun 2023 10:37:14 +0800 Subject: [PATCH 24/26] v1.4.0: save a little bit gas, authVersion -> AUTH_VERSION_INCREMENTOR in init createAccount: 212274 -> 212174 createAccount2 (2 devices): 263929 -> 263511 --- contracts/CoreWallet/CoreWallet.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/CoreWallet/CoreWallet.sol b/contracts/CoreWallet/CoreWallet.sol index 74da472..8d5ad34 100644 --- a/contracts/CoreWallet/CoreWallet.sol +++ b/contracts/CoreWallet/CoreWallet.sol @@ -206,8 +206,8 @@ contract CoreWallet is IERC1271 { // set initial authorization value authVersion = AUTH_VERSION_INCREMENTOR; // add initial authorized address - authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; - mergedKeys[authVersion + _mergedKeyIndexWithParity] = _mergedKey; + authorizations[AUTH_VERSION_INCREMENTOR + uint256(uint160(_authorizedAddress))] = _cosigner; + mergedKeys[AUTH_VERSION_INCREMENTOR + _mergedKeyIndexWithParity] = _mergedKey; emit Authorized(_authorizedAddress, _cosigner); } @@ -234,8 +234,8 @@ contract CoreWallet is IERC1271 { for (uint256 i = 0; i < _authorizedAddresses.length; i++) { address _authorizedAddress = _authorizedAddresses[i]; require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); - authorizations[authVersion + uint256(uint160(_authorizedAddress))] = _cosigner; - mergedKeys[authVersion + _mergedKeyIndexWithParitys[i]] = _mergedKeys[i]; + authorizations[AUTH_VERSION_INCREMENTOR + uint256(uint160(_authorizedAddress))] = _cosigner; + mergedKeys[AUTH_VERSION_INCREMENTOR + _mergedKeyIndexWithParitys[i]] = _mergedKeys[i]; emit Authorized(_authorizedAddress, _cosigner); } From 5c11442a50b7e6b6a9b8e2aa2f0a0326eb65038a Mon Sep 17 00:00:00 2001 From: kbehouse Date: Wed, 21 Jun 2023 11:22:55 +0800 Subject: [PATCH 25/26] v1.4.0: update deploy scripts & a little bit of contract update --- README.md | 43 ++++++++--------- contracts/BloctoAccount.sol | 7 +-- contracts/BloctoAccountCloneableWallet.sol | 1 + .../0_deploy_Account_Factory-and-addStake.ts | 14 ++++-- deploy/0_verify.ts | 24 +++++++--- deploy/0_verify_proxy.ts | 21 +++++++++ deploy/1_deploy_VerifyingPaymaster.ts | 47 +++++++++++++++++-- deploy/2_2_createMultipleSchnorrAccount.ts | 13 ++--- ...nt.ts => 2_createSchnorrAccount_verify.ts} | 20 ++++++-- hardhat.config.ts | 44 ++++++++++++++--- package.json | 4 +- 11 files changed, 178 insertions(+), 60 deletions(-) create mode 100644 deploy/0_verify_proxy.ts rename deploy/{2_createSchnorrAccount.ts => 2_createSchnorrAccount_verify.ts} (83%) diff --git a/README.md b/README.md index 11cbe0f..b150bcd 100644 --- a/README.md +++ b/README.md @@ -7,51 +7,46 @@ test yarn test ``` - -## Deploy - -deploy BloctoAccountCloneableWallet, BloctoAccountFactory, and addStake to BloctoAccountFactory +Schnorr Multi Sign Test ``` -yarn deploy --network mumbai +npx hardhat test test/schnorrMultiSign.test.ts ``` +## Deploy & Verify -deploy VerifyingPaymaster -``` -yarn deploy-verifyingpaymaster --network mumbai -``` - +deploy BloctoAccountCloneableWallet, BloctoAccountFactory, and addStake to BloctoAccountFactory -verify BloctoAccountCloneableWallet ``` -yarn verify-bloctoaccountcloneable --network mumbai +yarn deploy_verify --network goerli ``` - -verify BloctoAccountFactory +create a test account and verify ``` -yarn verify-accountfactory --network mumbai +npx hardhat run deploy/2_createSchnorrAccount_verify.ts --network goerli ``` -verify VerifyingPaymaster -``` -yarn verify-verifyingpaymaster --network mumbai -``` -## Schnorr Multi Sign Test +## Tool +check storage layout ``` -npx hardhat test test/schnorrMultiSign.test.ts +npx hardhat check ``` -## Tool +## Testnet chain info -check storage layout +goerli, arbitrum goerli, op goerli, mumbai, bsc testnet, avax testnet ``` -npx hardhat check +BloctoAccountCloneableWallet +0x490B5ED8A17224a553c34fAA642161c8472118dd +BloctoAccountFactory +0x285cc5232236D227FCb23E6640f87934C948a028 +VerifyingPaymaster +0x9C58dF1BB61a3f68C66Ef5fC7D8Ab4bd1DaEC9Ac ``` + ## Acknowledgement this repo fork from https://github.com/eth-infinitism/account-abstraction \ No newline at end of file diff --git a/contracts/BloctoAccount.sol b/contracts/BloctoAccount.sol index a2ecef6..6c8ce92 100644 --- a/contracts/BloctoAccount.sol +++ b/contracts/BloctoAccount.sol @@ -21,8 +21,12 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas */ string public constant VERSION = "1.4.0"; + /// @notice etnrypoint from 4337 official IEntryPoint private immutable _entryPoint; + /// @notice initialized _IMPLEMENTATION_SLOT + bool public initializedImplementation = false; + /** * constructor for BloctoAccount * @param anEntryPoint entrypoint address @@ -128,9 +132,6 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas entryPoint().withdrawTo(withdrawAddress, amount); } - /// @notice initialized _IMPLEMENTATION_SLOT - bool public initializedImplementation = false; - /// @notice Used to decorate the `init` function so this can only be called one time. Necessary /// since this contract will often be used as a "clone". (See above.) modifier onlyOnceInitImplementation() { diff --git a/contracts/BloctoAccountCloneableWallet.sol b/contracts/BloctoAccountCloneableWallet.sol index 11ec8f1..5eb1881 100644 --- a/contracts/BloctoAccountCloneableWallet.sol +++ b/contracts/BloctoAccountCloneableWallet.sol @@ -10,5 +10,6 @@ contract BloctoAccountCloneableWallet is BloctoAccount { /// @param anEntryPoint entrypoint address constructor(IEntryPoint anEntryPoint) BloctoAccount(anEntryPoint) { initialized = true; + initializedImplementation = true; } } diff --git a/deploy/0_deploy_Account_Factory-and-addStake.ts b/deploy/0_deploy_Account_Factory-and-addStake.ts index 24d7657..6966cb5 100644 --- a/deploy/0_deploy_Account_Factory-and-addStake.ts +++ b/deploy/0_deploy_Account_Factory-and-addStake.ts @@ -32,27 +32,31 @@ async function main (): Promise { console.log(`BloctoAccountFactory deployed to: ${accountFactory.address}`) // add stake - const tx = await accountFactory.addStake(BigNumber.from(86400 * 3650), { value: ethers.utils.parseEther('0.1') }) + const tx = await accountFactory.addStake(BigNumber.from(86400 * 3650), { value: ethers.utils.parseEther('0.001') }) await tx.wait() const entrypoint = EntryPoint__factory.connect(EntryPoint, ethers.provider) const depositInfo = await entrypoint.getDepositInfo(accountFactory.address) console.log('stake: ', ethers.utils.formatUnits(depositInfo.stake), ', unstakeDelaySec: ', depositInfo.unstakeDelaySec) + // sleep 10 seconds + console.log('sleep 10 seconds for chain sync...') + await new Promise(f => setTimeout(f, 10000)) + // verify BloctoAccountCloneableWallet await hre.run('verify:verify', { address: walletCloneable.address, + contract: 'contracts/BloctoAccountCloneableWallet.sol:BloctoAccountCloneableWallet', constructorArguments: [ EntryPoint ] }) - // verify BloctoAccountFactory + + // verify BloctoAccountFactory (if proxy) const accountFactoryImplAddress = await getImplementationAddress(ethers.provider, accountFactory.address) await hre.run('verify:verify', { address: accountFactoryImplAddress, - constructorArguments: [ - walletCloneable.address, EntryPoint - ] + contract: 'contracts/BloctoAccountFactory.sol:BloctoAccountFactory' }) } diff --git a/deploy/0_verify.ts b/deploy/0_verify.ts index 336692b..29c7fa6 100644 --- a/deploy/0_verify.ts +++ b/deploy/0_verify.ts @@ -1,8 +1,9 @@ import { getImplementationAddress } from '@openzeppelin/upgrades-core' import hre, { ethers } from 'hardhat' -const BloctoAccountCloneableWalletAddr = '0x592D3167Cbb926379c1527f078F22E82FfAFdAa3' -const BloctoAccountFactoryAddr = '0x4b0C9eCC8A4577525688232977A346c1232a377E' +const BloctoAccountCloneableWalletAddr = '0x490B5ED8A17224a553c34fAA642161c8472118dd' +const BloctoAccountFactoryAddr = '0x285cc5232236D227FCb23E6640f87934C948a028' +// const BloctoAccountProxyCloneAddr = '0x6672e24A9D809A1b03317e83949572e71afae5be' const EntryPoint = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' async function main (): Promise { @@ -16,16 +17,25 @@ async function main (): Promise { }) // verify BloctoAccountFactory (if proxy) - const accountFactoryImplAddress = await getImplementationAddress(ethers.provider, BloctoAccountFactoryAddr) + // const accountFactoryImplAddress = await getImplementationAddress(ethers.provider, BloctoAccountFactoryAddr) + // await hre.run('verify:verify', { + // address: accountFactoryImplAddress, + // contract: 'contracts/BloctoAccountFactory.sol:BloctoAccountFactory' + // }) + + // verify BloctoAccountFactory (if not proxy) await hre.run('verify:verify', { - address: BloctoAccountFactoryAddr, + address: '0x7db696a9130b0e2aea92b39bfe520861baa5fb83', contract: 'contracts/BloctoAccountFactory.sol:BloctoAccountFactory' }) - // verify BloctoAccountFactory (if not proxy) + // verify BloctoAccountProxy // await hre.run('verify:verify', { - // address: BloctoAccountFactoryAddr, - // contract: 'contracts/BloctoAccountFactory.sol:BloctoAccountFactory' + // address: BloctoAccountProxyCloneAddr, + // contract: 'contracts/BloctoAccountProxy.sol:BloctoAccountProxy', + // constructorArguments: [ + // BloctoAccountCloneableWalletAddr + // ] // }) } diff --git a/deploy/0_verify_proxy.ts b/deploy/0_verify_proxy.ts new file mode 100644 index 0000000..8704954 --- /dev/null +++ b/deploy/0_verify_proxy.ts @@ -0,0 +1,21 @@ +// update from https://github.com/borislav-itskov/schnorrkel.js +import hre from 'hardhat' + +const BloctoAccountCloableWallet = '0x490B5ED8A17224a553c34fAA642161c8472118dd' + +async function main (): Promise { + // ---------------Verify BloctoAccountProxy Contract---------------- // + await hre.run('verify:verify', { + address: '0xd448D0835731f5dDE3942993B2bE80DFC232Cc0f', + contract: 'contracts/BloctoAccountProxy.sol:BloctoAccountProxy', + constructorArguments: [ + BloctoAccountCloableWallet + ] + }) +} +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/deploy/1_deploy_VerifyingPaymaster.ts b/deploy/1_deploy_VerifyingPaymaster.ts index 0ece30e..fb9eacf 100644 --- a/deploy/1_deploy_VerifyingPaymaster.ts +++ b/deploy/1_deploy_VerifyingPaymaster.ts @@ -1,14 +1,43 @@ -import { ethers } from 'hardhat' +import hre, { ethers } from 'hardhat' + +// import { SignerWithAddress } from 'hardhat-deploy-ethers/signers' const ContractName = 'VerifyingPaymaster' // version 0.6.0 from https://mirror.xyz/erc4337official.eth/cSdZl9X-Hce71l_FzjVKQ5eN398ial7QmkDExmIIOQk const EntryPointAddress = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' -const VerifyingSigner = '0x086443C6bA8165a684F3e316Da42D3A2F0a2330a' +const VerifyingSigner = '0x31E2FD21F2ad34bBf56B08baD57438869aED12eF' +const AligementNonce = 15 const GasLimit = 6000000 +async function alignNonce (signer: any, targetNonce: number): Promise { + let nowNonce = await signer.getTransactionCount() + + while (nowNonce < targetNonce) { + console.log('nonce not aligned, now: ', nowNonce, ', target: ', targetNonce, '-> send a tx to align nonce') + await signer.sendTransaction({ + to: '0x914171a48aa2c306DD2D68c6810D6E2B4F4ACdc7', + value: 0// 0 ether + }) + + console.log('sleep 10 seconds for chain sync...') + await new Promise(f => setTimeout(f, 20000)) + nowNonce = await signer.getTransactionCount() + } +} + async function main (): Promise { - // const lockedAmount = ethers.utils.parseEther("1"); + const [owner] = await ethers.getSigners() + const nowNonce = await owner.getTransactionCount() + console.log('deploy with account: ', owner.address, ', nonce: ', nowNonce) + if (nowNonce == AligementNonce) { + console.log('nonce aligned') + } else if (nowNonce < AligementNonce) { + await alignNonce(owner, AligementNonce) + } else { + throw new Error('nonce is larger than target nonce') + } + console.log(`deploying ${ContractName}...`) const factory = await ethers.getContractFactory(ContractName) const contract = await factory.deploy(EntryPointAddress, VerifyingSigner, { gasLimit: GasLimit // set the gas limit to 6 million @@ -17,6 +46,18 @@ async function main (): Promise { await contract.deployed() console.log(`${ContractName} deployed to: ${contract.address}`) + + // sleep 10 seconds + console.log('sleep 10 seconds for chain sync...') + await new Promise(f => setTimeout(f, 10000)) + // ---------------Verify BloctoAccountProxy Contract---------------- // + await hre.run('verify:verify', { + address: contract.address, + contract: 'contracts/Paymaster/VerifyingPaymaster.sol:VerifyingPaymaster', + constructorArguments: [ + EntryPointAddress, VerifyingSigner + ] + }) } // We recommend this pattern to be able to use async/await everywhere diff --git a/deploy/2_2_createMultipleSchnorrAccount.ts b/deploy/2_2_createMultipleSchnorrAccount.ts index be1544f..f12efcd 100644 --- a/deploy/2_2_createMultipleSchnorrAccount.ts +++ b/deploy/2_2_createMultipleSchnorrAccount.ts @@ -5,13 +5,13 @@ import { getMergedKey } from '../test/testutils' -const FactoryAddress = '0x0194b9278b1f2ED8b2Ab5070382EAB890C2B199f' +const FactoryAddress = '0x285cc5232236D227FCb23E6640f87934C948a028' const RecoverAddress = '0x0c558b2735286533b834bd1172bcA43DBD2970f7' const ethersSigner = ethers.provider.getSigner() -const SALT = 4521523 +const SALT = 45215234123 async function main (): Promise { // const lockedAmount = ethers.utils.parseEther("1"); @@ -31,21 +31,22 @@ async function main (): Promise { const [px, pxIndexWithParity] = getMergedKey(authorizedWallet, cosignerWallet, 0) const [px2, pxIndexWithParity2] = getMergedKey(authorizedWallet2, cosignerWallet2, 1) + const [px3, pxIndexWithParity3] = getMergedKey(authorizedWallet2, cosignerWallet, 2) console.log('authorizedWallet.getAddress(): ', await authorizedWallet.getAddress(), ', cosignerWallet.getAddress()', await cosignerWallet.getAddress()) console.log('ethersSigner address: ', await ethersSigner.getAddress()) console.log('factory.address', factory.address) - const tx = await factory.createAccount2([authorizedWallet.address, authorizedWallet2.address], + const tx = await factory.createAccount2([authorizedWallet.address, authorizedWallet2.address, cosignerWallet.address], cosignerWallet.address, RecoverAddress, SALT, // random salt - [pxIndexWithParity, pxIndexWithParity2], - [px, px2]) + [pxIndexWithParity, pxIndexWithParity2, pxIndexWithParity3], + [px, px2, px3]) console.log('after createAccount2') const receipt = await tx.wait() - console.log(receipt) + console.log(receipt.gasUsed) } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. diff --git a/deploy/2_createSchnorrAccount.ts b/deploy/2_createSchnorrAccount_verify.ts similarity index 83% rename from deploy/2_createSchnorrAccount.ts rename to deploy/2_createSchnorrAccount_verify.ts index 411db6d..1e3e4f4 100644 --- a/deploy/2_createSchnorrAccount.ts +++ b/deploy/2_createSchnorrAccount_verify.ts @@ -1,5 +1,5 @@ // update from https://github.com/borislav-itskov/schnorrkel.js -import { ethers } from 'hardhat' +import hre, { ethers } from 'hardhat' import { BigNumber } from 'ethers' import { expect } from 'chai' import { @@ -13,7 +13,8 @@ import { DefaultSigner } from '../test/schnorrUtils' const ERC1271_MAGICVALUE_BYTES32 = '0x1626ba7e' -const FactoryAddress = '0x0194b9278b1f2ED8b2Ab5070382EAB890C2B199f' +const BloctoAccountCloableWallet = '0x490B5ED8A17224a553c34fAA642161c8472118dd' +const FactoryAddress = '0x285cc5232236D227FCb23E6640f87934C948a028' const RecoverAddress = '0x0c558b2735286533b834bd1172bcA43DBD2970f7' @@ -22,10 +23,10 @@ const ethersSigner = ethers.provider.getSigner() // multisig const msg = 'just a test message' -const SALT = 3521523151 +const SALT = 515233151 async function main (): Promise { - // const lockedAmount = ethers.utils.parseEther("1"); + // ---------------Create Account---------------- // const AccountFactory = await ethers.getContractFactory('BloctoAccountFactory') const factory = await AccountFactory.attach(FactoryAddress) @@ -44,6 +45,7 @@ async function main (): Promise { console.log('ethersSigner address: ', await ethersSigner.getAddress()) console.log('factory.address', factory.address) + console.log('creating account...') const account = await createAccount( ethersSigner, await authorizedWallet.getAddress(), @@ -57,6 +59,16 @@ async function main (): Promise { console.log('account create success! SCW Address: ', account.address) + // ---------------Verify BloctoAccountProxy Contract---------------- // + await hre.run('verify:verify', { + address: account.address, + contract: 'contracts/BloctoAccountProxy.sol:BloctoAccountProxy', + constructorArguments: [ + BloctoAccountCloableWallet + ] + }) + + // ---------------Verify Signature---------------- // const msgKeccak256 = ethers.utils.solidityKeccak256(['string'], [msg]) const msgEIP191V0 = hashMessageEIP191V0(account.address, msgKeccak256) // note: following line multiSignMessage ignore hash message diff --git a/hardhat.config.ts b/hardhat.config.ts index 960f7b2..22a796a 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -11,9 +11,14 @@ const { POLYGONSCAN_API_KEY, // polygonscan API KEY BSCSCAN_API_KEY, // bscscan API KEY SNOWTRACE_API_KEY, // avalanche scan (snowtrace) API KEY - ARBSCAN_API_KEY // arbitrum scan API KEY + ARBSCAN_API_KEY, // arbitrum scan API KEY + OP_API_KEY } = process.env +function getDeployAccount (): string[] { + return (process.env.ETH_PRIVATE_KEY !== undefined) ? [process.env.ETH_PRIVATE_KEY] : [] +} + // You need to export an object to set up your config // Go to https://hardhat.org/config/ to learn more const config: HardhatUserConfig = { @@ -29,13 +34,35 @@ const config: HardhatUserConfig = { hardhat: { allowUnlimitedContractSize: true }, + goerli: { + url: 'https://ethereum-goerli.publicnode.com', + accounts: getDeployAccount(), + chainId: 5 + }, + optimism_testnet: { + url: 'https://goerli.optimism.io', + accounts: getDeployAccount(), + chainId: 420 + }, + arbitrum_testnet: { + url: 'https://arbitrum-goerli.publicnode.com', + accounts: getDeployAccount(), + chainId: 421613 + }, mumbai: { - url: 'https://rpc.ankr.com/polygon_mumbai', - accounts: - process.env.ETH_PRIVATE_KEY !== undefined - ? [process.env.ETH_PRIVATE_KEY] - : [], + url: 'https://polygon-mumbai.gateway.tenderly.co', + accounts: getDeployAccount(), chainId: 80001 + }, + bsc_testnet: { + url: 'https://data-seed-prebsc-2-s1.binance.org:8545', + accounts: getDeployAccount(), + chainId: 97 + }, + avalanche_testnet: { + url: 'https://api.avax-test.network/ext/bc/C/rpc', + accounts: getDeployAccount(), + chainId: 43113 } }, mocha: { @@ -50,9 +77,12 @@ const config: HardhatUserConfig = { bsc: BSCSCAN_API_KEY, bscTestnet: BSCSCAN_API_KEY, avalanche: SNOWTRACE_API_KEY, + avalancheFujiTestnet: SNOWTRACE_API_KEY, goerli: ETHERSCAN_API_KEY, arbitrumOne: ARBSCAN_API_KEY, - arbitrumGoerli: ARBSCAN_API_KEY + arbitrumGoerli: ARBSCAN_API_KEY, + optimisticEthereum: OP_API_KEY, + optimisticGoerli: OP_API_KEY } } diff --git a/package.json b/package.json index 9f8ce2e..1513a17 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "verify-bloctoaccountcloneable": "npx hardhat verify --contract contracts/BloctoAccountCloneableWallet.sol:BloctoAccountCloneableWallet 0x66d4d44e4957F70dF0127b3de2dBaD9fF9058B5B 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", "verify-bloctoaccountproxy": "npx hardhat verify --contract contracts/BloctoAccountProxy.sol:BloctoAccountProxy 0xa38dB698ec2Ec25ac7b2919fD69F49a7A975a724 0xee8EED6701EfD1d4de6B3930859919C966c6B00C ", "verify-bloctoaccountfactory": "npx hardhat verify 0xC261555D0e4623BF709d3f22Bf58A6275CE4d17D 0x66d4d44e4957F70dF0127b3de2dBaD9fF9058B5B 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", - "verify-verifyingpaymaster": "npx hardhat verify 0xE671dEee9c758e642d50c32e13FD5fC6D42C98F1 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 0x086443C6bA8165a684F3e316Da42D3A2F0a2330a" + "verify-verifyingpaymaster": "npx hardhat verify 0xE671dEee9c758e642d50c32e13FD5fC6D42C98F1 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 0x086443C6bA8165a684F3e316Da42D3A2F0a2330a", + "deploy_verify": "hardhat run deploy/0_deploy_Account_Factory-and-addStake.ts", + "deploy_verify_paymaster": "hardhat run deploy/1_deploy_VerifyingPaymaster.ts" }, "devDependencies": { "@account-abstraction/contracts": "^0.6.0", From ddabd4efd6a01ae580b68e4e143d204551f4255f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 11:13:08 +0000 Subject: [PATCH 26/26] build(deps): bump word-wrap from 1.2.3 to 1.2.4 Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1f4fd34..bb58194 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10310,9 +10310,9 @@ window-size@^0.2.0: integrity sha512-UD7d8HFA2+PZsbKyaOCEy8gMh1oDtHgJh1LfgjQ4zVXmYjAT/kvz3PueITKuqDiIXQe7yzpPnxX3lNc+AhQMyw== word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== wordwrap@^1.0.0: version "1.0.0"