-
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
Conversation
- Adding packed paymasterGasLimits for validatePaymasterUserOp and postOp - Packing gas limits of validateUserOp and call gas limits into accountGasLimits
@@ -163,7 +163,7 @@ contract EIP4337Manager is IAccount, GnosisSafeStorage, Executor { | |||
sig[2] = bytes1(uint8(1)); | |||
sig[35] = bytes1(uint8(1)); | |||
uint256 nonce = uint256(IEntryPoint(manager.entryPoint()).getNonce(address(safe), 0)); | |||
UserOperation memory userOp = UserOperation(address(safe), nonce, "", "", 0, 1000000, 0, 0, 0, "", sig); | |||
UserOperation memory userOp = UserOperation(address(safe), nonce, "", "", bytes32(bytes3(0x0f4240)), 0, 0, 0, "", sig); |
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 is this needed?
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.
What is "this" that you're asking about? bytes32(bytes3(0x0f4240))
is there since it's the account gas limits that are now packed.
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.
should use packGasLimit(callGasLimit, verificationGasLimit)
same func can be used for pmgas and for acc.gas
return (uint128(bytes16(accountGasLimits)), uint128(uint256(accountGasLimits))); | ||
} | ||
|
||
function unpackPaymasterStaticFields( |
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.
I don't think we ever decode the paymaster itself, only paymasterGaslimits
(the reason is that this method is expected to be used from within validatePaymasterUserOp itself)
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.
We do decode it..
contracts/samples/TokenPaymaster.sol
Outdated
@@ -123,15 +123,15 @@ contract TokenPaymaster is BasePaymaster, UniswapHelper, OracleHelper { | |||
override | |||
returns (bytes memory context, uint256 validationResult) {unchecked { | |||
uint256 priceMarkup = tokenPaymasterConfig.priceMarkup; | |||
uint256 paymasterAndDataLength = userOp.paymasterAndData.length - 20; | |||
uint256 paymasterAndDataLength = userOp.paymasterAndData.length - 52; |
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.
we need to define these as constants, in UserOpLib. its not good to have so many explicit numerics spread in our code.
e.g.
PAYMASTER_GASLIMIT_OFFSET (offset of (paymasterGasLimit,paymasterPostOpGasLimit))
PAYMASTER_DATA_OFFSET (offset of "rest of paymaster data")
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.
Done
userOp.callGasLimit, | ||
userOp.verificationGasLimit, | ||
userOp.accountGasLimits, | ||
uint256(bytes32(userOp.paymasterAndData[20:52])), |
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.
afaik, bytes32 is enough (bytes32(userOp.paymasterAndData[PAYMASTER_GASLIMT_OFFSET])
)
contracts/core/EntryPoint.sol
Outdated
@@ -325,8 +329,12 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |||
"AA93 invalid paymasterAndData" | |||
); | |||
mUserOp.paymaster = address(bytes20(paymasterAndData[:20])); | |||
mUserOp.paymasterVerificationGasLimit = uint128(bytes16(paymasterAndData[20:36])); |
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.
- use decode helper
- need to check if need all those in mUserOp
} else { | ||
mUserOp.paymaster = address(0); | ||
mUserOp.paymasterVerificationGasLimit = 0; |
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.
Solidity structs are pre-initialized to all-zeros.
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we can assume zeros
all storage variables in solidity are initialized to zero - either "stack" or memory variables (structs and arrays).
(there are Yul injected functions "allocate_and_zero_memory..." for that)
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we can assume zeros all storage variables in solidity are initialized to zero - either "stack" or memory variables (structs and arrays). (there are Yul injected functions "allocate_and_zero_memory..." for that)
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)
sender: account.address, | ||
paymasterAndData: paymaster.address, | ||
paymaster: paymaster.address, | ||
paymasterPostOpGasLimit: 3e5, |
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.
just like other fields, fillAndSign should have default value for postOpGasLimit.
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.
I don't think that postOp should have a default non-zero value
@@ -313,7 +312,7 @@ describe('TokenPaymaster', function () { | |||
}) | |||
|
|||
const preChargeTokens = decodedLogs[0].args.value | |||
const requiredGas = BigNumber.from(op.callGasLimit).add(BigNumber.from(op.verificationGasLimit).mul(2)).add(op.preVerificationGas).add(40000 /* REFUND_POSTOP_COST */) | |||
const requiredGas = BigNumber.from(op.callGasLimit).add(BigNumber.from(op.verificationGasLimit).add(BigNumber.from(op.paymasterVerificationGasLimit))).add(BigNumber.from(op.paymasterPostOpGasLimit)).add(op.preVerificationGas).add(40000 /* REFUND_POSTOP_COST */) |
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.
we're missing
bnSum = function(){ return [...arguments].reduce((sum,x)=>bn.from(x).add(sum)) }
and then
bnSum(op.callGasLimit, op.paymasterVerificationGasLimit, op.paymasterPostOpGasLimit, 40000)
@@ -275,14 +276,18 @@ describe('EntryPoint', function () { | |||
// we need maxFeeperGas > block.basefee + maxPriorityFeePerGas so requiredPrefund onchain is basefee + maxPriorityFeePerGas | |||
maxFeePerGas: block.baseFeePerGas.mul(3), |
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.
actually, this field looks anachronism: why doesn't it use DefaultUserOp or fillUserOp ?
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.
I didn't touch this code at all
test/verifying_paymaster.test.ts
Outdated
@@ -43,9 +43,17 @@ describe('EntryPoint with VerifyingPaymaster', function () { | |||
|
|||
describe('#parsePaymasterAndData', () => { | |||
it('should parse data properly', async () => { | |||
const paymasterAndData = hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), MOCK_SIG]) | |||
const paymasterAndData = hexConcat([ |
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.
missing helper packPaymasterAndData(paymasterr, pmGas, postOpGas, data?)
we don't want to mix "protocol" encoding (pm, gas), with "app" encoding (time-range)
gasleft() < callGasLimit + mUserOp.verificationGasLimit + 5000 | ||
gasleft() < | ||
callGasLimit + | ||
mUserOp.paymasterPostOpGasLimit + |
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 the postOp
will not be called. I think it may make sense to allow the transaction to go on in this case, instead of always adding paymasterPostOpGasLimit
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.
@@ -313,8 +318,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, | |||
) internal pure { | |||
mUserOp.sender = userOp.sender; |
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 not have the entire UserOperation
=> MemoryUserOp
conversion in the UserOperationLib
library? Construction of a MemoryUserOp
takes a lot of space and will call to the UserOperationLib
at least twice.
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.
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)
UserOperationLib is for general-use functions, that are expected to be used even outside the EntryPoint itself.
sender: testWarmColdAccount.address | ||
} | ||
const beneficiaryAddress = createAddress() | ||
const badOpPacked = packUserOp(badOp) |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
4a41d5e
to
ffdb104
Compare
ca96976
to
277c379
Compare
5426ba2
to
effa64f
Compare
This PR is the biggest change we propose for erc4337 v0.7
While the changes below are distinct, it was best to implement this as a single PR
The changes:
verificationGasLimit
field was used for three different purposes (account validation, paymaster validation, and paymaster postOp). We discovered it makes the paymaster vulnerable.As a result, we added two new gas fields:
paymasterValidationGasLimit
andpostOpGasLimit
This means that if no paymaster is in use, there is only a single “zeros” field, instead of four.
Changes to Account
Supporting multiple entrypoints is still possible:
validateUserOperation(PackedUserOperation)
is the new method that supports the new entrypoint.Changes to Paymasters
Paymaster’s paymasterAndData field processing now needs to account for both the 20 bytes of paymaster address and the 32 bytes of gas fields.
We added PAYMASTER_DATA_OFFSET constant to indicate the offset where paymaster-specific data starts