Skip to content
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

Add PaymasterNFT and PaymasterERC20 #75

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## XX-XX-XXXX

- `PaymasterNFT`: Extension of `PaymasterCore` that approves sponsoring of user operation based on ownership of an ERC-721 NFT.
- `PaymasterERC20`: Extension of `PaymasterCore` that sponsors user operations against payment in ERC-20 tokens.

## 31-01-2025

- `PaymasterCore`: Add a simple ERC-4337 paymaster implementation with minimal logic.
Expand Down
1 change: 0 additions & 1 deletion contracts/account/paymaster/PaymasterCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pragma solidity ^0.8.20;

import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {IEntryPoint, IPaymaster, PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";

/**
* @dev A simple ERC4337 paymaster implementation. This base implementation only includes the minimal logic to validate
Expand Down
101 changes: 101 additions & 0 deletions contracts/account/paymaster/PaymasterERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {PaymasterCore} from "./PaymasterCore.sol";
import {AbstractSigner} from "../../utils/cryptography/AbstractSigner.sol";

/**
* @dev Extension of {PaymasterCore} supports user paying gas with ERC-20 tokens.
*/
abstract contract PaymasterERC20 is PaymasterCore {
using ERC4337Utils for *;
using Math for *;
using SafeERC20 for IERC20;

event UserOperationSponsored(
bytes32 indexed userOpHash,
address indexed user,
address indexed guarantor,
uint256 tokenAmount,
uint256 tokenPrice,
bool paidByGuarantor
);

// Over-estimations: ERC-20 balances/allowances may be cold and contracts may not be optimized
uint256 private constant POST_OP_COST = 30_000;
uint256 private constant POST_OP_COST_WITH_GUARANTOR = 50_000;

/// @inheritdoc PaymasterCore
function _validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) internal virtual override returns (bytes memory context, uint256 validationData) {
(IERC20 token, uint48 validAfter, uint48 validUntil, uint256 tokenPrice, address guarantor) = _fetchDetails(
userOp.paymasterData()
);

uint256 prefundAmount = (maxCost +
(guarantor == address(0)).ternary(POST_OP_COST, POST_OP_COST_WITH_GUARANTOR) *
userOp.maxFeePerGas()).mulDiv(tokenPrice, _tokenPriceDenominator());

return (
abi.encodePacked(userOpHash, token, prefundAmount, tokenPrice, userOp.sender, guarantor),
token
.trySafeTransferFrom(guarantor == address(0) ? userOp.sender : guarantor, address(this), prefundAmount)
.packValidationData(validAfter, validUntil)
);
}

/// @inheritdoc PaymasterCore
function _postOp(
PostOpMode /* mode */,
bytes calldata context,
uint256 actualGasCost,
uint256 actualUserOpFeePerGas
) internal virtual override {
bytes32 userOpHash = bytes32(context[0x00:0x20]);
IERC20 token = IERC20(address(bytes20(context[0x20:0x34])));
uint256 prefundAmount = uint256(bytes32(context[0x34:0x54]));
uint256 tokenPrice = uint256(bytes32(context[0x54:0x74]));
address user = address(bytes20(context[0x74:0x88]));
address guarantor = address(bytes20(context[0x88:0x9C]));

uint256 actualAmount = (actualGasCost +
(guarantor == address(0)).ternary(POST_OP_COST, POST_OP_COST_WITH_GUARANTOR) *
actualUserOpFeePerGas).mulDiv(tokenPrice, _tokenPriceDenominator());

if (guarantor == address(0)) {
token.safeTransfer(user, prefundAmount - actualAmount);
emit UserOperationSponsored(userOpHash, user, address(0), actualAmount, tokenPrice, false);
} else if (token.trySafeTransferFrom(user, address(this), actualAmount)) {
token.safeTransfer(guarantor, prefundAmount);
emit UserOperationSponsored(userOpHash, user, guarantor, actualAmount, tokenPrice, false);
} else {
token.safeTransfer(guarantor, prefundAmount - actualAmount);
emit UserOperationSponsored(userOpHash, user, guarantor, actualAmount, tokenPrice, true);
}
}

function _fetchDetails(
bytes calldata paymasterData
)
internal
view
virtual
returns (IERC20 token, uint48 validAfter, uint48 validUntil, uint256 tokenPrice, address guarantor);

function _tokenPriceDenominator() internal view virtual returns (uint256) {
return 1e6;
}

function withdrawTokens(IERC20 token, address recipient, uint256 amount) public virtual onlyWithdrawer {
if (amount == type(uint256).max) amount = token.balanceOf(address(this));
token.safeTransfer(recipient, amount);
}
}
49 changes: 49 additions & 0 deletions contracts/account/paymaster/PaymasterNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol";
import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {PaymasterCore} from "./PaymasterCore.sol";

