-
Notifications
You must be signed in to change notification settings - Fork 670
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
AA-216 Proposal: Packing UserOperation and paymaster gas limits #363
Changes from all commits
3181436
1e2fe50
bef7e4d
41bad54
f27b2d6
287fe60
b2a9e61
46fb3ba
7494872
0cc1d67
6ffd042
56a5710
2e3ceff
d95187b
415ef69
ffdb104
7b6949a
47a57fa
a54d10b
08ef38e
052a8ad
87a8eac
277c379
effa64f
8ff7490
24cf39e
b9504f4
6252387
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,7 +25,7 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; | |
*/ | ||
contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, OpenZeppelin.ERC165 { | ||
|
||
using UserOperationLib for UserOperation; | ||
using UserOperationLib for PackedUserOperation; | ||
|
||
SenderCreator private senderCreator = new SenderCreator(); | ||
|
||
|
@@ -71,7 +71,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
*/ | ||
function _executeUserOp( | ||
uint256 opIndex, | ||
UserOperation calldata userOp, | ||
PackedUserOperation calldata userOp, | ||
UserOpInfo memory opInfo | ||
) | ||
internal | ||
|
@@ -141,7 +141,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
|
||
/// @inheritdoc IEntryPoint | ||
function handleOps( | ||
UserOperation[] calldata ops, | ||
PackedUserOperation[] calldata ops, | ||
address payable beneficiary | ||
) public nonReentrant { | ||
uint256 opslen = ops.length; | ||
|
@@ -183,7 +183,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
uint256 totalOps = 0; | ||
for (uint256 i = 0; i < opasLen; i++) { | ||
UserOpsPerAggregator calldata opa = opsPerAggregator[i]; | ||
UserOperation[] calldata ops = opa.userOps; | ||
PackedUserOperation[] calldata ops = opa.userOps; | ||
IAggregator aggregator = opa.aggregator; | ||
|
||
//address(1) is special marker of "signature error" | ||
|
@@ -207,7 +207,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
uint256 opIndex = 0; | ||
for (uint256 a = 0; a < opasLen; a++) { | ||
UserOpsPerAggregator calldata opa = opsPerAggregator[a]; | ||
UserOperation[] calldata ops = opa.userOps; | ||
PackedUserOperation[] calldata ops = opa.userOps; | ||
IAggregator aggregator = opa.aggregator; | ||
|
||
uint256 opslen = ops.length; | ||
|
@@ -234,7 +234,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
for (uint256 a = 0; a < opasLen; a++) { | ||
UserOpsPerAggregator calldata opa = opsPerAggregator[a]; | ||
emit SignatureAggregatorChanged(address(opa.aggregator)); | ||
UserOperation[] calldata ops = opa.userOps; | ||
PackedUserOperation[] calldata ops = opa.userOps; | ||
uint256 opslen = ops.length; | ||
|
||
for (uint256 i = 0; i < opslen; i++) { | ||
|
@@ -254,8 +254,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
struct MemoryUserOp { | ||
address sender; | ||
uint256 nonce; | ||
uint256 callGasLimit; | ||
uint256 verificationGasLimit; | ||
uint128 verificationGasLimit; | ||
uint128 callGasLimit; | ||
uint128 paymasterVerificationGasLimit; | ||
uint128 paymasterPostOpGasLimit; | ||
uint256 preVerificationGas; | ||
address paymaster; | ||
uint256 maxFeePerGas; | ||
|
@@ -290,7 +292,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
unchecked { | ||
// handleOps was called with gas limit too low. abort entire bundle. | ||
if ( | ||
gasleft() < callGasLimit + mUserOp.verificationGasLimit + 5000 | ||
gasleft() < | ||
callGasLimit + | ||
mUserOp.paymasterPostOpGasLimit + | ||
5000 | ||
) { | ||
assembly { | ||
mstore(0, INNER_OUT_OF_GAS) | ||
|
@@ -325,7 +330,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
|
||
/// @inheritdoc IEntryPoint | ||
function getUserOpHash( | ||
UserOperation calldata userOp | ||
PackedUserOperation calldata userOp | ||
) public view returns (bytes32) { | ||
return | ||
keccak256(abi.encode(userOp.hash(), address(this), block.chainid)); | ||
|
@@ -337,25 +342,26 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
* @param mUserOp - The memory user operation. | ||
*/ | ||
function _copyUserOpToMemory( | ||
UserOperation calldata userOp, | ||
PackedUserOperation calldata userOp, | ||
MemoryUserOp memory mUserOp | ||
) internal pure { | ||
mUserOp.sender = userOp.sender; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not have the entire There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This copy is an internal optimization of EntryPoint. we might even remove copying some fields if we find they are not needed (e.g. used exactly once) |
||
mUserOp.nonce = userOp.nonce; | ||
mUserOp.callGasLimit = userOp.callGasLimit; | ||
mUserOp.verificationGasLimit = userOp.verificationGasLimit; | ||
(mUserOp.verificationGasLimit, mUserOp.callGasLimit) = UserOperationLib.unpackAccountGasLimits(userOp.accountGasLimits); | ||
mUserOp.preVerificationGas = userOp.preVerificationGas; | ||
mUserOp.maxFeePerGas = userOp.maxFeePerGas; | ||
mUserOp.maxPriorityFeePerGas = userOp.maxPriorityFeePerGas; | ||
bytes calldata paymasterAndData = userOp.paymasterAndData; | ||
if (paymasterAndData.length > 0) { | ||
require( | ||
paymasterAndData.length >= 20, | ||
paymasterAndData.length >= UserOperationLib.PAYMASTER_DATA_OFFSET, | ||
"AA93 invalid paymasterAndData" | ||
); | ||
mUserOp.paymaster = address(bytes20(paymasterAndData[:20])); | ||
(mUserOp.paymaster, mUserOp.paymasterVerificationGasLimit, mUserOp.paymasterPostOpGasLimit) = UserOperationLib.unpackPaymasterStaticFields(paymasterAndData); | ||
} else { | ||
mUserOp.paymaster = address(0); | ||
mUserOp.paymasterVerificationGasLimit = 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Solidity structs are pre-initialized to all-zeros. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Except when one does crazy stuff like recycling memory by using the free-pointer back - which we do :) So we can't assume zeros anywhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we can assume zeros There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The paymaster initialization to address(0) was there, I only added the other fields. Do you want to remove all of them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Interesting, I wonder why they do that. Solidity always allocates new memory (containing zeros) so why do they need to explicitly write zeros? (We do, due to our asm code, but normal solidity doesn't) |
||
mUserOp.paymasterPostOpGasLimit = 0; | ||
} | ||
} | ||
|
||
|
@@ -367,12 +373,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
MemoryUserOp memory mUserOp | ||
) internal pure returns (uint256 requiredPrefund) { | ||
unchecked { | ||
// When using a Paymaster, the verificationGasLimit is used also to as a limit for the postOp call. | ||
// Our security model might call postOp eventually twice. | ||
uint256 mul = mUserOp.paymaster != address(0) ? 2 : 1; | ||
uint256 requiredGas = mUserOp.callGasLimit + | ||
mUserOp.verificationGasLimit * | ||
mul + | ||
uint256 requiredGas = mUserOp.verificationGasLimit + | ||
mUserOp.callGasLimit + | ||
mUserOp.paymasterVerificationGasLimit + | ||
mUserOp.paymasterPostOpGasLimit + | ||
mUserOp.preVerificationGas; | ||
|
||
requiredPrefund = requiredGas * mUserOp.maxFeePerGas; | ||
|
@@ -430,18 +434,16 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
*/ | ||
function _validateAccountPrepayment( | ||
uint256 opIndex, | ||
UserOperation calldata op, | ||
PackedUserOperation calldata op, | ||
UserOpInfo memory opInfo, | ||
uint256 requiredPrefund | ||
) | ||
internal | ||
returns ( | ||
uint256 gasUsedByValidateAccountPrepayment, | ||
uint256 validationData | ||
) | ||
{ | ||
unchecked { | ||
uint256 preGas = gasleft(); | ||
MemoryUserOp memory mUserOp = opInfo.mUserOp; | ||
address sender = mUserOp.sender; | ||
_createSenderIfNeeded(opIndex, opInfo, op.initCode); | ||
|
@@ -470,7 +472,6 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
} | ||
senderInfo.deposit = deposit - requiredPrefund; | ||
} | ||
gasUsedByValidateAccountPrepayment = preGas - gasleft(); | ||
} | ||
} | ||
|
||
|
@@ -484,25 +485,15 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
* @param op - The user operation. | ||
* @param opInfo - The operation info. | ||
* @param requiredPreFund - The required prefund amount. | ||
* @param gasUsedByValidateAccountPrepayment - The gas used by _validateAccountPrepayment. | ||
*/ | ||
function _validatePaymasterPrepayment( | ||
uint256 opIndex, | ||
UserOperation calldata op, | ||
PackedUserOperation calldata op, | ||
UserOpInfo memory opInfo, | ||
uint256 requiredPreFund, | ||
uint256 gasUsedByValidateAccountPrepayment | ||
uint256 requiredPreFund | ||
) internal returns (bytes memory context, uint256 validationData) { | ||
unchecked { | ||
MemoryUserOp memory mUserOp = opInfo.mUserOp; | ||
uint256 verificationGasLimit = mUserOp.verificationGasLimit; | ||
require( | ||
verificationGasLimit > gasUsedByValidateAccountPrepayment, | ||
"AA41 too little verificationGas" | ||
); | ||
uint256 gas = verificationGasLimit - | ||
gasUsedByValidateAccountPrepayment; | ||
|
||
address paymaster = mUserOp.paymaster; | ||
DepositInfo storage paymasterInfo = deposits[paymaster]; | ||
uint256 deposit = paymasterInfo.deposit; | ||
|
@@ -511,7 +502,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
} | ||
paymasterInfo.deposit = deposit - requiredPreFund; | ||
try | ||
IPaymaster(paymaster).validatePaymasterUserOp{gas: gas}( | ||
IPaymaster(paymaster).validatePaymasterUserOp{gas: mUserOp.paymasterVerificationGasLimit}( | ||
shahafn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
op, | ||
opInfo.userOpHash, | ||
requiredPreFund | ||
|
@@ -586,7 +577,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
*/ | ||
function _validatePrepayment( | ||
uint256 opIndex, | ||
UserOperation calldata userOp, | ||
PackedUserOperation calldata userOp, | ||
UserOpInfo memory outOpInfo | ||
) | ||
internal | ||
|
@@ -602,16 +593,14 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
uint256 maxGasValues = mUserOp.preVerificationGas | | ||
mUserOp.verificationGasLimit | | ||
mUserOp.callGasLimit | | ||
mUserOp.paymasterVerificationGasLimit | | ||
mUserOp.paymasterPostOpGasLimit | | ||
userOp.maxFeePerGas | | ||
userOp.maxPriorityFeePerGas; | ||
require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); | ||
|
||
uint256 gasUsedByValidateAccountPrepayment; | ||
uint256 requiredPreFund = _getRequiredPrefund(mUserOp); | ||
( | ||
gasUsedByValidateAccountPrepayment, | ||
validationData | ||
) = _validateAccountPrepayment( | ||
validationData = _validateAccountPrepayment( | ||
opIndex, | ||
userOp, | ||
outOpInfo, | ||
|
@@ -628,14 +617,13 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
opIndex, | ||
userOp, | ||
outOpInfo, | ||
requiredPreFund, | ||
gasUsedByValidateAccountPrepayment | ||
requiredPreFund | ||
); | ||
} | ||
unchecked { | ||
uint256 gasUsed = preGas - gasleft(); | ||
|
||
if (userOp.verificationGasLimit < gasUsed) { | ||
if (mUserOp.verificationGasLimit + mUserOp.paymasterVerificationGasLimit < gasUsed) { | ||
revert FailedOp(opIndex, "AA40 over verificationGasLimit"); | ||
} | ||
outOpInfo.prefund = requiredPreFund; | ||
|
@@ -676,7 +664,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
actualGasCost = actualGas * gasPrice; | ||
if (mode != IPaymaster.PostOpMode.postOpReverted) { | ||
try IPaymaster(paymaster).postOp{ | ||
gas: mUserOp.verificationGasLimit | ||
gas: mUserOp.paymasterPostOpGasLimit | ||
}(mode, context, actualGasCost) | ||
// solhint-disable-next-line no-empty-blocks | ||
{} catch { | ||
shahafn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
@@ -693,7 +681,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |
uint256 executionGasLimit = mUserOp.callGasLimit; | ||
// Note that 'verificationGasLimit' here is the limit given to the 'postOp' which is part of execution | ||
if (context.length > 0){ | ||
executionGasLimit += mUserOp.verificationGasLimit; | ||
executionGasLimit += mUserOp.paymasterPostOpGasLimit; | ||
} | ||
uint256 executionGasUsed = actualGas - opInfo.preOpGas; | ||
// this check is required for the gas used within EntryPoint and not covered by explicit gas limits | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point we already have a
context
, so if it is empty thepostOp
will not be called. I think it may make sense to allow the transaction to go on in this case, instead of always addingpaymasterPostOpGasLimit
to the required gas left?Say, the UserOp have requested 1'000'000 gas for a
postOp
but it does not need it any more, why should this check fail?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why should we (entrypoint) waste gas to check it? it is the paymaster's job to put a valid postOpGasLimit, even if context is empty.
There is another edge case:
validatePaymasterUserOp() returns a
context, and the
postOpGasLimit` is zero: in this case, we still execute the callData and revert the postOp (on OOG) - again, because it is not the EntryPoint's job to validate the userop fields.