Skip to content

Commit 67b44ad

Browse files
mdehooganikaraghu
andauthored
Metapaymaster (#22)
* Metapaymaster * Code review * Add overhead calculation * Fix total calculation for negative amounts * Update script/DeployMetaPaymaster.s.sol Co-authored-by: anikaraghu <[email protected]> * Explicitly pass msgSender * Add docs * Add abstract class for easy use of metapaymaster * Remove unnecessary import * Script for setting meta-paymaster balance --------- Co-authored-by: anikaraghu <[email protected]>
1 parent 9b1acc3 commit 67b44ad

10 files changed

+281
-1
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ docs/
1616
# Intellij
1717
.idea/
1818

19+
/records/

.gitmodules

+6
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,9 @@
77
[submodule "lib/openzeppelin-contracts"]
88
path = lib/openzeppelin-contracts
99
url = https://github.com/OpenZeppelin/openzeppelin-contracts
10+
[submodule "lib/openzeppelin-contracts-upgradeable"]
11+
path = lib/openzeppelin-contracts-upgradeable
12+
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
13+
[submodule "lib/solady"]
14+
path = lib/solady
15+
url = https://github.com/Vectorized/solady

foundry.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ optimizer_runs = 999999
88
solc_version = "0.8.20"
99
remappings = [
1010
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts',
11-
"@account-abstraction/=lib/account-abstraction/contracts",
11+
'@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts',
12+
'@account-abstraction/=lib/account-abstraction/contracts',
13+
'@solady/=lib/solady/src',
1214
]
1315

lib/solady

Submodule solady added at fad3f67

script/DeployMetaPaymaster.s.sol

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import "forge-std/Script.sol";
5+
import "../src/meta/MetaPaymaster.sol";
6+
import "@account-abstraction/interfaces/IEntryPoint.sol";
7+
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
8+
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
9+
10+
// This script deploys a Paymaster and sets the deployer as the owner address
11+
contract DeployPaymaster is Script {
12+
address entryPoint = 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789;
13+
14+
function run() public {
15+
vm.broadcast();
16+
MetaPaymaster paymaster = new MetaPaymaster();
17+
vm.broadcast();
18+
ProxyAdmin admin = new ProxyAdmin();
19+
bytes memory data = abi.encodeWithSignature("initialize(address,address)", tx.origin, IEntryPoint(entryPoint));
20+
vm.broadcast();
21+
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(address(paymaster), address(admin), data);
22+
require(address(MetaPaymaster(payable(proxy)).entryPoint()) == entryPoint);
23+
require(MetaPaymaster(payable(proxy)).owner() == tx.origin);
24+
require(admin.owner() == tx.origin);
25+
}
26+
}

script/SetMetaPaymasterBalance.s.sol

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import "forge-std/Script.sol";
5+
import "../src/meta/MetaPaymaster.sol";
6+
7+
contract SetMetaPaymasterBalance is Script {
8+
MetaPaymaster metaPaymaster = MetaPaymaster(payable(0x75B9328BB753144705b77b215E304eC7ef45235C));
9+
10+
function run(address account, uint256 amount) public {
11+
vm.broadcast();
12+
metaPaymaster.setBalance(account, amount);
13+
require(metaPaymaster.balanceOf(account) == amount);
14+
}
15+
}

src/meta/BaseFundedPaymaster.sol

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.20;
3+
4+
import "@account-abstraction/core/BasePaymaster.sol";
5+
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
6+
import "./MetaPaymaster.sol";
7+
8+
/**
9+
* Abstract paymaster that uses the `MetaPaymaster` for funding the
10+
* gas costs of each userOp by calling the `fund` method in `postOp`.
11+
*/
12+
abstract contract BaseFundedPaymaster is BasePaymaster {
13+
MetaPaymaster public immutable metaPaymaster;
14+
15+
uint256 private constant POST_OP_OVERHEAD = 34982;
16+
17+
constructor(IEntryPoint _entryPoint, MetaPaymaster _metaPaymaster) BasePaymaster(_entryPoint) Ownable() {
18+
metaPaymaster = _metaPaymaster;
19+
}
20+
21+
function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 requiredPreFund)
22+
internal override returns (bytes memory context, uint256 validationData) {
23+
validationData = __validatePaymasterUserOp(userOp, userOpHash, requiredPreFund);
24+
return (abi.encode(userOp.maxFeePerGas, userOp.maxPriorityFeePerGas), validationData);
25+
}
26+
27+
function __validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost)
28+
internal virtual returns (uint256 validationData);
29+
30+
function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) internal override {
31+
if (mode != PostOpMode.postOpReverted) {
32+
(uint256 maxFeePerGas, uint256 maxPriorityFeePerGas) = abi.decode(context, (uint256, uint256));
33+
uint256 gasPrice = min(maxFeePerGas, maxPriorityFeePerGas + block.basefee);
34+
metaPaymaster.fund(address(this), actualGasCost + POST_OP_OVERHEAD*gasPrice);
35+
}
36+
}
37+
38+
function min(uint256 a, uint256 b) internal pure returns (uint256) {
39+
return a < b ? a : b;
40+
}
41+
}