/**
* @dev Extension of {PaymasterCore} that supports account based on ownership of an ERC-721 token
*/
abstract contract PaymasterNFT is PaymasterCore {
IERC721 private _token;

event PaymasterNFTTokenSet(IERC721 token);

constructor(IERC721 token_) {
_setToken(token_);
}

function token() public virtual returns (IERC721) {
return _token;
}

function _setToken(IERC721 token_) internal virtual {
_token = token_;
emit PaymasterNFTTokenSet(token_);
}

/**
* @dev Internal validation of whether the paymaster is willing to pay for the user operation.
* Returns the context to be passed to postOp and the validation data.
*
* NOTE: The `context` returned is `bytes(0)`. Developers overriding this function MUST
* override {_postOp} to process the context passed along.
Comment on lines +34 to +35
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* NOTE: The `context` returned is `bytes(0)`. Developers overriding this function MUST
* override {_postOp} to process the context passed along.
* NOTE: The default `context` returned is `bytes(0)`. Developers overriding this function, such that context is not `bytes(0)`, MUST
* override {_postOp} to process the context passed along.

*/
function _validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 /* userOpHash */,
uint256 /* maxCost */
) internal virtual override returns (bytes memory context, uint256 validationData) {
return (
bytes(""),
token().balanceOf(userOp.sender) == 0
? ERC4337Utils.SIG_VALIDATION_FAILED
: ERC4337Utils.SIG_VALIDATION_SUCCESS
);
}
}
8 changes: 3 additions & 5 deletions contracts/account/paymaster/PaymasterSigner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

pragma solidity ^0.8.20;

import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {Calldata} from "@openzeppelin/contracts/utils/Calldata.sol";
import {PaymasterCore} from "./PaymasterCore.sol";
import {AbstractSigner} from "../../utils/cryptography/AbstractSigner.sol";

