Skip to content

Commit 55ecaff

Browse files
committed
adds mattie's changes to overview
1 parent 78c5ae2 commit 55ecaff

File tree

1 file changed

+344
-0
lines changed

1 file changed

+344
-0
lines changed
Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
# Paymaster Contract for Sponsored USDC Transfers
2+
3+
!!! info
4+
NOTE: Like all of crypto, payments require handling sensitive data. The following examples are demonstrations of integrations but should not be used in production. In production, sensitive information such as API keys, private-key wallets, etc., should be put in a secrets manager or vault.
5+
6+
## Overview
7+
8+
A paymaster contract allows you to sponsor gas fees for users when they transfer USDC, enabling gasless transactions. This implementation uses EIP-4337 Account Abstraction to sponsor USDC transfers on Polygon.
9+
10+
## Prerequisites
11+
12+
- Understanding of EIP-4337 Account Abstraction
13+
- Familiarity with Solidity and smart contracts
14+
- Access to a Polygon node
15+
- USDC contract address on Polygon: `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359`
16+
17+
## Paymaster Contract Implementation
18+
19+
```solidity
20+
// SPDX-License-Identifier: MIT
21+
pragma solidity ^0.8.19;
22+
23+
import "@account-abstraction/contracts/core/BasePaymaster.sol";
24+
import "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
25+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
26+
import "@openzeppelin/contracts/access/Ownable.sol";
27+
28+
contract USDCPaymaster is BasePaymaster, Ownable {
29+
IERC20 public immutable usdc;
30+
31+
// Mapping to track sponsored accounts
32+
mapping(address => bool) public sponsoredAccounts;
33+
34+
// Maximum gas cost we're willing to sponsor
35+
uint256 public maxGasCost = 0.01 ether; // 0.01 MATIC
36+
37+
// Events
38+
event UserOperationSponsored(address indexed account, uint256 gasCost);
39+
event AccountSponsored(address indexed account, bool sponsored);
40+
event MaxGasCostUpdated(uint256 newMaxGasCost);
41+
42+
constructor(
43+
IEntryPoint _entryPoint,
44+
address _usdc,
45+
address _owner
46+
) BasePaymaster(_entryPoint) {
47+
usdc = IERC20(_usdc);
48+
_transferOwnership(_owner);
49+
}
50+
51+
/**
52+
* @dev Validates if a user operation should be sponsored
53+
* @param userOp The user operation to validate
54+
* @param userOpHash Hash of the user operation
55+
* @param maxCost Maximum cost of the operation
56+
* @return context Validation context
57+
* @return validationResult Validation result
58+
*/
59+
function _validatePaymasterUserOp(
60+
UserOperation calldata userOp,
61+
bytes32 userOpHash,
62+
uint256 maxCost
63+
) internal view override returns (bytes memory context, uint256 validationResult) {
64+
// Check if the operation is within our gas cost limit
65+
require(maxCost <= maxGasCost, "USDCPaymaster: gas cost too high");
66+
67+
// Decode the calldata to ensure it's a USDC transfer
68+
bytes4 selector = bytes4(userOp.callData[:4]);
69+
70+
// Check if it's a direct USDC transfer or approve+transfer
71+
bool isValidUSDCOperation = _isValidUSDCOperation(userOp.callData, userOp.sender);
72+
require(isValidUSDCOperation, "USDCPaymaster: not a valid USDC operation");
73+
74+
// Check if account is sponsored or if it's a first-time user with USDC balance
75+
address account = userOp.sender;
76+
if (!sponsoredAccounts[account]) {
77+
uint256 usdcBalance = usdc.balanceOf(account);
78+
require(usdcBalance > 0, "USDCPaymaster: no USDC balance");
79+
}
80+
81+
// Return validation success
82+
return (abi.encode(account, maxCost), 0);
83+
}
84+
85+
/**
86+
* @dev Validates if the operation is a valid USDC transfer
87+
* @param callData The call data from the user operation
88+
* @param sender The sender of the operation
89+
* @return true if it's a valid USDC operation
90+
*/
91+
function _isValidUSDCOperation(bytes calldata callData, address sender) internal view returns (bool) {
92+
// Check if it's a direct call to USDC contract
93+
if (callData.length >= 68) { // 4 bytes selector + 32 bytes address + 32 bytes amount
94+
bytes4 selector = bytes4(callData[:4]);
95+
96+
// Check for transfer(address,uint256) or transferFrom(address,address,uint256)
97+
if (selector == IERC20.transfer.selector || selector == IERC20.transferFrom.selector) {
98+
return true;
99+
}
100+
101+
// Check for approve(address,uint256) - common pattern before transfer
102+
if (selector == IERC20.approve.selector) {
103+
return true;
104+
}
105+
}
106+
107+
// Check if it's a multicall that includes USDC operations
108+
return _containsUSDCOperation(callData);
109+
}
110+
111+
/**
112+
* @dev Checks if multicall contains USDC operations
113+
* @param callData The call data to analyze
114+
* @return true if USDC operations are found
115+
*/
116+
function _containsUSDCOperation(bytes calldata callData) internal pure returns (bool) {
117+
// Basic implementation - could be enhanced for complex multicalls
118+
// Look for USDC contract address in the calldata
119+
bytes32 usdcAddress = bytes32(uint256(uint160(0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359)));
120+
121+
for (uint256 i = 0; i < callData.length - 32; i++) {
122+
if (bytes32(callData[i:i+32]) == usdcAddress) {
123+
return true;
124+
}
125+
}
126+
127+
return false;
128+
}
129+
130+
/**
131+
* @dev Called after the user operation is executed
132+
* @param context The context returned from validation
133+
* @param actualGasCost The actual gas cost of the operation
134+
*/
135+
function _postOp(
136+
PostOpMode mode,
137+
bytes calldata context,
138+
uint256 actualGasCost
139+
) internal override {
140+
if (mode == PostOpMode.opSucceeded) {
141+
(address account, uint256 maxCost) = abi.decode(context, (address, uint256));
142+
emit UserOperationSponsored(account, actualGasCost);
143+
}
144+
}
145+
146+
// Admin functions
147+
148+
/**
149+
* @dev Add or remove an account from sponsorship
150+
* @param account The account to sponsor/unsponsor
151+
* @param sponsored Whether to sponsor this account
152+
*/
153+
function setSponsoredAccount(address account, bool sponsored) external onlyOwner {
154+
sponsoredAccounts[account] = sponsored;
155+
emit AccountSponsored(account, sponsored);
156+
}
157+
158+
/**
159+
* @dev Update maximum gas cost willing to sponsor
160+
* @param newMaxGasCost New maximum gas cost
161+
*/
162+
function setMaxGasCost(uint256 newMaxGasCost) external onlyOwner {
163+
maxGasCost = newMaxGasCost;
164+
emit MaxGasCostUpdated(newMaxGasCost);
165+
}
166+
167+
/**
168+
* @dev Withdraw contract balance to owner
169+
*/
170+
function withdraw() external onlyOwner {
171+
uint256 balance = address(this).balance;
172+
require(balance > 0, "USDCPaymaster: no balance to withdraw");
173+
174+
(bool success, ) = payable(owner()).call{value: balance}("");
175+
require(success, "USDCPaymaster: withdrawal failed");
176+
}
177+
178+
/**
179+
* @dev Add stake to the EntryPoint
180+
* @param unstakeDelaySec Unstake delay in seconds
181+
*/
182+
function addStake(uint32 unstakeDelaySec) external payable onlyOwner {
183+
entryPoint.addStake{value: msg.value}(unstakeDelaySec);
184+
}
185+
186+
/**
187+
* @dev Unlock stake from the EntryPoint
188+
*/
189+
function unlockStake() external onlyOwner {
190+
entryPoint.unlockStake();
191+
}
192+
193+
/**
194+
* @dev Withdraw stake from the EntryPoint
195+
* @param withdrawAddress Address to withdraw to
196+
*/
197+
function withdrawStake(address payable withdrawAddress) external onlyOwner {
198+
entryPoint.withdrawStake(withdrawAddress);
199+
}
200+
201+
/**
202+
* @dev Deposit funds to the EntryPoint for gas sponsorship
203+
*/
204+
function deposit() public payable {
205+
entryPoint.depositTo{value: msg.value}(address(this));
206+
}
207+
208+
/**
209+
* @dev Get deposit balance
210+
*/
211+
function getDeposit() public view returns (uint256) {
212+
return entryPoint.balanceOf(address(this));
213+
}
214+
215+
receive() external payable {
216+
deposit();
217+
}
218+
}
219+
```
220+
221+
## Integration Example
222+
223+
Here's how to integrate the paymaster with a client application:
224+
225+
```typescript
226+
import { createPublicClient, createWalletClient, http, parseUnits } from "viem";
227+
import { polygon } from "viem/chains";
228+
import { privateKeyToAccount } from "viem/accounts";
229+
230+
// Contract addresses
231+
const USDC = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";
232+
const PAYMASTER_ADDRESS = "0xYourPaymasterAddress..."; // Deploy the contract above
233+
const ENTRY_POINT = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; // Standard EntryPoint
234+
235+
const erc20Abi = [
236+
{ type: "function", name: "transfer", stateMutability: "nonpayable", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "bool" }] },
237+
];
238+
239+
const account = privateKeyToAccount(process.env.USER_PRIVATE_KEY as `0x${string}`);
240+
const publicClient = createPublicClient({
241+
chain: polygon,
242+
transport: http(process.env.POLYGON_RPC_URL)
243+
});
244+
245+
async function sponsoredUSDCTransfer(
246+
recipient: `0x${string}`,
247+
amount: string
248+
) {
249+
// 1. Prepare the USDC transfer call data
250+
const transferCallData = encodeFunctionData({
251+
abi: erc20Abi,
252+
functionName: "transfer",
253+
args: [recipient, parseUnits(amount, 6)] // USDC has 6 decimals
254+
});
255+
256+
// 2. Create user operation with paymaster
257+
const userOp = {
258+
sender: account.address,
259+
nonce: await getNonce(account.address), // Get from EntryPoint
260+
initCode: "0x", // Empty if account already deployed
261+
callData: transferCallData,
262+
callGasLimit: 100000n,
263+
verificationGasLimit: 150000n,
264+
preVerificationGas: 21000n,
265+
maxFeePerGas: parseUnits("20", "gwei"),
266+
maxPriorityFeePerGas: parseUnits("2", "gwei"),
267+
paymasterAndData: PAYMASTER_ADDRESS, // This enables gas sponsorship
268+
signature: "0x" // Will be populated after signing
269+
};
270+
271+
// 3. Sign and submit the user operation
272+
// This would typically involve signing the userOpHash and submitting to a bundler
273+
console.log("User operation prepared for sponsored USDC transfer:", userOp);
274+
275+
// Note: Full implementation would require bundler integration
276+
// and proper user operation signing according to EIP-4337
277+
}
278+
279+
// Helper function to get nonce from EntryPoint
280+
async function getNonce(address: `0x${string}`): Promise<bigint> {
281+
// Implementation depends on your account abstraction setup
282+
return 0n;
283+
}
284+
```
285+
286+
## Deployment Script
287+
288+
```solidity
289+
// deploy.sol
290+
pragma solidity ^0.8.19;
291+
292+
import "forge-std/Script.sol";
293+
import "../src/USDCPaymaster.sol";
294+
295+
contract DeployUSDCPaymaster is Script {
296+
function run() external {
297+
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
298+
address entryPoint = 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789; // Standard EntryPoint
299+
address usdc = 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359; // USDC on Polygon
300+
301+
vm.startBroadcast(deployerPrivateKey);
302+
303+
USDCPaymaster paymaster = new USDCPaymaster(
304+
IEntryPoint(entryPoint),
305+
usdc,
306+
msg.sender
307+
);
308+
309+
// Add initial stake and deposit
310+
paymaster.addStake{value: 1 ether}(86400); // 1 day unstake delay
311+
paymaster.deposit{value: 5 ether}(); // 5 MATIC for gas sponsorship
312+
313+
vm.stopBroadcast();
314+
315+
console.log("USDCPaymaster deployed at:", address(paymaster));
316+
}
317+
}
318+
```
319+
320+
## Key Features
321+
322+
1. **Gas Sponsorship**: Sponsors gas fees for USDC transfers
323+
2. **Access Control**: Only sponsored accounts or accounts with USDC balance can use
324+
3. **Gas Limits**: Configurable maximum gas cost per operation
325+
4. **Security**: Validates that operations are legitimate USDC transfers
326+
5. **Admin Functions**: Owner can manage sponsored accounts and withdraw funds
327+
328+
## Security Considerations
329+
330+
- Validate all user operations to prevent abuse
331+
- Set reasonable gas limits to control costs
332+
- Monitor for suspicious patterns
333+
- Keep paymaster funded but not over-funded
334+
- Regular security audits for production use
335+
336+
## Testing
337+
338+
Before production deployment:
339+
340+
1. Test on Polygon testnet (Amoy)
341+
2. Verify gas estimation accuracy
342+
3. Test edge cases and error conditions
343+
4. Monitor gas consumption patterns
344+
5. Implement proper monitoring and alerting

0 commit comments

Comments
 (0)