-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
811b335
commit 4212551
Showing
12 changed files
with
292 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.24; | ||
|
||
/* solhint-disable reason-string */ | ||
|
||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; | ||
import { IPaymaster } from "account-abstraction/contracts/interfaces/IPaymaster.sol"; | ||
import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; | ||
import "account-abstraction/contracts/core/UserOperationLib.sol"; | ||
/** | ||
* Helper class for creating a paymaster. | ||
* provides helper methods for staking. | ||
* Validates that the postOp is called only by the entryPoint. | ||
*/ | ||
abstract contract BasePaymaster is IPaymaster, Ownable { | ||
IEntryPoint public immutable entryPoint; | ||
|
||
uint256 internal constant PAYMASTER_VALIDATION_GAS_OFFSET = UserOperationLib.PAYMASTER_VALIDATION_GAS_OFFSET; | ||
uint256 internal constant PAYMASTER_POSTOP_GAS_OFFSET = UserOperationLib.PAYMASTER_POSTOP_GAS_OFFSET; | ||
uint256 internal constant PAYMASTER_DATA_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET; | ||
|
||
constructor(IEntryPoint _entryPoint) Ownable(msg.sender) { | ||
_validateEntryPointInterface(_entryPoint); | ||
entryPoint = _entryPoint; | ||
} | ||
|
||
//sanity check: make sure this EntryPoint was compiled against the same | ||
// IEntryPoint of this paymaster | ||
function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual { | ||
require(IERC165(address(_entryPoint)).supportsInterface(type(IEntryPoint).interfaceId), "IEntryPoint interface mismatch"); | ||
} | ||
|
||
/// @inheritdoc IPaymaster | ||
function validatePaymasterUserOp( | ||
PackedUserOperation calldata userOp, | ||
bytes32 userOpHash, | ||
uint256 maxCost | ||
) external override returns (bytes memory context, uint256 validationData) { | ||
_requireFromEntryPoint(); | ||
return _validatePaymasterUserOp(userOp, userOpHash, maxCost); | ||
} | ||
|
||
/** | ||
* Validate a user operation. | ||
* @param userOp - The user operation. | ||
* @param userOpHash - The hash of the user operation. | ||
* @param maxCost - The maximum cost of the user operation. | ||
*/ | ||
function _validatePaymasterUserOp( | ||
PackedUserOperation calldata userOp, | ||
bytes32 userOpHash, | ||
uint256 maxCost | ||
) internal virtual returns (bytes memory context, uint256 validationData); | ||
|
||
/// @inheritdoc IPaymaster | ||
function postOp( | ||
PostOpMode mode, | ||
bytes calldata context, | ||
uint256 actualGasCost, | ||
uint256 actualUserOpFeePerGas | ||
) external override { | ||
_requireFromEntryPoint(); | ||
_postOp(mode, context, actualGasCost, actualUserOpFeePerGas); | ||
} | ||
|
||
/** | ||
* Post-operation handler. | ||
* (verified to be called only through the entryPoint) | ||
* @dev If subclass returns a non-empty context from validatePaymasterUserOp, | ||
* it must also implement this method. | ||
* @param mode - Enum with the following options: | ||
* opSucceeded - User operation succeeded. | ||
* opReverted - User op reverted. The paymaster still has to pay for gas. | ||
* postOpReverted - never passed in a call to postOp(). | ||
* @param context - The context value returned by validatePaymasterUserOp | ||
* @param actualGasCost - Actual gas used so far (without this postOp call). | ||
* @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas | ||
* and maxPriorityFee (and basefee) | ||
* It is not the same as tx.gasprice, which is what the bundler pays. | ||
*/ | ||
function _postOp( | ||
PostOpMode mode, | ||
bytes calldata context, | ||
uint256 actualGasCost, | ||
uint256 actualUserOpFeePerGas | ||
) internal virtual { | ||
(mode, context, actualGasCost, actualUserOpFeePerGas); // unused params | ||
// subclass must override this method if validatePaymasterUserOp returns a context | ||
revert("must override"); | ||
} | ||
|
||
/** | ||
* Add a deposit for this paymaster, used for paying for transaction fees. | ||
*/ | ||
function deposit() public payable { | ||
entryPoint.depositTo{value: msg.value}(address(this)); | ||
} | ||
|
||
/** | ||
* Withdraw value from the deposit. | ||
* @param withdrawAddress - Target to send to. | ||
* @param amount - Amount to withdraw. | ||
*/ | ||
function withdrawTo( | ||
address payable withdrawAddress, | ||
uint256 amount | ||
) public onlyOwner { | ||
entryPoint.withdrawTo(withdrawAddress, amount); | ||
} | ||
|
||
/** | ||
* Add stake for this paymaster. | ||
* This method can also carry eth value to add to the current stake. | ||
* @param unstakeDelaySec - The unstake delay for this paymaster. Can only be increased. | ||
*/ | ||
function addStake(uint32 unstakeDelaySec) external payable onlyOwner { | ||
entryPoint.addStake{value: msg.value}(unstakeDelaySec); | ||
} | ||
|
||
/** | ||
* Return current paymaster's deposit on the entryPoint. | ||
*/ | ||
function getDeposit() public view returns (uint256) { | ||
return entryPoint.balanceOf(address(this)); | ||
} | ||
|
||
/** | ||
* Unlock the stake, in order to withdraw it. | ||
* The paymaster can't serve requests once unlocked, until it calls addStake again | ||
*/ | ||
function unlockStake() external onlyOwner { | ||
entryPoint.unlockStake(); | ||
} | ||
|
||
/** | ||
* Withdraw the entire paymaster's stake. | ||
* stake must be unlocked first (and then wait for the unstakeDelay to be over) | ||
* @param withdrawAddress - The address to send withdrawn value. | ||
*/ | ||
function withdrawStake(address payable withdrawAddress) external onlyOwner { | ||
entryPoint.withdrawStake(withdrawAddress); | ||
} | ||
|
||
/** | ||
* Validate the call is made from a valid entrypoint | ||
*/ | ||
function _requireFromEntryPoint() internal virtual { | ||
require(msg.sender == address(entryPoint), "Sender not EntryPoint"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.24; | ||
|
||
/* solhint-disable reason-string */ | ||
/* solhint-disable no-inline-assembly */ | ||
|
||
import "account-abstraction/contracts/core/BasePaymaster.sol"; | ||
import "account-abstraction/contracts/core/UserOperationLib.sol"; | ||
import "account-abstraction/contracts/core/Helpers.sol"; | ||
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | ||
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; | ||
|
||
/** | ||
* A sample paymaster that uses external service to decide whether to pay for the UserOp. | ||
* The paymaster trusts an external signer to sign the transaction. | ||
* The calling user must pass the UserOp to that external signer first, which performs | ||
* whatever off-chain verification before signing the UserOp. | ||
* Note that this signature is NOT a replacement for the account-specific signature: | ||
* - the paymaster checks a signature to agree to PAY for GAS. | ||
* - the account checks a signature to prove identity and account ownership. | ||
*/ | ||
contract VerifyingPaymaster is BasePaymaster { | ||
|
||
using UserOperationLib for PackedUserOperation; | ||
|
||
address public immutable verifyingSigner; | ||
|
||
uint256 private constant VALID_TIMESTAMP_OFFSET = PAYMASTER_DATA_OFFSET; | ||
|
||
uint256 private constant SIGNATURE_OFFSET = VALID_TIMESTAMP_OFFSET + 64; | ||
|
||
constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) { | ||
verifyingSigner = _verifyingSigner; | ||
} | ||
|
||
/** | ||
* return the hash we're going to sign off-chain (and validate on-chain) | ||
* this method is called by the off-chain service, to sign the request. | ||
* it is called on-chain from the validatePaymasterUserOp, to validate the signature. | ||
* note that this signature covers all fields of the UserOperation, except the "paymasterAndData", | ||
* which will carry the signature itself. | ||
*/ | ||
function getHash(PackedUserOperation calldata userOp, uint48 validUntil, uint48 validAfter) | ||
public view returns (bytes32) { | ||
//can't use userOp.hash(), since it contains also the paymasterAndData itself. | ||
address sender = userOp.getSender(); | ||
return | ||
keccak256( | ||
abi.encode( | ||
sender, | ||
userOp.nonce, | ||
keccak256(userOp.initCode), | ||
keccak256(userOp.callData), | ||
userOp.accountGasLimits, | ||
uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_DATA_OFFSET])), | ||
userOp.preVerificationGas, | ||
userOp.gasFees, | ||
block.chainid, | ||
address(this), | ||
validUntil, | ||
validAfter | ||
) | ||
); | ||
} | ||
|
||
/** | ||
* verify our external signer signed this request. | ||
* the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params | ||
* paymasterAndData[:20] : address(this) | ||
* paymasterAndData[20:84] : abi.encode(validUntil, validAfter) | ||
* paymasterAndData[84:] : signature | ||
*/ | ||
function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund) | ||
internal view override returns (bytes memory context, uint256 validationData) { | ||
(requiredPreFund); | ||
|
||
(uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData); | ||
//ECDSA library supports both 64 and 65-byte long signatures. | ||
// we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA" | ||
require(signature.length == 64 || signature.length == 65, "VerifyingPaymaster: invalid signature length in paymasterAndData"); | ||
bytes32 hash = MessageHashUtils.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter)); | ||
|
||
//don't revert on signature failure: return SIG_VALIDATION_FAILED | ||
if (verifyingSigner != ECDSA.recover(hash, signature)) { | ||
return ("", _packValidationData(true, validUntil, validAfter)); | ||
} | ||
|
||
//no need for other on-chain validation: entire UserOp should have been checked | ||
// by the external service prior to signing it. | ||
return ("", _packValidationData(false, validUntil, validAfter)); | ||
} | ||
|
||
function parsePaymasterAndData(bytes calldata paymasterAndData) public pure returns (uint48 validUntil, uint48 validAfter, bytes calldata signature) { | ||
(validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET :], (uint48, uint48)); | ||
signature = paymasterAndData[SIGNATURE_OFFSET :]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.24; | ||
|
||
/* solhint-disable reason-string */ | ||
|
||
import "../base/BasePaymaster.sol"; | ||
import "account-abstraction/contracts/core/UserOperationLib.sol"; | ||
import "account-abstraction/contracts/core/Helpers.sol"; | ||
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | ||
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; | ||
import { SignatureCheckerLib } from "solady/src/utils/SignatureCheckerLib.sol"; | ||
import { ECDSA as ECDSA_solady } from "solady/src/utils/ECDSA.sol"; |
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.24; | ||
|
||
contract Counter { | ||
uint256 private _number; | ||
|
||
function incrementNumber() public { | ||
_number++; | ||
} | ||
|
||
function decrementNumber() public { | ||
_number--; | ||
} | ||
|
||
function getNumber() public view returns (uint256) { | ||
return _number; | ||
} | ||
|
||
function revertOperation() public pure { | ||
revert("Counter: Revert operation"); | ||
} | ||
|
||
function test_() public pure { | ||
// This function is used to ignore file in coverage report | ||
} | ||
} |
Oops, something went wrong.