Update docs
This commit is contained in:
288
test/account/utils/draft-ERC4337Utils.test.js
Normal file
288
test/account/utils/draft-ERC4337Utils.test.js
Normal file
@ -0,0 +1,288 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { packValidationData, UserOperation } = require('../../helpers/erc4337');
|
||||
const { deployEntrypoint } = require('../../helpers/erc4337-entrypoint');
|
||||
const { MAX_UINT48 } = require('../../helpers/constants');
|
||||
const ADDRESS_ONE = '0x0000000000000000000000000000000000000001';
|
||||
|
||||
const fixture = async () => {
|
||||
const { entrypoint } = await deployEntrypoint();
|
||||
const [authorizer, sender, factory, paymaster] = await ethers.getSigners();
|
||||
const utils = await ethers.deployContract('$ERC4337Utils');
|
||||
const SIG_VALIDATION_SUCCESS = await utils.$SIG_VALIDATION_SUCCESS();
|
||||
const SIG_VALIDATION_FAILED = await utils.$SIG_VALIDATION_FAILED();
|
||||
return { utils, authorizer, sender, entrypoint, factory, paymaster, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED };
|
||||
};
|
||||
|
||||
describe('ERC4337Utils', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
describe('parseValidationData', function () {
|
||||
it('parses the validation data', async function () {
|
||||
const authorizer = this.authorizer;
|
||||
const validUntil = 0x12345678n;
|
||||
const validAfter = 0x9abcdef0n;
|
||||
const validationData = packValidationData(validAfter, validUntil, authorizer);
|
||||
|
||||
expect(this.utils.$parseValidationData(validationData)).to.eventually.deep.equal([
|
||||
authorizer.address,
|
||||
validAfter,
|
||||
validUntil,
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns an type(uint48).max if until is 0', async function () {
|
||||
const authorizer = this.authorizer;
|
||||
const validAfter = 0x12345678n;
|
||||
const validationData = packValidationData(validAfter, 0, authorizer);
|
||||
|
||||
expect(this.utils.$parseValidationData(validationData)).to.eventually.deep.equal([
|
||||
authorizer.address,
|
||||
validAfter,
|
||||
MAX_UINT48,
|
||||
]);
|
||||
});
|
||||
|
||||
it('parse canonical values', async function () {
|
||||
expect(this.utils.$parseValidationData(this.SIG_VALIDATION_SUCCESS)).to.eventually.deep.equal([
|
||||
ethers.ZeroAddress,
|
||||
0n,
|
||||
MAX_UINT48,
|
||||
]);
|
||||
|
||||
expect(this.utils.$parseValidationData(this.SIG_VALIDATION_FAILED)).to.eventually.deep.equal([
|
||||
ADDRESS_ONE,
|
||||
0n,
|
||||
MAX_UINT48,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('packValidationData', function () {
|
||||
it('packs the validation data', async function () {
|
||||
const authorizer = this.authorizer;
|
||||
const validUntil = 0x12345678n;
|
||||
const validAfter = 0x9abcdef0n;
|
||||
const validationData = packValidationData(validAfter, validUntil, authorizer);
|
||||
|
||||
expect(
|
||||
this.utils.$packValidationData(ethers.Typed.address(authorizer), validAfter, validUntil),
|
||||
).to.eventually.equal(validationData);
|
||||
});
|
||||
|
||||
it('packs the validation data (bool)', async function () {
|
||||
const success = false;
|
||||
const validUntil = 0x12345678n;
|
||||
const validAfter = 0x9abcdef0n;
|
||||
const validationData = packValidationData(validAfter, validUntil, false);
|
||||
|
||||
expect(this.utils.$packValidationData(ethers.Typed.bool(success), validAfter, validUntil)).to.eventually.equal(
|
||||
validationData,
|
||||
);
|
||||
});
|
||||
|
||||
it('packing reproduced canonical values', async function () {
|
||||
expect(this.utils.$packValidationData(ethers.Typed.address(ethers.ZeroAddress), 0n, 0n)).to.eventually.equal(
|
||||
this.SIG_VALIDATION_SUCCESS,
|
||||
);
|
||||
expect(this.utils.$packValidationData(ethers.Typed.bool(true), 0n, 0n)).to.eventually.equal(
|
||||
this.SIG_VALIDATION_SUCCESS,
|
||||
);
|
||||
expect(this.utils.$packValidationData(ethers.Typed.address(ADDRESS_ONE), 0n, 0n)).to.eventually.equal(
|
||||
this.SIG_VALIDATION_FAILED,
|
||||
);
|
||||
expect(this.utils.$packValidationData(ethers.Typed.bool(false), 0n, 0n)).to.eventually.equal(
|
||||
this.SIG_VALIDATION_FAILED,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('combineValidationData', function () {
|
||||
const validUntil1 = 0x12345678n;
|
||||
const validAfter1 = 0x9abcdef0n;
|
||||
const validUntil2 = 0x87654321n;
|
||||
const validAfter2 = 0xabcdef90n;
|
||||
|
||||
it('combines the validation data', async function () {
|
||||
const validationData1 = packValidationData(validAfter1, validUntil1, ethers.ZeroAddress);
|
||||
const validationData2 = packValidationData(validAfter2, validUntil2, ethers.ZeroAddress);
|
||||
const expected = packValidationData(validAfter2, validUntil1, true);
|
||||
|
||||
// check symmetry
|
||||
expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected);
|
||||
expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected);
|
||||
});
|
||||
|
||||
for (const [authorizer1, authorizer2] of [
|
||||
[ethers.ZeroAddress, '0xbf023313b891fd6000544b79e353323aa94a4f29'],
|
||||
['0xbf023313b891fd6000544b79e353323aa94a4f29', ethers.ZeroAddress],
|
||||
]) {
|
||||
it('returns SIG_VALIDATION_FAILURE if one of the authorizers is not address(0)', async function () {
|
||||
const validationData1 = packValidationData(validAfter1, validUntil1, authorizer1);
|
||||
const validationData2 = packValidationData(validAfter2, validUntil2, authorizer2);
|
||||
const expected = packValidationData(validAfter2, validUntil1, false);
|
||||
|
||||
// check symmetry
|
||||
expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected);
|
||||
expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('getValidationData', function () {
|
||||
it('returns the validation data with valid validity range', async function () {
|
||||
const aggregator = this.authorizer;
|
||||
const validAfter = 0;
|
||||
const validUntil = MAX_UINT48;
|
||||
const validationData = packValidationData(validAfter, validUntil, aggregator);
|
||||
|
||||
expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, false]);
|
||||
});
|
||||
|
||||
it('returns the validation data with invalid validity range (expired)', async function () {
|
||||
const aggregator = this.authorizer;
|
||||
const validAfter = 0;
|
||||
const validUntil = 1;
|
||||
const validationData = packValidationData(validAfter, validUntil, aggregator);
|
||||
|
||||
expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, true]);
|
||||
});
|
||||
|
||||
it('returns the validation data with invalid validity range (not yet valid)', async function () {
|
||||
const aggregator = this.authorizer;
|
||||
const validAfter = MAX_UINT48;
|
||||
const validUntil = MAX_UINT48;
|
||||
const validationData = packValidationData(validAfter, validUntil, aggregator);
|
||||
|
||||
expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, true]);
|
||||
});
|
||||
|
||||
it('returns address(0) and false for validationData = 0', function () {
|
||||
expect(this.utils.$getValidationData(0n)).to.eventually.deep.equal([ethers.ZeroAddress, false]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hash', function () {
|
||||
it('returns the operation hash with specified entrypoint and chainId', async function () {
|
||||
const userOp = new UserOperation({ sender: this.sender, nonce: 1 });
|
||||
const chainId = await ethers.provider.getNetwork().then(({ chainId }) => chainId);
|
||||
const otherChainId = 0xdeadbeef;
|
||||
|
||||
// check that helper matches entrypoint logic
|
||||
expect(this.entrypoint.getUserOpHash(userOp.packed)).to.eventually.equal(userOp.hash(this.entrypoint, chainId));
|
||||
|
||||
// check library against helper
|
||||
expect(this.utils.$hash(userOp.packed, this.entrypoint, chainId)).to.eventually.equal(
|
||||
userOp.hash(this.entrypoint, chainId),
|
||||
);
|
||||
expect(this.utils.$hash(userOp.packed, this.entrypoint, otherChainId)).to.eventually.equal(
|
||||
userOp.hash(this.entrypoint, otherChainId),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('userOp values', function () {
|
||||
describe('intiCode', function () {
|
||||
beforeEach(async function () {
|
||||
this.userOp = new UserOperation({
|
||||
sender: this.sender,
|
||||
nonce: 1,
|
||||
verificationGas: 0x12345678n,
|
||||
factory: this.factory,
|
||||
factoryData: '0x123456',
|
||||
});
|
||||
|
||||
this.emptyUserOp = new UserOperation({
|
||||
sender: this.sender,
|
||||
nonce: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns factory', async function () {
|
||||
expect(this.utils.$factory(this.userOp.packed)).to.eventually.equal(this.factory);
|
||||
expect(this.utils.$factory(this.emptyUserOp.packed)).to.eventually.equal(ethers.ZeroAddress);
|
||||
});
|
||||
|
||||
it('returns factoryData', async function () {
|
||||
expect(this.utils.$factoryData(this.userOp.packed)).to.eventually.equal('0x123456');
|
||||
expect(this.utils.$factoryData(this.emptyUserOp.packed)).to.eventually.equal('0x');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns verificationGasLimit', async function () {
|
||||
const userOp = new UserOperation({ sender: this.sender, nonce: 1, verificationGas: 0x12345678n });
|
||||
expect(this.utils.$verificationGasLimit(userOp.packed)).to.eventually.equal(userOp.verificationGas);
|
||||
});
|
||||
|
||||
it('returns callGasLimit', async function () {
|
||||
const userOp = new UserOperation({ sender: this.sender, nonce: 1, callGas: 0x12345678n });
|
||||
expect(this.utils.$callGasLimit(userOp.packed)).to.eventually.equal(userOp.callGas);
|
||||
});
|
||||
|
||||
it('returns maxPriorityFeePerGas', async function () {
|
||||
const userOp = new UserOperation({ sender: this.sender, nonce: 1, maxPriorityFee: 0x12345678n });
|
||||
expect(this.utils.$maxPriorityFeePerGas(userOp.packed)).to.eventually.equal(userOp.maxPriorityFee);
|
||||
});
|
||||
|
||||
it('returns maxFeePerGas', async function () {
|
||||
const userOp = new UserOperation({ sender: this.sender, nonce: 1, maxFeePerGas: 0x12345678n });
|
||||
expect(this.utils.$maxFeePerGas(userOp.packed)).to.eventually.equal(userOp.maxFeePerGas);
|
||||
});
|
||||
|
||||
it('returns gasPrice', async function () {
|
||||
const userOp = new UserOperation({
|
||||
sender: this.sender,
|
||||
nonce: 1,
|
||||
maxPriorityFee: 0x12345678n,
|
||||
maxFeePerGas: 0x87654321n,
|
||||
});
|
||||
expect(this.utils.$gasPrice(userOp.packed)).to.eventually.equal(userOp.maxPriorityFee);
|
||||
});
|
||||
|
||||
describe('paymasterAndData', function () {
|
||||
beforeEach(async function () {
|
||||
this.userOp = new UserOperation({
|
||||
sender: this.sender,
|
||||
nonce: 1,
|
||||
paymaster: this.paymaster,
|
||||
paymasterVerificationGasLimit: 0x12345678n,
|
||||
paymasterPostOpGasLimit: 0x87654321n,
|
||||
paymasterData: '0xbeefcafe',
|
||||
});
|
||||
|
||||
this.emptyUserOp = new UserOperation({
|
||||
sender: this.sender,
|
||||
nonce: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns paymaster', async function () {
|
||||
expect(this.utils.$paymaster(this.userOp.packed)).to.eventually.equal(this.userOp.paymaster);
|
||||
expect(this.utils.$paymaster(this.emptyUserOp.packed)).to.eventually.equal(ethers.ZeroAddress);
|
||||
});
|
||||
|
||||
it('returns verificationGasLimit', async function () {
|
||||
expect(this.utils.$paymasterVerificationGasLimit(this.userOp.packed)).to.eventually.equal(
|
||||
this.userOp.paymasterVerificationGasLimit,
|
||||
);
|
||||
expect(this.utils.$paymasterVerificationGasLimit(this.emptyUserOp.packed)).to.eventually.equal(0n);
|
||||
});
|
||||
|
||||
it('returns postOpGasLimit', async function () {
|
||||
expect(this.utils.$paymasterPostOpGasLimit(this.userOp.packed)).to.eventually.equal(
|
||||
this.userOp.paymasterPostOpGasLimit,
|
||||
);
|
||||
expect(this.utils.$paymasterPostOpGasLimit(this.emptyUserOp.packed)).to.eventually.equal(0n);
|
||||
});
|
||||
|
||||
it('returns data', async function () {
|
||||
expect(this.utils.$paymasterData(this.userOp.packed)).to.eventually.equal(this.userOp.paymasterData);
|
||||
expect(this.utils.$paymasterData(this.emptyUserOp.packed)).to.eventually.equal('0x');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
421
test/account/utils/draft-ERC7579Utils.t.sol
Normal file
421
test/account/utils/draft-ERC7579Utils.t.sol
Normal file
@ -0,0 +1,421 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
// Parts of this test file are adapted from Adam Egyed (@adamegyed) proof of concept available at:
|
||||
// https://github.com/adamegyed/erc7579-execute-vulnerability/tree/4589a30ff139e143d6c57183ac62b5c029217a90
|
||||
//
|
||||
// solhint-disable no-console
|
||||
|
||||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
||||
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
|
||||
import {PackedUserOperation, IAccount, IEntryPoint} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
|
||||
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
|
||||
import {ERC7579Utils, Mode, CallType, ExecType, ModeSelector, ModePayload, Execution} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
|
||||
import {Test, Vm, console} from "forge-std/Test.sol";
|
||||
|
||||
contract SampleAccount is IAccount, Ownable {
|
||||
using ECDSA for *;
|
||||
using MessageHashUtils for *;
|
||||
using ERC4337Utils for *;
|
||||
using ERC7579Utils for *;
|
||||
|
||||
IEntryPoint internal constant ENTRY_POINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032));
|
||||
|
||||
event Log(bool duringValidation, Execution[] calls);
|
||||
|
||||
error UnsupportedCallType(CallType callType);
|
||||
|
||||
constructor(address initialOwner) Ownable(initialOwner) {}
|
||||
|
||||
function validateUserOp(
|
||||
PackedUserOperation calldata userOp,
|
||||
bytes32 userOpHash,
|
||||
uint256 missingAccountFunds
|
||||
) external override returns (uint256 validationData) {
|
||||
require(msg.sender == address(ENTRY_POINT), "only from EP");
|
||||
// Check signature
|
||||
if (userOpHash.toEthSignedMessageHash().recover(userOp.signature) != owner()) {
|
||||
revert OwnableUnauthorizedAccount(_msgSender());
|
||||
}
|
||||
|
||||
// If this is an execute call with a batch operation, log the call details from the calldata
|
||||
if (bytes4(userOp.callData[0x00:0x04]) == this.execute.selector) {
|
||||
(CallType callType, , , ) = Mode.wrap(bytes32(userOp.callData[0x04:0x24])).decodeMode();
|
||||
|
||||
if (callType == ERC7579Utils.CALLTYPE_BATCH) {
|
||||
// Remove the selector
|
||||
bytes calldata params = userOp.callData[0x04:];
|
||||
|
||||
// Use the same vulnerable assignment technique here, but assert afterwards that the checks aren't
|
||||
// broken here by comparing to the result of `abi.decode(...)`.
|
||||
bytes calldata executionCalldata;
|
||||
assembly ("memory-safe") {
|
||||
let dataptr := add(params.offset, calldataload(add(params.offset, 0x20)))
|
||||
executionCalldata.offset := add(dataptr, 32)
|
||||
executionCalldata.length := calldataload(dataptr)
|
||||
}
|
||||
// Check that this decoding step is done correctly.
|
||||
(, bytes memory executionCalldataMemory) = abi.decode(params, (bytes32, bytes));
|
||||
|
||||
require(
|
||||
keccak256(executionCalldata) == keccak256(executionCalldataMemory),
|
||||
"decoding during validation failed"
|
||||
);
|
||||
// Now, we know that we have `bytes calldata executionCalldata` as would be decoded by the solidity
|
||||
// builtin decoder for the `execute` function.
|
||||
|
||||
// This is where the vulnerability from ExecutionLib results in a different result between validation
|
||||
// andexecution.
|
||||
|
||||
emit Log(true, executionCalldata.decodeBatch());
|
||||
}
|
||||
}
|
||||
|
||||
if (missingAccountFunds > 0) {
|
||||
(bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
|
||||
success; // Silence warning. The entrypoint should validate the result.
|
||||
}
|
||||
|
||||
return ERC4337Utils.SIG_VALIDATION_SUCCESS;
|
||||
}
|
||||
|
||||
function execute(Mode mode, bytes calldata executionCalldata) external payable {
|
||||
require(msg.sender == address(this) || msg.sender == address(ENTRY_POINT), "not auth");
|
||||
|
||||
(CallType callType, ExecType execType, , ) = mode.decodeMode();
|
||||
|
||||
// check if calltype is batch or single
|
||||
if (callType == ERC7579Utils.CALLTYPE_SINGLE) {
|
||||
executionCalldata.execSingle(execType);
|
||||
} else if (callType == ERC7579Utils.CALLTYPE_BATCH) {
|
||||
executionCalldata.execBatch(execType);
|
||||
|
||||
emit Log(false, executionCalldata.decodeBatch());
|
||||
} else if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) {
|
||||
executionCalldata.execDelegateCall(execType);
|
||||
} else {
|
||||
revert UnsupportedCallType(callType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contract ERC7579UtilsTest is Test {
|
||||
using MessageHashUtils for *;
|
||||
using ERC4337Utils for *;
|
||||
using ERC7579Utils for *;
|
||||
|
||||
IEntryPoint private constant ENTRYPOINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032));
|
||||
address private _owner;
|
||||
uint256 private _ownerKey;
|
||||
address private _account;
|
||||
address private _beneficiary;
|
||||
address private _recipient1;
|
||||
address private _recipient2;
|
||||
|
||||
constructor() {
|
||||
vm.etch(0x0000000071727De22E5E9d8BAf0edAc6f37da032, vm.readFileBinary("test/bin/EntryPoint070.bytecode"));
|
||||
vm.etch(0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C, vm.readFileBinary("test/bin/SenderCreator070.bytecode"));
|
||||
|
||||
// signing key
|
||||
(_owner, _ownerKey) = makeAddrAndKey("owner");
|
||||
|
||||
// ERC-4337 account
|
||||
_account = address(new SampleAccount(_owner));
|
||||
vm.deal(_account, 1 ether);
|
||||
|
||||
// other
|
||||
_beneficiary = makeAddr("beneficiary");
|
||||
_recipient1 = makeAddr("recipient1");
|
||||
_recipient2 = makeAddr("recipient2");
|
||||
}
|
||||
|
||||
function testExecuteBatchDecodeCorrectly() public {
|
||||
Execution[] memory calls = new Execution[](2);
|
||||
calls[0] = Execution({target: _recipient1, value: 1 wei, callData: ""});
|
||||
calls[1] = Execution({target: _recipient2, value: 1 wei, callData: ""});
|
||||
|
||||
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
|
||||
userOps[0] = PackedUserOperation({
|
||||
sender: _account,
|
||||
nonce: 0,
|
||||
initCode: "",
|
||||
callData: abi.encodeCall(
|
||||
SampleAccount.execute,
|
||||
(
|
||||
ERC7579Utils.encodeMode(
|
||||
ERC7579Utils.CALLTYPE_BATCH,
|
||||
ERC7579Utils.EXECTYPE_DEFAULT,
|
||||
ModeSelector.wrap(0x00),
|
||||
ModePayload.wrap(0x00)
|
||||
),
|
||||
ERC7579Utils.encodeBatch(calls)
|
||||
)
|
||||
),
|
||||
accountGasLimits: _packGas(500_000, 500_000),
|
||||
preVerificationGas: 0,
|
||||
gasFees: _packGas(1, 1),
|
||||
paymasterAndData: "",
|
||||
signature: ""
|
||||
});
|
||||
|
||||
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
|
||||
_ownerKey,
|
||||
this.hashUserOperation(userOps[0]).toEthSignedMessageHash()
|
||||
);
|
||||
userOps[0].signature = abi.encodePacked(r, s, v);
|
||||
|
||||
vm.recordLogs();
|
||||
ENTRYPOINT.handleOps(userOps, payable(_beneficiary));
|
||||
|
||||
assertEq(_recipient1.balance, 1 wei);
|
||||
assertEq(_recipient2.balance, 1 wei);
|
||||
|
||||
_collectAndPrintLogs(false);
|
||||
}
|
||||
|
||||
function testExecuteBatchDecodeEmpty() public {
|
||||
bytes memory fakeCalls = abi.encodePacked(
|
||||
uint256(1), // Length of execution[]
|
||||
uint256(0x20), // offset
|
||||
uint256(uint160(_recipient1)), // target
|
||||
uint256(1), // value: 1 wei
|
||||
uint256(0x60), // offset of data
|
||||
uint256(0) // length of
|
||||
);
|
||||
|
||||
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
|
||||
userOps[0] = PackedUserOperation({
|
||||
sender: _account,
|
||||
nonce: 0,
|
||||
initCode: "",
|
||||
callData: abi.encodeCall(
|
||||
SampleAccount.execute,
|
||||
(
|
||||
ERC7579Utils.encodeMode(
|
||||
ERC7579Utils.CALLTYPE_BATCH,
|
||||
ERC7579Utils.EXECTYPE_DEFAULT,
|
||||
ModeSelector.wrap(0x00),
|
||||
ModePayload.wrap(0x00)
|
||||
),
|
||||
abi.encodePacked(
|
||||
uint256(0x70) // fake offset pointing to paymasterAndData
|
||||
)
|
||||
)
|
||||
),
|
||||
accountGasLimits: _packGas(500_000, 500_000),
|
||||
preVerificationGas: 0,
|
||||
gasFees: _packGas(1, 1),
|
||||
paymasterAndData: abi.encodePacked(address(0), fakeCalls),
|
||||
signature: ""
|
||||
});
|
||||
|
||||
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
|
||||
_ownerKey,
|
||||
this.hashUserOperation(userOps[0]).toEthSignedMessageHash()
|
||||
);
|
||||
userOps[0].signature = abi.encodePacked(r, s, v);
|
||||
|
||||
vm.expectRevert(
|
||||
abi.encodeWithSelector(
|
||||
IEntryPoint.FailedOpWithRevert.selector,
|
||||
0,
|
||||
"AA23 reverted",
|
||||
abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector)
|
||||
)
|
||||
);
|
||||
ENTRYPOINT.handleOps(userOps, payable(_beneficiary));
|
||||
|
||||
_collectAndPrintLogs(false);
|
||||
}
|
||||
|
||||
function testExecuteBatchDecodeDifferent() public {
|
||||
bytes memory execCallData = abi.encodePacked(
|
||||
uint256(0x20), // offset pointing to the next segment
|
||||
uint256(5), // Length of execution[]
|
||||
uint256(0), // offset of calls[0], and target (!!)
|
||||
uint256(0x20), // offset of calls[1], and value (!!)
|
||||
uint256(0), // offset of calls[2], and rel offset of data (!!)
|
||||
uint256(0) // offset of calls[3].
|
||||
// There is one more to read by the array length, but it's not present here. This will be
|
||||
// paymasterAndData.length during validation, pointing to an all-zero call.
|
||||
// During execution, the offset will be 0, pointing to a call with value.
|
||||
);
|
||||
|
||||
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
|
||||
userOps[0] = PackedUserOperation({
|
||||
sender: _account,
|
||||
nonce: 0,
|
||||
initCode: "",
|
||||
callData: abi.encodePacked(
|
||||
SampleAccount.execute.selector,
|
||||
ERC7579Utils.encodeMode(
|
||||
ERC7579Utils.CALLTYPE_BATCH,
|
||||
ERC7579Utils.EXECTYPE_DEFAULT,
|
||||
ModeSelector.wrap(0x00),
|
||||
ModePayload.wrap(0x00)
|
||||
),
|
||||
uint256(0x5c), // offset pointing to the next segment
|
||||
uint224(type(uint224).max), // Padding to align the `bytes` types
|
||||
// type(uint256).max, // unknown padding
|
||||
uint256(execCallData.length), // Length of the data
|
||||
execCallData
|
||||
),
|
||||
accountGasLimits: _packGas(500_000, 500_000),
|
||||
preVerificationGas: 0,
|
||||
gasFees: _packGas(1, 1),
|
||||
paymasterAndData: abi.encodePacked(uint256(0), uint256(0)), // padding length to create an offset
|
||||
signature: ""
|
||||
});
|
||||
|
||||
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
|
||||
_ownerKey,
|
||||
this.hashUserOperation(userOps[0]).toEthSignedMessageHash()
|
||||
);
|
||||
userOps[0].signature = abi.encodePacked(r, s, v);
|
||||
|
||||
vm.expectRevert(
|
||||
abi.encodeWithSelector(
|
||||
IEntryPoint.FailedOpWithRevert.selector,
|
||||
0,
|
||||
"AA23 reverted",
|
||||
abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector)
|
||||
)
|
||||
);
|
||||
ENTRYPOINT.handleOps(userOps, payable(_beneficiary));
|
||||
|
||||
_collectAndPrintLogs(true);
|
||||
}
|
||||
|
||||
function testDecodeBatch() public {
|
||||
// BAD: buffer empty
|
||||
vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector);
|
||||
this.callDecodeBatch("");
|
||||
|
||||
// BAD: buffer too short
|
||||
vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector);
|
||||
this.callDecodeBatch(abi.encodePacked(uint128(0)));
|
||||
|
||||
// GOOD
|
||||
this.callDecodeBatch(abi.encode(0));
|
||||
// Note: Solidity also supports this even though it's odd. Offset 0 means array is at the same location, which
|
||||
// is interpreted as an array of length 0, which doesn't require any more data
|
||||
// solhint-disable-next-line var-name-mixedcase
|
||||
uint256[] memory _1 = abi.decode(abi.encode(0), (uint256[]));
|
||||
_1;
|
||||
|
||||
// BAD: offset is out of bounds
|
||||
vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector);
|
||||
this.callDecodeBatch(abi.encode(1));
|
||||
|
||||
// GOOD
|
||||
this.callDecodeBatch(abi.encode(32, 0));
|
||||
|
||||
// BAD: reported array length extends beyond bounds
|
||||
vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector);
|
||||
this.callDecodeBatch(abi.encode(32, 1));
|
||||
|
||||
// GOOD
|
||||
this.callDecodeBatch(abi.encode(32, 1, 0));
|
||||
|
||||
// GOOD
|
||||
//
|
||||
// 0000000000000000000000000000000000000000000000000000000000000020 (32) offset
|
||||
// 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length
|
||||
// 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset
|
||||
// 000000000000000000000000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (recipient) target for element #0
|
||||
// 000000000000000000000000000000000000000000000000000000000000002a (42) value for element #0
|
||||
// 0000000000000000000000000000000000000000000000000000000000000060 (96) offset to calldata for element #0
|
||||
// 000000000000000000000000000000000000000000000000000000000000000c (12) length of the calldata for element #0
|
||||
// 48656c6c6f20576f726c64210000000000000000000000000000000000000000 (..) buffer for the calldata for element #0
|
||||
assertEq(
|
||||
bytes("Hello World!"),
|
||||
this.callDecodeBatchAndGetFirstBytes(
|
||||
abi.encode(32, 1, 32, _recipient1, 42, 96, 12, bytes12("Hello World!"))
|
||||
)
|
||||
);
|
||||
|
||||
// This is invalid, the first element of the array points is out of bounds
|
||||
// but we allow it past initial validation, because solidity will validate later when the bytes field is accessed
|
||||
//
|
||||
// 0000000000000000000000000000000000000000000000000000000000000020 (32) offset
|
||||
// 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length
|
||||
// 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset
|
||||
// <missing element>
|
||||
bytes memory invalid = abi.encode(32, 1, 32);
|
||||
this.callDecodeBatch(invalid);
|
||||
vm.expectRevert();
|
||||
this.callDecodeBatchAndGetFirst(invalid);
|
||||
|
||||
// this is invalid: the bytes field of the first element of the array is out of bounds
|
||||
// but we allow it past initial validation, because solidity will validate later when the bytes field is accessed
|
||||
//
|
||||
// 0000000000000000000000000000000000000000000000000000000000000020 (32) offset
|
||||
// 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length
|
||||
// 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset
|
||||
// 000000000000000000000000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (recipient) target for element #0
|
||||
// 000000000000000000000000000000000000000000000000000000000000002a (42) value for element #0
|
||||
// 0000000000000000000000000000000000000000000000000000000000000060 (96) offset to calldata for element #0
|
||||
// <missing data>
|
||||
bytes memory invalidDeeply = abi.encode(32, 1, 32, _recipient1, 42, 96);
|
||||
this.callDecodeBatch(invalidDeeply);
|
||||
// Note that this is ok because we don't return the value. Returning it would introduce a check that would fails.
|
||||
this.callDecodeBatchAndGetFirst(invalidDeeply);
|
||||
vm.expectRevert();
|
||||
this.callDecodeBatchAndGetFirstBytes(invalidDeeply);
|
||||
}
|
||||
|
||||
function callDecodeBatch(bytes calldata executionCalldata) public pure {
|
||||
ERC7579Utils.decodeBatch(executionCalldata);
|
||||
}
|
||||
|
||||
function callDecodeBatchAndGetFirst(bytes calldata executionCalldata) public pure {
|
||||
ERC7579Utils.decodeBatch(executionCalldata)[0];
|
||||
}
|
||||
|
||||
function callDecodeBatchAndGetFirstBytes(bytes calldata executionCalldata) public pure returns (bytes calldata) {
|
||||
return ERC7579Utils.decodeBatch(executionCalldata)[0].callData;
|
||||
}
|
||||
|
||||
function hashUserOperation(PackedUserOperation calldata useroperation) public view returns (bytes32) {
|
||||
return useroperation.hash(address(ENTRYPOINT), block.chainid);
|
||||
}
|
||||
|
||||
function _collectAndPrintLogs(bool includeTotalValue) internal {
|
||||
Vm.Log[] memory logs = vm.getRecordedLogs();
|
||||
for (uint256 i = 0; i < logs.length; i++) {
|
||||
if (logs[i].emitter == _account) {
|
||||
_printDecodedCalls(logs[i].data, includeTotalValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _printDecodedCalls(bytes memory logData, bool includeTotalValue) internal pure {
|
||||
(bool duringValidation, Execution[] memory calls) = abi.decode(logData, (bool, Execution[]));
|
||||
|
||||
console.log(
|
||||
string.concat(
|
||||
"Batch execute contents, as read during ",
|
||||
duringValidation ? "validation" : "execution",
|
||||
": "
|
||||
)
|
||||
);
|
||||
console.log(" Execution[] length: %s", calls.length);
|
||||
|
||||
uint256 totalValue = 0;
|
||||
for (uint256 i = 0; i < calls.length; ++i) {
|
||||
console.log(string.concat(" calls[", vm.toString(i), "].target = ", vm.toString(calls[i].target)));
|
||||
console.log(string.concat(" calls[", vm.toString(i), "].value = ", vm.toString(calls[i].value)));
|
||||
console.log(string.concat(" calls[", vm.toString(i), "].data = ", vm.toString(calls[i].callData)));
|
||||
totalValue += calls[i].value;
|
||||
}
|
||||
|
||||
if (includeTotalValue) {
|
||||
console.log(" Total value: %s", totalValue);
|
||||
}
|
||||
}
|
||||
|
||||
function _packGas(uint256 upper, uint256 lower) internal pure returns (bytes32) {
|
||||
return bytes32(uint256((upper << 128) | uint128(lower)));
|
||||
}
|
||||
}
|
||||
353
test/account/utils/draft-ERC7579Utils.test.js
Normal file
353
test/account/utils/draft-ERC7579Utils.test.js
Normal file
@ -0,0 +1,353 @@
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user