Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SimpleAccountFactory avoid extra address calculation #326

Closed
wants to merge 7 commits into from

Conversation

drortirosh
Copy link
Contributor

Optimize SimpleAccountFactory, to try to create the account first, and calculate the counterfactual address only if it fails.

The address calculation is only required in static call, and it is a waste to calculate it always on-chain just before "create2")

(The downside is the requirement to use assembly to create the account, as we need the catch the creation failure without a revert)

Optimize SimpleAccountFactory, to try to create the account first,
and calculate the counterfactual address only if it fails.

The address calculation is only required in static call, and it is a
waste to calculate it always on-chain just before "create2")

(The downside is the requirement to use assembly to create the account,
as we need the catch the creation failure without a revert)
Slightly different impl with gnosis factory:
- the low-level proxy factory always revert if already exists.
- it is in a separate contract, so we can try/catch it
@leekt
Copy link
Contributor

leekt commented Aug 29, 2023

Actually i tried this one with different factory but seems the factory will consume every gas if create2 tries to deploy to existing address.
https://github.com/ethereum/go-ethereum/blob/5976e58415a633c24a0d903e8a60a3780abdfe59/core/vm/evm.go#L440

I'm not sure saving gas for checking address is deployed or not is worth a risk
though the "risk" itself is quite low when we are dealing through entrypoint cause it will check the address is deployed or not before trying to deploy the address

also, since EXTCODESIZE has been called in entrypoint before the factory, address of userOp.sender is warm before entering the factory, gas we are saving is 100

@@ -26,27 +27,35 @@ contract SimpleAccountFactory {
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
*/
function createAccount(address owner,uint256 salt) public returns (SimpleAccount ret) {
address addr = getAddress(owner, salt);
uint codeSize = addr.code.length;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the real gas cost saving here is not the addr.code.length (which indeed, is only 100, since the address will get warm), but the getAddress: there is no need to calculate the full create2 address (including bringing into memory the entire initCode and hash it)
Since we only need the address calculation if the account is not deployed, it makes sense to make it only after create2 marks that the account is already deployed (which can only happen if the method is used off-chain.

@@ -5,6 +5,7 @@ import "@openzeppelin/contracts/utils/Create2.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import "./SimpleAccount.sol";
import "hardhat/console.sol";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not commit console.log code

// must use assembly and not "new{salt}()", so it doesn't revert on failure.
// solhint-disable-next-line no-inline-assembly
assembly {
ret := create2(0, add(proxyConstructor, 0x20), mload(proxyConstructor), salt)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EIP-1014 states that the creation throws immediately in case there is already a code at the destination address, why do you expect it to return address(0)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR: you're right. it should fail according to the spec, but all node implementations support it. I'm not hesitate to rely on it in our reference impl.

According to the spec, create2 should revert if target already exists.
but we have an explicit test - that shows this method works...

What I did find out that it works - but consumes more gas - estimating "create" takes ~280Kgas, but estimating again after create consumes >500Kgas (and 2M gas on hardhat)
Gas doesn't really matter, as it is meant for being a helper for off-chain method.

I think that what happens on failure is that the GO implementation doesn't revert, but consume all gas - but create behaves like a "call", and set aside 1/64 of the gas.
If that 1/64 gas left is enough, the method may succeed.
So if the view method is called with enough (e.g. default 5M gas), then the 1/64 excess that is left is enough to complete the createSender method....

solidity compiler is also aware if this behavior

     let expr_19_address := create2(0, _2, sub(_3, _2), expr_18_salt)
     if iszero(expr_19_address) { revert_forward_1() }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the part where i was concerned about, for example, if there is a smart contract that relies on the entrypoint.getSenderAddress(), and do something afterwards that makes it non-view, then won't it be a easy honeypot to make end user to burn the eth gas? Yes it can be avoided by checking the gas limit and preview amount of gas spent but metamask does not show gas limit by default.

Maybe some easy prevention will be making it clear calling the entryPoint.getSenderAddress() in other contract is risky.

eg)

address factory;
...
function claimYourTokenToAA(bytes calldata initCode) external {
    address wallet = entryPoint.getSenderAddress(abi.encodePacked(factory,initCode)); // this will cost around 580k as you said
    token.transfer(wallet, 1e18);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using initCode in an application seems a bit contrived (considering it requires entire constructor parameters).
It is expected to be filled by wallet software.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still, I'm a bit hesitant about including this as-is, as it depends on gas-left (and probably a misinterpretation of the "geth" code of the spec - though this implementation became the new standard)

@@ -4,6 +4,7 @@ pragma solidity ^0.8.12;
import "@openzeppelin/contracts/utils/Create2.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";
import "./EIP4337Manager.sol";
import "hardhat/console.sol";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove as well

@drortirosh
Copy link
Contributor Author

postpone this PR, as it depends on non-deterministic behaviour of create2 when target contract exists (documented as "revert" but implemented as "waste all gas")

@drortirosh drortirosh closed this Oct 8, 2023
@drortirosh drortirosh deleted the SimpleAccount-factory-optim branch January 28, 2024 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants