Skip to content

Commit ebf05c1

Browse files
committed
comprehensive accounting tests
1 parent f5acd1a commit ebf05c1

5 files changed

+150
-51
lines changed

contracts/interfaces/IBiconomySponsorshipPaymaster.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pragma solidity ^0.8.26;
33

44
interface IBiconomySponsorshipPaymaster {
5-
event PostopCostChanged(uint256 indexed oldValue, uint256 indexed newValue);
5+
event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue);
66
event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue);
77

88
event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor);

contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol

+18-13
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,21 @@ contract BiconomySponsorshipPaymaster is
5151
address _owner,
5252
IEntryPoint _entryPoint,
5353
address _verifyingSigner,
54-
address _feeCollector
54+
address _feeCollector,
55+
uint48 _unaccountedGas
5556
)
5657
BasePaymaster(_owner, _entryPoint)
5758
{
5859
if (_verifyingSigner == address(0)) {
5960
revert VerifyingSignerCanNotBeZero();
6061
} else if (_feeCollector == address(0)) {
6162
revert FeeCollectorCanNotBeZero();
63+
} else if (_unaccountedGas > 200_000) {
64+
revert UnaccountedGasTooHigh();
6265
}
6366
verifyingSigner = _verifyingSigner;
6467
feeCollector = _feeCollector;
68+
unaccountedGas = _unaccountedGas;
6569
}
6670