Expand All @@ -26,7 +24,7 @@ import {AbstractSigner} from "../../utils/cryptography/AbstractSigner.sol";
abstract contract PaymasterSigner is AbstractSigner, EIP712, PaymasterCore {
using ERC4337Utils for *;

bytes32 internal constant _USER_OPERATION_REQUEST =
bytes32 private constant USER_OPERATION_REQUEST_TYPEHASH =
keccak256(
"UserOperationRequest(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,uint256 paymasterVerificationGasLimit,uint256 paymasterPostOpGasLimit,uint48 validAfter,uint48 validUntil)"
);
Expand All @@ -45,7 +43,7 @@ abstract contract PaymasterSigner is AbstractSigner, EIP712, PaymasterCore {
_hashTypedDataV4(
keccak256(
abi.encode(
_USER_OPERATION_REQUEST,
USER_OPERATION_REQUEST_TYPEHASH,
userOp.sender,
userOp.nonce,
keccak256(userOp.initCode),
Expand Down
7 changes: 7 additions & 0 deletions contracts/mocks/ERC20Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

abstract contract ERC20Mock is ERC20 {}
4 changes: 2 additions & 2 deletions contracts/mocks/ERC721Mock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

pragma solidity ^0.8.20;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

abstract contract ERC721Mock is ERC721 {}
abstract contract ERC721Mock is ERC721Enumerable {}
29 changes: 29 additions & 0 deletions contracts/mocks/account/paymaster/PaymasterERC20Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {PaymasterERC20, IERC20} from "../../../account/paymaster/PaymasterERC20.sol";

abstract contract PaymasterERC20Mock is PaymasterERC20, Ownable {
/// Note: this is for testing purpose only. Rate should be fetched from a trusted source (or signed).
function _fetchDetails(
bytes calldata paymasterData
)
internal
view
virtual
override
returns (IERC20 token, uint48 validAfter, uint48 validUntil, uint256 tokenPrice, address guarantor)
{
return (
IERC20(address(bytes20(paymasterData[0x00:0x14]))),
uint48(bytes6(paymasterData[0x14:0x1a])),
uint48(bytes6(paymasterData[0x1a:0x20])),
uint256(bytes32(paymasterData[0x20:0x40])),
address(bytes20(paymasterData[0x40:0x54]))
);
}

function _authorizeWithdraw() internal override onlyOwner {}
}
38 changes: 38 additions & 0 deletions contracts/mocks/account/paymaster/PaymasterNFTMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {PaymasterNFT} from "../../../account/paymaster/PaymasterNFT.sol";

abstract contract PaymasterNFTContextNoPostOpMock is PaymasterNFT, Ownable {
using ERC4337Utils for *;

function _validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 requiredPreFund
) internal override returns (bytes memory context, uint256 validationData) {
// use the userOp's callData as context;
context = userOp.callData;
// super call (PaymasterNFT) for the validation data
(, validationData) = super._validatePaymasterUserOp(userOp, userOpHash, requiredPreFund);
}

function _authorizeWithdraw() internal override onlyOwner {}
}

abstract contract PaymasterNFTMock is PaymasterNFTContextNoPostOpMock {
event PaymasterDataPostOp(bytes paymasterData);

function _postOp(
PostOpMode mode,
bytes calldata context,
uint256 actualGasCost,
uint256 actualUserOpFeePerGas
) internal override {
emit PaymasterDataPostOp(context);
super._postOp(mode, context, actualGasCost, actualUserOpFeePerGas);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,28 @@
pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {PaymasterSigner} from "../../../account/paymaster/PaymasterSigner.sol";
import {SignerECDSA} from "../../../utils/cryptography/SignerECDSA.sol";

abstract contract PaymasterCoreContextNoPostOpMock is PaymasterSigner, SignerECDSA, Ownable {
abstract contract PaymasterSignerContextNoPostOpMock is PaymasterSigner, SignerECDSA, Ownable {
using ERC4337Utils for *;

function _validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 requiredPreFund
) internal override returns (bytes memory context, uint256 validationData) {
// use the userOp's paymasterData as context;
context = userOp.paymasterData();
// use the userOp's callData as context;
context = userOp.callData;
// super call (PaymasterSigner + SignerECDSA) for the validation data
(, validationData) = super._validatePaymasterUserOp(userOp, userOpHash, requiredPreFund);
}

function _authorizeWithdraw() internal override onlyOwner {}
}

abstract contract PaymasterCoreMock is PaymasterCoreContextNoPostOpMock {
abstract contract PaymasterSignerMock is PaymasterSignerContextNoPostOpMock {
event PaymasterDataPostOp(bytes paymasterData);

function _postOp(
Expand Down
2 changes: 1 addition & 1 deletion contracts/mocks/docs/account/paymaster/MyPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {PaymasterCore} from "../../../../account/paymaster/PaymasterCore.sol";
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
import {PaymasterCore} from "../../../../account/paymaster/PaymasterCore.sol";

contract MyPaymaster is PaymasterCore, Ownable {
constructor(address withdrawer) Ownable(withdrawer) {}
Expand Down
6 changes: 3 additions & 3 deletions contracts/mocks/docs/account/paymaster/MyPaymasterECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {PaymasterSigner, EIP712} from "../../../../account/paymaster/PaymasterSigner.sol";
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
import {PaymasterSigner, EIP712} from "../../../../account/paymaster/PaymasterSigner.sol";
import {SignerECDSA} from "../../../../utils/cryptography/SignerECDSA.sol";

contract MyPaymasterECDSA is PaymasterSigner, SignerECDSA, Ownable {
constructor(address paymasterSignerAddr, address withdrawer) EIP712("MyPaymasterECDSA", "1") Ownable(withdrawer) {
_setSigner(paymasterSignerAddr);
constructor(address signer, address withdrawer) EIP712("MyPaymasterECDSA", "1") Ownable(withdrawer) {
_setSigner(signer);
}

function _authorizeWithdraw() internal override onlyOwner {}
Expand Down
Loading