AA-91 factories (#151)

* fix factories

BLSFactory use same model as SimpleAccount, using immutable wallet and
only user-specific params in initializer
add factory for TestAggregatedAccount sapmle contract
Create2Factory - use arachnid's de-facto standard deployer, instead of of
the nonstandard EIP2470 (specifically, arachnid's deployer revert on errors)

* gnosis account factory
now Gnosis-Safe  based account uses only standard gnosis contracts. The new GnosisSafeAcccountFactory only wraps the standard GnosisSafeProxyFactory to create the proxy (and initialize it with our modules)
This commit is contained in:
Dror Tirosh
2022-12-20 20:35:08 +02:00
committed by GitHub
parent 34afdaae28
commit 976d3f2758
19 changed files with 1177 additions and 1133 deletions

2
.gitignore vendored
View File

@@ -16,3 +16,5 @@ artifacts
/coverage.json
/.DS_Store
.DS_Store
/contracts/dist/
/contracts/types/

View File

@@ -1,19 +1,54 @@
// 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 "../interfaces/IEntryPoint.sol";
import "./BLSAccount.sol";
/**
* Based n SimpleAccountFactory
* can't be a subclass, since both constructor and createAccount depend on the
* actual wallet contract constructor and initializer
*/
contract BLSAccountFactory {
address private immutable accountImplementation;
BLSAccount public immutable accountImplementation;
constructor(address _accountImplementation){
accountImplementation = _accountImplementation;
constructor(IEntryPoint entryPoint, address aggregator){
accountImplementation = new BLSAccount(entryPoint, aggregator);
}
function createAccount(IEntryPoint anEntryPoint, uint salt, uint256[4] memory aPublicKey) public returns (BLSAccount) {
return BLSAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}(accountImplementation, abi.encodeWithSelector(BLSAccount.initialize.selector, anEntryPoint, aPublicKey))));
/**
* create an account, and return its address.
* returns the address even if the account is already deployed.
* Note that during UserOperation execution, this method is called only if the account is not deployed.
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
* Also note that out BLSSignatureAggregator requires that the public-key is the last parameter
*/
function createAccount(uint salt, uint256[4] memory aPublicKey) public returns (BLSAccount) {
address addr = getAddress(salt, aPublicKey);
uint codeSize = addr.code.length;
if (codeSize > 0) {
return BLSAccount(payable(addr));
}
return BLSAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}(
address(accountImplementation),
abi.encodeCall(BLSAccount.initialize, aPublicKey)
)));
}
/**
* calculate the counterfactual address of this account as it would be returned by createAccount()
*/
function getAddress(uint salt, uint256[4] memory aPublicKey) public view returns (address) {
return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked(
type(ERC1967Proxy).creationCode,
abi.encode(
address(accountImplementation),
abi.encodeCall(BLSAccount.initialize, (aPublicKey))
)
)));
}
}

View File

