Skip to content

Commit

Permalink
Merge branch 'master' into aa/account-extension-erc7579
Browse files Browse the repository at this point in the history
  • Loading branch information
Amxx committed Dec 20, 2024
2 parents 8f18ac6 + abeec1a commit 23fbdf3
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 151 deletions.
10 changes: 8 additions & 2 deletions contracts/account/AccountCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,14 @@ abstract contract AccountCore is AbstractSigner, EIP712, IAccount, IAccountExecu
PackedUserOperation calldata userOp,
bytes32 /*userOpHash*/
) public virtual onlyEntryPointOrSelf {
(address target, uint256 value, bytes memory data) = abi.decode(userOp.callData[4:], (address, uint256, bytes));
Address.functionCallWithValue(target, data, value);
// decode packed calldata
address target = address(bytes20(userOp.callData[4:24]));
uint256 value = uint256(bytes32(userOp.callData[24:56]));
bytes calldata data = userOp.callData[56:];

// we cannot use `Address.functionCallWithValue` here as it would revert on EOA targets
(bool success, bytes memory returndata) = target.call{value: value}(data);
Address.verifyCallResult(success, returndata);
}

/**
Expand Down
225 changes: 95 additions & 130 deletions test/account/Account.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ const {
shouldSupportInterfaces,
} = require('@openzeppelin/contracts/test/utils/introspection/SupportsInterface.behavior');

function shouldBehaveLikeAnAccountBase() {
const value = ethers.parseEther('0.1');

function shouldBehaveLikeAccountCore() {
describe('entryPoint', function () {
it('should return the canonical entrypoint', async function () {
await this.mock.deploy();
expect(await this.mock.entryPoint()).to.equal(entrypoint);
expect(this.mock.entryPoint()).to.eventually.equal(entrypoint);
});
});

Expand All @@ -23,18 +25,8 @@ function shouldBehaveLikeAnAccountBase() {
});

it('should revert if the caller is not the canonical entrypoint', async function () {
const selector = this.mock.interface.getFunction('executeUserOp').selector;
const operation = await this.mock
.createUserOp({
callData: ethers.concat([
selector,
ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256', 'bytes'],
[this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')],
),
]),
})
.then(op => this.signUserOp(op));
// empty operation (does nothing)
const operation = await this.mock.createUserOp({}).then(op => this.signUserOp(op));

await expect(this.mock.connect(this.other).validateUserOp(operation.packed, operation.hash(), 0))
.to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
Expand All @@ -47,18 +39,8 @@ function shouldBehaveLikeAnAccountBase() {
});

it('should return SIG_VALIDATION_SUCCESS if the signature is valid', async function () {
const selector = this.mock.interface.getFunction('executeUserOp').selector;
const operation = await this.mock
.createUserOp({
callData: ethers.concat([
selector,
ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256', 'bytes'],
[this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')],
),
]),
})
.then(op => this.signUserOp(op));
// empty operation (does nothing)
const operation = await this.mock.createUserOp({}).then(op => this.signUserOp(op));

expect(
await this.mock
Expand All @@ -68,17 +50,8 @@ function shouldBehaveLikeAnAccountBase() {
});

it('should return SIG_VALIDATION_FAILURE if the signature is invalid', async function () {
const selector = this.mock.interface.getFunction('executeUserOp').selector;
const operation = await this.mock.createUserOp({
callData: ethers.concat([
selector,
ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256', 'bytes'],
[this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')],
),
]),
});

// empty operation (does nothing)
const operation = await this.mock.createUserOp({});
operation.signature = '0x00';

expect(
Expand All @@ -89,46 +62,24 @@ function shouldBehaveLikeAnAccountBase() {
});

it('should pay missing account funds for execution', async function () {
const selector = this.mock.interface.getFunction('executeUserOp').selector;
const operation = await this.mock
.createUserOp({
callData: ethers.concat([
selector,
ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256', 'bytes'],
[this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')],
),
]),
})
.then(op => this.signUserOp(op));

const prevAccountBalance = await ethers.provider.getBalance(this.mock);
const prevEntrypointBalance = await ethers.provider.getBalance(entrypoint);
const amount = ethers.parseEther('0.1');

const tx = await this.mock
.connect(this.entrypointAsSigner)
.validateUserOp(operation.packed, operation.hash(), amount);
// empty operation (does nothing)
const operation = await this.mock.createUserOp({}).then(op => this.signUserOp(op));

const receipt = await tx.wait();
const callerFees = receipt.gasUsed * tx.gasPrice;

expect(await ethers.provider.getBalance(this.mock)).to.equal(prevAccountBalance - amount);
expect(await ethers.provider.getBalance(entrypoint)).to.equal(prevEntrypointBalance + amount - callerFees);
await expect(
this.mock.connect(this.entrypointAsSigner).validateUserOp(operation.packed, operation.hash(), value),
).to.changeEtherBalances([this.mock, entrypoint], [-value, value]);
});
});
});

describe('fallback', function () {
it('should receive ether', async function () {
await this.mock.deploy();
await setBalance(this.other.address, ethers.parseEther('1'));

const prevBalance = await ethers.provider.getBalance(this.mock);
const amount = ethers.parseEther('0.1');
await this.other.sendTransaction({ to: this.mock, value: amount });

expect(await ethers.provider.getBalance(this.mock)).to.equal(prevBalance + amount);
await expect(this.other.sendTransaction({ to: this.mock, value })).to.changeEtherBalances(
[this.other, this.mock],
[-value, value],
);
});
});
}
Expand All @@ -147,76 +98,85 @@ function shouldBehaveLikeAccountHolder() {
const data = '0x12345678';

beforeEach(async function () {
[this.owner] = await ethers.getSigners();
this.token = await ethers.deployContract('$ERC1155Mock', ['https://somedomain.com/{id}.json']);
await this.token.$_mintBatch(this.owner, ids, values, '0x');
await this.token.$_mintBatch(this.other, ids, values, '0x');
});

it('receives ERC1155 tokens from a single ID', async function () {
await this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, ids[0], values[0], data);
expect(await this.token.balanceOf(this.mock, ids[0])).to.equal(values[0]);
for (let i = 1; i < ids.length; i++) {
expect(await this.token.balanceOf(this.mock, ids[i])).to.equal(0n);
}
await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, ids[0], values[0], data);

expect(
this.token.balanceOfBatch(
ids.map(() => this.mock),
ids,
),
).to.eventually.deep.equal(values.map((v, i) => (i == 0 ? v : 0n)));
});

it('receives ERC1155 tokens from a multiple IDs', async function () {
expect(
await this.token.balanceOfBatch(
this.token.balanceOfBatch(
ids.map(() => this.mock),
ids,
),
).to.deep.equal(ids.map(() => 0n));
await this.token.connect(this.owner).safeBatchTransferFrom(this.owner, this.mock, ids, values, data);
).to.eventually.deep.equal(ids.map(() => 0n));

await this.token.connect(this.other).safeBatchTransferFrom(this.other, this.mock, ids, values, data);
expect(
await this.token.balanceOfBatch(
this.token.balanceOfBatch(
ids.map(() => this.mock),
ids,
),
).to.deep.equal(values);
).to.eventually.deep.equal(values);
});
});

describe('onERC721Received', function () {
it('receives an ERC721 token', async function () {
const name = 'Some NFT';
const symbol = 'SNFT';
const tokenId = 1n;
const tokenId = 1n;

const [owner] = await ethers.getSigners();

const token = await ethers.deployContract('$ERC721Mock', [name, symbol]);
await token.$_mint(owner, tokenId);
beforeEach(async function () {
this.token = await ethers.deployContract('$ERC721Mock', ['Some NFT', 'SNFT']);
await this.token.$_mint(this.other, tokenId);
});

await token.connect(owner).safeTransferFrom(owner, this.mock, tokenId);
it('receives an ERC721 token', async function () {
await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, tokenId);

expect(await token.ownerOf(tokenId)).to.equal(this.mock);
expect(this.token.ownerOf(tokenId)).to.eventually.equal(this.mock);
});
});
});
}

function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) {
function shouldBehaveLikeAccountExecutor({ deployable = true } = {}) {
describe('executeUserOp', function () {
beforeEach(async function () {
// give eth to the account (before deployment)
await setBalance(this.mock.target, ethers.parseEther('1'));
expect(await ethers.provider.getCode(this.mock)).to.equal('0x');
this.entrypointAsSigner = await impersonate(entrypoint.target);

// account is not initially deployed
expect(ethers.provider.getCode(this.mock)).to.eventually.equal('0x');

this.encodeUserOpCalldata = (to, value, calldata) =>
ethers.concat([
this.mock.interface.getFunction('executeUserOp').selector,
ethers.solidityPacked(
['address', 'uint256', 'bytes'],
[to.target ?? to.address ?? to, value ?? 0, calldata ?? '0x'],
),
]);
});

it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
await this.mock.deploy();

const selector = this.mock.interface.getFunction('executeUserOp').selector;
const operation = await this.mock
.createUserOp({
callData: ethers.concat([
selector,
ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256', 'bytes'],
[this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')],
),
]),
callData: this.encodeUserOpCalldata(
this.target,
0,
this.target.interface.encodeFunctionData('mockFunctionExtra'),
),
})
.then(op => this.signUserOp(op));

Expand All @@ -228,39 +188,34 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) {
if (deployable) {
describe('when not deployed', function () {
it('should be created with handleOps and increase nonce', async function () {
const selector = this.mock.interface.getFunction('executeUserOp').selector;
const operation = await this.mock
.createUserOp({
callData: ethers.concat([
selector,
ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256', 'bytes'],
[this.target.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')],
),
]),
callData: this.encodeUserOpCalldata(
this.target,
17,
this.target.interface.encodeFunctionData('mockFunctionExtra'),
),
})
.then(op => op.addInitCode())
.then(op => this.signUserOp(op));

expect(this.mock.getNonce()).to.eventually.equal(0);
await expect(entrypoint.handleOps([operation.packed], this.beneficiary))
.to.emit(entrypoint, 'AccountDeployed')
.withArgs(operation.hash(), this.mock, this.factory, ethers.ZeroAddress)
.to.emit(this.target, 'MockFunctionCalledExtra')
.withArgs(this.mock, 17);
expect(await this.mock.getNonce()).to.equal(1);
expect(this.mock.getNonce()).to.eventually.equal(1);
});

it('should revert if the signature is invalid', async function () {
const selector = this.mock.interface.getFunction('executeUserOp').selector;
const operation = await this.mock
.createUserOp({
callData: ethers.concat([
selector,
ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256', 'bytes'],
[this.target.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')],
),
]),
callData: this.encodeUserOpCalldata(
this.target,
17,
this.target.interface.encodeFunctionData('mockFunctionExtra'),
),
})
.then(op => op.addInitCode());

Expand All @@ -277,31 +232,41 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) {
});

it('should increase nonce and call target', async function () {
const selector = this.mock.interface.getFunction('executeUserOp').selector;
const operation = await this.mock
.createUserOp({
callData: ethers.concat([
selector,
ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256', 'bytes'],
[this.target.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')],
),
]),
callData: this.encodeUserOpCalldata(
this.target,
42,
this.target.interface.encodeFunctionData('mockFunctionExtra'),
),
})
.then(op => this.signUserOp(op));

expect(await this.mock.getNonce()).to.equal(0);
expect(this.mock.getNonce()).to.eventually.equal(0);
await expect(entrypoint.handleOps([operation.packed], this.beneficiary))
.to.emit(this.target, 'MockFunctionCalledExtra')
.withArgs(this.mock, 42);
expect(await this.mock.getNonce()).to.equal(1);
expect(this.mock.getNonce()).to.eventually.equal(1);
});

it('should support sending eth to an EOA', async function () {
const operation = await this.mock
.createUserOp({ callData: this.encodeUserOpCalldata(this.other, value) })
.then(op => this.signUserOp(op));

expect(this.mock.getNonce()).to.eventually.equal(0);
await expect(entrypoint.handleOps([operation.packed], this.beneficiary)).to.changeEtherBalance(
this.other,
value,
);
expect(this.mock.getNonce()).to.eventually.equal(1);
});
});
});
}

module.exports = {
shouldBehaveLikeAnAccountBase,
shouldBehaveLikeAccountCore,
shouldBehaveLikeAccountHolder,
shouldBehaveLikeAnAccountBaseExecutor,
shouldBehaveLikeAccountExecutor,
};
6 changes: 3 additions & 3 deletions test/account/AccountBase.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { ERC4337Helper } = require('../helpers/erc4337');
const { NonNativeSigner } = require('../helpers/signers');

const { shouldBehaveLikeAnAccountBase, shouldBehaveLikeAnAccountBaseExecutor } = require('./Account.behavior');
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountExecutor } = require('./Account.behavior');

async function fixture() {
// EOAs and environment
Expand Down Expand Up @@ -31,6 +31,6 @@ describe('AccountBase', function () {
Object.assign(this, await loadFixture(fixture));
});

shouldBehaveLikeAnAccountBase();
shouldBehaveLikeAnAccountBaseExecutor();
shouldBehaveLikeAccountCore();
shouldBehaveLikeAccountExecutor();
});
Loading

0 comments on commit 23fbdf3

Please sign in to comment.