6771
receive() external payable {
@@ -119,13 +123,13 @@ contract BiconomySponsorshipPaymaster is
119123
* @param value The new value to be set as the unaccountedEPGasOverhead.
120124
* @notice only to be called by the owner of the contract.
121125
*/
122-
function setPostopCost(uint48 value) external payable onlyOwner {
126+
function setUnaccountedGas(uint48 value) external payable onlyOwner {
123127
if (value > 200_000) {
124128
revert UnaccountedGasTooHigh();
125129
}
126130
uint256 oldValue = unaccountedGas;
127131
unaccountedGas = value;
128-
emit PostopCostChanged(oldValue, value);
132+
emit UnaccountedGasChanged(oldValue, value);
129133
}
130134

131135
/**
@@ -254,18 +258,21 @@ contract BiconomySponsorshipPaymaster is
254258
(address paymasterId, uint32 dynamicAdjustment, bytes32 userOpHash) =
255259
abi.decode(context, (address, uint32, bytes32));
256260

257-
uint256 totalGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas);
258-
uint256 adjustedGasCost = (totalGasCost * dynamicAdjustment) / PRICE_DENOMINATOR;
261+
// Include unaccountedGas since EP doesn't include this in actualGasCost
262+
// unaccountedGas = postOpGas + EP overhead gas + estimated penalty
263+
actualGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas);
264+
// Apply the dynamic adjustment
265+
uint256 adjustedGasCost = (actualGasCost * dynamicAdjustment) / PRICE_DENOMINATOR;
259266

260267
// Deduct the adjusted cost
261268
paymasterIdBalances[paymasterId] -= adjustedGasCost;
262269

263270
if (adjustedGasCost > actualGasCost) {
264-
// Add dynamicAdjustment to fee
265-
uint256 dynamicAdjustment = adjustedGasCost - actualGasCost;
266-
paymasterIdBalances[feeCollector] += dynamicAdjustment;
271+
// Apply dynamicAdjustment to fee collector balance
272+
uint256 premium = adjustedGasCost - actualGasCost;
273+
paymasterIdBalances[feeCollector] += premium;
267274
// Review if we should emit adjustedGasCost as well
268-
emit DynamicAdjustmentCollected(paymasterId, dynamicAdjustment);
275+
emit DynamicAdjustmentCollected(paymasterId, premium);
269276
}
270277

271278
emit GasBalanceDeducted(paymasterId, adjustedGasCost, userOpHash);
@@ -292,8 +299,8 @@ contract BiconomySponsorshipPaymaster is
292299
override
293300
returns (bytes memory context, uint256 validationData)
294301
{
295-
(address paymasterId, uint48 validUntil, uint48 validAfter, uint32 dynamicAdjustment, bytes calldata signature) =
296-
parsePaymasterAndData(userOp.paymasterAndData);
302+
(address paymasterId, uint48 validUntil, uint48 validAfter, uint32 dynamicAdjustment, bytes calldata signature)
303+
= parsePaymasterAndData(userOp.paymasterAndData);
297304
//ECDSA library supports both 64 and 65-byte long signatures.
298305
// we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and
299306
// not "ECDSA"
@@ -325,8 +332,6 @@ contract BiconomySponsorshipPaymaster is
325332

326333
context = abi.encode(paymasterId, dynamicAdjustment, userOpHash);
327334

328-
context = abi.encode(paymasterId, dynamicAdjustment, userOpHash);
329-
330335
//no need for other on-chain validation: entire UserOp should have been checked
331336
// by the external service prior to signing it.
332337
return (context, _packValidationData(false, validUntil, validAfter));

test/foundry/base/TestBase.sol

+53-14
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import "solady/src/utils/ECDSA.sol";
88

99
import { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol";
1010
import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol";
11+
import { IAccount } from "account-abstraction/contracts/interfaces/IAccount.sol";
12+
import { Exec } from "account-abstraction/contracts/utils/Exec.sol";
1113
import { IPaymaster } from "account-abstraction/contracts/interfaces/IPaymaster.sol";
1214
import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol";
1315

@@ -20,7 +22,8 @@ import { Bootstrap, BootstrapConfig } from "nexus/contracts/utils/Bootstrap.sol"
2022
import { CheatCodes } from "nexus/test/foundry/utils/CheatCodes.sol";
2123
import { BaseEventsAndErrors } from "./BaseEventsAndErrors.sol";
2224

23-
import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol";
25+
import { BiconomySponsorshipPaymaster } from
26+
"../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol";
2427

2528
abstract contract TestBase is CheatCodes, BaseEventsAndErrors {
2629
// -----------------------------------------
@@ -59,9 +62,12 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors {
5962
BiconomyMetaFactory internal META_FACTORY;
6063
MockValidator internal VALIDATOR_MODULE;
6164
Nexus internal ACCOUNT_IMPLEMENTATION;
62-
6365
Bootstrap internal BOOTSTRAPPER;
6466

67+
// Used to buffer user op gas limits
68+
// GAS_LIMIT = (ESTIMATED_GAS * GAS_BUFFER_RATIO) / 100
69+
uint8 private constant GAS_BUFFER_RATIO = 110;
70+
6571
// -----------------------------------------
6672
// Modifiers
6773
// -----------------------------------------
@@ -332,23 +338,50 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors {
332338
assertTrue(res, "Pre-funding account should succeed");
333339
}
334340

341+
function estimateUserOpGasCosts(PackedUserOperation memory userOp)
342+
internal
343+
prankModifier(ENTRYPOINT_ADDRESS)
344+
returns (uint256 verificationGasUsed, uint256 callGasUsed, uint256 verificationGasLimit, uint256 callGasLimit)
345+
{
346+
bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp);
347+
verificationGasUsed = gasleft();
348+
IAccount(userOp.sender).validateUserOp(userOp, userOpHash, 0);
349+
verificationGasUsed = verificationGasUsed - gasleft();
350+
351+
callGasUsed = gasleft();
352+
bool success = Exec.call(userOp.sender, 0, userOp.callData, 3e6);
353+
callGasUsed = callGasUsed - gasleft();
354+
assert(success);
355+
356+
verificationGasLimit = (verificationGasUsed * GAS_BUFFER_RATIO) / 100;
357+
callGasLimit = (callGasUsed * GAS_BUFFER_RATIO) / 100;
358+
}
359+
335360
function estimatePaymasterGasCosts(
336361
BiconomySponsorshipPaymaster paymaster,
337362
PackedUserOperation memory userOp,
338-
bytes32 userOpHash,
339363
uint256 requiredPreFund
340364
)
341365
internal
342366
prankModifier(ENTRYPOINT_ADDRESS)
343-
returns (uint256 validationGasLimit, uint256 postopGasLimit)
367+
returns (uint256 validationGasUsed, uint256 postopGasUsed, uint256 validationGasLimit, uint256 postopGasLimit)
344368
{
345-
validationGasLimit = gasleft();
369+
bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp);
370+
// Warm up accounts to get more accurate gas estimations
346371
(bytes memory context,) = paymaster.validatePaymasterUserOp(userOp, userOpHash, requiredPreFund);
347-
validationGasLimit = validationGasLimit - gasleft();
372+
paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 1e12, 3e6);
373+
374+
// Estimate gas used
375+
validationGasUsed = gasleft();
376+
(context,) = paymaster.validatePaymasterUserOp(userOp, userOpHash, requiredPreFund);
377+
validationGasUsed = validationGasUsed - gasleft();
348378

349-
postopGasLimit = gasleft();
379+
postopGasUsed = gasleft();
350380
paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 1e12, 3e6);
351-
postopGasLimit = postopGasLimit - gasleft();
381+
postopGasUsed = (postopGasUsed - gasleft());
382+
383+
validationGasLimit = (validationGasUsed * GAS_BUFFER_RATIO) / 100;
384+
postopGasLimit = (postopGasUsed * GAS_BUFFER_RATIO) / 100;
352385
}
353386

354387
function createUserOp(
@@ -359,24 +392,30 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors {
359392
internal
360393
returns (PackedUserOperation memory userOp, bytes32 userOpHash)
361394
{
362-
// Create userOp with no paymaster gas estimates
395+
// Create userOp with no gas estimates
363396
uint48 validUntil = uint48(block.timestamp + 1 days);
364397
uint48 validAfter = uint48(block.timestamp);
365398

366399
userOp = buildUserOpWithCalldata(sender, "", address(VALIDATOR_MODULE));
367400

368-
(userOp.paymasterAndData, ) = generateAndSignPaymasterData(
401+
(userOp.paymasterAndData,) = generateAndSignPaymasterData(
369402
userOp, PAYMASTER_SIGNER, paymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, dynamicAdjustment
370403
);
371404
userOp.signature = signUserOp(sender, userOp);
372405

406+
(,, uint256 verificationGasLimit, uint256 callGasLimit) = estimateUserOpGasCosts(userOp);
373407
// Estimate paymaster gas limits
374-
userOpHash = ENTRYPOINT.getUserOpHash(userOp);
375-
(uint256 validationGasLimit, uint256 postopGasLimit) =
376-
estimatePaymasterGasCosts(paymaster, userOp, userOpHash, 5e4);
408+
(, uint256 postopGasUsed, uint256 validationGasLimit, uint256 postopGasLimit) =
409+
estimatePaymasterGasCosts(paymaster, userOp, 5e4);
410+
411+
vm.startPrank(paymaster.owner());
412+
// Set unaccounted gas to be gas used in postop + 1000 for EP overhead and penalty
413+
paymaster.setUnaccountedGas(uint48(postopGasUsed + 1000));
414+
vm.stopPrank();
377415

378416
// Ammend the userop to have new gas limits and signature
379-
(userOp.paymasterAndData, ) = generateAndSignPaymasterData(
417+
userOp.accountGasLimits = bytes32(abi.encodePacked(uint128(verificationGasLimit), uint128(callGasLimit)));
418+
(userOp.paymasterAndData,) = generateAndSignPaymasterData(
380419
userOp,
381420
PAYMASTER_SIGNER,
382421
paymaster,

0 commit comments

Comments
 (0)