You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
354 lines
13 KiB
354 lines
13 KiB
1 week ago
|
const { ethers } = require('hardhat');
|
||
|
const { expect } = require('chai');
|
||
|
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||
|
const {
|
||
|
EXEC_TYPE_DEFAULT,
|
||
|
EXEC_TYPE_TRY,
|
||
|
encodeSingle,
|
||
|
encodeBatch,
|
||
|
encodeDelegate,
|
||
|
CALL_TYPE_CALL,
|
||
|
CALL_TYPE_BATCH,
|
||
|
encodeMode,
|
||
|
} = require('../../helpers/erc7579');
|
||
|
const { selector } = require('../../helpers/methods');
|
||
|
|
||
|
const coder = ethers.AbiCoder.defaultAbiCoder();
|
||
|
|
||
|
const fixture = async () => {
|
||
|
const [sender] = await ethers.getSigners();
|
||
|
const utils = await ethers.deployContract('$ERC7579Utils', { value: ethers.parseEther('1') });
|
||
|
const utilsGlobal = await ethers.deployContract('$ERC7579UtilsGlobalMock');
|
||
|
const target = await ethers.deployContract('CallReceiverMock');
|
||
|
const anotherTarget = await ethers.deployContract('CallReceiverMock');
|
||
|
return { utils, utilsGlobal, target, anotherTarget, sender };
|
||
|
};
|
||
|
|
||
|
describe('ERC7579Utils', function () {
|
||
|
beforeEach(async function () {
|
||
|
Object.assign(this, await loadFixture(fixture));
|
||
|
});
|
||
|
|
||
|
describe('execSingle', function () {
|
||
|
it('calls the target with value', async function () {
|
||
|
const value = 0x012;
|
||
|
const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction'));
|
||
|
|
||
|
await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT)).to.emit(this.target, 'MockFunctionCalled');
|
||
|
|
||
|
expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value);
|
||
|
});
|
||
|
|
||
|
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']),
|
||
|
);
|
||
|
|
||
|
await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT))
|
||
|
.to.emit(this.target, 'MockFunctionCalledWithArgs')
|
||
|
.withArgs(42, '0x1234');
|
||
|
|
||
|
expect(ethers.provider.getBalance(this.target)).to.eventually.equal(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.utils.$execSingle(data, EXEC_TYPE_DEFAULT)).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.utils.$execSingle(data, EXEC_TYPE_TRY))
|
||
|
.to.emit(this.utils, 'ERC7579TryExecuteFail')
|
||
|
.withArgs(
|
||
|
CALL_TYPE_CALL,
|
||
|
ethers.solidityPacked(
|
||
|
['bytes4', 'bytes'],
|
||
|
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
|
||
|
),
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('reverts with an invalid exec type', async function () {
|
||
|
const value = 0x012;
|
||
|
const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction'));
|
||
|
|
||
|
await expect(this.utils.$execSingle(data, '0x03'))
|
||
|
.to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
|
||
|
.withArgs('0x03');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('execBatch', function () {
|
||
|
it('calls the targets with value', 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('mockFunction')],
|
||
|
);
|
||
|
|
||
|
await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT))
|
||
|
.to.emit(this.target, 'MockFunctionCalled')
|
||
|
.to.emit(this.anotherTarget, 'MockFunctionCalled');
|
||
|
|
||
|
expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
|
||
|
expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(value2);
|
||
|
});
|
||
|
|
||
|
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']),
|
||
|
],
|
||
|
);
|
||
|
|
||
|
await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT))
|
||
|
.to.emit(this.target, 'MockFunctionCalledWithArgs')
|
||
|
.to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs');
|
||
|
|
||
|
expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
|
||
|
expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(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.utils.$execBatch(data, EXEC_TYPE_DEFAULT)).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')],
|
||
|
);
|
||
|
|
||
|
await expect(this.utils.$execBatch(data, EXEC_TYPE_TRY))
|
||
|
.to.emit(this.utils, 'ERC7579TryExecuteFail')
|
||
|
.withArgs(
|
||
|
CALL_TYPE_BATCH,
|
||
|
ethers.solidityPacked(
|
||
|
['bytes4', 'bytes'],
|
||
|
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
|
||
|
),
|
||
|
);
|
||
|
|
||
|
// Check balances
|
||
|
expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
|
||
|
expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(0);
|
||
|
});
|
||
|
|
||
|
it('reverts with an invalid exec type', 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('mockFunction')],
|
||
|
);
|
||
|
|
||
|
await expect(this.utils.$execBatch(data, '0x03'))
|
||
|
.to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
|
||
|
.withArgs('0x03');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('execDelegateCall', 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]),
|
||
|
);
|
||
|
|
||
|
expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(ethers.ZeroHash);
|
||
|
await this.utils.$execDelegateCall(data, EXEC_TYPE_DEFAULT);
|
||
|
expect(ethers.provider.getStorage(this.utils.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.utils.$execDelegateCall(data, EXEC_TYPE_DEFAULT)).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.utils.$execDelegateCall(data, EXEC_TYPE_TRY))
|
||
|
.to.emit(this.utils, 'ERC7579TryExecuteFail')
|
||
|
.withArgs(
|
||
|
CALL_TYPE_CALL,
|
||
|
ethers.solidityPacked(
|
||
|
['bytes4', 'bytes'],
|
||
|
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
|
||
|
),
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('reverts with an invalid exec type', async function () {
|
||
|
const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunction'));
|
||
|
await expect(this.utils.$execDelegateCall(data, '0x03'))
|
||
|
.to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
|
||
|
.withArgs('0x03');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('encodes Mode', async function () {
|
||
|
const callType = CALL_TYPE_BATCH;
|
||
|
const execType = EXEC_TYPE_TRY;
|
||
|
const selector = '0x12345678';
|
||
|
const payload = ethers.toBeHex(0, 22);
|
||
|
|
||
|
expect(this.utils.$encodeMode(callType, execType, selector, payload)).to.eventually.equal(
|
||
|
encodeMode({
|
||
|
callType,
|
||
|
execType,
|
||
|
selector,
|
||
|
payload,
|
||
|
}),
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('decodes Mode', async function () {
|
||
|
const callType = CALL_TYPE_BATCH;
|
||
|
const execType = EXEC_TYPE_TRY;
|
||
|
const selector = '0x12345678';
|
||
|
const payload = ethers.toBeHex(0, 22);
|
||
|
|
||
|
expect(
|
||
|
this.utils.$decodeMode(
|
||
|
encodeMode({
|
||
|
callType,
|
||
|
execType,
|
||
|
selector,
|
||
|
payload,
|
||
|
}),
|
||
|
),
|
||
|
).to.eventually.deep.equal([callType, execType, selector, payload]);
|
||
|
});
|
||
|
|
||
|
it('encodes single', async function () {
|
||
|
const target = this.target;
|
||
|
const value = 0x123;
|
||
|
const data = '0x12345678';
|
||
|
|
||
|
expect(this.utils.$encodeSingle(target, value, data)).to.eventually.equal(encodeSingle(target, value, data));
|
||
|
});
|
||
|
|
||
|
it('decodes single', async function () {
|
||
|
const target = this.target;
|
||
|
const value = 0x123;
|
||
|
const data = '0x12345678';
|
||
|
|
||
|
expect(this.utils.$decodeSingle(encodeSingle(target, value, data))).to.eventually.deep.equal([
|
||
|
target.target,
|
||
|
value,
|
||
|
data,
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('encodes batch', async function () {
|
||
|
const entries = [
|
||
|
[this.target, 0x123, '0x12345678'],
|
||
|
[this.anotherTarget, 0x456, '0x12345678'],
|
||
|
];
|
||
|
|
||
|
expect(this.utils.$encodeBatch(entries)).to.eventually.equal(encodeBatch(...entries));
|
||
|
});
|
||
|
|
||
|
it('decodes batch', async function () {
|
||
|
const entries = [
|
||
|
[this.target.target, 0x123, '0x12345678'],
|
||
|
[this.anotherTarget.target, 0x456, '0x12345678'],
|
||
|
];
|
||
|
|
||
|
expect(this.utils.$decodeBatch(encodeBatch(...entries))).to.eventually.deep.equal(entries);
|
||
|
});
|
||
|
|
||
|
it('encodes delegate', async function () {
|
||
|
const target = this.target;
|
||
|
const data = '0x12345678';
|
||
|
|
||
|
expect(this.utils.$encodeDelegate(target, data)).to.eventually.equal(encodeDelegate(target, data));
|
||
|
});
|
||
|
|
||
|
it('decodes delegate', async function () {
|
||
|
const target = this.target;
|
||
|
const data = '0x12345678';
|
||
|
|
||
|
expect(this.utils.$decodeDelegate(encodeDelegate(target, data))).to.eventually.deep.equal([target.target, data]);
|
||
|
});
|
||
|
|
||
|
describe('global', function () {
|
||
|
describe('eqCallTypeGlobal', function () {
|
||
|
it('returns true if both call types are equal', async function () {
|
||
|
expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_BATCH, CALL_TYPE_BATCH)).to.eventually.be.true;
|
||
|
});
|
||
|
|
||
|
it('returns false if both call types are different', async function () {
|
||
|
expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_CALL, CALL_TYPE_BATCH)).to.eventually.be.false;
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('eqExecTypeGlobal', function () {
|
||
|
it('returns true if both exec types are equal', async function () {
|
||
|
expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_TRY, EXEC_TYPE_TRY)).to.eventually.be.true;
|
||
|
});
|
||
|
|
||
|
it('returns false if both exec types are different', async function () {
|
||
|
expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY)).to.eventually.be.false;
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('eqModeSelectorGlobal', function () {
|
||
|
it('returns true if both selectors are equal', async function () {
|
||
|
expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x12345678')).to.eventually.be.true;
|
||
|
});
|
||
|
|
||
|
it('returns false if both selectors are different', async function () {
|
||
|
expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x87654321')).to.eventually.be.false;
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('eqModePayloadGlobal', function () {
|
||
|
it('returns true if both payloads are equal', async function () {
|
||
|
expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(0, 22))).to.eventually.be
|
||
|
.true;
|
||
|
});
|
||
|
|
||
|
it('returns false if both payloads are different', async function () {
|
||
|
expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(1, 22))).to.eventually.be
|
||
|
.false;
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|