src/meta/FundedPaymaster.sol

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity 0.8.20;
3+
4+
import "@account-abstraction/core/BasePaymaster.sol";
5+
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
6+
import "./BaseFundedPaymaster.sol";
7+
8+
/**
9+
* A paymaster that uses external service to decide whether to pay for the UserOp.
10+
* The paymaster trusts an external signer to sign the transaction.
11+
* The calling user must pass the UserOp to that external signer first, which performs
12+
* whatever off-chain verification before signing the UserOp.\
13+
* Actual funding is provided by a meta-paymaster.
14+
*/
15+
contract FundedPaymaster is BaseFundedPaymaster {
16+
using UserOperationLib for UserOperation;
17+
18+
address public immutable verifyingSigner;
19+
20+
uint256 private constant VALID_TIMESTAMP_OFFSET = 20;
21+
uint256 private constant SIGNATURE_OFFSET = VALID_TIMESTAMP_OFFSET + 64;
22+
23+
constructor(IEntryPoint _entryPoint, MetaPaymaster _metaPaymaster, address _verifyingSigner) BaseFundedPaymaster(_entryPoint, _metaPaymaster) {
24+
verifyingSigner = _verifyingSigner;
25+
}
26+
27+
/**
28+
* return the hash we're going to sign off-chain (and validate on-chain)
29+
* this method is called by the off-chain service, to sign the request.
30+
* it is called on-chain from the validatePaymasterUserOp, to validate the signature.
31+
* note that this signature covers all fields of the UserOperation, except the "paymasterAndData",
32+
* which will carry the signature itself.
33+
*/
34+
function getHash(UserOperation calldata userOp, uint48 validUntil, uint48 validAfter)
35+
public view returns (bytes32) {
36+
// can't use userOp.hash(), since it contains also the paymasterAndData itself.
37+
return keccak256(
38+
abi.encode(
39+
userOp.getSender(),
40+
userOp.nonce,
41+
calldataKeccak(userOp.initCode),
42+
calldataKeccak(userOp.callData),
43+
userOp.callGasLimit,
44+
userOp.verificationGasLimit,
45+
userOp.preVerificationGas,
46+
userOp.maxFeePerGas,
47+
userOp.maxPriorityFeePerGas,
48+
block.chainid,
49+
address(this),
50+
validUntil,
51+
validAfter
52+
)
53+
);
54+
}
55+
56+
/**
57+
* verify our external signer signed this request.
58+
* the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params
59+
* paymasterAndData[:20] : address(this)
60+
* paymasterAndData[20:84] : abi.encode(validUntil, validAfter)
61+
* paymasterAndData[84:] : signature
62+
*/
63+
function __validatePaymasterUserOp(UserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 /*requiredPreFund*/)
64+
internal override view returns (uint256) {
65+
(uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData);
66+
// Only support 65-byte signatures, to avoid potential replay attacks.
67+
require(signature.length == 65, "Paymaster: invalid signature length in paymasterAndData");
68+
bytes32 hash = ECDSA.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter));
69+
70+
// don't revert on signature failure: return SIG_VALIDATION_FAILED
71+
if (verifyingSigner != ECDSA.recover(hash, signature)) {
72+
return _packValidationData(true, validUntil, validAfter);
73+
}
74+
75+
// no need for other on-chain validation: entire UserOp should have been checked
76+
// by the external service prior to signing it.
77+
return _packValidationData(false, validUntil, validAfter);
78+
}
79+
80+
function parsePaymasterAndData(bytes calldata paymasterAndData)
81+
internal pure returns(uint48 validUntil, uint48 validAfter, bytes calldata signature) {
82+
(validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET:SIGNATURE_OFFSET],(uint48, uint48));
83+
signature = paymasterAndData[SIGNATURE_OFFSET:];
84+
}
85+
86+
receive() external payable {
87+
// use address(this).balance rather than msg.value in case of force-send
88+
(bool callSuccess, ) = payable(address(entryPoint)).call{value: address(this).balance}("");
89+
require(callSuccess, "Deposit failed");
90+
}
91+
}

