Add Account framework (#5657)
This commit is contained in:
563
test/account/extensions/AccountERC7579.behavior.js
Normal file
563
test/account/extensions/AccountERC7579.behavior.js
Normal file
@ -0,0 +1,563 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { impersonate } = require('../../helpers/account');
|
||||
const { selector } = require('../../helpers/methods');
|
||||
const { zip } = require('../../helpers/iterate');
|
||||
const {
|
||||
encodeMode,
|
||||
encodeBatch,
|
||||
encodeSingle,
|
||||
encodeDelegate,
|
||||
MODULE_TYPE_VALIDATOR,
|
||||
MODULE_TYPE_EXECUTOR,
|
||||
MODULE_TYPE_FALLBACK,
|
||||
MODULE_TYPE_HOOK,
|
||||
CALL_TYPE_CALL,
|
||||
CALL_TYPE_BATCH,
|
||||
CALL_TYPE_DELEGATE,
|
||||
EXEC_TYPE_DEFAULT,
|
||||
EXEC_TYPE_TRY,
|
||||
} = require('../../helpers/erc7579');
|
||||
|
||||
const CALL_TYPE_INVALID = '0x42';
|
||||
const EXEC_TYPE_INVALID = '0x17';
|
||||
const MODULE_TYPE_INVALID = 999n;
|
||||
|
||||
const coder = ethers.AbiCoder.defaultAbiCoder();
|
||||
|
||||
function shouldBehaveLikeAccountERC7579({ withHooks = false } = {}) {
|
||||
describe('AccountERC7579', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mock.deploy();
|
||||
await this.other.sendTransaction({ to: this.mock.target, value: ethers.parseEther('1') });
|
||||
|
||||
this.modules = {};
|
||||
this.modules[MODULE_TYPE_VALIDATOR] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_VALIDATOR]);
|
||||
this.modules[MODULE_TYPE_EXECUTOR] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_EXECUTOR]);
|
||||
this.modules[MODULE_TYPE_FALLBACK] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_FALLBACK]);
|
||||
this.modules[MODULE_TYPE_HOOK] = await ethers.deployContract('$ERC7579HookMock');
|
||||
|
||||
this.mockFromEntrypoint = this.mock.connect(await impersonate(entrypoint.v08.target));
|
||||
this.mockFromExecutor = this.mock.connect(await impersonate(this.modules[MODULE_TYPE_EXECUTOR].target));
|
||||
});
|
||||
|
||||
describe('accountId', function () {
|
||||
it('should return the account ID', async function () {
|
||||
await expect(this.mock.accountId()).to.eventually.equal(
|
||||
withHooks
|
||||
? '@openzeppelin/community-contracts.AccountERC7579Hooked.v0.0.0'
|
||||
: '@openzeppelin/community-contracts.AccountERC7579.v0.0.0',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('supportsExecutionMode', function () {
|
||||
for (const [callType, execType] of zip(
|
||||
[CALL_TYPE_CALL, CALL_TYPE_BATCH, CALL_TYPE_DELEGATE, CALL_TYPE_INVALID],
|
||||
[EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY, EXEC_TYPE_INVALID],
|
||||
)) {
|
||||
const result = callType != CALL_TYPE_INVALID && execType != EXEC_TYPE_INVALID;
|
||||
|
||||
it(`${
|
||||
result ? 'does not support' : 'supports'
|
||||
} CALL_TYPE=${callType} and EXEC_TYPE=${execType} execution mode`, async function () {
|
||||
await expect(this.mock.supportsExecutionMode(encodeMode({ callType, execType }))).to.eventually.equal(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('supportsModule', function () {
|
||||
it('supports MODULE_TYPE_VALIDATOR module type', async function () {
|
||||
await expect(this.mock.supportsModule(MODULE_TYPE_VALIDATOR)).to.eventually.equal(true);
|
||||
});
|
||||
|
||||
it('supports MODULE_TYPE_EXECUTOR module type', async function () {
|
||||
await expect(this.mock.supportsModule(MODULE_TYPE_EXECUTOR)).to.eventually.equal(true);
|
||||
});
|
||||
|
||||
it('supports MODULE_TYPE_FALLBACK module type', async function () {
|
||||
await expect(this.mock.supportsModule(MODULE_TYPE_FALLBACK)).to.eventually.equal(true);
|
||||
});
|
||||
|
||||
it(
|
||||
withHooks ? 'supports MODULE_TYPE_HOOK module type' : 'does not support MODULE_TYPE_HOOK module type',
|
||||
async function () {
|
||||
await expect(this.mock.supportsModule(MODULE_TYPE_HOOK)).to.eventually.equal(withHooks);
|
||||
},
|
||||
);
|
||||
|
||||
it('does not support invalid module type', async function () {
|
||||
await expect(this.mock.supportsModule(MODULE_TYPE_INVALID)).to.eventually.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('module installation', function () {
|
||||
it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
|
||||
await expect(this.mock.connect(this.other).installModule(MODULE_TYPE_VALIDATOR, this.mock, '0x'))
|
||||
.to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
|
||||
.withArgs(this.other);
|
||||
});
|
||||
|
||||
it('should revert if the module type is not supported', async function () {
|
||||
await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_INVALID, this.mock, '0x'))
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedModuleType')
|
||||
.withArgs(MODULE_TYPE_INVALID);
|
||||
});
|
||||
|
||||
it('should revert if the module is not the provided type', async function () {
|
||||
const instance = this.modules[MODULE_TYPE_EXECUTOR];
|
||||
await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_VALIDATOR, instance, '0x'))
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579MismatchedModuleTypeId')
|
||||
.withArgs(MODULE_TYPE_VALIDATOR, instance);
|
||||
});
|
||||
|
||||
for (const moduleTypeId of [
|
||||
MODULE_TYPE_VALIDATOR,
|
||||
MODULE_TYPE_EXECUTOR,
|
||||
MODULE_TYPE_FALLBACK,
|
||||
withHooks && MODULE_TYPE_HOOK,
|
||||
].filter(Boolean)) {
|
||||
const prefix = moduleTypeId == MODULE_TYPE_FALLBACK ? '0x12345678' : '0x';
|
||||
const initData = ethers.hexlify(ethers.randomBytes(256));
|
||||
const fullData = ethers.concat([prefix, initData]);
|
||||
|
||||
it(`should install a module of type ${moduleTypeId}`, async function () {
|
||||
const instance = this.modules[moduleTypeId];
|
||||
|
||||
await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(false);
|
||||
|
||||
await expect(this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData))
|
||||
.to.emit(this.mock, 'ModuleInstalled')
|
||||
.withArgs(moduleTypeId, instance)
|
||||
.to.emit(instance, 'ModuleInstalledReceived')
|
||||
.withArgs(this.mock, initData); // After decoding MODULE_TYPE_FALLBACK, it should remove the fnSig
|
||||
|
||||
await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
|
||||
});
|
||||
|
||||
it(`does not allow to install a module of ${moduleTypeId} id twice`, async function () {
|
||||
const instance = this.modules[moduleTypeId];
|
||||
|
||||
await this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData);
|
||||
|
||||
await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
|
||||
|
||||
await expect(this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData))
|
||||
.to.be.revertedWithCustomError(
|
||||
this.mock,
|
||||
moduleTypeId == MODULE_TYPE_HOOK ? 'ERC7579HookModuleAlreadyPresent' : 'ERC7579AlreadyInstalledModule',
|
||||
)
|
||||
.withArgs(...[moduleTypeId != MODULE_TYPE_HOOK && moduleTypeId, instance].filter(Boolean));
|
||||
});
|
||||
}
|
||||
|
||||
withHooks &&
|
||||
describe('with hook', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
|
||||
});
|
||||
|
||||
it('should call the hook of the installed module when performing an module install', async function () {
|
||||
const instance = this.modules[MODULE_TYPE_EXECUTOR];
|
||||
const initData = ethers.hexlify(ethers.randomBytes(256));
|
||||
|
||||
const precheckData = this.mock.interface.encodeFunctionData('installModule', [
|
||||
MODULE_TYPE_EXECUTOR,
|
||||
instance.target,
|
||||
initData,
|
||||
]);
|
||||
|
||||
await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_EXECUTOR, instance, initData))
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
|
||||
.withArgs(entrypoint.v08, 0n, precheckData)
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
|
||||
.withArgs(precheckData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('module uninstallation', function () {
|
||||
it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
|
||||
await expect(this.mock.connect(this.other).uninstallModule(MODULE_TYPE_VALIDATOR, this.mock, '0x'))
|
||||
.to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
|
||||
.withArgs(this.other);
|
||||
});
|
||||
|
||||
it('should revert if the module type is not supported', async function () {
|
||||
await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_INVALID, this.mock, '0x'))
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedModuleType')
|
||||
.withArgs(MODULE_TYPE_INVALID);
|
||||
});
|
||||
|
||||
for (const moduleTypeId of [
|
||||
MODULE_TYPE_VALIDATOR,
|
||||
MODULE_TYPE_EXECUTOR,
|
||||
MODULE_TYPE_FALLBACK,
|
||||
withHooks && MODULE_TYPE_HOOK,
|
||||
].filter(Boolean)) {
|
||||
const prefix = moduleTypeId == MODULE_TYPE_FALLBACK ? '0x12345678' : '0x';
|
||||
const initData = ethers.hexlify(ethers.randomBytes(256));
|
||||
const fullData = ethers.concat([prefix, initData]);
|
||||
|
||||
it(`should uninstall a module of type ${moduleTypeId}`, async function () {
|
||||
const instance = this.modules[moduleTypeId];
|
||||
|
||||
await this.mock.$_installModule(moduleTypeId, instance, fullData);
|
||||
|
||||
await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
|
||||
|
||||
await expect(this.mockFromEntrypoint.uninstallModule(moduleTypeId, instance, fullData))
|
||||
.to.emit(this.mock, 'ModuleUninstalled')
|
||||
.withArgs(moduleTypeId, instance)
|
||||
.to.emit(instance, 'ModuleUninstalledReceived')
|
||||
.withArgs(this.mock, initData); // After decoding MODULE_TYPE_FALLBACK, it should remove the fnSig
|
||||
|
||||
await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(false);
|
||||
});
|
||||
|
||||
it(`should revert uninstalling a module of type ${moduleTypeId} if it was not installed`, async function () {
|
||||
const instance = this.modules[moduleTypeId];
|
||||
|
||||
await expect(this.mockFromEntrypoint.uninstallModule(moduleTypeId, instance, fullData))
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579UninstalledModule')
|
||||
.withArgs(moduleTypeId, instance);
|
||||
});
|
||||
}
|
||||
|
||||
it('should revert uninstalling a module of type MODULE_TYPE_FALLBACK if a different module was installed for the provided selector', async function () {
|
||||
const instance = this.modules[MODULE_TYPE_FALLBACK];
|
||||
const anotherInstance = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_FALLBACK]);
|
||||
const initData = '0x12345678abcdef';
|
||||
|
||||
await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_FALLBACK, instance, initData);
|
||||
await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_FALLBACK, anotherInstance, initData))
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579UninstalledModule')
|
||||
.withArgs(MODULE_TYPE_FALLBACK, anotherInstance);
|
||||
});
|
||||
|
||||
withHooks &&
|
||||
describe('with hook', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
|
||||
});
|
||||
|
||||
it('should call the hook of the installed module when performing a module uninstall', async function () {
|
||||
const instance = this.modules[MODULE_TYPE_EXECUTOR];
|
||||
const initData = ethers.hexlify(ethers.randomBytes(256));
|
||||
|
||||
const precheckData = this.mock.interface.encodeFunctionData('uninstallModule', [
|
||||
MODULE_TYPE_EXECUTOR,
|
||||
instance.target,
|
||||
initData,
|
||||
]);
|
||||
|
||||
await this.mock.$_installModule(MODULE_TYPE_EXECUTOR, instance, initData);
|
||||
await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_EXECUTOR, instance, initData))
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
|
||||
.withArgs(entrypoint.v08, 0n, precheckData)
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
|
||||
.withArgs(precheckData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('execution', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mock.$_installModule(MODULE_TYPE_EXECUTOR, this.modules[MODULE_TYPE_EXECUTOR], '0x');
|
||||
});
|
||||
|
||||
for (const [execFn, mock] of [
|
||||
['execute', 'mockFromEntrypoint'],
|
||||
['executeFromExecutor', 'mockFromExecutor'],
|
||||
]) {
|
||||
describe(`executing with ${execFn}`, function () {
|
||||
it('should revert if the call type is not supported', async function () {
|
||||
await expect(
|
||||
this[mock][execFn](encodeMode({ callType: CALL_TYPE_INVALID }), encodeSingle(this.other, 0, '0x')),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedCallType')
|
||||
.withArgs(ethers.solidityPacked(['bytes1'], [CALL_TYPE_INVALID]));
|
||||
});
|
||||
|
||||
it('should revert if the caller is not authorized / installed', async function () {
|
||||
const error = execFn == 'execute' ? 'AccountUnauthorized' : 'ERC7579UninstalledModule';
|
||||
const args = execFn == 'execute' ? [this.other] : [MODULE_TYPE_EXECUTOR, this.other];
|
||||
|
||||
await expect(
|
||||
this[mock]
|
||||
.connect(this.other)
|
||||
[execFn](encodeMode({ callType: CALL_TYPE_CALL }), encodeSingle(this.other, 0, '0x')),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.mock, error)
|
||||
.withArgs(...args);
|
||||
});
|
||||
|
||||
describe('single execution', function () {
|
||||
it('calls the target with value and args', async function () {
|
||||
const value = 0x432;
|
||||
const data = encodeSingle(
|
||||
this.target,
|
||||
value,
|
||||
this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
|
||||
);
|
||||
|
||||
const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL }), data);
|
||||
|
||||
await expect(tx).to.emit(this.target, 'MockFunctionCalledWithArgs').withArgs(42, '0x1234');
|
||||
await expect(tx).to.changeEtherBalances([this.mock, this.target], [-value, value]);
|
||||
});
|
||||
|
||||
it('reverts when target reverts in default ExecType', async function () {
|
||||
const value = 0x012;
|
||||
const data = encodeSingle(
|
||||
this.target,
|
||||
value,
|
||||
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
);
|
||||
|
||||
await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL }), data)).to.be.revertedWith(
|
||||
'CallReceiverMock: reverting',
|
||||
);
|
||||
});
|
||||
|
||||
it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
|
||||
const value = 0x012;
|
||||
const data = encodeSingle(
|
||||
this.target,
|
||||
value,
|
||||
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
);
|
||||
|
||||
await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL, execType: EXEC_TYPE_TRY }), data))
|
||||
.to.emit(this.mock, 'ERC7579TryExecuteFail')
|
||||
.withArgs(
|
||||
CALL_TYPE_CALL,
|
||||
ethers.solidityPacked(
|
||||
['bytes4', 'bytes'],
|
||||
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('batch execution', function () {
|
||||
it('calls the targets with value and args', async function () {
|
||||
const value1 = 0x012;
|
||||
const value2 = 0x234;
|
||||
const data = encodeBatch(
|
||||
[this.target, value1, this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234'])],
|
||||
[
|
||||
this.anotherTarget,
|
||||
value2,
|
||||
this.anotherTarget.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
|
||||
],
|
||||
);
|
||||
|
||||
const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH }), data);
|
||||
await expect(tx)
|
||||
.to.emit(this.target, 'MockFunctionCalledWithArgs')
|
||||
.to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs');
|
||||
await expect(tx).to.changeEtherBalances(
|
||||
[this.mock, this.target, this.anotherTarget],
|
||||
[-value1 - value2, value1, value2],
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when any target reverts in default ExecType', async function () {
|
||||
const value1 = 0x012;
|
||||
const value2 = 0x234;
|
||||
const data = encodeBatch(
|
||||
[this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
|
||||
[
|
||||
this.anotherTarget,
|
||||
value2,
|
||||
this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
],
|
||||
);
|
||||
|
||||
await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH }), data)).to.be.revertedWith(
|
||||
'CallReceiverMock: reverting',
|
||||
);
|
||||
});
|
||||
|
||||
it('emits ERC7579TryExecuteFail event when any target reverts in try ExecType', async function () {
|
||||
const value1 = 0x012;
|
||||
const value2 = 0x234;
|
||||
const data = encodeBatch(
|
||||
[this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
|
||||
[
|
||||
this.anotherTarget,
|
||||
value2,
|
||||
this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
],
|
||||
);
|
||||
|
||||
const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH, execType: EXEC_TYPE_TRY }), data);
|
||||
|
||||
await expect(tx)
|
||||
.to.emit(this.mock, 'ERC7579TryExecuteFail')
|
||||
.withArgs(
|
||||
CALL_TYPE_BATCH,
|
||||
ethers.solidityPacked(
|
||||
['bytes4', 'bytes'],
|
||||
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
|
||||
),
|
||||
);
|
||||
|
||||
await expect(tx).to.changeEtherBalances(
|
||||
[this.mock, this.target, this.anotherTarget],
|
||||
[-value1, value1, 0],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delegate call execution', function () {
|
||||
it('delegate calls the target', async function () {
|
||||
const slot = ethers.hexlify(ethers.randomBytes(32));
|
||||
const value = ethers.hexlify(ethers.randomBytes(32));
|
||||
const data = encodeDelegate(
|
||||
this.target,
|
||||
this.target.interface.encodeFunctionData('mockFunctionWritesStorage', [slot, value]),
|
||||
);
|
||||
|
||||
await expect(ethers.provider.getStorage(this.mock.target, slot)).to.eventually.equal(ethers.ZeroHash);
|
||||
await this[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE }), data);
|
||||
await expect(ethers.provider.getStorage(this.mock.target, slot)).to.eventually.equal(value);
|
||||
});
|
||||
|
||||
it('reverts when target reverts in default ExecType', async function () {
|
||||
const data = encodeDelegate(
|
||||
this.target,
|
||||
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
);
|
||||
await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE }), data)).to.be.revertedWith(
|
||||
'CallReceiverMock: reverting',
|
||||
);
|
||||
});
|
||||
|
||||
it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
|
||||
const data = encodeDelegate(
|
||||
this.target,
|
||||
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
);
|
||||
await expect(
|
||||
this[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE, execType: EXEC_TYPE_TRY }), data),
|
||||
)
|
||||
.to.emit(this.mock, 'ERC7579TryExecuteFail')
|
||||
.withArgs(
|
||||
CALL_TYPE_CALL,
|
||||
ethers.solidityPacked(
|
||||
['bytes4', 'bytes'],
|
||||
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
withHooks &&
|
||||
describe('with hook', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
|
||||
});
|
||||
|
||||
it(`should call the hook of the installed module when executing ${execFn}`, async function () {
|
||||
const caller = execFn === 'execute' ? entrypoint.v08 : this.modules[MODULE_TYPE_EXECUTOR];
|
||||
const value = 17;
|
||||
const data = this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']);
|
||||
|
||||
const mode = encodeMode({ callType: CALL_TYPE_CALL });
|
||||
const call = encodeSingle(this.target, value, data);
|
||||
const precheckData = this[mock].interface.encodeFunctionData(execFn, [mode, call]);
|
||||
|
||||
const tx = this[mock][execFn](mode, call, { value });
|
||||
|
||||
await expect(tx)
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
|
||||
.withArgs(caller, value, precheckData)
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
|
||||
.withArgs(precheckData);
|
||||
await expect(tx).to.changeEtherBalances([caller, this.mock, this.target], [-value, 0n, value]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('fallback', function () {
|
||||
beforeEach(async function () {
|
||||
this.fallbackHandler = await ethers.deployContract('$ERC7579FallbackHandlerMock');
|
||||
});
|
||||
|
||||
it('reverts if there is no fallback module installed', async function () {
|
||||
const { selector } = this.fallbackHandler.callPayable.getFragment();
|
||||
|
||||
await expect(this.fallbackHandler.attach(this.mock).callPayable())
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579MissingFallbackHandler')
|
||||
.withArgs(selector);
|
||||
});
|
||||
|
||||
describe('with a fallback module installed', function () {
|
||||
beforeEach(async function () {
|
||||
await Promise.all(
|
||||
[
|
||||
this.fallbackHandler.callPayable.getFragment().selector,
|
||||
this.fallbackHandler.callView.getFragment().selector,
|
||||
this.fallbackHandler.callRevert.getFragment().selector,
|
||||
].map(selector =>
|
||||
this.mock.$_installModule(
|
||||
MODULE_TYPE_FALLBACK,
|
||||
this.fallbackHandler,
|
||||
coder.encode(['bytes4', 'bytes'], [selector, '0x']),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards the call to the fallback handler', async function () {
|
||||
const calldata = this.fallbackHandler.interface.encodeFunctionData('callPayable');
|
||||
const value = 17n;
|
||||
|
||||
await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callPayable({ value }))
|
||||
.to.emit(this.fallbackHandler, 'ERC7579FallbackHandlerMockCalled')
|
||||
.withArgs(this.mock, this.other, value, calldata);
|
||||
});
|
||||
|
||||
it('returns answer from the fallback handler', async function () {
|
||||
await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callView()).to.eventually.deep.equal([
|
||||
this.mock.target,
|
||||
this.other.address,
|
||||
]);
|
||||
});
|
||||
|
||||
it('bubble up reverts from the fallback handler', async function () {
|
||||
await expect(
|
||||
this.fallbackHandler.attach(this.mock).connect(this.other).callRevert(),
|
||||
).to.be.revertedWithCustomError(this.fallbackHandler, 'ERC7579FallbackHandlerMockRevert');
|
||||
});
|
||||
|
||||
withHooks &&
|
||||
describe('with hook', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
|
||||
});
|
||||
|
||||
it('should call the hook of the installed module when performing a callback', async function () {
|
||||
const precheckData = this.fallbackHandler.interface.encodeFunctionData('callPayable');
|
||||
const value = 17n;
|
||||
|
||||
// call with interface: decode returned data
|
||||
await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callPayable({ value }))
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
|
||||
.withArgs(this.other, value, precheckData)
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
|
||||
.withArgs(precheckData);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeAccountERC7579,
|
||||
};
|
||||
60
test/account/extensions/AccountERC7579.test.js
Normal file
60
test/account/extensions/AccountERC7579.test.js
Normal file
@ -0,0 +1,60 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getDomain } = require('../../helpers/eip712');
|
||||
const { ERC4337Helper } = require('../../helpers/erc4337');
|
||||
const { PackedUserOperation } = require('../../helpers/eip712-types');
|
||||
|
||||
const { shouldBehaveLikeAccountCore } = require('../Account.behavior');
|
||||
const { shouldBehaveLikeAccountERC7579 } = require('./AccountERC7579.behavior');
|
||||
const { shouldBehaveLikeERC1271 } = require('../../utils/cryptography/ERC1271.behavior');
|
||||
|
||||
async function fixture() {
|
||||
// EOAs and environment
|
||||
const [other] = await ethers.getSigners();
|
||||
const target = await ethers.deployContract('CallReceiverMock');
|
||||
const anotherTarget = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
// ERC-7579 validator
|
||||
const validator = await ethers.deployContract('$ERC7579ValidatorMock');
|
||||
|
||||
// ERC-4337 signer
|
||||
const signer = ethers.Wallet.createRandom();
|
||||
|
||||
// ERC-4337 account
|
||||
const helper = new ERC4337Helper();
|
||||
const mock = await helper.newAccount('$AccountERC7579Mock', [
|
||||
validator,
|
||||
ethers.solidityPacked(['address'], [signer.address]),
|
||||
]);
|
||||
|
||||
// ERC-4337 Entrypoint domain
|
||||
const entrypointDomain = await getDomain(entrypoint.v08);
|
||||
|
||||
return { helper, validator, mock, entrypointDomain, signer, target, anotherTarget, other };
|
||||
}
|
||||
|
||||
describe('AccountERC7579', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
|
||||
this.signer.signMessage = message =>
|
||||
ethers.Wallet.prototype.signMessage
|
||||
.bind(this.signer)(message)
|
||||
.then(sign => ethers.concat([this.validator.target, sign]));
|
||||
this.signer.signTypedData = (domain, types, values) =>
|
||||
ethers.Wallet.prototype.signTypedData
|
||||
.bind(this.signer)(domain, types, values)
|
||||
.then(sign => ethers.concat([this.validator.target, sign]));
|
||||
this.signUserOp = userOp =>
|
||||
ethers.Wallet.prototype.signTypedData
|
||||
.bind(this.signer)(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
|
||||
.then(signature => Object.assign(userOp, { signature }));
|
||||
|
||||
this.userOp = { nonce: ethers.zeroPadBytes(ethers.hexlify(this.validator.target), 32) };
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccountCore();
|
||||
shouldBehaveLikeAccountERC7579();
|
||||
shouldBehaveLikeERC1271();
|
||||
});
|
||||
60
test/account/extensions/AccountERC7579Hooked.test.js
Normal file
60
test/account/extensions/AccountERC7579Hooked.test.js
Normal file
@ -0,0 +1,60 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getDomain } = require('../../helpers/eip712');
|
||||
const { ERC4337Helper } = require('../../helpers/erc4337');
|
||||
const { PackedUserOperation } = require('../../helpers/eip712-types');
|
||||
|
||||
const { shouldBehaveLikeAccountCore } = require('../Account.behavior');
|
||||
const { shouldBehaveLikeAccountERC7579 } = require('./AccountERC7579.behavior');
|
||||
const { shouldBehaveLikeERC1271 } = require('../../utils/cryptography/ERC1271.behavior');
|
||||
|
||||
async function fixture() {
|
||||
// EOAs and environment
|
||||
const [other] = await ethers.getSigners();
|
||||
const target = await ethers.deployContract('CallReceiverMock');
|
||||
const anotherTarget = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
// ERC-7579 validator
|
||||
const validator = await ethers.deployContract('$ERC7579ValidatorMock');
|
||||
|
||||
// ERC-4337 signer
|
||||
const signer = ethers.Wallet.createRandom();
|
||||
|
||||
// ERC-4337 account
|
||||
const helper = new ERC4337Helper();
|
||||
const mock = await helper.newAccount('$AccountERC7579HookedMock', [
|
||||
validator,
|
||||
ethers.solidityPacked(['address'], [signer.address]),
|
||||
]);
|
||||
|
||||
// ERC-4337 Entrypoint domain
|
||||
const entrypointDomain = await getDomain(entrypoint.v08);
|
||||
|
||||
return { helper, validator, mock, entrypointDomain, signer, target, anotherTarget, other };
|
||||
}
|
||||
|
||||
describe('AccountERC7579Hooked', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
|
||||
this.signer.signMessage = message =>
|
||||
ethers.Wallet.prototype.signMessage
|
||||
.bind(this.signer)(message)
|
||||
.then(sign => ethers.concat([this.validator.target, sign]));
|
||||
this.signer.signTypedData = (domain, types, values) =>
|
||||
ethers.Wallet.prototype.signTypedData
|
||||
.bind(this.signer)(domain, types, values)
|
||||
.then(sign => ethers.concat([this.validator.target, sign]));
|
||||
this.signUserOp = userOp =>
|
||||
ethers.Wallet.prototype.signTypedData
|
||||
.bind(this.signer)(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
|
||||
.then(signature => Object.assign(userOp, { signature }));
|
||||
|
||||
this.userOp = { nonce: ethers.zeroPadBytes(ethers.hexlify(this.validator.target), 32) };
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccountCore();
|
||||
shouldBehaveLikeAccountERC7579({ withHooks: true });
|
||||
shouldBehaveLikeERC1271();
|
||||
});
|
||||
145
test/account/extensions/ERC7821.behavior.js
Normal file
145
test/account/extensions/ERC7821.behavior.js
Normal file
@ -0,0 +1,145 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { CALL_TYPE_BATCH, encodeMode, encodeBatch } = require('../../helpers/erc7579');
|
||||
|
||||
function shouldBehaveLikeERC7821({ deployable = true } = {}) {
|
||||
describe('supports ERC-7821', function () {
|
||||
beforeEach(async function () {
|
||||
// give eth to the account (before deployment)
|
||||
await this.other.sendTransaction({ to: this.mock.target, value: ethers.parseEther('1') });
|
||||
|
||||
// account is not initially deployed
|
||||
await expect(ethers.provider.getCode(this.mock)).to.eventually.equal('0x');
|
||||
|
||||
this.encodeUserOpCalldata = (...calls) =>
|
||||
this.mock.interface.encodeFunctionData('execute', [
|
||||
encodeMode({ callType: CALL_TYPE_BATCH }),
|
||||
encodeBatch(...calls),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
|
||||
await this.mock.deploy();
|
||||
|
||||
await expect(
|
||||
this.mock.connect(this.other).execute(
|
||||
encodeMode({ callType: CALL_TYPE_BATCH }),
|
||||
encodeBatch({
|
||||
target: this.target,
|
||||
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
|
||||
.withArgs(this.other);
|
||||
});
|
||||
|
||||
if (deployable) {
|
||||
describe('when not deployed', function () {
|
||||
it('should be created with handleOps and increase nonce', async function () {
|
||||
const operation = await this.mock
|
||||
.createUserOp({
|
||||
callData: this.encodeUserOpCalldata({
|
||||
target: this.target,
|
||||
value: 17,
|
||||
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
|
||||
}),
|
||||
})
|
||||
.then(op => op.addInitCode())
|
||||
.then(op => this.signUserOp(op));
|
||||
|
||||
// Can't call the account to get its nonce before it's deployed
|
||||
await expect(entrypoint.v08.getNonce(this.mock.target, 0)).to.eventually.equal(0);
|
||||
await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary))
|
||||
.to.emit(entrypoint.v08, 'AccountDeployed')
|
||||
.withArgs(operation.hash(), this.mock, this.helper.factory, ethers.ZeroAddress)
|
||||
.to.emit(this.target, 'MockFunctionCalledExtra')
|
||||
.withArgs(this.mock, 17);
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(1);
|
||||
});
|
||||
|
||||
it('should revert if the signature is invalid', async function () {
|
||||
const operation = await this.mock
|
||||
.createUserOp({
|
||||
callData: this.encodeUserOpCalldata({
|
||||
target: this.target,
|
||||
value: 17,
|
||||
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
|
||||
}),
|
||||
})
|
||||
.then(op => op.addInitCode());
|
||||
|
||||
operation.signature = '0x00';
|
||||
|
||||
await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary)).to.be.reverted;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('when deployed', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mock.deploy();
|
||||
});
|
||||
|
||||
it('should increase nonce and call target', async function () {
|
||||
const operation = await this.mock
|
||||
.createUserOp({
|
||||
callData: this.encodeUserOpCalldata({
|
||||
target: this.target,
|
||||
value: 42,
|
||||
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
|
||||
}),
|
||||
})
|
||||
.then(op => this.signUserOp(op));
|
||||
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(0);
|
||||
await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary))
|
||||
.to.emit(this.target, 'MockFunctionCalledExtra')
|
||||
.withArgs(this.mock, 42);
|
||||
await 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({ target: this.other, value: 42 }) })
|
||||
.then(op => this.signUserOp(op));
|
||||
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(0);
|
||||
await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary)).to.changeEtherBalance(
|
||||
this.other,
|
||||
42,
|
||||
);
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(1);
|
||||
});
|
||||
|
||||
it('should support batch execution', async function () {
|
||||
const value1 = 43374337n;
|
||||
const value2 = 69420n;
|
||||
|
||||
const operation = await this.mock
|
||||
.createUserOp({
|
||||
callData: this.encodeUserOpCalldata(
|
||||
{ target: this.other, value: value1 },
|
||||
{
|
||||
target: this.target,
|
||||
value: value2,
|
||||
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
|
||||
},
|
||||
),
|
||||
})
|
||||
.then(op => this.signUserOp(op));
|
||||
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(0);
|
||||
const tx = entrypoint.v08.handleOps([operation.packed], this.beneficiary);
|
||||
await expect(tx).to.changeEtherBalances([this.other, this.target], [value1, value2]);
|
||||
await expect(tx).to.emit(this.target, 'MockFunctionCalledExtra').withArgs(this.mock, value2);
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC7821,
|
||||
};
|
||||
Reference in New Issue
Block a user