From 5628e7f9a4bf7ad79fb49ca6ccbcef4c879dbfbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90or=C4=91e=20Mijovi=C4=87?= Date: Mon, 20 Oct 2025 11:52:12 +0200 Subject: [PATCH] Handling ssc cost via stakes. --- contracts/core/EntryPoint.sol | 88 ++++++++++++++++++++++------ contracts/core/StakeManager.sol | 16 +++++ contracts/interfaces/IEntryPoint.sol | 2 +- contracts/interfaces/ISscOpcodes.sol | 10 ++++ 4 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 contracts/interfaces/ISscOpcodes.sol diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 4bc9e1db0..e85f7d05a 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -7,6 +7,7 @@ import "../interfaces/IAccount.sol"; import "../interfaces/IAccountExecute.sol"; import "../interfaces/IPaymaster.sol"; import "../interfaces/IEntryPoint.sol"; +import "../interfaces/ISscOpcodes.sol"; import "../utils/Exec.sol"; import "./StakeManager.sol"; @@ -28,8 +29,20 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, using UserOperationLib for PackedUserOperation; + int256 constant public SSC_STORAGE_SLOT_COST = 12800000000000000; + int256 constant public SSC_STORAGE_SLOT_REFUND = 12672000000000000; + + int256 constant public SSC_ACCOUNT_COST = 12800000000000000; + int256 constant public SSC_ACCOUNT_REFUND = 12672000000000000; + + int256 constant public SSC_CODE_CREATED_COST = 12800000000000000; + + uint256 constant public SSC_MAX_SPEND_FACTOR = 10; + SenderCreator private immutable _senderCreator = new SenderCreator(); + ISscOpcodes private immutable _ssc = ISscOpcodes(address(0x665e930982A9a03c844641d453a2C3462ED7Ff41)); + function senderCreator() internal view virtual returns (SenderCreator) { return _senderCreator; } @@ -174,10 +187,12 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, function handleOps( PackedUserOperation[] calldata ops, address payable beneficiary - ) public nonReentrant { + ) public nonReentrant payable { uint256 opslen = ops.length; UserOpInfo[] memory opInfos = new UserOpInfo[](opslen); + int256 preSSC = _netSSC(); + unchecked { for (uint256 i = 0; i < opslen; i++) { UserOpInfo memory opInfo = opInfos[i]; @@ -200,7 +215,12 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, collected += _executeUserOp(i, ops[i], opInfos[i]); } - _compensate(beneficiary, collected); + // this can be negative in cases of refunds + int256 netSSC = _netSSC() - preSSC; + // In case of negative netSSC msg.value must be higher than abs(netSSC) + uint256 sscCompensation = uint256(int256(msg.value) + netSSC); + + _compensate(beneficiary, collected + sscCompensation); } } @@ -303,23 +323,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, uint256 preOpGas; } - /** - * Inner function to handle a UserOperation. - * Must be declared "external" to open a call context, but it can only be called by handleOps. - * @param callData - The callData to execute. - * @param opInfo - The UserOpInfo struct. - * @param context - The context bytes. - * @return actualGasCost - the actual cost in eth this UserOperation paid for gas - */ - function innerHandleOp( - bytes memory callData, - UserOpInfo memory opInfo, - bytes calldata context - ) external returns (uint256 actualGasCost) { - uint256 preGas = gasleft(); + function innerExecuteCall(MemoryUserOp memory mUserOp, bytes memory callData) external { require(msg.sender == address(this), "AA92 internal call only"); - MemoryUserOp memory mUserOp = opInfo.mUserOp; + int256 preSSC = _netSSC(); uint256 callGasLimit = mUserOp.callGasLimit; unchecked { // handleOps was called with gas limit too low. abort entire bundle. @@ -335,12 +342,46 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, } } } + if (callData.length > 0) { + bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit); + if (!success) { + require(false, string(Exec.getReturnData(REVERT_REASON_MAX_LEN))); + } else { + int256 netSSC = _netSSC() - preSSC; + if (netSSC < 0) { + // increment deposit for the user's account + _incrementDeposit(mUserOp.sender, uint256(-netSSC)); + } else { + require(uint256(netSSC) <= SSC_MAX_SPEND_FACTOR * callGasLimit * getUserOpGasPrice(mUserOp), "AA97 ssc max cost violation"); + _decrementDeposit(mUserOp.sender, uint256(netSSC)); + } + } + } + } + + /** + * Inner function to handle a UserOperation. + * Must be declared "external" to open a call context, but it can only be called by handleOps. + * @param callData - The callData to execute. + * @param opInfo - The UserOpInfo struct. + * @param context - The context bytes. + * @return actualGasCost - the actual cost in eth this UserOperation paid for gas + */ + function innerHandleOp( + bytes memory callData, + UserOpInfo memory opInfo, + bytes calldata context + ) external returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + require(msg.sender == address(this), "AA92 internal call only"); + MemoryUserOp memory mUserOp = opInfo.mUserOp; IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded; if (callData.length > 0) { - bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit); + (bool success, bytes memory result) = address(this).call( + abi.encodeWithSelector(this.innerExecuteCall.selector, mUserOp, callData) + ); if (!success) { - bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); if (result.length > 0) { emit UserOperationRevertReason( opInfo.userOpHash, @@ -413,6 +454,15 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, } } + /** + * Calculates total ssc cost / refund + */ + function _netSSC() internal view returns (int256) { + return int256(_ssc.accountsCreated()) * SSC_ACCOUNT_COST - int256(_ssc.accountsCleared()) * SSC_ACCOUNT_REFUND + + int256(_ssc.slotsCreated()) * SSC_STORAGE_SLOT_COST - int256(_ssc.slotsCleared()) * SSC_STORAGE_SLOT_REFUND + + int256(_ssc.codeCreated()) * SSC_CODE_CREATED_COST; + } + /** * Create sender smart contract account if init code is provided. * @param opIndex - The operation index. diff --git a/contracts/core/StakeManager.sol b/contracts/core/StakeManager.sol index f90210b7e..fea499eff 100644 --- a/contracts/core/StakeManager.sol +++ b/contracts/core/StakeManager.sol @@ -56,6 +56,22 @@ abstract contract StakeManager is IStakeManager { return newAmount; } + /** + * Increments an account's deposit. + * @param account - The account to increment. + * @param amount - The amount to increment by. + * @return the updated deposit of this account + */ + function _decrementDeposit(address account, uint256 amount) internal returns (uint256) { + unchecked { + DepositInfo storage info = deposits[account]; + require(info.deposit >= amount, "cannot decrement stake"); + uint256 newAmount = info.deposit - amount; + info.deposit = newAmount; + return newAmount; + } + } + /** * Add to the deposit of the given account. * @param account - The account to add to. diff --git a/contracts/interfaces/IEntryPoint.sol b/contracts/interfaces/IEntryPoint.sol index 28c26f98e..0395ac5ee 100644 --- a/contracts/interfaces/IEntryPoint.sol +++ b/contracts/interfaces/IEntryPoint.sol @@ -154,7 +154,7 @@ interface IEntryPoint is IStakeManager, INonceManager { function handleOps( PackedUserOperation[] calldata ops, address payable beneficiary - ) external; + ) external payable; /** * Execute a batch of UserOperation with Aggregators diff --git a/contracts/interfaces/ISscOpcodes.sol b/contracts/interfaces/ISscOpcodes.sol new file mode 100644 index 000000000..fff544cf1 --- /dev/null +++ b/contracts/interfaces/ISscOpcodes.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +interface ISscOpcodes { + function accountsCreated() external view returns (uint256); // D0 + function accountsCleared() external view returns (uint256); // D1 + function slotsCreated() external view returns (uint256); // D2 + function slotsCleared() external view returns (uint256); // D3 + function codeCreated() external view returns (uint256); // D4 +} \ No newline at end of file