Skip to content

Commit

Permalink
erc20 paymaster wip
Browse files Browse the repository at this point in the history
  • Loading branch information
mmv08 committed May 22, 2024
1 parent f645d0b commit 0edbbfe
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 62 deletions.
2 changes: 1 addition & 1 deletion examples/4337-gas-metering/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"dotenv": "16.4.5",
"ethers": "^6.12.1",
"permissionless": "0.1.29",
"viem": "2.10.8"
"viem": "2.11.0"
},
"devDependencies": {
"@types/node": "20.12.12",
Expand Down
1 change: 0 additions & 1 deletion examples/4337-gas-metering/pimlico/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ Gas Used (Account or Paymaster): 423757
Gas Used (Transaction): 409872
```


## Safe Deployment with Pimlico Paymaster (Own Sponsorship)

```
Expand Down
96 changes: 49 additions & 47 deletions examples/4337-gas-metering/pimlico/pimlico.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { baseSepolia, sepolia } from 'viem/chains'
import { getAccountAddress, getAccountInitCode } from '../utils/safe'
import { SAFE_ADDRESSES_MAP } from '../utils/address'
import { UserOperation, submitUserOperationPimlico, signUserOperation, txTypes, createCallData } from '../utils/userOps'
import { transferETH } from '../utils/nativeTransfer'
import { getERC20Decimals, getERC20Balance, transferERC20Token } from '../utils/erc20'

dotenv.config()
// For Paymaster Identification.
Expand Down Expand Up @@ -171,7 +171,7 @@ const txCallData: `0x${string}` = await createCallData(
)

// Create User Operation Object.
const userOp: UserOperation = {
const sponsoredUserOperation: UserOperation = {
sender: senderAddress,
nonce: newNonce,
factory: contractCode ? undefined : chainAddresses.SAFE_PROXY_FACTORY_ADDRESS,
Expand All @@ -190,68 +190,70 @@ const userOp: UserOperation = {
}

// Sign the User Operation.
userOp.signature = await signUserOperation(userOp, signer, chainID, ENTRYPOINT_ADDRESS_V07, chainAddresses.SAFE_4337_MODULE_ADDRESS)
sponsoredUserOperation.signature = await signUserOperation(
sponsoredUserOperation,
signer,
chainID,
ENTRYPOINT_ADDRESS_V07,
chainAddresses.SAFE_4337_MODULE_ADDRESS,
)

// Fetch Max Gas Price from Bundler.
// Estimate gas and gas price for the User Operation.
const gasEstimate = await bundlerClient.estimateUserOperationGas({
userOperation: sponsoredUserOperation,
})
const maxGasPriceResult = await bundlerClient.getUserOperationGasPrice()
userOp.maxFeePerGas = maxGasPriceResult.fast.maxFeePerGas
userOp.maxPriorityFeePerGas = maxGasPriceResult.fast.maxPriorityFeePerGas
sponsoredUserOperation.maxFeePerGas = maxGasPriceResult.fast.maxFeePerGas
sponsoredUserOperation.maxPriorityFeePerGas = maxGasPriceResult.fast.maxPriorityFeePerGas

sponsoredUserOperation.callGasLimit = gasEstimate.callGasLimit
sponsoredUserOperation.verificationGasLimit = gasEstimate.verificationGasLimit
sponsoredUserOperation.preVerificationGas = gasEstimate.preVerificationGas
sponsoredUserOperation.paymasterVerificationGasLimit = gasEstimate.paymasterVerificationGasLimit
sponsoredUserOperation.paymasterPostOpGasLimit = gasEstimate.paymasterPostOpGasLimit

// If Paymaster is used, then sponsor the User Operation.
if (usePaymaster) {
const sponsorResult = await pimlicoPaymasterClient.sponsorUserOperation({
userOperation: {
sender: userOp.sender,
nonce: userOp.nonce,
factory: userOp.factory,
factoryData: userOp.factoryData,
callData: userOp.callData,
maxFeePerGas: userOp.maxFeePerGas,
maxPriorityFeePerGas: userOp.maxPriorityFeePerGas,
signature: userOp.signature,
sender: sponsoredUserOperation.sender,
nonce: sponsoredUserOperation.nonce,
factory: sponsoredUserOperation.factory,
factoryData: sponsoredUserOperation.factoryData,
callData: sponsoredUserOperation.callData,
maxFeePerGas: sponsoredUserOperation.maxFeePerGas,
maxPriorityFeePerGas: sponsoredUserOperation.maxPriorityFeePerGas,
signature: sponsoredUserOperation.signature,
},
sponsorshipPolicyId: policyID,
})

userOp.callGasLimit = sponsorResult.callGasLimit
userOp.verificationGasLimit = sponsorResult.verificationGasLimit
userOp.preVerificationGas = sponsorResult.preVerificationGas
userOp.paymasterData = sponsorResult.paymasterData
userOp.paymasterVerificationGasLimit = sponsorResult.paymasterVerificationGasLimit
userOp.paymasterPostOpGasLimit = sponsorResult.paymasterPostOpGasLimit
sponsoredUserOperation.callGasLimit = sponsorResult.callGasLimit
sponsoredUserOperation.verificationGasLimit = sponsorResult.verificationGasLimit
sponsoredUserOperation.preVerificationGas = sponsorResult.preVerificationGas
sponsoredUserOperation.paymasterData = sponsorResult.paymasterData
sponsoredUserOperation.paymasterVerificationGasLimit = sponsorResult.paymasterVerificationGasLimit
sponsoredUserOperation.paymasterPostOpGasLimit = sponsorResult.paymasterPostOpGasLimit
} else {
userOp.paymaster = undefined

// Estimate Gas for the User Operation.
const gasEstimate = await bundlerClient.estimateUserOperationGas({
userOperation: userOp,
})

userOp.callGasLimit = gasEstimate.callGasLimit
userOp.verificationGasLimit = gasEstimate.verificationGasLimit
userOp.preVerificationGas = gasEstimate.preVerificationGas

// Check Sender ETH Balance.
let senderETHBalance = await publicClient.getBalance({ address: senderAddress })
console.log('\nSender ETH Balance:', ethers.formatEther(senderETHBalance))

// Checking required preFund.
const requiredPrefund = getRequiredPrefund({ userOperation: userOp, entryPoint: ENTRYPOINT_ADDRESS_V07 })
console.log('\nRequired Prefund:', ethers.formatEther(requiredPrefund))

const requiredBalance = requiredPrefund + (txType == 'native-transfer' ? parseEther('0.000001') : 0n)

if (senderETHBalance < requiredBalance) {
await transferETH(publicClient, signer, senderAddress, requiredBalance - senderETHBalance, chain, paymaster)
while (senderETHBalance < requiredBalance) {
// Fetch USDC balance of sender
const usdcDecimals = BigInt(await getERC20Decimals(usdcTokenAddress, publicClient))
const usdcAmount = 10n ** usdcDecimals
let senderUSDCBalance = await getERC20Balance(usdcTokenAddress, publicClient, senderAddress)
console.log('\nSafe Wallet USDC Balance:', Number(senderUSDCBalance / usdcAmount))

if (senderUSDCBalance < BigInt(1) * usdcAmount) {
console.log('\nTransferring 1 USDC Token for paying the Paymaster from Sender to Safe.')
await transferERC20Token(usdcTokenAddress, publicClient, signer, senderAddress, BigInt(1) * usdcAmount, chain, paymaster)
while (senderUSDCBalance < BigInt(1) * usdcAmount) {
await setTimeout(15000)
senderETHBalance = await publicClient.getBalance({ address: senderAddress })
senderUSDCBalance = await getERC20Balance(usdcTokenAddress, publicClient, senderAddress)
}
console.log('\nUpdated Safe Wallet USDC Balance:', Number(senderUSDCBalance / usdcAmount))
}
}

// Sign the User Operation.
userOp.signature = await signUserOperation(userOp, signer, chainID, ENTRYPOINT_ADDRESS_V07, chainAddresses.SAFE_4337_MODULE_ADDRESS)
sponsoredUserOperation.signature = await signUserOperation(sponsoredUserOperation, signer, chainID, ENTRYPOINT_ADDRESS_V07, chainAddresses.SAFE_4337_MODULE_ADDRESS)

// Submit the User Operation.
await submitUserOperationPimlico(userOp, bundlerClient, ENTRYPOINT_ADDRESS_V07, chain)
await submitUserOperationPimlico(sponsoredUserOperation, bundlerClient, ENTRYPOINT_ADDRESS_V07, chain)
17 changes: 12 additions & 5 deletions examples/4337-gas-metering/utils/erc20.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import dotenv from 'dotenv'
import { http, Address, encodeFunctionData, createWalletClient, PrivateKeyAccount, PublicClient } from 'viem'
import { HttpTransport, http, Address, encodeFunctionData, createWalletClient, PrivateKeyAccount, PublicClient } from 'viem'
import { baseSepolia, goerli, polygonMumbai, sepolia } from 'viem/chains'
import {
ERC20_TOKEN_APPROVE_ABI,
Expand Down Expand Up @@ -32,7 +32,10 @@ export const generateTransferCallData = (to: Address, value: bigint) => {
return transferData
}

export const getERC20Decimals = async (erc20TokenAddress: Address, publicClient: PublicClient): Promise<bigint> => {
export const getERC20Decimals = async (
erc20TokenAddress: Address,
publicClient: PublicClient<HttpTransport, typeof baseSepolia | typeof sepolia>,
): Promise<bigint> => {
const erc20Decimals = (await publicClient.readContract({
abi: ERC20_TOKEN_DECIMALS_ABI,
address: erc20TokenAddress,
Expand All @@ -42,7 +45,11 @@ export const getERC20Decimals = async (erc20TokenAddress: Address, publicClient:
return erc20Decimals
}

export const getERC20Balance = async (erc20TokenAddress: Address, publicClient: PublicClient, owner: Address): Promise<bigint> => {
export const getERC20Balance = async (
erc20TokenAddress: Address,
publicClient: PublicClient<HttpTransport, typeof baseSepolia | typeof sepolia>,
owner: Address,
): Promise<bigint> => {
const senderERC20Balance = (await publicClient.readContract({
abi: ERC20_TOKEN_BALANCE_OF_ABI,
address: erc20TokenAddress,
Expand All @@ -55,7 +62,7 @@ export const getERC20Balance = async (erc20TokenAddress: Address, publicClient:

export const mintERC20Token = async (
erc20TokenAddress: Address,
publicClient: PublicClient,
publicClient: PublicClient<HttpTransport, typeof baseSepolia | typeof sepolia>,
signer: PrivateKeyAccount,
to: Address,
amount: bigint,
Expand Down Expand Up @@ -132,7 +139,7 @@ export const mintERC20Token = async (

export const transferERC20Token = async (
erc20TokenAddress: Address,
publicClient: PublicClient,
publicClient: PublicClient<HttpTransport, typeof baseSepolia | typeof sepolia>,
signer: PrivateKeyAccount,
to: Address,
amount: bigint,
Expand Down
16 changes: 8 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0edbbfe

Please sign in to comment.