Skip to content

Commit

Permalink
Await .eventually test matchers
Browse files Browse the repository at this point in the history
  • Loading branch information
ernestognw committed Dec 27, 2024
1 parent e518a4b commit e1c49fc
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 66 deletions.
22 changes: 16 additions & 6 deletions contracts/utils/cryptography/ERC7739Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ library ERC7739Utils {
* @dev Parse the type name out of the ERC-7739 contents type description. Supports both the implicit and explicit
* modes.
*
* Following ERC-7739 specifications, a `contentsTypeName` is considered invalid if it's empty or it contains
* any of the following bytes , )\x00
* Following ERC-7739 specifications, a `contentsTypeName` is considered invalid if it's empty, it contains
* any of the following bytes (`, )\x00`) or it starts with a forbidden character (a-z or `(`).
*
* If the `contentsType` is invalid, this returns an empty string. Otherwise, the return string has non-zero
* length.
Expand All @@ -173,10 +173,12 @@ library ERC7739Utils {
for (uint256 i = 0; i < buffer.length; ++i) {
bytes1 current = buffer[i];
if (current == bytes1("(")) {
// if name is empty - passthrough (fail)
if (i == 0) break;
if (_isForbiddenFirstChar(buffer[0])) {
// we found an invalid first character (forbidden) - passthrough (fail)
break;
}
// we found the end of the contentsTypeName
return (string(buffer[:i]), contentsDescr);
return (string(buffer[:i]), string(buffer));
} else if (_isForbiddenChar(current)) {
// we found an invalid character (forbidden) - passthrough (fail)
break;
Expand All @@ -187,6 +189,10 @@ library ERC7739Utils {
for (uint256 i = buffer.length; i > 0; --i) {
bytes1 current = buffer[i - 1];
if (current == bytes1(")")) {
if (i >= buffer.length || _isForbiddenFirstChar(buffer[i])) {
// we found an invalid first character (forbidden) - passthrough (fail)
break;
}
// we found the end of the contentsTypeName
return (string(buffer[i:]), string(buffer[:i]));
} else if (_isForbiddenChar(current)) {
Expand Down Expand Up @@ -215,6 +221,10 @@ library ERC7739Utils {
}

function _isForbiddenChar(bytes1 char) private pure returns (bool) {
return char == 0x00 || char == bytes1(" ") || char == bytes1(",") || char == bytes1("(") || char == bytes1(")");
return char == 0x00 || char == bytes1(" ") || char == bytes1(",") || char == bytes1(")");
}

function _isForbiddenFirstChar(bytes1 char) private pure returns (bool) {
return (char > 0x60 && char < 0x7b) || char == bytes1("(");
}
}
29 changes: 15 additions & 14 deletions test/account/Account.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function shouldBehaveLikeAccountCore() {
describe('entryPoint', function () {
it('should return the canonical entrypoint', async function () {
await this.mock.deploy();
expect(this.mock.entryPoint()).to.eventually.equal(entrypoint);
await expect(this.mock.entryPoint()).to.eventually.equal(entrypoint);
});
});

Expand Down Expand Up @@ -106,7 +106,7 @@ function shouldBehaveLikeAccountHolder() {
it('receives ERC1155 tokens from a single ID', async function () {
await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, ids[0], values[0], data);

expect(
await expect(
this.token.balanceOfBatch(
ids.map(() => this.mock),
ids,
Expand All @@ -115,15 +115,15 @@ function shouldBehaveLikeAccountHolder() {
});

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

await this.token.connect(this.other).safeBatchTransferFrom(this.other, this.mock, ids, values, data);
expect(
await expect(
this.token.balanceOfBatch(
ids.map(() => this.mock),
ids,
Expand All @@ -143,7 +143,7 @@ function shouldBehaveLikeAccountHolder() {
it('receives an ERC721 token', async function () {
await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, tokenId);

expect(this.token.ownerOf(tokenId)).to.eventually.equal(this.mock);
await expect(this.token.ownerOf(tokenId)).to.eventually.equal(this.mock);
});
});
});
Expand All @@ -156,7 +156,7 @@ function shouldBehaveLikeAccountERC7821({ deployable = true } = {}) {
await setBalance(this.mock.target, ethers.parseEther('1'));

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

this.encodeUserOpCalldata = (...calls) =>
this.mock.interface.encodeFunctionData('execute', [
Expand Down Expand Up @@ -195,13 +195,14 @@ function shouldBehaveLikeAccountERC7821({ deployable = true } = {}) {
.then(op => op.addInitCode())
.then(op => this.signUserOp(op));

expect(this.mock.getNonce()).to.eventually.equal(0);
// Can't call the account to get its nonce before it's deployed
await expect(entrypoint.getNonce(this.mock.target, 0)).to.eventually.equal(0);
await expect(entrypoint.handleOps([operation.packed], this.beneficiary))
.to.emit(entrypoint, 'AccountDeployed')
.withArgs(operation.hash(), this.mock, this.factory, ethers.ZeroAddress)
.to.emit(this.target, 'MockFunctionCalledExtra')
.withArgs(this.mock, 17);
expect(this.mock.getNonce()).to.eventually.equal(1);
await expect(this.mock.getNonce()).to.eventually.equal(1);
});

it('should revert if the signature is invalid', async function () {
Expand Down Expand Up @@ -238,24 +239,24 @@ function shouldBehaveLikeAccountERC7821({ deployable = true } = {}) {
})
.then(op => this.signUserOp(op));

expect(this.mock.getNonce()).to.eventually.equal(0);
await expect(this.mock.getNonce()).to.eventually.equal(0);
await expect(entrypoint.handleOps([operation.packed], this.beneficiary))
.to.emit(this.target, 'MockFunctionCalledExtra')
.withArgs(this.mock, 42);
expect(this.mock.getNonce()).to.eventually.equal(1);
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 }) })
.then(op => this.signUserOp(op));

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

it('should support batch execution', async function () {
Expand All @@ -275,11 +276,11 @@ function shouldBehaveLikeAccountERC7821({ deployable = true } = {}) {
})
.then(op => this.signUserOp(op));

expect(this.mock.getNonce()).to.eventually.equal(0);
await expect(this.mock.getNonce()).to.eventually.equal(0);
const tx = entrypoint.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);
expect(this.mock.getNonce()).to.eventually.equal(1);
await expect(this.mock.getNonce()).to.eventually.equal(1);
});
});
});
Expand Down
12 changes: 6 additions & 6 deletions test/crosschain/axelar/AxelarGateway.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ describe('AxelarGateway', function () {
});

it('initial setup', async function () {
expect(this.srcGateway.localGateway()).to.eventually.equal(this.axelar);
expect(this.srcGateway.getEquivalentChain(this.CAIP2)).to.eventually.equal('local');
expect(this.srcGateway.getRemoteGateway(this.CAIP2)).to.eventually.equal(getAddress(this.dstGateway));
await expect(this.srcGateway.localGateway()).to.eventually.equal(this.axelar);
await expect(this.srcGateway.getEquivalentChain(this.CAIP2)).to.eventually.equal('local');
await expect(this.srcGateway.getRemoteGateway(this.CAIP2)).to.eventually.equal(getAddress(this.dstGateway));

expect(this.dstGateway.localGateway()).to.eventually.equal(this.axelar);
expect(this.dstGateway.getEquivalentChain(this.CAIP2)).to.eventually.equal('local');
expect(this.dstGateway.getRemoteGateway(this.CAIP2)).to.eventually.equal(getAddress(this.srcGateway));
await expect(this.dstGateway.localGateway()).to.eventually.equal(this.axelar);
await expect(this.dstGateway.getEquivalentChain(this.CAIP2)).to.eventually.equal('local');
await expect(this.dstGateway.getRemoteGateway(this.CAIP2)).to.eventually.equal(getAddress(this.srcGateway));
});

it('workflow', async function () {
Expand Down
2 changes: 1 addition & 1 deletion test/helpers/erc7739.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class TypedDataSignHelper {
ethers.concat([
signature,
ethers.TypedDataEncoder.hashDomain(domain), // appDomainSeparator
ethers.TypedDataEncoder.hashStruct(this.contentsTypeName, this.allTypes, message.contents), // contentsHash
this.hashStruct(this.contentsTypeName, message.contents), // contentsHash
ethers.toUtf8Bytes(this.contentDescr),
ethers.toBeHex(this.contentDescr.length, 2),
]),
Expand Down
17 changes: 8 additions & 9 deletions test/utils/cryptography/ERC7739Signer.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ function shouldBehaveLikeERC7739Signer() {
const hash = PersonalSignHelper.hash(text);
const signature = await PersonalSignHelper.sign(this.signTypedData, text, this.domain);

expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE);
await expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE);
});

it('returns false for an invalid personal signature', async function () {
const hash = PersonalSignHelper.hash('Message the app expects');
const signature = await PersonalSignHelper.sign(this.signTypedData, 'Message signed is different', this.domain);

expect(this.mock.isValidSignature(hash, signature)).to.eventually.not.equal(MAGIC_VALUE);
await expect(this.mock.isValidSignature(hash, signature)).to.eventually.not.equal(MAGIC_VALUE);
});
});

