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

Trampoline demo - USDC Paymaster Example using Pimlico #19

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
7 changes: 7 additions & 0 deletions README.md
Expand Up @@ -2,6 +2,13 @@

Trampoline is a chrome extension boilerplate code to showcase your own Smart Contract Wallets with React 18 and Webpack 5 support.


> [!NOTE]
> This branch is the implementation of how to send a transaction using ERC20 Tokens.
> See [SimpleAccountWithPaymaster Contract](https://github.com/eth-infinitism/trampoline/blob/trampoline-demo-erc20-paymaster/contracts/SimpleAccountWithPaymaster.sol) for the changes we have done to allow ERC20 approval while in it's creation phase. NOTE: The override will not be allowed by every bundler as this access storage which is not allowed by ERC 4337 standard.
>
> Read more about it on this [blog](https://erc4337.mirror.xyz/7DUTUn2eNrjvum3tWAnRih8576IrX13E6WnZeADvAHQ)

## Installation and Running

### Steps:
Expand Down
19 changes: 19 additions & 0 deletions contracts/SimpleAccountWithPaymaster.sol
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

import '@account-abstraction/contracts/samples/SimpleAccount.sol';

contract SimpleAccountWithPaymaster is SimpleAccount {
constructor(IEntryPoint anEntryPoint) SimpleAccount(anEntryPoint) {}

/**
* @dev The _entryPoint member is immutable, to reduce gas consumption. To upgrade EntryPoint,
* a new implementation of SimpleAccount must be deployed with the new EntryPoint address, then upgrading
* the implementation by calling `upgradeTo()`
*/
function initialize(address anOwner, address dest, uint256 value, bytes calldata func) public virtual initializer {
initialize(anOwner);
_call(dest, value, func);
}

}
47 changes: 47 additions & 0 deletions contracts/SimpleAccountWithPaymasterFactory.sol
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;


import "@openzeppelin/contracts/utils/Create2.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "./SimpleAccountWithPaymaster.sol";

contract SimpleAccountWithPaymasterFactory {
SimpleAccountWithPaymaster public immutable accountImplementation;

constructor(IEntryPoint _entryPoint) {
accountImplementation = new SimpleAccountWithPaymaster(_entryPoint);
}

/**
* create an account, and return its address.
* returns the address even if the account is already deployed.
* Note that during UserOperation execution, this method is called only if the account is not deployed.
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
*/
function createAccount(address owner,uint256 salt, address dest, uint256 value, bytes calldata func) public returns (SimpleAccountWithPaymaster ret) {
address addr = getAddress(owner, salt, dest, value, func);
uint codeSize = addr.code.length;
if (codeSize > 0) {
return SimpleAccountWithPaymaster(payable(addr));
}
ret = SimpleAccountWithPaymaster(payable(new ERC1967Proxy{salt : bytes32(salt)}(
address(accountImplementation),
abi.encodeCall(SimpleAccountWithPaymaster.initialize, (owner, dest, value, func))
)));
}

/**
* calculate the counterfactual address of this account as it would be returned by createAccount()
*/
function getAddress(address owner,uint256 salt, address dest, uint256 value, bytes calldata func) public view returns (address) {
return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked(
type(ERC1967Proxy).creationCode,
abi.encode(
address(accountImplementation),
abi.encodeCall(SimpleAccountWithPaymaster.initialize, (owner, dest,value, func))
)
)));
}

}
5 changes: 3 additions & 2 deletions deploy/deploy.ts
@@ -1,12 +1,13 @@
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { DeployFunction } from 'hardhat-deploy/types';
import config from '../src/exconfig';

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const accounts = await hre.getUnnamedAccounts();
await hre.deployments.deploy('Greeter', {
await hre.deployments.deploy('SimpleAccountWithPaymasterFactory', {
from: accounts[0],
deterministicDeployment: true,
args: ['Test'],
args: [config.network.entryPointAddress],
log: true,
});
};
Expand Down
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -19,6 +19,7 @@
"dependencies": {
"@account-abstraction/contracts": "^0.6.0",
"@account-abstraction/sdk": "^0.6.0",
"@account-abstraction/utils": "^0.6.0",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@fontsource/roboto": "^4.5.8",
Expand All @@ -28,6 +29,7 @@
"@nomicfoundation/hardhat-toolbox": "^2.0.1",
"@peculiar/asn1-ecc": "^2.3.4",
"@peculiar/asn1-schema": "^2.3.3",
"@pimlico/erc20-paymaster": "^0.0.12",
"@redux-devtools/cli": "^2.0.0",
"@redux-devtools/remote": "^0.8.0",
"@reduxjs/toolkit": "^1.9.1",
Expand Down
18 changes: 9 additions & 9 deletions src/exconfig.ts
Expand Up @@ -4,21 +4,21 @@ import { EVMNetwork } from './pages/Background/types/network';
export default {
enablePasswordEncryption: false,
showTransactionConfirmationScreen: true,
factory_address: '0x9406Cc6185a346906296840746125a0E44976454',
factory_address: '0xb6f4fB799a085ef048c796F22a74B5c646BF77d4',
stateVersion: '0.1',
network: {
chainID: '11155111',
chainID: '80001',
family: 'EVM',
name: 'Sepolia',
provider: 'https://sepolia.infura.io/v3/bdabe9d2f9244005af0f566398e648da',
name: 'Mumbai',
provider:
'https://polygon-mumbai.g.alchemy.com/v2/YdsNfZkPMSAefI7wwCnBFfXK0ZRz2F-k',
entryPointAddress: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
bundler: 'https://sepolia.voltaire.candidewallet.com/rpc',
bundler: 'https://mumbai.voltaire.candidewallet.com/rpc',
baseAsset: {
symbol: 'ETH',
name: 'ETH',
symbol: 'MATIC',
name: 'MATIC',
decimals: 18,
image:
'https://ethereum.org/static/6b935ac0e6194247347855dc3d328e83/6ed5f/eth-diamond-black.webp',
image: 'https://cryptologos.cc/logos/polygon-matic-logo.png?v=025',
},
} satisfies EVMNetwork,
};
153 changes: 145 additions & 8 deletions src/pages/Account/account-api/account-api.ts
@@ -1,11 +1,22 @@
import { ethers, Wallet } from 'ethers';
import { constants, ethers, Wallet } from 'ethers';
import { UserOperationStruct } from '@account-abstraction/contracts';

import { AccountApiParamsType, AccountApiType } from './types';
import { MessageSigningRequest } from '../../Background/redux-slices/signing';
import { TransactionDetailsForUserOp } from '@account-abstraction/sdk/dist/src/TransactionDetailsForUserOp';
import config from '../../../exconfig';
import { SimpleAccountAPI } from '@account-abstraction/sdk';
import { HttpRpcClient, SimpleAccountAPI } from '@account-abstraction/sdk';
import { hexConcat, resolveProperties } from 'ethers/lib/utils.js';
import {
ERC20__factory,
ERC20Paymaster,
getERC20Paymaster,
} from '@pimlico/erc20-paymaster';
import {
SimpleAccountWithPaymasterFactory,
SimpleAccountWithPaymasterFactory__factory,
} from './typechain-types';
import { NotPromise } from '@account-abstraction/utils';

const FACTORY_ADDRESS = config.factory_address;

Expand All @@ -20,6 +31,10 @@ class SimpleAccountTrampolineAPI
extends SimpleAccountAPI
implements AccountApiType
{
factoryWithPaymaster?: SimpleAccountWithPaymasterFactory;
erc20Paymaster?: ERC20Paymaster;
bundler: HttpRpcClient;

/**
*
* We create a new private key or use the one provided in the
Expand All @@ -34,6 +49,12 @@ class SimpleAccountTrampolineAPI
: ethers.Wallet.createRandom(),
factoryAddress: FACTORY_ADDRESS,
});
this.bundler = params.bundler;
}

async init(): Promise<this> {
this.erc20Paymaster = await getERC20Paymaster(this.provider, 'USDC');
return super.init();
}

/**
Expand All @@ -48,6 +69,45 @@ class SimpleAccountTrampolineAPI
};
};

/**
* return the value to put into the "initCode" field, if the account is not yet deployed.
* this value holds the "factory" address, followed by this account's information
*/
async getAccountInitCode() {
if (this.factoryAddress === undefined)
throw new Error('no factory to get initCode');

if (!this.erc20Paymaster) throw new Error('erc20Paymaster not initialized');

if (this.factoryWithPaymaster == null) {
this.factoryWithPaymaster =
SimpleAccountWithPaymasterFactory__factory.connect(
this.factoryAddress,
this.provider
);
}

const usdcTokenAddress = await this.erc20Paymaster.contract.token();
const usdcToken = ERC20__factory.connect(usdcTokenAddress, this.owner);
const erc20PaymasterAddress = this.erc20Paymaster.contract.address;

const approveData = usdcToken.interface.encodeFunctionData('approve', [
erc20PaymasterAddress,
constants.MaxUint256,
]);

return hexConcat([
this.factoryWithPaymaster.address,
this.factoryWithPaymaster.interface.encodeFunctionData('createAccount', [
await this.owner.getAddress(),
this.index,
usdcToken.address,
0,
approveData,
]),
]);
}

/**
* Called when the Dapp requests eth_signTypedData
*/
Expand All @@ -58,6 +118,88 @@ class SimpleAccountTrampolineAPI
throw new Error('signMessage method not implemented.');
};

adjustGasParameters = async (
userOp: NotPromise<UserOperationStruct>
): Promise<NotPromise<UserOperationStruct>> => {
userOp.nonce = ethers.BigNumber.from(userOp.nonce).toHexString();
userOp.callGasLimit = ethers.BigNumber.from(
userOp.callGasLimit
).toHexString();
userOp.verificationGasLimit = ethers.BigNumber.from(
userOp.verificationGasLimit
).toHexString();
userOp.preVerificationGas = ethers.BigNumber.from(
userOp.preVerificationGas
).toHexString();
userOp.maxFeePerGas = ethers.BigNumber.from(userOp.maxFeePerGas)
.mul(3) // Alchemy vs Candide have different gas prices
.toHexString();
userOp.maxPriorityFeePerGas = ethers.BigNumber.from(
userOp.maxPriorityFeePerGas
)
.mul(3)
.toHexString()
.toLowerCase();

const gasParameters = await this.bundler.estimateUserOpGas(
await this.signUserOp(userOp)
);

const estimatedGasLimit = ethers.BigNumber.from(
gasParameters?.callGasLimit
);
const estimateVerificationGasLimit = ethers.BigNumber.from(
gasParameters?.verificationGas
);
const estimatePreVerificationGas = ethers.BigNumber.from(
gasParameters?.preVerificationGas
);

userOp.callGasLimit = estimatedGasLimit.gt(
ethers.BigNumber.from(userOp.callGasLimit)
)
? estimatedGasLimit.toHexString()
: userOp.callGasLimit;

userOp.verificationGasLimit = estimateVerificationGasLimit.gt(
ethers.BigNumber.from(userOp.verificationGasLimit)
)
? estimateVerificationGasLimit.toHexString()
: userOp.verificationGasLimit;

userOp.preVerificationGas = estimatePreVerificationGas.gt(
ethers.BigNumber.from(userOp.preVerificationGas)
)
? estimatePreVerificationGas.toHexString()
: userOp.preVerificationGas;

return userOp;
};

createUnsignedUserOp = async (
info: TransactionDetailsForUserOp
): Promise<UserOperationStruct> => {
const userOp = await resolveProperties(
await super.createUnsignedUserOp(info)
);
// preVerificationGas predictions doesn't work properly on Mumbai network
userOp.preVerificationGas = ethers.BigNumber.from(
userOp.preVerificationGas
).gt(62660)
? userOp.preVerificationGas
: ethers.BigNumber.from(62660).toHexString();

if (!this.erc20Paymaster) throw new Error('erc20Paymaster not initialized');
const erc20PaymasterAndData =
await this.erc20Paymaster.generatePaymasterAndData(
await this.adjustGasParameters(userOp)
);
return {
...userOp,
paymasterAndData: erc20PaymasterAndData ? erc20PaymasterAndData : '0x',
};
};

/**
* Called after the user is presented with the pre-transaction confirmation screen
* The context passed to this method is the same as the one passed to the
Expand All @@ -67,12 +209,7 @@ class SimpleAccountTrampolineAPI
info: TransactionDetailsForUserOp,
preTransactionConfirmationContext?: any
): Promise<UserOperationStruct> {
return {
...(await this.createUnsignedUserOp(info)),
paymasterAndData: preTransactionConfirmationContext?.paymasterAndData
? preTransactionConfirmationContext?.paymasterAndData
: '0x',
};
return this.createUnsignedUserOp(info);
}

/**
Expand Down
Expand Up @@ -9,7 +9,7 @@ import {
TextField,
Typography,
} from '@mui/material';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import {
PreTransactionConfirmation,
PreTransactionConfirmationtProps,
Expand Down Expand Up @@ -116,32 +116,16 @@ const PreTransactionConfirmationComponent: PreTransactionConfirmation = ({
transaction,
onReject,
}: PreTransactionConfirmationtProps) => {
const [loader, setLoader] = React.useState<boolean>(false);
const [paymasterAndData, setPaymasterAndDataLocal] = useState<string>('');

return (
<>
<CardContent>
<Typography variant="h3" gutterBottom>
Dummy Component
Paymaster Demo
</Typography>
<Typography variant="body1" color="text.secondary">
You can show as many steps as you want in this dummy component. You
need to call the function <b>onComplete</b> passed as a props to this
component. <br />
<br />
The function takes a modifiedTransactions & context as a parameter,
the context will be passed to your AccountApi when creating a new
account. While modifiedTransactions will be agreed upon by the user.
<br />
This Component is defined in exported in{' '}
</Typography>
<Typography variant="caption">
trampoline/src/pages/Account/components/transaction/pre-transaction-confirmation.ts
We will be using <a href="https://docs.pimlico.io">docs.pimlico.io</a>{' '}
as our paymaster for the purpose of this demo.
</Typography>
<Box sx={{ mt: 4, mb: 4 }}>
<AddPaymasterAndData setPaymasterAndData={setPaymasterAndDataLocal} />
</Box>
</CardContent>
<CardActions sx={{ width: '100%' }}>
<Stack spacing={2} sx={{ width: '100%' }}>
Expand All @@ -150,8 +134,7 @@ const PreTransactionConfirmationComponent: PreTransactionConfirmation = ({
size="large"
variant="contained"
onClick={() => {
onComplete(transaction, { paymasterAndData });
setLoader(true);
onComplete(transaction);
}}
>
Continue
Expand Down