src/meta/MetaPaymaster.sol

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.20;
3+
4+
import "@account-abstraction/interfaces/IEntryPoint.sol";
5+
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
6+
import "@solady/utils/SafeTransferLib.sol";
7+
8+
/**
9+
* A meta-paymaster that deposits funds to the 4337 entryPoint on behalf
10+
* of allowlisted paymasters, just-in-time per userOperation.
11+
*/
12+
contract MetaPaymaster is OwnableUpgradeable {
13+
IEntryPoint public entryPoint;
14+
15+
// Funds available to each individual paymaster
16+
mapping(address => uint256) public balanceOf;
17+
18+
// Total funds allocated to all paymasters (sum of all balanceOf).
19+
// Tracked separately from the contract's balance, since balances
20+
// can be undercollateralized.
21+
uint256 public total;
22+
23+
/// @custom:oz-upgrades-unsafe-allow constructor
24+
constructor() {
25+
_disableInitializers();
26+
}
27+
28+
/**
29+
* @notice Initializes the contract.
30+
* @param _owner The owner of the contract.
31+
* @param _entryPoint The 4337 EntryPoint contract.
32+
*/
33+
function initialize(address _owner, IEntryPoint _entryPoint) public initializer {
34+
__Ownable_init(_owner);
35+
entryPoint = _entryPoint;
36+
}
37+
38+
/**
39+
* @notice Helper function to check if this contract will fund a userOperation.
40+
* @param account The address that will call `fund` (usually the paymaster).
41+
* @param actualGasCost The actual gas cost of the userOp, including postOp overhead.
42+
* @return True if this contract will fund the given gas cost.
43+
*/
44+
function willFund(address account, uint256 actualGasCost) public view returns (bool) {
45+
return balanceOf[account] >= actualGasCost && address(this).balance >= actualGasCost;
46+
}
47+
48+
/**
49+
* @notice Deposits funds to the 4337 entryPoint on behalf of a paymaster.
50+
* @dev `actualGasCost` can be calculated using the formula:
51+
* `postOp.actualGasCost + postOpOverhead * gasPrice`, where `postOpOverhead`
52+
* is a constant representing the gas usage of the postOp function.
53+
* @param paymaster The paymaster to fund (`address(this)` when called from the paymaster).
54+
* @param actualGasCost The actual gas cost of the userOp, including postOp overhead.
55+
*/
56+
function fund(address paymaster, uint256 actualGasCost) external {
57+
require(balanceOf[msg.sender] >= actualGasCost);
58+
total -= actualGasCost;
59+
balanceOf[msg.sender] -= actualGasCost;
60+
entryPoint.depositTo{value: actualGasCost}(paymaster);
61+
}
62+
63+
/**
64+
* @notice Helper to deposit + associate funds with a particular paymaster.
65+
* @param account The address to associate the funds with.
66+
*/
67+
function depositTo(address account) public payable {
68+
total += msg.value;
69+
balanceOf[account] += msg.value;
70+
}
71+
72+
/**
73+
* @notice Sets the balance of a particular account / paymaster.
74+
* @param account The account to set the balance of.
75+
* @param amount The amount to set the balance to.
76+
*/
77+
function setBalance(address account, uint256 amount) public onlyOwner {
78+
if (amount > balanceOf[account]) {
79+
total += amount - balanceOf[account];
80+
} else {
81+
total -= balanceOf[account] - amount;
82+
}
83+
balanceOf[account] = amount;
84+
}
85+
86+
/**
87+
* @notice Withdraws funds from the contract.
88+
* @param withdrawAddress The address to withdraw to.
89+
* @param withdrawAmount The amount to withdraw.
90+
*/
91+
function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) public onlyOwner {
92+
SafeTransferLib.safeTransferETH(withdrawAddress, withdrawAmount);
93+
}
94+
95+
receive() external payable {}
96+
}

0 commit comments

Comments
 (0)