Expand Down Expand Up @@ -56,7 +56,7 @@ function shouldBehaveLikeERC7739Signer() {
const hash = ethers.TypedDataEncoder.hash(this.appDomain, { Permit }, message.contents);
const signature = await TypedDataSignHelper.sign(this.signTypedData, this.appDomain, { Permit }, message);

expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE);
await expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE);
});

it('returns true for valid typed data signature (nested types)', async function () {
Expand All @@ -72,7 +72,7 @@ function shouldBehaveLikeERC7739Signer() {
const hash = TypedDataSignHelper.hash(this.appDomain, contentsTypes, message.contents);
const signature = await TypedDataSignHelper.sign(this.signTypedData, this.appDomain, contentsTypes, message);

expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE);
await expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE);
});

it('returns false for an invalid typed data signature', async function () {
Expand All @@ -89,14 +89,13 @@ function shouldBehaveLikeERC7739Signer() {
const hash = ethers.TypedDataEncoder.hash(this.appDomain, { Permit }, appContents);
const signature = await TypedDataSignHelper.sign(this.signTypedData, this.appDomain, { Permit }, message);

expect(this.mock.isValidSignature(hash, signature)).to.eventually.not.equal(MAGIC_VALUE);
await expect(this.mock.isValidSignature(hash, signature)).to.eventually.not.equal(MAGIC_VALUE);
});
});