@@ -58,32 +58,16 @@ contract EIP4337Manager is GnosisSafe, IAccount {
/**
* set up a safe as EIP-4337 enabled.
* called from the GnosisSafeProxy4337 during construction time
* called from the GnosisSafeAccountFactory during construction time
* - enable 3 modules (this module, fallback and the entrypoint)
* - this method is called with delegateCall, so the module (usually itself) is passed as parameter, and "this" is the safe itself
*/
function setupEIP4337(
address singleton,
EIP4337Manager manager,
address owner
function setup4337Modules(
EIP4337Manager manager //the manager (this contract)
) external {
address eip4337fallback = manager.eip4337Fallback();
address[] memory owners = new address[](1);
owners[0] = owner;
uint threshold = 1;
execute(singleton, 0, abi.encodeCall(GnosisSafe.setup, (
owners, threshold,
address(0), "", //no delegate call
eip4337fallback,
address(0), 0, payable(0) //no payment receiver
)),
Enum.Operation.DelegateCall, gasleft()
);
_enableModule(manager.entryPoint());
_enableModule(eip4337fallback);
GnosisSafe safe = GnosisSafe(payable(this));
safe.enableModule(manager.entryPoint());
safe.enableModule(manager.eip4337Fallback());
}
/**
@@ -145,24 +129,6 @@ contract EIP4337Manager is GnosisSafe, IAccount {
require(success, "delegate failed");
}
/// copied from GnosisSafe ModuleManager, FallbackManager
/// enableModule is "external authorizeOnly", can't be used during construction using a "delegatecall"
/// @dev Allows to add a module to the whitelist.
/// this is a variant of enableModule that is used only during construction
/// @notice Enables the module `module` for the Safe.
/// @param module Module to be whitelisted.
function _enableModule(address module) private {
// Module address cannot be null or sentinel.
require(module != address(0) && module != SENTINEL_MODULES, "GS101");
// Module cannot be added twice.
require(modules[module] == address(0), "GS102");
modules[module] = modules[SENTINEL_MODULES];
modules[SENTINEL_MODULES] = module;
emit EnabledModule(module);
}
/**
* enumerate modules, and find the currently active EIP4337 manager (and previous module)
* @return prev prev module, needed by replaceEIP4337Manager

View File

@@ -0,0 +1,61 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;
import "@openzeppelin/contracts/utils/Create2.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";
import "./EIP4337Manager.sol";
import "../utils/Exec.sol";
/**
* A wrapper factory contract to deploy GnosisSafe as an Account-Abstraction wallet contract.
*/
contract GnosisSafeAccountFactory {
GnosisSafeProxyFactory public immutable proxyFactory;
address public immutable safeSingleton;
EIP4337Manager public immutable eip4337Manager;
constructor(GnosisSafeProxyFactory _proxyFactory, address _safeSingleton, EIP4337Manager _eip4337Manager) {
proxyFactory = _proxyFactory;
safeSingleton = _safeSingleton;
eip4337Manager = _eip4337Manager;
}
function createAccount(address owner, uint salt) public returns (address) {
address addr = getAddress(owner, salt);
uint codeSize = addr.code.length;
if (codeSize > 0) {
return addr;
}
return address(proxyFactory.createProxyWithNonce(
safeSingleton, getInitializer(owner), salt));
}
function getInitializer(address owner) internal view returns (bytes memory) {
address[] memory owners = new address[](1);
owners[0] = owner;
uint threshold = 1;
address eip4337fallback = eip4337Manager.eip4337Fallback();
bytes memory setup4337Modules = abi.encodeCall(
EIP4337Manager.setup4337Modules, (eip4337Manager));
return abi.encodeCall(GnosisSafe.setup, (
owners, threshold,
address (eip4337Manager), setup4337Modules,
eip4337fallback,
address(0), 0, payable(0) //no payment receiver
));
}
/**
* calculate the counterfactual address of this account as it would be returned by createAccount()
* (uses the same "create2 signature" used by GnosisSafeProxyFactory.createProxyWithNonce)
*/
function getAddress(address owner, uint salt) public view returns (address) {
bytes memory initializer = getInitializer(owner);
//copied from deployProxyWithNonce
bytes32 salt2 = keccak256(abi.encodePacked(keccak256(initializer), salt));
bytes memory deploymentData = abi.encodePacked(type(GnosisSafeProxy).creationCode, uint256(uint160(safeSingleton)));
return Create2.computeAddress(bytes32(salt2), keccak256(deploymentData), address (proxyFactory));
}
}

View File

@@ -1,24 +0,0 @@
//SPDX-License-Identifier: GPL
pragma solidity ^0.8.7;
/* solhint-disable avoid-low-level-calls */
import "./EIP4337Manager.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxy.sol";
/**
* Create a proxy to a GnosisSafe, which accepts calls through Account-Abstraction.
* The created GnosisSafe has a single owner.
* It is possible to add more owners, but currently, it can only be accessed via Account-Abstraction
* if the owners threshold is exactly 1.
*/
contract SafeProxy4337 is GnosisSafeProxy {
constructor(
address singleton, EIP4337Manager aaModule,
address owner
) GnosisSafeProxy(singleton) {
(bool success,bytes memory ret) = address(aaModule).delegatecall(abi.encodeCall(
EIP4337Manager.setupEIP4337, (singleton, aaModule, owner)));
require(success, string(ret));
}
}

View File

@@ -22,5 +22,10 @@
"license": "MIT",
"bugs": {
"url": "https://github.com/eth-infinitism/account-abstraction/issues"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@gnosis.pm/safe-contracts": "^1.3.0"
}
}

View File

@@ -14,11 +14,9 @@ import "./SimpleAccount.sol";
*/
contract SimpleAccountFactory {
SimpleAccount public immutable accountImplementation;
IEntryPoint private immutable entryPoint;
constructor(IEntryPoint _entryPoint){
accountImplementation = new SimpleAccount(_entryPoint);
entryPoint = _entryPoint;
}
/**

View File

@@ -0,0 +1,51 @@
// 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 "./TestAggregatedAccount.sol";
/**
* Based n SimpleAccountFactory
* can't be a subclass, since both constructor and createAccount depend on the
* actual wallet contract constructor, initializer
*/
contract TestAggregatedAccountFactory {
TestAggregatedAccount public immutable accountImplementation;
constructor(IEntryPoint anEntryPoint, address anAggregator){
accountImplementation = new TestAggregatedAccount(anEntryPoint, anAggregator);
}
/**
* create an account, and return its address.
* returns the address even if the account is already deployed.
* Note that during UserOperation execution, this method is called only if the account is not deployed.
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
*/
function createAccount(address owner, uint salt) public returns (TestAggregatedAccount ret) {
address addr = getAddress(owner, salt);
uint codeSize = addr.code.length;
if (codeSize > 0) {
return TestAggregatedAccount(payable(addr));
}
ret = TestAggregatedAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}(
address(accountImplementation),
abi.encodeCall(TestAggregatedAccount.initialize, (owner))
)));
}
/**
* calculate the counterfactual address of this account as it would be returned by createAccount()
*/
function getAddress(address owner, uint salt) public view returns (address) {
return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked(
type(ERC1967Proxy).creationCode,
abi.encode(
address(accountImplementation),
abi.encodeCall(TestAggregatedAccount.initialize, (owner))
)
)));
}
}

