Skip to content

Commit

Permalink
AA-525: Create Simple7702Account (#536)
Browse files Browse the repository at this point in the history
Sample ERC-4337 account that uses EIP-7702.
Supports also direct (batched) calls from the EOA account itself.
  • Loading branch information
drortirosh authored Feb 16, 2025
1 parent 2bda8f9 commit 78e0a76
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 0 deletions.
86 changes: 86 additions & 0 deletions contracts/samples/Simple7702Account.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "../core/Helpers.sol";
import "../core/BaseAccount.sol";

/**
* Simple7702Account.sol
* A minimal account to be used with EIP-7702 (for batching) and ERC-4337 (for gas sponsoring)
*/
contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC721Holder {

struct Call {
address target;
uint256 value;
bytes data;
}

// temporary address of entryPoint v0.8
function entryPoint() public pure override returns (IEntryPoint) {
return IEntryPoint(0xE2b1C20D236ECe93b91f0656A9428C072e3F88Ad);
}

/**
* Make this account callable through ERC-4337 EntryPoint.
* The UserOperation should be signed by this account's private key.
*/
function _validateSignature(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal virtual override returns (uint256 validationData) {

return _checkSignature(userOpHash, userOp.signature) ? 0 : SIG_VALIDATION_FAILED;
}

function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) {
return _checkSignature(hash, signature) ? this.isValidSignature.selector : bytes4(0);
}

function _checkSignature(bytes32 hash, bytes memory signature) internal view returns (bool) {
return ECDSA.recover(hash, signature) == address(this);
}

function _requireFromSelfOrEntryPoint() internal view virtual {
require(
msg.sender == address(this) ||
msg.sender == address(entryPoint()),
"not from self or EntryPoint"
);
}


function execute(Call[] calldata calls) external {
_requireFromSelfOrEntryPoint();

for (uint256 i = 0; i < calls.length; i++) {
Call calldata call = calls[i];
(bool ok, bytes memory ret) = call.target.call{value: call.value}(call.data);
if (!ok) {
// solhint-disable-next-line no-inline-assembly
assembly {revert(add(ret, 32), mload(ret))}
}
}
}

function supportsInterface(bytes4 id) public override(ERC1155Holder, IERC165) pure returns (bool) {
return
id == type(IERC165).interfaceId ||
id == type(IAccount).interfaceId ||
id == type(IERC1271).interfaceId ||
id == type(IERC1155Receiver).interfaceId ||
id == type(IERC721Receiver).interfaceId;
}

// accept incoming calls (with or without value), to mimic an EOA.
fallback() external payable {
}

receive() external payable {
}
}
151 changes: 151 additions & 0 deletions test/eip7702-wallet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { expect } from 'chai'

import { Simple7702Account, Simple7702Account__factory, EntryPoint, TestPaymasterAcceptAll__factory } from '../typechain'
import { createAccountOwner, createAddress, deployEntryPoint } from './testutils'
import { fillAndSign, INITCODE_EIP7702_MARKER, packUserOp } from './UserOp'
import { hexConcat, parseEther } from 'ethers/lib/utils'
import { signEip7702Authorization } from './eip7702helpers'
import { GethExecutable } from './GethExecutable'
import { Wallet } from 'ethers'

describe('Simple7702Account.sol', function () {
// can't deploy coverage "entrypoint" on geth (contract too large)
if (process.env.COVERAGE != null) {
return
}

let entryPoint: EntryPoint

let eip7702delegate: Simple7702Account
let geth: GethExecutable

before(async function () {
geth = new GethExecutable()
await geth.init()

entryPoint = await deployEntryPoint(geth.provider)

eip7702delegate = await new Simple7702Account__factory(geth.provider.getSigner()).deploy()
expect(await eip7702delegate.entryPoint()).to.equal(entryPoint.address, 'fix entryPoint in Simple7702Account.sol')
console.log('set eip7702delegate=', eip7702delegate.address)
})

after(() => {
geth.done()
})

describe('sanity: normal 7702 batching', () => {
let eoa: Wallet
before(async () => {
eoa = createAccountOwner(geth.provider)

const auth = await signEip7702Authorization(eoa, {
chainId: 0,
nonce: 0,
address: eip7702delegate.address
})
const sendVal = parseEther('10')
const tx = {
to: eoa.address,
value: sendVal.toHexString(),
gas: 1e6,
authorizationList: [auth]
}
await geth.sendTx(tx)
expect(await geth.provider.getBalance(eoa.address)).to.equal(sendVal)
expect(await geth.provider.getCode(eoa.address)).to.equal(hexConcat(['0xef0100', eip7702delegate.address]))
})

it('should fail call from another account', async () => {
const wallet1 = Simple7702Account__factory.connect(eoa.address, geth.provider.getSigner())
await expect(wallet1.execute([])).to.revertedWith('not from self or EntryPoint')
})

it('should succeed sending a batch', async () => {
// submit a batch
const wallet2 = Simple7702Account__factory.connect(eoa.address, eoa)
console.log('eoa balance=', await geth.provider.getBalance(eoa.address))

const addr1 = createAddress()
const addr2 = createAddress()

await wallet2.execute([{
target: addr1, value: 1, data: '0x'
}, {
target: addr2, value: 2, data: '0x'
}]).then(async tx => tx.wait())
expect(await geth.provider.getBalance(addr1)).to.equal(1)
expect(await geth.provider.getBalance(addr2)).to.equal(2)
})
})

it('should be able to use EntryPoint without paymaster', async () => {
const addr1 = createAddress()
const eoa = createAccountOwner(geth.provider)

const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{
target: addr1,
value: 1,
data: '0x'
}]])
const userop = await fillAndSign({
sender: eoa.address,
initCode: INITCODE_EIP7702_MARKER,
nonce: 0,
callData
}, eoa, entryPoint, { eip7702delegate: eip7702delegate.address })

await geth.sendTx({ to: eoa.address, value: parseEther('1') })
const auth = await signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address })
const beneficiary = createAddress()
// submit separate tx with tuple: geth's estimateGas doesn't work, and its easier to detect thrown errors..
await geth.sendTx({
to: entryPoint.address,
data: '0x',
gas: 1000000,
authorizationList: [auth]
})
const handleOps = entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(userop)], beneficiary])
const tx = {
to: entryPoint.address,
data: handleOps
}
await geth.sendTx(tx)
})

it('should use EntryPoint with paymaster', async () => {
const addr1 = createAddress()
const eoa = createAccountOwner(geth.provider)
const paymaster = await new TestPaymasterAcceptAll__factory(geth.provider.getSigner()).deploy(entryPoint.address)
await paymaster.deposit({ value: parseEther('1') })
const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{
target: addr1,
value: 1,
data: '0x'
}]])
const userop = await fillAndSign({
sender: eoa.address,
paymaster: paymaster.address,
initCode: INITCODE_EIP7702_MARKER,
nonce: 0,
callData
}, eoa, entryPoint, { eip7702delegate: eip7702delegate.address })

const auth = await signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address })
const beneficiary = createAddress()
console.log('delegate=', eip7702delegate.address)
// submit separate tx with tuple: geth's estimateGas doesn't work, and its easier to detect thrown errors..
await geth.sendTx({
to: entryPoint.address,
data: '0x',
gas: 1000000,
authorizationList: [auth]
})
const handleOps = entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(userop)], beneficiary])
const tx = {
to: entryPoint.address,
data: handleOps
}
await geth.sendTx(tx)
})
})

0 comments on commit 78e0a76

Please sign in to comment.