it('support detection', function () {
expect(
this.mock.isValidSignature('0x7739773977397739773977397739773977397739773977397739773977397739', ''),
).to.eventually.equal('0x77390001');
it('support detection', async function () {
const hash = '0x7739773977397739773977397739773977397739773977397739773977397739';
await expect(this.mock.isValidSignature(hash, '0x')).to.eventually.equal('0x77390001');
});
});
}
Expand Down
4 changes: 2 additions & 2 deletions test/utils/cryptography/ERC7739Signer.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { ethers } = require('hardhat');
const { shouldBehaveLikeERC7739Signer } = require('./ERC7739Signer.behavior');
const { NonNativeSigner, P256SigningKey, RSASigningKey } = require('../../helpers/signers');
const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey } = require('../../helpers/signers');

describe('ERC7739Signer', function () {
describe('for an ECDSA signer', function () {
Expand All @@ -26,7 +26,7 @@ describe('ERC7739Signer', function () {

describe('for an RSA signer', function () {
before(async function () {
this.signer = new NonNativeSigner(RSASigningKey.random());
this.signer = new NonNativeSigner(RSASHA256SigningKey.random());
this.mock = await ethers.deployContract('ERC7739SignerRSAMock', [
this.signer.signingKey.publicKey.e,
this.signer.signingKey.publicKey.n,
Expand Down
56 changes: 28 additions & 28 deletions test/utils/cryptography/ERC7739Utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,12 @@ const { expect } = require('chai');
const { ethers } = require('hardhat');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');

const { Permit, domainType } = require('@openzeppelin/contracts/test/helpers/eip712');
const { Permit } = require('@openzeppelin/contracts/test/helpers/eip712');
const { PersonalSignHelper, TypedDataSignHelper } = require('../../helpers/erc7739');

// Helper for ERC20Permit applications
const helper = TypedDataSignHelper.from({ Permit });

function domainComponentsType(domain) {
return ethers.TypedDataEncoder.from({ EIP712Domain: domainType(domain) })
.encodeType('EIP712Domain')
.replace(/EIP712Domain\((.*)\)/, (_, s) => s);
}

function domainComponentsBytes(domain) {
return ethers.hexlify(
ethers
.getBytes(ethers.TypedDataEncoder.from({ EIP712Domain: domainType(domain) }).encodeData('EIP712Domain', domain))
.slice(32),
);
}

const fixture = async () => {
const mock = await ethers.deployContract('$ERC7739Utils');
const domain = {
Expand Down Expand Up @@ -65,9 +51,9 @@ describe('ERC7739Utils', function () {
ethers.toBeHex(contentDescr.length, 2),
]);

expect(this.mock.$encodeTypedDataSig(signature, appSeparator, contentsHash, contentDescr)).to.eventually.equal(
encoded,
);
await expect(
this.mock.$encodeTypedDataSig(signature, appSeparator, contentsHash, contentDescr),
).to.eventually.equal(encoded);
});
});

Expand All @@ -85,7 +71,7 @@ describe('ERC7739Utils', function () {
ethers.toBeHex(contentDescr.length, 2),
]);

expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
await expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
ethers.hexlify(signature),
appSeparator,
contentsHash,
Expand All @@ -95,7 +81,7 @@ describe('ERC7739Utils', function () {

it('returns default empty values if the signature is too short', async function () {
const encoded = ethers.randomBytes(65); // DOMAIN_SEPARATOR (32 bytes) + CONTENTS (32 bytes) + CONTENTS_TYPE_LENGTH (2 bytes) - 1
expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
await expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
'0x',
ethers.ZeroHash,
ethers.ZeroHash,
Expand All @@ -105,7 +91,7 @@ describe('ERC7739Utils', function () {

it('returns default empty values if the length is invalid', async function () {
const encoded = ethers.concat([ethers.randomBytes(64), '0x3f']); // Can't be less than 64 bytes
expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
await expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
'0x',
ethers.ZeroHash,
ethers.ZeroHash,
Expand All @@ -118,7 +104,7 @@ describe('ERC7739Utils', function () {
it('should produce a personal signature EIP-712 nested type', async function () {
const text = 'Hello, world!';

expect(this.mock.$personalSignStructHash(ethers.hashMessage(text))).to.eventually.equal(
await expect(this.mock.$personalSignStructHash(ethers.hashMessage(text))).to.eventually.equal(
ethers.TypedDataEncoder.hashStruct('PersonalSign', PersonalSignHelper.types, PersonalSignHelper.prepare(text)),
);
});
Expand All @@ -131,22 +117,36 @@ describe('ERC7739Utils', function () {
const contentsHash = helper.hashStruct('Permit', message.contents);
const hash = helper.hashStruct('TypedDataSign', message);

expect(
const domainBytes = ethers.AbiCoder.defaultAbiCoder().encode(
['bytes32', 'bytes32', 'uint256', 'address', 'bytes32'],
[
ethers.id(this.domain.name),
ethers.id(this.domain.version),
this.domain.chainId,
this.domain.verifyingContract,
ethers.ZeroHash,
],
);

await expect(
this.mock.$typedDataSignStructHash(
helper.contentDescr,
helper.contentsTypeName,
ethers.Typed.string(helper.contentDescr),
contentsHash,
domainComponentsType(this.domain),
domainComponentsBytes(this.domain),
domainBytes,
),
).to.eventually.equal(hash);
await expect(
this.mock.$typedDataSignStructHash(helper.contentDescr, contentsHash, domainBytes),
).to.eventually.equal(hash);
});
});

describe('typedDataSignTypehash', function () {
it('should match', async function () {
const typedDataSignType = ethers.TypedDataEncoder.from(helper.allTypes).encodeType('TypedDataSign');

expect(
await expect(
this.mock.$typedDataSignTypehash(
helper.contentsTypeName,
typedDataSignType.slice(typedDataSignType.indexOf(')') + 1),
Expand Down Expand Up @@ -203,7 +203,7 @@ describe('ERC7739Utils', function () {
})),
)) {
it(descr, async function () {
expect(this.mock.$decodeContentsDescr(contentDescr)).to.eventually.deep.equal([
await expect(this.mock.$decodeContentsDescr(contentDescr)).to.eventually.deep.equal([
contentTypeName ?? '',
contentTypeName ? contentType ?? contentDescr : '',
]);
Expand Down

0 comments on commit e1c49fc

Please sign in to comment.