View File

@@ -16,8 +16,8 @@ const deployEntryPoint: DeployFunction = async function (hre: HardhatRuntimeEnvi
deterministicDeployment: true
})
console.log('==entrypoint addr=', ret.address)
/*
const entryPointAddress = ret.address
const w = await hre.deployments.deploy(
'SimpleAccount', {
from,
@@ -33,6 +33,7 @@ const deployEntryPoint: DeployFunction = async function (hre: HardhatRuntimeEnvi
deterministicDeployment: true
})
console.log('==testCounter=', t.address)
*/
}
export default deployEntryPoint

View File

@@ -2,34 +2,34 @@
the destination is "account.nonce()", which is known to be "hot" address used by this account
it little higher than EOA call: its an exec from entrypoint (or account owner) into account contract, verifying msg.sender and exec to target)
- gas estimate "simple" - 31045
- gas estimate "big tx 5k" - 127283
- gas estimate "big tx 5k" - 127295
╔════════════════════════════════╤═══════╤═══════════════╤════════════════╤═════════════════════╗
║ handleOps description │ count │ total gasUsed │ per UserOp gas │ per UserOp overhead ║
║ │ │ │ (delta for │ (compared to ║
║ │ │ │ one UserOp) │ account.exec()) ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple │ 1 │ 77970 │ │ ║
║ simple │ 1 │ 77911 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple - diff from previous │ 2 │ │ 43448 │ 12403
║ simple - diff from previous │ 2 │ │ 43377 │ 12332
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple │ 10 │ 469271 │ │ ║
║ simple │ 10 │ 468533 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple - diff from previous │ 11 │ │ 43664 │ 12619
║ simple - diff from previous │ 11 │ │ 43573 │ 12528
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster │ 1 │ 77990 │ │ ║
║ simple paymaster │ 1 │ 77899 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster with diff │ 2 │ │ 43392 │ 12347
║ simple paymaster with diff │ 2 │ │ 43401 │ 12356
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster │ 10 │ 469199 │ │ ║
║ simple paymaster │ 10 │ 468593 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster with diff │ 11 │ │ 43628 │ 12583
║ simple paymaster with diff │ 11 │ │ 43537 │ 12492
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx 5k │ 1 │ 179677 │ │ ║
║ big tx 5k │ 1 │ 179630 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx - diff from previous │ 2 │ │ 144783 │ 17500
║ big tx - diff from previous │ 2 │ │ 144712 │ 17417
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx 5k │ 10 │ 1488303 │ │ ║
║ big tx 5k │ 10 │ 1487657 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx - diff from previous │ 11 │ │ 146422 │ 19139
║ big tx - diff from previous │ 11 │ │ 146231 │ 18936
╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝

View File

