-
Notifications
You must be signed in to change notification settings - Fork 687
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
AA-525: Create Simple7702Account (#536)
Sample ERC-4337 account that uses EIP-7702. Supports also direct (batched) calls from the EOA account itself.
- Loading branch information
1 parent
2bda8f9
commit 78e0a76
Showing
2 changed files
with
237 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) |