@@ -1,5 +1,5 @@
// from https://eips.ethereum.org/EIPS/eip-2470
import { BigNumber, BigNumberish, Contract, ethers, Signer } from 'ethers'
// 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'
@@ -7,19 +7,21 @@ import { TransactionRequest } from '@ethersproject/abstract-provider'
export class Create2Factory {
factoryDeployed = false
static readonly contractAddress = '0xce0042B868300000d44A59004Da54A005ffdcf9f'
static readonly factoryDeployer = '0xBb6e024b9cFFACB947A71991E386681B1Cd1477D'
static readonly factoryTx = '0xf9016c8085174876e8008303c4d88080b90154608060405234801561001057600080fd5b50610134806100206000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634af63f0214602d575b600080fd5b60cf60048036036040811015604157600080fd5b810190602081018135640100000000811115605b57600080fd5b820183602082011115606c57600080fd5b80359060200191846001830284011164010000000083111715608d57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550509135925060eb915050565b604080516001600160a01b039092168252519081900360200190f35b6000818351602085016000f5939250505056fea26469706673582212206b44f8a82cb6b156bfcc3dc6aadd6df4eefd204bc928a4397fd15dacf6d5320564736f6c634300060200331b83247000822470'
static readonly factoryTxHash = '0x803351deb6d745e91545a6a3e1c0ea3e9a6a02a1a4193b70edfcd2f40f71a01c'
static readonly factoryDeploymentFee = (0.0247 * 1e18).toString()
// 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 EIP-2470 deployer.
* The delpoyer is deployed (unless it is already deployed)
* 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
@@ -32,15 +34,17 @@ export class Create2Factory {
initCode = (initCode as TransactionRequest).data!.toString()
}
const addr = this.getDeployedAddress(initCode, salt)
const addr = Create2Factory.getDeployedAddress(initCode, salt)
if (await this.provider.getCode(addr).then(code => code.length) > 2) {
return addr
}
const factory = new Contract(Create2Factory.contractAddress, ['function deploy(bytes _initCode, bytes32 _salt) returns(address)'], this.signer)
const saltBytes32 = hexZeroPad(hexlify(salt), 32)
const deployTx = {
to: Create2Factory.contractAddress,
data: this.getDeployTransactionCallData(initCode, salt)
}
if (gasLimit === 'estimate') {
gasLimit = await factory.estimateGas.deploy(initCode, saltBytes32)
gasLimit = await this.signer.estimateGas(deployTx)
}
// manual estimation (its bit larger: we don't know actual deployed code size)
@@ -57,7 +61,7 @@ export class Create2Factory {
gasLimit = Math.floor(gasLimit * 64 / 63)
}
const ret = await factory.deploy(initCode, saltBytes32, { gasLimit })
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')
@@ -66,9 +70,11 @@ export class Create2Factory {
}
getDeployTransactionCallData (initCode: string, salt: BigNumberish = 0): string {
const factory = new Contract(Create2Factory.contractAddress, ['function deploy(bytes _initCode, bytes32 _salt) returns(address)'])
const saltBytes32 = hexZeroPad(hexlify(salt), 32)
return factory.interface.encodeFunctionData('deploy', [initCode, saltBytes32])
return hexConcat([
saltBytes32,
initCode
])
}
/**
@@ -77,7 +83,7 @@ export class Create2Factory {
* @param initCode
* @param salt
*/
getDeployedAddress (initCode: string, salt: BigNumberish): string {
static getDeployedAddress (initCode: string, salt: BigNumberish): string {
const saltBytes32 = hexZeroPad(hexlify(salt), 32)
return '0x' + keccak256(hexConcat([
'0xff',
@@ -87,8 +93,7 @@ export class Create2Factory {
])).slice(-40)
}
// deploy the EIP2470 factory, if not already deployed.
// (note that it requires to have a "signer" with 0.0247 eth, to fund the deployer's deployment
// deploy the factory, if not already deployed.
async deployFactory (signer?: Signer): Promise<void> {
if (await this._isFactoryDeployed()) {
return
@@ -99,7 +104,7 @@ export class Create2Factory {
})
await this.provider.sendTransaction(Create2Factory.factoryTx)
if (!await this._isFactoryDeployed()) {
throw new Error('fatal: failed to deploy Eip2470factory')
throw new Error('fatal: failed to deploy deterministic deployer')
}
}

View File

@@ -1,12 +1,11 @@
import {
arrayify,
defaultAbiCoder,
getCreate2Address,
hexDataSlice,
keccak256
} from 'ethers/lib/utils'
import { BigNumber, Contract, Signer, Wallet } from 'ethers'
import { AddressZero, callDataCost, HashZero, rethrow } from './testutils'
import { AddressZero, callDataCost, rethrow } from './testutils'
import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util'
import {
EntryPoint
@@ -175,10 +174,11 @@ export async function fillUserOp (op: Partial<UserOperation>, entryPoint?: Entry
const initCallData = hexDataSlice(op1.initCode!, 20)
if (op1.nonce == null) op1.nonce = 0
if (op1.sender == null) {
// hack: if the init contract is our deployer, then we know what the address would be, without a view call
// 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] = defaultAbiCoder.decode(['bytes', 'bytes32'], '0x' + initCallData.slice(10))
op1.sender = getCreate2Address(initAddr, HashZero, keccak256(ctr))
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')

View File

@@ -20,7 +20,7 @@ describe('test Create2Factory', () => {
it('should deploy to known address', async () => {
const initCode = TestToken__factory.bytecode
const addr = factory.getDeployedAddress(initCode, 0)
const addr = Create2Factory.getDeployedAddress(initCode, 0)
expect(await provider.getCode(addr).then(code => code.length)).to.equal(2)
await factory.deploy(initCode, 0)
@@ -29,7 +29,7 @@ describe('test Create2Factory', () => {
it('should deploy to different address based on salt', async () => {
const initCode = TestToken__factory.bytecode
const addr = factory.getDeployedAddress(initCode, 123)
const addr = Create2Factory.getDeployedAddress(initCode, 123)
expect(await provider.getCode(addr).then(code => code.length)).to.equal(2)
await factory.deploy(initCode, 123)

View File

@@ -5,6 +5,8 @@ import {
EntryPoint,
SimpleAccount,
SimpleAccountFactory,
TestAggregatedAccount__factory,
TestAggregatedAccountFactory__factory,
TestCounter,
TestCounter__factory,
TestExpirePaymaster,
@@ -12,7 +14,10 @@ import {
TestExpiryAccount,
TestExpiryAccount__factory,
TestPaymasterAcceptAll,
TestPaymasterAcceptAll__factory
TestPaymasterAcceptAll__factory,
TestAggregatedAccount,
TestSignatureAggregator,
TestSignatureAggregator__factory
} from '../typechain'
import {
AddressZero,
@@ -43,12 +48,6 @@ import { ethers } from 'hardhat'
import { defaultAbiCoder, hexConcat, hexZeroPad, parseEther } from 'ethers/lib/utils'
import { debugTransaction } from './debugTx'
import { BytesLike } from '@ethersproject/bytes'
import { TestSignatureAggregator } from '../typechain/contracts/samples/TestSignatureAggregator'
import { TestAggregatedAccount } from '../typechain/contracts/samples/TestAggregatedAccount'
import {
TestSignatureAggregator__factory
} from '../typechain/factories/contracts/samples/TestSignatureAggregator__factory'
import { TestAggregatedAccount__factory } from '../typechain/factories/contracts/samples/TestAggregatedAccount__factory'
import { toChecksumAddress, zeroAddress } from 'ethereumjs-util'
describe('EntryPoint', function () {
@@ -759,8 +758,8 @@ describe('EntryPoint', function () {
let addr: string
let userOp: UserOperation
before(async () => {
const aggregatedAccountImplementation = await new TestAggregatedAccount__factory(ethersSigner).deploy(entryPoint.address, aggregator.address)
initCode = await getAggregatedAccountInitCode(entryPoint.address, aggregatedAccountImplementation.address)
const factory = await new TestAggregatedAccountFactory__factory(ethersSigner).deploy(entryPoint.address, aggregator.address)
initCode = await getAggregatedAccountInitCode(entryPoint.address, factory)
addr = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender)
await ethersSigner.sendTransaction({ to: addr, value: parseEther('0.1') })
userOp = await fillAndSign({

View File

@@ -7,9 +7,11 @@ import {
EntryPoint,
EntryPoint__factory,
GnosisSafe,
GnosisSafeAccountFactory,
GnosisSafeAccountFactory__factory,
GnosisSafeProxy,
GnosisSafeProxyFactory__factory,
GnosisSafe__factory,
SafeProxy4337,
SafeProxy4337__factory,
TestCounter,
TestCounter__factory
} from '../typechain'
@@ -23,9 +25,8 @@ import {
isDeployed
} from './testutils'
import { fillAndSign } from './UserOp'
import { defaultAbiCoder, hexConcat, hexValue, hexZeroPad, parseEther } from 'ethers/lib/utils'
import { defaultAbiCoder, hexConcat, hexZeroPad, parseEther } from 'ethers/lib/utils'
import { expect } from 'chai'
import { Create2Factory } from '../src/Create2Factory'
describe('Gnosis Proxy', function () {
this.timeout(30000)
@@ -34,13 +35,15 @@ describe('Gnosis Proxy', function () {
let safeSingleton: GnosisSafe
let owner: Signer
let ownerAddress: string
let proxy: SafeProxy4337
let proxy: GnosisSafeProxy
let manager: EIP4337Manager
let entryPoint: EntryPoint
let counter: TestCounter
let proxySafe: GnosisSafe
let safe_execTxCallData: string
let accountFactory: GnosisSafeAccountFactory
before('before', async function () {
// EIP4337Manager fails to compile with solc-coverage
if (process.env.COVERAGE != null) {
@@ -49,18 +52,33 @@ describe('Gnosis Proxy', function () {
const provider = ethers.provider
ethersSigner = provider.getSigner()
// standard safe singleton contract (implementation)
safeSingleton = await new GnosisSafe__factory(ethersSigner).deploy()
// standard safe proxy factory
const proxyFactory = await new GnosisSafeProxyFactory__factory(ethersSigner).deploy()
entryPoint = await deployEntryPoint()
manager = await new EIP4337Manager__factory(ethersSigner).deploy(entryPoint.address)
owner = createAccountOwner()
ownerAddress = await owner.getAddress()
counter = await new TestCounter__factory(ethersSigner).deploy()
proxy = await new SafeProxy4337__factory(ethersSigner).deploy(safeSingleton.address, manager.address, ownerAddress)
accountFactory = await new GnosisSafeAccountFactory__factory(ethersSigner)
.deploy(proxyFactory.address, safeSingleton.address, manager.address)
proxySafe = GnosisSafe__factory.connect(proxy.address, owner)
await accountFactory.createAccount(ownerAddress, 0)
// we use our accountFactory to create and configure the proxy.
// but the actual deployment is done internally by the gnosis factory
const ev = await proxyFactory.queryFilter(proxyFactory.filters.ProxyCreation())
const addr = ev[0].args.proxy
await ethersSigner.sendTransaction({ to: proxy.address, value: parseEther('0.1') })
proxy =
proxySafe = GnosisSafe__factory.connect(addr, owner)
await ethersSigner.sendTransaction({
to: proxy.address,
value: parseEther('0.1')
})
const counter_countCallData = counter.interface.encodeFunctionData('count')
safe_execTxCallData = safeSingleton.interface.encodeFunctionData('execTransactionFromModule', [counter.address, 0, counter_countCallData, 0])
@@ -113,17 +131,20 @@ describe('Gnosis Proxy', function () {
let counterfactualAddress: string
it('should create account', async function () {
const ctrCode = hexValue(await new SafeProxy4337__factory(ethersSigner).getDeployTransaction(safeSingleton.address, manager.address, ownerAddress).data!)
const initCode = hexConcat([
Create2Factory.contractAddress,
new Create2Factory(ethers.provider).getDeployTransactionCallData(ctrCode, 0)
accountFactory.address,
accountFactory.interface.encodeFunctionData('createAccount', [ownerAddress, 123])
])
counterfactualAddress = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender)
counterfactualAddress = await accountFactory.callStatic.getAddress(ownerAddress, 123)
expect(!await isDeployed(counterfactualAddress))
await ethersSigner.sendTransaction({ to: counterfactualAddress, value: parseEther('0.1') })
await ethersSigner.sendTransaction({
to: counterfactualAddress,
value: parseEther('0.1')
})
const op = await fillAndSign({
sender: counterfactualAddress,
initCode,
verificationGasLimit: 400000
}, owner, entryPoint)
@@ -186,7 +207,10 @@ describe('Gnosis Proxy', function () {
expect(await proxySafe.isModuleEnabled(oldFallback)).to.equal(true)
expect(oldManager.toLowerCase()).to.eq(manager.address.toLowerCase())
await ethersSigner.sendTransaction({ to: ownerAddress, value: parseEther('0.1') })
await ethersSigner.sendTransaction({
to: ownerAddress,
value: parseEther('0.1')
})
const replaceManagerCallData = manager.interface.encodeFunctionData('replaceEIP4337Manager',
[prev, oldManager, newManager.address])

View File

@@ -2,27 +2,24 @@ import { ethers } from 'hardhat'
import {
arrayify,
hexConcat,
Interface,
keccak256,
parseEther
} from 'ethers/lib/utils'
import { BigNumber, BigNumberish, Contract, ContractReceipt, Signer, Wallet } from 'ethers'
import {
ERC1967Proxy__factory,
EntryPoint,
EntryPoint__factory,
IERC20,
IEntryPoint,
SimpleAccount,
SimpleAccountFactory__factory,
SimpleAccount__factory, SimpleAccountFactory
SimpleAccount__factory, SimpleAccountFactory, TestAggregatedAccountFactory
} from '../typechain'
import { BytesLike, hexValue } from '@ethersproject/bytes'
import { BytesLike } from '@ethersproject/bytes'
import { expect } from 'chai'
import { Create2Factory } from '../src/Create2Factory'
import { debugTransaction } from './debugTx'
import { UserOperation } from './UserOperation'
import { zeroAddress } from 'ethereumjs-util'
export const AddressZero = ethers.constants.AddressZero
export const HashZero = ethers.constants.HashZero
@@ -105,15 +102,12 @@ export function getAccountInitCode (owner: string, factory: SimpleAccountFactory
])
}
export async function getAggregatedAccountInitCode (entryPoint: string, implementationAddress: string): Promise<BytesLike> {
const initializeCall = new Interface(SimpleAccount__factory.abi).encodeFunctionData('initialize', [zeroAddress()])
const accountCtr = new ERC1967Proxy__factory(ethers.provider.getSigner()).getDeployTransaction(implementationAddress, initializeCall).data!
const factory = new Create2Factory(ethers.provider)
const initCallData = factory.getDeployTransactionCallData(hexValue(accountCtr), 0)
export async function getAggregatedAccountInitCode (entryPoint: string, factory: TestAggregatedAccountFactory, salt = 0): Promise<BytesLike> {
// the test aggregated account doesn't check the owner...
const owner = AddressZero
return hexConcat([
Create2Factory.contractAddress,
initCallData
factory.address,
factory.interface.encodeFunctionData('createAccount', [owner, salt])
])
}

View File

@@ -43,8 +43,7 @@ describe('bls account', function () {
signer1 = fact.getSigner(arrayify(BLS_DOMAIN), '0x01')
signer2 = fact.getSigner(arrayify(BLS_DOMAIN), '0x02')
const blsAccountImplementation = await new BLSAccount__factory(etherSigner).deploy(entrypoint.address, blsAgg.address)
accountDeployer = await new BLSAccountFactory__factory(etherSigner).deploy(blsAccountImplementation.address)
accountDeployer = await new BLSAccountFactory__factory(etherSigner).deploy(entrypoint.address, blsAgg.address)
// TODO: these two are not created via the 'accountDeployer' for some reason - I am not touching it for now
account1 = await new BLSAccount__factory(etherSigner).deploy(entrypoint.address, blsAgg.address)
@@ -137,7 +136,7 @@ describe('bls account', function () {
signer3 = fact.getSigner(arrayify(BLS_DOMAIN), '0x03')
initCode = hexConcat([
accountDeployer.address,
accountDeployer.interface.encodeFunctionData('createAccount', [entrypoint.address, 0, signer3.pubkey])
accountDeployer.interface.encodeFunctionData('createAccount', [0, signer3.pubkey])
])
})

View File

@@ -1,269 +0,0 @@
/* eslint-disable no-unreachable */
import './aa.init'
import { describe } from 'mocha'
import { BigNumber, Wallet } from 'ethers'
import { expect } from 'chai'
import {
SimpleAccount,
SimpleAccount__factory,
EntryPoint,
TestCounter,
TestCounter__factory,
SimpleAccountFactory
} from '../typechain'
import {
createAccountOwner,
fund,
checkForGeth,
rethrow,
getAccountInitCode,
tonumber,
deployEntryPoint,
callDataCost, createAddress, getAccountAddress, simulationResultCatch
} from './testutils'
import { fillAndSign } from './UserOp'
import { UserOperation } from './UserOperation'
import { PopulatedTransaction } from 'ethers/lib/ethers'
import { ethers } from 'hardhat'
import { toBuffer } from 'ethereumjs-util'
import { defaultAbiCoder } from 'ethers/lib/utils'
describe('Batch gas testing', function () {
// this test is currently useless. client need to do better work with preVerificationGas calculation.
// we do need a better recommendation for bundlers how to validate those values before accepting a request.
return
let once = true
const ethersSigner = ethers.provider.getSigner()
let entryPoint: EntryPoint
let simpleAccountFactory: SimpleAccountFactory
let accountOwner: Wallet
let account: SimpleAccount
const results: Array<() => void> = []
before(async function () {
this.skip()
await checkForGeth()
entryPoint = await deployEntryPoint()
// static call must come from address zero, to validate it can only be called off-chain.
accountOwner = createAccountOwner()
account = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address)
await account.initialize(await accountOwner.getAddress())
await fund(account)
})
after(async () => {
if (results.length === 0) {
return
}
console.log('== Summary')
console.log('note: negative "overpaid" means the client should compensate the relayer with higher priority fee')
for (const result of results) {
await result()
}
});
[1,
10
].forEach(maxCount => {
describe(`test batches maxCount=${maxCount}`, () => {
/**
* attempt big batch.
*/
let counter: TestCounter
let accountExecCounterFromEntryPoint: PopulatedTransaction
let execCounterCount: PopulatedTransaction
const beneficiaryAddress = createAddress()
before(async () => {
counter = await new TestCounter__factory(ethersSigner).deploy()
const count = await counter.populateTransaction.count()
execCounterCount = await account.populateTransaction.execute(counter.address, 0, count.data!)
accountExecCounterFromEntryPoint = await account.populateTransaction.execute(counter.address, 0, count.data!)
})
const accounts: Array<{ w: string, owner: Wallet }> = []
it('batch of create', async () => {
const ops: UserOperation[] = []
let count = 0
const maxTxGas = 12e6
let opsGasCollected = 0
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
while (++count) {
const accountOwner1 = createAccountOwner()
const account1 = await getAccountAddress(accountOwner1.address, simpleAccountFactory)
await fund(account1, '0.5')
const op1 = await fillAndSign({
initCode: getAccountInitCode(accountOwner1.address, simpleAccountFactory),
nonce: 0,
// callData: accountExecCounterFromEntryPoint.data,
maxPriorityFeePerGas: 1e9
}, accountOwner1, entryPoint)
// requests are the same, so estimate is the same too.
const { preOpGas } = await entryPoint.callStatic.simulateValidation(op1, { gasPrice: 1e9 }).catch(simulationResultCatch)
const txgas = BigNumber.from(preOpGas).add(op1.callGasLimit).toNumber()
// console.log('colected so far', opsGasCollected, 'estim', verificationGasLimit, 'max', maxTxGas)
if (opsGasCollected + txgas > maxTxGas) {
break
}
opsGasCollected += txgas
ops.push(op1)
accounts.push({ owner: accountOwner1, w: account1 })
if (accounts.length >= maxCount) break
}
await call_handleOps_and_stats('Create', ops, count)
})
it('batch of tx', async function () {
this.timeout(30000)
if (accounts.length === 0) {
this.skip()
}
const ops: UserOperation[] = []
for (const { w, owner } of accounts) {
const op1 = await fillAndSign({
sender: w,
callData: accountExecCounterFromEntryPoint.data,
maxPriorityFeePerGas: 1e9,
verificationGasLimit: 1.3e6
}, owner, entryPoint)
ops.push(op1)
if (once) {
once = false
console.log('direct call:', await counter.estimateGas.count())
console.log('through account:', await ethers.provider.estimateGas({
from: accountOwner.address,
to: account.address,
data: execCounterCount.data!
}), 'datacost=', callDataCost(execCounterCount.data!))
console.log('through handleOps:', await entryPoint.estimateGas.handleOps([op1], beneficiaryAddress))
}
}
await call_handleOps_and_stats('Simple Ops', ops, ops.length)
})
it('batch of expensive ops', async function () {
this.timeout(30000)
if (accounts.length === 0) {
this.skip()
}
const waster = await counter.populateTransaction.gasWaster(40, '')
const accountExecFromEntryPoint_waster: PopulatedTransaction =
await account.populateTransaction.execute(counter.address, 0, waster.data!)
const ops: UserOperation[] = []
for (const { w, owner } of accounts) {
const op1 = await fillAndSign({
sender: w,
callData: accountExecFromEntryPoint_waster.data,
maxPriorityFeePerGas: 1e9,
verificationGasLimit: 1.3e6
}, owner, entryPoint)
ops.push(op1)
}
await call_handleOps_and_stats('Expensive Ops', ops, ops.length)
})
it('batch of large ops', async function () {
this.timeout(30000)
if (accounts.length === 0) {
this.skip()
}
const waster = await counter.populateTransaction.gasWaster(0, '1'.repeat(16384))
const accountExecFromEntryPoint_waster: PopulatedTransaction =
await account.populateTransaction.execute(counter.address, 0, waster.data!)
const ops: UserOperation[] = []
for (const { w, owner } of accounts) {
const op1 = await fillAndSign({
sender: w,
callData: accountExecFromEntryPoint_waster.data,
maxPriorityFeePerGas: 1e9,
verificationGasLimit: 1.3e6
}, owner, entryPoint)
ops.push(op1)
}
await call_handleOps_and_stats('Large (16k) Ops', ops, ops.length)
})
})
})
async function call_handleOps_and_stats (title: string, ops: UserOperation[], count: number): Promise<void> {
const beneficiaryAddress = createAddress()
const sender = ethersSigner // ethers.provider.getSigner(5)
const senderPrebalance = await ethers.provider.getBalance(await sender.getAddress())
const entireTxEncoded = toBuffer(await entryPoint.populateTransaction.handleOps(ops, beneficiaryAddress).then(tx => tx.data))
function callDataCost (data: Buffer | string): number {
if (typeof data === 'string') {
data = toBuffer(data)
}
return data.map(b => b === 0 ? 4 : 16).reduce((sum, b) => sum + b)
}
// data cost of entire bundle
const entireTxDataCost = callDataCost(entireTxEncoded)
// data cost of a single op in the bundle:
const handleOpFunc = Object.values(entryPoint.interface.functions).find(func => func.name === 'handleOp')!
const opEncoding = handleOpFunc.inputs[0]
const opEncoded = defaultAbiCoder.encode([opEncoding], [ops[0]])
const opDataCost = callDataCost(opEncoded)
console.log('== calldataoverhead=', entireTxDataCost, 'len=', entireTxEncoded.length / 2, 'opcost=', opDataCost, opEncoded.length / 2)
console.log('== per-op overhead:', entireTxDataCost - (opDataCost * count), 'count=', count)
// for slack testing, we set TX priority same as UserOp
// (real miner may create tx with priorityFee=0, to avoid paying from the "sender" to coinbase)
const { maxPriorityFeePerGas } = ops[0]
const ret = await entryPoint.connect(sender).handleOps(ops, beneficiaryAddress, {
gasLimit: 13e6,
maxPriorityFeePerGas
}).catch((rethrow())).then(async r => await r!.wait())
// const allocatedGas = ops.map(op => parseInt(op.callGasLimit.toString()) + parseInt(op.verificationGasLimit.toString())).reduce((sum, x) => sum + x)
// console.log('total allocated gas (callGasLimit+verificationGasLimit):', allocatedGas)
// remove "revert reason" events
const events1 = ret.events!.filter((e: any) => e.event === 'UserOperationEvent')!
// console.log(events1.map(e => ({ev: e.event, ...objdump(e.args!)})))
if (events1.length !== ret.events!.length) {
console.log('== reverted: ', ret.events!.length - events1.length)
}
// note that in theory, each could can have different gasPrice (depends on its prio/max), but in our
// test they are all the same.
const { actualGasPrice } = events1[0]!.args!
const totalEventsGasCost = parseInt(events1.map((x: any) => x.args!.actualGasCost).reduce((sum: any, x: any) => sum.add(x)).toString())
const senderPaid = parseInt(senderPrebalance.sub(await ethers.provider.getBalance(await sender.getAddress())).toString())
let senderRedeemed = await ethers.provider.getBalance(beneficiaryAddress).then(tonumber)
expect(senderRedeemed).to.equal(totalEventsGasCost)
// for slack calculations, add the calldataoverhead. should be part of the relayer fee.
senderRedeemed += entireTxDataCost * actualGasPrice
console.log('provider gasprice:', await ethers.provider.getGasPrice())
console.log('userop gasPrice:', actualGasPrice)
const opGasUsed = Math.floor(senderPaid / actualGasPrice / count)
const opGasPaid = Math.floor(senderRedeemed / actualGasPrice / count)
console.log('senderPaid= ', senderPaid, '(wei)\t', (senderPaid / actualGasPrice).toFixed(0), '(gas)', opGasUsed, '(gas/op)', count)
console.log('redeemed= ', senderRedeemed, '(wei)\t', (senderRedeemed / actualGasPrice).toFixed(0), '(gas)', opGasPaid, '(gas/op)')
// console.log('slack=', ((senderRedeemed - senderPaid) * 100 / senderPaid).toFixed(2), '%', opGasUsed - opGasPaid)
const dumpResult = async (): Promise<void> => {
console.log('==>', `${title} (count=${count}) : `.padEnd(30), 'per-op gas overpaid:', opGasPaid - opGasUsed)
}
await dumpResult()
results.push(dumpResult)
}
})

1623
yarn.lock

File diff suppressed because it is too large Load Diff